What you’ll be able to do
- Create a MIP20 hold.
- Make a payment from held funds.
- Release any unused funds.
owner in the hold API - approves that spender
first. Held funds leave the holder’s spendable balance and cannot pay gas, while
the spender controls capture and release. heldTotalOf(owner) reports the funds
still attributed to that holder across all of its holds.
The token records one aggregate hold for each holder and spender. The
application records which orders share that hold and how much remains assigned
to each. MIP20 supplies no order ID, automatic expiry or release initiated by the holder.
Configure the holder and spender
UseownerWallet for the holder’s signing account and spenderWallet for the
spender’s. Each pays gas for its own transactions.
For a contract spender, its own application entry point must make the token
calls below with appropriate access controls.
Install @metalntwx/metal-ts@0.3.2 and viem@2.57.7 with pnpm. Resolve token
and its decimals using Read metadata.
Validate the order’s amount and convert it to base-unit reserved using
Convert a payment amount.
Read the holder’s spendable balance and retain enough outside the hold for gas costs.
Approve and place the hold
Use the holder’s wallet client,ownerWallet, to grant the spender an
allowance of reserved using Approve a
spender. If an allowance already
exists, reconcile its intended use first. approve replaces the allowance,
including a nonzero allowance. Coordinate with the spender so outstanding
transactions cannot spend an old allowance while it is being replaced.
The spender places the hold. Estimate and simulate this exact call using
Estimate and record fees, with the
spender’s signing account. Its balance in the gas token pays the transaction
fee. Set gasLimit from that estimate and use the quoted price as
maxFeePerGas.
await signed.send(). Use the
same sign, save, submit and transaction receipt procedure for each capture and
release, with a new transaction attempt and an estimate for its actual
arguments.
Require successful execution and canonical finality. The transaction receipt must contain
HoldPlaced(owner, spender, reserved) and the token’s Transfer(owner, token, reserved). Read heldBalanceOf(owner, spender) and require
heldBefore + reserved when no other transaction changed the hold concurrently. Verify
that the finite allowance decreased by reserved and the holder’s spendable
balance decreased accordingly. Account separately for gas if the holder also
submits a transaction.
Allocate the held amount to orders
Store the chain ID, token, holder and spender with each allocation. Track reserved, captured and released amounts as base-unit integers, and retain each transaction attempt and verified event identity. The remaining allocation isreserved - captured - released.
A capture that enters quarantine still consumes its allocation. Record delivery
separately as paid or quarantined, with the quarantine receipt for quarantined
funds. Leaving those funds available for another capture would spend the order’s
allocation twice.
Serialize changes to a shared aggregate hold and update allocations atomically
with their verified results. The sum of outstanding allocations must never
exceed the onchain hold. Refuse a capture larger than the order’s remaining
allocation even when the aggregate hold has enough funds from other orders.
Use the storage and concurrency procedure in
Verify and reconcile payments.
Capture part of the hold
Setrecipient from the immutable order and choose captured such that
0 < captured < reserved for a partial payment. The spender assigned to the hold calls
captureHold, which releases that amount from escrow internally and then
transfers it from the holder to the recipient.
capture with spenderWallet, save its separate attempt, and submit it
using the placement procedure. Verify these token events in the same successful,
finalized transaction, in this order:
Transfer(token, owner, captured)returns escrowed funds internally.HoldReleased(owner, spender, captured)records the hold reduction.Transfer(owner, recipient, captured)records the payment.
captured. The holder’s spendable balance has no net
increase from capture. Credit the recipient only for the transfer from the
holder to the recipient, using the finalized transfer
checks.
If receive policy refuses the recipient credit, capture forces quarantine even
when the recipient uses the default REVERT mode. The first two events still
occur, followed by a ReceivePolicy Quarantined event and the token’s
Transfer(owner, ReceivePolicy, captured). The hold and heldTotalOf(owner)
decrease, but the intended recipient receives no tokens. Verify and save the
quarantine receipt, including its token, originator,
intended recipient (receiver), amount and recovery authority. Mark the allocation captured
and quarantined, leave the order unpaid, and recover through that receipt.
Do not repeat the capture to try to pay the same order.
Release the remainder
Only the spender assigned to the hold can release it. The holder sends a cancellation request to the application, which checks the order and instructs that spender. For this partial-payment flow, releasereserved - captured.
Transfer(token, owner, remainder), HoldReleased(owner, spender, remainder) and finality before
closing the allocation. With no other holds, heldBalanceOf is now zero. For
an existing aggregate hold, it returns to heldBefore. Revoke any unused
allowance separately.
Placement and release bypass receive screening. Release remains available even if
the holder’s receive policy would refuse an ordinary credit. They emit
transfers into and out of the token contract, which must not be credited as customer payments.