结算交易

SettleDvp 由交易记录中指定的结算授权方签名。在一笔交易中,它会将资产腿的 amount_a 转入现金腿一方的目标账户,将现金腿的 amount_b 转入资产腿一方的目标账户,把任一托管账户中的多余部分退还给该腿所属的一方,并关闭交易记录和两个托管账户。若其中任何一笔转账无法完成,该指令会失败,且不会有任何资产发生移动。

本页承接创建交易和为各腿注资;客户端、签名者、trade 以及各方的 token account 均沿用。

签名前重新验证

结算授权方签名的这笔交易会转移两条腿的资产,因此它在签名前要像各参与方一样执行同样的读取检查:所有者、大小、参与方、mint、金额、到期时间以及两个目标账户(参见为各腿注资)。程序本身在结算时还会再次检查:每个 mint 是否仍属于创建时记录的 token program,是否不含被禁止的扩展,以及 mint authority 是否与创建交易时相同。因此,如果 mint 被重新创建后带有被禁止的扩展、使用了不同的 token program 或不同的 mint authority,则无法完成结算。

创建接收方账户

Settle 会写入四个程序不会创建的 token account:

账户接收内容所有者
userADestinationAtaB现金腿(amount_b)user_a_settlement_destination
userBDestinationAtaA资产腿(amount_a)user_b_settlement_destination
userAAtaA资产腿的任何多余部分user_a
userBAtaB现金腿的任何多余部分user_b

只有当托管账户中的余额超过约定金额时,才会用到多余部分的接收账户。但任何持有该 mint 的人只需向托管账户转入几个最小单位,就能制造出多余部分。如果退款账户不存在,整个 Settle 就会失败,因此请在同一笔交易中以幂等方式创建全部四个账户。

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,
}),
);

结算

legAExtrasCount 用于告知程序:在固定列表之后追加的账户中,有多少个属于资产腿的 Transfer Hook,其余的则属于现金腿。对于没有 Transfer Hook 的 mint,传入零且不追加任何账户。memo program account 是该指令必需的账户,仅用于要求附带 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 Hook

生成的 IDL 只列出固定账户,因此生成的构建器并不知道 hook 账户。请按照直接转移该代币时的相同方式,解析每个带 hook 的 mint 的额外账户元数据(account metas),然后将它们追加到指令中:先是资产腿的账户,再是现金腿的账户,并将 legAExtrasCount 设置为第一组的账户数量。在 TypeScript 中,将解析出的 metas 展开到指令的 accounts 数组中;在 Rust 中,使用构建器上的 add_remaining_accounts。每条腿的上限为 32 个账户,其中包括 hook 程序和验证账户;当两条腿都带有 hook 时,交易的账户数量上限可能会先达到。程序会移除所有转发账户的签名者标记,因此 hook 无法通过此路径获取结算授权方的签名。

记录结果

当 Settle 交易达到 finalized 状态时,才可将该交易视为已结算。结算后,交易记录和两个托管账户将不复存在,因此如果系统日后需要这些条款,应从结算前的读取结果或创建交易中保存。已关闭账户的 rent 已归还给结算授权方。

Settle 错误

LegNotFunded 表示某个托管账户的余额低于约定金额;DvpExpired 和 SettlementTooEarly 表示错过了时间窗口;MintAuthorityChanged 和 BlockedMintExtension 表示某个 mint 在创建后发生了变化;RecipientAtaMismatch 表示目标账户或退款 token account 缺失,或不再属于预期的所有者和 mint,通常是因为跳过了上述创建账户的步骤。无论哪种情况,都不会有任何资产发生移动,各参与方可以撤销交易,但需受 mint 相关权限方的约束(参见撤销交易)。

后续步骤

  • 撤销交易:交易未能结算时,如何取回已注资的各腿资产

Is this page helpful?

Table of Contents

Edit Page
©️ 2026 Solana 基金会版权所有