Unwind a Trade

Four instructions return funds without settling: Reclaim, Cancel, and Reject while the trade is open, and Recover for a deposit that arrives after it has closed. None of them checks the expiry. A frozen escrow or a changed mint can still hold a leg, as the notes at the end describe.

InstructionSignerScopeTrade afterwards
ReclaimDvpuser_a or user_bThe signer's own legStill open
CancelDvpSettlement authorityBoth legsClosed
RejectDvpuser_a or user_bBoth legsClosed
RecoverDvpuser_a or user_bThe signer's leg, after closeAlready closed

In each case the escrow is drained of whatever it holds to the leg's party: escrow A to user_a's token account for mint_a, escrow B to user_b's for mint_b, whoever funded it. The memo program account is supplied for refund accounts that require a memo.

This page continues from the earlier steps: the client and signers from create a trade, userAAtaA and userBAtaB from fund the legs, and MEMO_PROGRAM_ID and authority from settle the trade carry over.

Reclaim a leg

Reclaim returns the signer's leg and leaves the trade open, so the leg can be funded again. Use it when the issue is on the counterparty's leg; Cancel and Reject close the whole trade.

import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
import { getReclaimDvpInstruction } from "./dvp";
// The asset-leg party takes its asset back.
const reclaimInstruction = getReclaimDvpInstruction({
signer: userASigner,
swapDvp,
mint: mintA,
dvpSourceAta: escrowA,
signerDestAta: userAAtaA,
tokenProgram: tokenProgramA,
memoProgram: MEMO_PROGRAM_ADDRESS,
});
await client.sendTransaction([reclaimInstruction]);

Cancel or reject

Cancel is signed by the settlement authority and Reject by either party. Both refund whatever each escrow holds to the leg's party and close the record and both escrows, with the closed-account rent going to the signer. The two instructions take the same accounts; only the signer differs.

import { getCancelDvpInstruction, getRejectDvpInstruction } from "./dvp";
const unwindAccounts = {
swapDvp,
mintA,
mintB,
dvpAtaA: escrowA,
dvpAtaB: escrowB,
userAAtaA,
userBAtaB,
tokenProgramA,
tokenProgramB,
memoProgram: MEMO_PROGRAM_ADDRESS,
legAExtrasCount: 0,
};
// Signed by the settlement authority:
const cancelInstruction = getCancelDvpInstruction({
settlementAuthority: authoritySigner,
...unwindAccounts,
});
// Or signed by either party:
const rejectInstruction = getRejectDvpInstruction({
signer: userBSigner,
...unwindAccounts,
});

The refund accounts (userAAtaA, userBAtaB) must exist for any leg that holds a balance; an uninitialized refund account makes the token transfer fail and reverts the instruction. A leg that was never funded needs no transfer and tolerates a missing account.

Recover a late deposit

Settle, Cancel, and Reject all close both escrow token accounts, so a plain transfer into a closed escrow fails. A deposit only lands after close if something recreates the escrow first, for example a funding transaction that includes a create-idempotent associated token account instruction ahead of the transfer and was in flight when the counterparty sent Reject. The tokens then sit in an escrow whose trade no longer exists: the record is gone, so Reclaim cannot find it, and the nonce tombstone prevents a new trade from being created at the same address and capturing the deposit.

To use RecoverDvp, the leg's party supplies the original seed inputs (settlement authority, both parties, both mints, nonce). The program re-derives the record address, requires its tombstone as proof the trade existed, drains the escrow to the signer's own token account, and closes it, returning the escrow rent to the signer. The signer's leg is chosen by which party signs: mint_a for user_a, mint_b for user_b.

import { getAddressEncoder, getProgramDerivedAddress } from "@solana/kit";
import { DVP_SWAP_PROGRAM_PROGRAM_ADDRESS, getRecoverDvpInstruction } from "./dvp";
const [nonceTombstone] = await getProgramDerivedAddress({
programAddress: DVP_SWAP_PROGRAM_PROGRAM_ADDRESS,
seeds: ["nonce", getAddressEncoder().encode(swapDvp)],
});
const recoverInstruction = getRecoverDvpInstruction({
signer: userASigner,
swapDvp,
nonceTombstone,
mint: mintA,
dvpEscrowAta: escrowA,
signerDestAta: userAAtaA,
tokenProgram: tokenProgramA,
memoProgram: MEMO_PROGRAM_ADDRESS,
settlementAuthority,
userA,
userB,
mintA,
mintB,
nonce,
});
await client.sendTransaction([recoverInstruction]);

Recovery depends on the party keeping the create parameters. A system that funds trades stores the seed inputs with each order until the trade has closed and the escrow balances have been confirmed empty.

Notes

  • Frozen legs. On a gated mint, a frozen escrow blocks the token transfer these instructions perform until the freeze authority thaws it. See the overview.
  • Mint changes after creation. The unwind paths skip the extension and mint-authority checks so the program's own checks do not block a recovery. The token program's own rules still apply. A mint can only be closed at zero supply, so it cannot change while a balance sits in escrow, but a mint closed and recreated as NonTransferable or with a transfer fee before a late deposit lands can make that deposit unmovable or fee-bearing. That is the mint-authority dependency listed under supported tokens.

Next steps

Is this page helpful?

목차

페이지 편집
Unwind a DvP Trade on Solana | Solana