거래 생성하기

CreateDvp는 거래 조건을 기록하고 당사자들이 자금을 넣을 두 개의 에스크로 계정을 생성합니다. 토큰은 전혀 이동하지 않습니다. 누구나 이 명령을 전송할 수 있으므로 레코드는 거래 상대방, 정산 권한자(settlement authority)의 운영자, 또는 제3자가 생성할 수 있으며, 다른 모든 참여자는 저장된 조건을 신뢰하기 전에 직접 검증해야 합니다 (레그에 자금 예치하기 참고).

이 스니펫은 생성된 클라이언트를 플레이스홀더 주소와 함께 직접 사용하므로, 관련된 계정과 서명자를 그대로 확인할 수 있습니다. 대상은 devnet이며, 프로그램은 mainnet-beta와 동일한 주소에 배포되어 있습니다.

설치

2026년 10월 2일 기준으로 DvP 클라이언트용으로 게시된 패키지는 없습니다. 두 클라이언트 모두 저장소의 프로그램 IDL에서 생성되며 소스에서 직접 사용합니다.

git clone https://github.com/solana-foundation/dvp.git
cd dvp && git checkout df9919ed02c25a93620e7f6820107c9050ce3b92
pnpm install && pnpm generate-clients && pnpm build
# The client is clients/typescript/src; copy or link it into your project as ./dvp
pnpm 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 asset
amountB: 998_500_000n, // 998.50 units of a 6-decimal cash token
expiryTimestamp: nowSeconds + 60n * 60n * 24n, // one day
nonce,
refString: "ORDER-2026-10-02-0001",
userASettlementDestination: null, // defaults to userA
userBSettlementDestination: null, // defaults to userB
earliestSettlementTimestamp: 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?

목차

페이지 편집