CreateDvp는 거래 조건을 기록하고 당사자들이 자금을 넣을 두 개의 에스크로 계정을
생성합니다. 토큰은 전혀 이동하지 않습니다. 누구나 이 명령을 전송할 수 있으므로
레코드는 거래 상대방, 정산 권한자(settlement authority)의 운영자, 또는 제3자가
생성할 수 있으며, 다른 모든 참여자는 저장된 조건을 신뢰하기 전에 직접 검증해야 합니다
(레그에 자금 예치하기 참고).
이 스니펫은 생성된 클라이언트를 플레이스홀더 주소와 함께 직접 사용하므로, 관련된 계정과 서명자를 그대로 확인할 수 있습니다. 대상은 devnet이며, 프로그램은 mainnet-beta와 동일한 주소에 배포되어 있습니다.
설치
2026년 10월 2일 기준으로 DvP 클라이언트용으로 게시된 패키지는 없습니다. 두 클라이언트 모두 저장소의 프로그램 IDL에서 생성되며 소스에서 직접 사용합니다.
git clone https://github.com/solana-foundation/dvp.gitcd dvp && git checkout df9919ed02c25a93620e7f6820107c9050ce3b92pnpm install && pnpm generate-clients && pnpm build# The client is clients/typescript/src; copy or link it into your project as ./dvppnpm add @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token-2022 @solana-program/memo
이 페이지들은 커밋 df9919e를 기준으로 작성되었으며, TypeScript에서는 @solana/kit 8,
Rust에서는 solana-* 2.x 크레이트를 사용합니다.
각 단계 페이지의 스니펫은 서로 이어집니다. 각 페이지는 앞선 페이지에서 정의한 클라이언트,
서명자, 주소를 사용합니다. payerSigner, userASigner, userBSigner,
authoritySigner는 각각 rent 납부자, 두 당사자, 정산 권한자를 위한
TransactionSigner이며, 예를 들어 createKeyPairSignerFromBytes로 만들 수 있습니다.
Rust 스니펫은 인스트럭션을 빌드하기만 하고, 서명과 전송은 호출자에게 맡깁니다.
주소 파생하기
거래 레코드는 정산 권한자, 두 당사자, 두 민트, 그리고 nonce를 기반으로 한 Program Derived Address입니다. 각 에스크로는 두 민트 중 하나에 대한 레코드의 associated token account입니다. 두 파생 과정 모두 네트워크 접근이 필요하지 않습니다.
모든 거래에는 무작위 64비트 nonce를 사용하세요. 생성에는 권한이 필요하지 않으므로,
예측 가능한 nonce를 사용하면 제3자가 거래에 사용될 주소를 미리 선점할 수 있습니다.
TypeScript에서는 nonce와 금액을 bigint로 전달하세요. 생성된 클라이언트는
number를 거부합니다.
실행하기 전에 각 ..._HERE 플레이스홀더를 실제 주소로 교체하세요.
address()는 base58을 검증하며 플레이스홀더가 있으면 예외를 던지고, pubkey!는
컴파일에 실패합니다.
import { address, createClient } from "@solana/kit";import { solanaRpc } from "@solana/kit-plugin-rpc";import { signer } from "@solana/kit-plugin-signer";import { TOKEN_2022_PROGRAM_ADDRESS } from "@solana-program/token-2022";import { findSwapDvpEscrowAta, findSwapDvpPda } from "./dvp";const client = createClient().use(signer(payerSigner)).use(solanaRpc({rpcUrl: "https://api.devnet.solana.com",rpcSubscriptionsUrl: "wss://api.devnet.solana.com",}),);const settlementAuthority = address("SETTLEMENT_AUTHORITY_ADDRESS_HERE");const userA = address("ASSET_LEG_PARTY_ADDRESS_HERE");const userB = address("CASH_LEG_PARTY_ADDRESS_HERE");const mintA = address("ASSET_MINT_ADDRESS_HERE");const mintB = address("CASH_MINT_ADDRESS_HERE");const tokenProgramA = TOKEN_2022_PROGRAM_ADDRESS;const tokenProgramB = TOKEN_2022_PROGRAM_ADDRESS;// A fresh random nonce per trade, as a bigint.const nonce = new DataView(crypto.getRandomValues(new Uint8Array(8)).buffer,).getBigUint64(0);const [swapDvp] = await findSwapDvpPda({settlementAuthority,userA,userB,mintA,mintB,nonce,});const [escrowA] = await findSwapDvpEscrowAta({swapDvp,mint: mintA,tokenProgram: tokenProgramA,});const [escrowB] = await findSwapDvpEscrowAta({swapDvp,mint: mintB,tokenProgram: tokenProgramB,});
조건 기록하기
CreateDvp는 기본 단위의 두 금액, 만료 시각, nonce, 그리고 선택 필드 네 가지를 받습니다.
선택 필드는 최대 64바이트의 불투명 참조값, 각 측의 정산 목적지, 가장 이른 정산 시각입니다(참고 사항 참조).
정산 목적지는 정산 시 해당 당사자의 대금이 도착하는 곳으로, 예를 들어 수탁 기관의
입금 지갑입니다. 기본값은 당사자 본인의 주소이며, 레코드가 생성되면 변경할 수 없습니다.
환불과 회수는 누가 에스크로에 자금을 넣었든 항상 해당 레그의 민트에 대한 당사자 본인의
token account로 전송되며, 정산 목적지로는 전송되지 않습니다.
payer가 레코드, nonce 툼스톤, 두 에스크로의 rent를 부담합니다.
종료 시 해당 rent는 payer에게 돌아가지 않고 종료 인스트럭션에 서명한 쪽에게 지급됩니다.
툼스톤은 닫히지 않으므로 각 거래마다 소액의 잔액이 영구적으로 남습니다.
payer의 SOL을 계획할 때는 rent 금액을 하드코딩하지 말고
getMinimumBalanceForRentExemption RPC 메서드로 구하세요.
import { getCreateDvpInstruction } from "./dvp";const nowSeconds = BigInt(Math.floor(Date.now() / 1000));const createInstruction = getCreateDvpInstruction({payer: payerSigner,swapDvp,nonceTombstone: (await getProgramDerivedAddress({programAddress: DVP_SWAP_PROGRAM_PROGRAM_ADDRESS,seeds: ["nonce", getAddressEncoder().encode(swapDvp)],}))[0],settlementAuthority,userA,userB,mintA,mintB,dvpAtaA: escrowA,dvpAtaB: escrowB,tokenProgramA,tokenProgramB,amountA: 1_000_000_000n, // 1,000 units of a 6-decimal assetamountB: 998_500_000n, // 998.50 units of a 6-decimal cash tokenexpiryTimestamp: nowSeconds + 60n * 60n * 24n, // one daynonce,refString: "ORDER-2026-10-02-0001",userASettlementDestination: null, // defaults to userAuserBSettlementDestination: null, // defaults to userBearliestSettlementTimestamp: null,});const { context } = await client.sendTransaction([createInstruction]);console.log("Trade record:", swapDvp, "signature:", context.signature);
TypeScript 스니펫을 사용하려면 @solana/kit의 getProgramDerivedAddress와
getAddressEncoder, 그리고 클라이언트의 DVP_SWAP_PROGRAM_PROGRAM_ADDRESS를
스코프에 가져와야 합니다. Rust 빌더는 system program과 associated token program을
기본값 그대로 둡니다.
참고 사항
- 만료. 만료 시각은 Settle만 확인합니다. Reclaim, Cancel, Reject는 만료 이후에도 동작하며, 만료 후에 입금된 자금은 해당 레그의 당사자가 회수할 수 있습니다. 만료 시각은 생성 후 최대 1년까지 설정할 수 있습니다.
- 시간은 네트워크 시간입니다. 프로그램은 온체인 클록을 읽으며, 이는 실제 시계 시간과 때로는 수십 초까지 차이가 날 수 있고 앞으로 건너뛸 수도 있습니다. 만료 시각과 의도한 정산 시각 사이에 여유를 두고, 어느 쪽 경계 근처에서도 자금 입금이나 정산을 피하세요. 또한 만료 시각으로 초 단위의 마감을 강제할 수 있다고 기대하지 마세요. 프로그램 README는 약 1분의 오차를 감안할 것을 권장합니다.
- 가장 이른 정산 시각. 선택 사항입니다. 설정하면 해당 시각 이전의 Settle은 거부되며, 이 시각은 만료 시각보다 늦을 수 없습니다.
- 생성 시점 검사. 당사자가 System Program이 소유한 계정이 아니면(SPL Token 멀티시그는
이로 인해 제외됩니다) Create가
PartyNotSignerCapable로 실패하며, 그 밖에도SettlementDestinationIsSwapDvp,SelfDvp,SameMint,ZeroAmount,SettlementAuthorityIsParty,ExpiryNotInFuture로 실패할 수 있습니다. 전체 목록은 오류 표에서 확인할 수 있습니다.
다음 단계
- 레그에 자금 예치하기: 각 당사자가 자신의 레그를 거래의 에스크로로 전송합니다
Is this page helpful?