Settle the Trade

SettleDvp is signed by the settlement authority named in the trade record. In one transaction it moves amount_a of the asset leg to the cash-leg party's destination and amount_b of the cash leg to the asset-leg party's destination, returns any surplus in either escrow to the leg's party, and closes the record and both escrows. If any of those transfers cannot complete, the instruction fails and nothing moves.

This page continues from create a trade and fund the legs; the client, signers, trade, and the parties' token accounts carry over.

Re-validate before signing

The settlement authority signs the transaction that moves both legs, so it performs the same read as each party before it signs: owner, size, parties, mints, amounts, expiry, and both destinations (see fund the legs). The program itself re-checks at settlement that each mint still belongs to the token program recorded at creation, carries no blocked extension, and has the same mint authority as when the trade was created, so a mint recreated with a blocked extension, a different token program, or a different mint authority cannot settle.

Create the recipient accounts

Settle writes to four token accounts that the program does not create:

AccountReceivesOwner
userADestinationAtaBThe cash leg (amount_b)user_a_settlement_destination
userBDestinationAtaAThe asset leg (amount_a)user_b_settlement_destination
userAAtaAAny surplus on the asset leguser_a
userBAtaBAny surplus on the cash leguser_b

The surplus accounts are only used when an escrow holds more than the agreed amount, but anyone holding the mint can create a surplus by sending a few base units into the escrow. If the refund account doesn't exist, the whole Settle fails, so create all four idempotently in the same transaction.

import {
findAssociatedTokenPda,
getCreateAssociatedTokenIdempotentInstruction,
} from "@solana-program/token-2022";
const destinationA = t.userASettlementDestination; // defaults to userA
const destinationB = t.userBSettlementDestination; // defaults to userB
const [userADestinationAtaB] = await findAssociatedTokenPda({
mint: mintB,
owner: destinationA,
tokenProgram: tokenProgramB,
});
const [userBDestinationAtaA] = await findAssociatedTokenPda({
mint: mintA,
owner: destinationB,
tokenProgram: tokenProgramA,
});
// userAAtaA and userBAtaB are from fund the legs.
const createRecipientAccounts = [
{ ata: userADestinationAtaB, owner: destinationA, mint: mintB, tokenProgram: tokenProgramB },
{ ata: userBDestinationAtaA, owner: destinationB, mint: mintA, tokenProgram: tokenProgramA },
{ ata: userAAtaA, owner: userA, mint: mintA, tokenProgram: tokenProgramA },
{ ata: userBAtaB, owner: userB, mint: mintB, tokenProgram: tokenProgramB },
].map(({ ata, owner, mint, tokenProgram }) =>
getCreateAssociatedTokenIdempotentInstruction({
payer: authoritySigner,
ata,
owner,
mint,
tokenProgram,
}),
);

Settle

legAExtrasCount tells the program how many of the accounts appended after the fixed list belong to the asset leg's transfer hook; the rest go to the cash leg's. For mints without transfer hooks, pass zero and append nothing. The memo program account is required by the instruction and is used only for destinations that require a memo.

import { MEMO_PROGRAM_ADDRESS } from "@solana-program/memo";
import { getSettleDvpInstruction } from "./dvp";
const settleInstruction = getSettleDvpInstruction({
settlementAuthority: authoritySigner,
swapDvp,
mintA,
mintB,
dvpAtaA: escrowA,
dvpAtaB: escrowB,
userADestinationAtaB,
userBDestinationAtaA,
userAAtaA,
userBAtaB,
tokenProgramA,
tokenProgramB,
memoProgram: MEMO_PROGRAM_ADDRESS,
legAExtrasCount: 0,
});
const { context: settleContext } = await client.sendTransaction([
...createRecipientAccounts,
settleInstruction,
]);
const settleSignature = settleContext.signature;
console.log("Submitted:", settleSignature);
// sendTransaction returns at the client's commitment (confirmed by default).
// Wait for finalized before recording the trade as settled.
let finalized = false;
for (let attempt = 0; attempt < 30 && !finalized; attempt++) {
const {
value: [status],
} = await client.rpc.getSignatureStatuses([settleSignature]).send();
if (status?.err) {
throw new Error("Settle transaction failed");
}
finalized = status?.confirmationStatus === "finalized";
if (!finalized) {
await new Promise((resolve) => setTimeout(resolve, 2_000));
}
}
if (!finalized) {
// A closed record means it settled; an open one is safe to settle again.
throw new Error("Settle not finalized after 60s; read the trade record");
}
console.log("Settled:", settleSignature);

Transfer hooks

The generated IDL lists only the fixed accounts, so the generated builders do not know about hook accounts. Resolve each hook mint's extra account metas the same way you would for a direct transfer of that token, then append them to the instruction: the asset leg's accounts first, then the cash leg's, with legAExtrasCount set to the number in the first group. In TypeScript, spread the resolved metas onto the instruction's accounts array; in Rust, use add_remaining_accounts on the builder. The cap is 32 accounts per leg, including the hook program and validation account; the transaction account limit can bind first when both legs carry hooks. The program strips the signer flag from every forwarded account, so a hook cannot obtain the settlement authority's signature through this path.

Record the result

Treat the trade as settled when the Settle transaction reaches finalized. The record and both escrows no longer exist after settlement, so a system that needs the terms later stores them from its pre-settlement read or from the creating transaction. The rent of the closed accounts has gone to the settlement authority.

Settle errors

LegNotFunded means an escrow holds less than its agreed amount; DvpExpired and SettlementTooEarly mean the time window was missed; MintAuthorityChanged and BlockedMintExtension mean a mint changed after creation; RecipientAtaMismatch means a destination or refund token account is missing or no longer belongs to the expected owner and mint, usually because the create-account step above was skipped. In every case nothing has moved, and the parties can unwind, subject to the mint's authorities (see unwind a trade).

Next steps

Is this page helpful?

Inhoudsopgave

Pagina Bewerken
© 2026 Solana Foundation. Alle rechten voorbehouden.
Settle a DvP Trade on Solana | Solana