์คํฌ๋ฆฝํธ ์คํ ๋ช
๋ น์ด
#1) MPT๋ก ์์คํฌ๋ก ์์ฑํ๊ธฐ
$npx ts-node xrpl/TokenEscrow/escrowCreateMPT.ts
#createIssuance ์คํ ๊ฒฐ๊ณผ์์ ๋ณต์ฌํ MPT IssuanceID(48hex)๋ฅผ ๋ฃ์ด์ค์ผ ํจ.
#const ISSUANCE_ID = "0049CE349E4215DD8AC6196A0A5027DF489AEC3B17BD6211"
#2) IOU๋ก ์์คํฌ๋ก ์์ฑํ๊ธฐ (Admin ๊ณ์ ์ 17๋ฒ ํ๋๊ทธ๋ก AccountSet ํ์)
$npx ts-node xrpl/TokenEscrow/escrowCreateIOU.ts
์์ฑ์ด ๋๋ฉด, EscrowCreate(MPT UserโUser2) -> Owner=<User์ฃผ์>, OfferSequence=123456 ๋ผ๊ณ ๋จ๋๋ฐ,
์ฌ๊ธฐ์ OfferSequence๋ฅผ ๊ธฐ๋กํด 3,4๋ฒ ์คํฌ๋ฆฝํธ์ ์ฌ์ฉ
#3) ์์คํฌ๋ก ๋๋ด๊ธฐ (์กฐ๊ฑด ๋๋๊ธฐ ์ ์๋ ์๋จ)
$npx ts-node xrpl/TokenEscrow/escrowFinish.ts
#4) ์์คํฌ๋ก ์ทจ์ํ๊ธฐ (์กฐ๊ฑด ๋๋๊ธฐ ์ ์๋ ์๋จ)
$npx ts-node xrpl/TokenEscrow/escrowCancel.ts
Bash
๋ณต์ฌ
1. TokenEscrow๋?
TokenEscrow๋ XRPL์ ๊ธฐ์กด XRP ์ ์ฉ Escrow ๊ธฐ๋ฅ์ ํ์ฅํด,
IOU ํ ํฐ๊ณผ MPT(Multi-Purpose Token)๋ ์์คํฌ๋ก์ ๊ฑธ ์ ์๊ฒ ํ๋ ๊ธฐ๋ฅ์ด๋ค.
ํ ํฐ ํ์
๋ณ ํน์ง:
โข
IOU
โฆ
๋ฐํ์์ ๋ณด์ ์ ๊ฐ Trustline ํ์
โฆ
๋ฐํ์ ์ ์ฑ
: RequireAuth, Freeze, TransferRate
โฆ
Escrow ํ์ฉ ํ๋๊ทธ: lsfAllowTrustLineLocking : 17
โข
MPT
โฆ
Trustline ์์
โฆ
๋ฐํ์ ์ ์ฑ
: RequireAuth, Lock, TransferFee
โฆ
Escrow ํ์ฉ ํ๋๊ทธ: tfMPTCanEscrow (MPTokenIssuance ํธ๋์ญ์
์ ๋ช
์ํด์ผ Escrow ๊ฐ๋ฅ)
โฆ
์ ์ก ๊ฐ๋ฅ ํ๋๊ทธ: tfMPTCanTransfer (MPTokenIssuance ํธ๋์ญ์
์ ๋ช
์, ์์ผ๋ฉด ๋ฐํ์์๊ฒ๋ง ์ ์ก ๊ฐ๋ฅ)
2. ์ ํ์ํ๊ฐ?
โข
์์ฐ ์ ๊ธ ์ง์ ํ๋
โฆ
XRP๋ฟ ์๋๋ผ, ๋ฐํ ํ ํฐ(์คํ
์ด๋ธ์ฝ์ธยทํฌ์ธํธยท๊ถ๋ฆฌ์ฆ์ ๋ฑ)๋ ์์คํฌ๋ก ๊ฐ๋ฅ.
โข
์ ์ฐ ์์ธก์ฑ ๋ณด์ฅ
โฆ
Escrow ์์ฑ ์์ ์ TransferRate/TransferFee๋ฅผ ๊ณ ์ , ์ดํ ๋ณ๊ฒฝ์๋ ์ํฅ์ ๋ฐ์ง ์์.
โข
๋ฐํ์ ์ ์ฑ
์ค์
โฆ
RequireAuthยทFreezeยทLock ๋ฑ์ ์ ์ด๊ฐ Escrow์๋ ๋์ผํ๊ฒ ์ ์ฉ.
โข
๋ค์ํ ์๋๋ฆฌ์ค ์ง์
โฆ
๊ณ์ฝ๊ธ, ์ง๊ธ ๋ณด๋ฅ, ์กฐ๊ฑด๋ถ ์ง๊ธ, ๋ฒ ์คํ
(vesting) ๋ฑ์ IOU/MPT ์ฌ์ฉ ๊ฐ๋ฅ.
3. ์๋๋ฆฌ์ค: escrowCreate โ escrowFinish โ (์ต์ ) escrowCancel
Step 1. ์์คํฌ๋ก ์์ฑ (escrowCreate.ts)
โข
์ฃผ์ฒด: ์ก๊ธ์(Source) : User
โข
ํ๋: EscrowCreate ํธ๋์ญ์
์ ์ก
โข
Amount ํ์:
โฆ
XRP: drops ๋ฌธ์์ด
โฆ
IOU/MPT: CurrencyAmount ๊ฐ์ฒด
โข
ํ์ ์กฐ๊ฑด:
โฆ
IOU/MPT โ CancelAfter ํ์
โฆ
๋ฐํ์๊ฐ Source์ด๋ฉด ๋ถ๊ฐ
โฆ
๋ฐํ์ Escrow ํ์ฉ ํ๋๊ทธ ์ค์ ํ์
โฆ
Source๊ฐ RequireAuth ํ ํฐ์ด๋ฉด ์ฌ์ ์น์ธ ํ์
โฆ
Source ์์กยทTrustlineยทAuthorize ์ํ ์ ์
+์ฃผ์ํ ์ : IOU์ ๊ฒฝ์ฐ Escrow ์์ฑํ๊ธฐ ์ ์ Admin ๊ณ์ ์ 17๋ฒ ํ๋๊ทธ(asfAllowTrustLineLocking)๋ก accountSet์ ํด ์ค์ผ ํจ!
import { Client, Wallet, Transaction } from "xrpl"
import { encodeForSigning, encode } from "ripple-binary-codec"
import { sign as kpSign, deriveKeypair } from "ripple-keypairs"
import path from "path"
import dotenv from "dotenv"
dotenv.config({ path: path.join(__dirname, "..", ".env") })
// XRPL ๋ฆฌํ ์ํญ ์๊ฐ ๋ณํ
function Now() {
return Math.floor(Date.now() / 1000) - 946_684_800
}
export async function escrowCreateMPT() {
const client = new Client("wss://s.devnet.rippletest.net:51233")
await client.connect()
// ๐ ์ง๊ฐ ๋ก๋
const ADMIN_SEED = process.env.ADMIN_SEED!
const USER_SEED = process.env.USER_SEED!
const USER2_SEED = process.env.USER2_SEED!
const admin = Wallet.fromSeed(ADMIN_SEED) // MPT ๋ฐํ์(์ฐธ์กฐ์ฉ)
const user = Wallet.fromSeed(USER_SEED) // Escrow ์์ค
const user2 = Wallet.fromSeed(USER2_SEED) // Escrow ๋ชฉ์ ์ง
try {
// โ ๏ธ createIssuance ์คํ ๊ฒฐ๊ณผ์์ ๋ณต์ฌํ MPT IssuanceID(48hex)
const ISSUANCE_ID = "0049CE349E4215DD8AC6196A0A5027DF489AEC3B17BD6211"
// ๐ EscrowCreate ํธ๋์ญ์
(MPTAmount ์ฌ์ฉ)
const tx: Transaction = {
TransactionType: "EscrowCreate",
Account: user.address, // ์์ค(User)
Destination: user2.address, // ๋ชฉ์ ์ง(User2)
Amount: {
mpt_issuance_id: ISSUANCE_ID,
value: "50" // ์ ์ก ์๋
} as any, // XRPL SDK์์ MPTAmount ๋ฏธ์ง์ โ any ์ฒ๋ฆฌ
FinishAfter: Now() + 30, // ์ต์ ์๋ฃ ์๊ฐ (30์ด ํ)
CancelAfter: Now() + 120 // ์ทจ์ ๊ฐ๋ฅ ์๊ฐ (120์ด ํ)
}
// 1) autofill โ ๊ธฐ๋ณธ ํ๋ ์ฑ์ (Sequence, Fee ๋ฑ)
const prepared = await client.autofill(tx)
// 2) ์๋ช
๋์ ๊ฐ์ฒด์ SigningPubKey๋ฅผ ๋ฃ์
const toSign = {
...prepared,
SigningPubKey: user.publicKey,
}
// 3) seed ๊ธฐ๋ฐ keypair ์์ฑ
const { privateKey } = deriveKeypair(USER_SEED)
// 4) ํ๋ฆฌํฝ์ค ํฌํจ ๋ฐ์ดํฐ ์๋ช
const signingData = encodeForSigning(toSign as any)
const signature = kpSign(signingData, privateKey)
// 5) ์ต์ข
ํธ๋์ญ์
์ TxnSignature ์ถ๊ฐ ํ ์ธ์ฝ๋ฉ
const signedTx = { ...toSign, TxnSignature: signature }
const tx_blob = encode(signedTx)
// 6) ์ ์ถ & ๊ฒฐ๊ณผ ํ์ธ
const result = await client.submitAndWait(tx_blob)
console.log(JSON.stringify(result, null, 2))
console.log(`โ
EscrowCreate(MPT) ์ฑ๊ณต: Owner=${user.address}, OfferSequence=${prepared.Sequence}`)
return result
} finally {
await client.disconnect()
console.log("๐ ์ฐ๊ฒฐ ์ข
๋ฃ")
}
}
// ์ง์ ์คํ
if (require.main === module) {
escrowCreateMPT().catch(e => { console.error(e); process.exit(1) })
}
JSON
๋ณต์ฌ
โข
์๋ช
๋ฐฉ์ : xrpl.js๊ฐ ์์ง EscrowCreate์ Amount๋ฅผ โXRP ๋ฌธ์์ดโ๋ก๋ง ๊ฒ์ฆ/์ง๋ ฌํํ๋ ค๊ณ ํด์ IOU/MPT ๊ฐ์ฒด ๋ฃ์ผ๋ฉด ํด๋ผ์ด์ธํธ์์ ๋งํ Invalid signature๊ฐ ๋จ.
โข
๊ทธ๋์ raw ์๋ช
(encodeForSigning + ripple-keypairs.sign) ์ผ๋ก ๊ฒ์ฆ/์ง๋ ฌํ๋ฅผ ์ฐํํ๋ฉด ์๋ฒ๊ฐ ๊ทธ๋๋ก ๋ฐ์์ ์ ์ ์ฒ๋ฆฌ.
//๊ธฐ์กด ๋ฐฉ์
const prepared = await client.autofill(tx)
const signed = user.sign(prepared)
const result = await client.submitAndWait(signed.tx_blob)
TypeScript
๋ณต์ฌ
//TokenEscrow์์ ์ฌ์ฉํด์ผ ํ๋ ์๋ช
๋ฐฉ์
import { encodeForSigning, encode } from "ripple-binary-codec"
import { sign as kpSign, deriveKeypair } from "ripple-keypairs"
const prepared = await client.autofill(tx)
// 1) ์๋ช
๋์ ๊ฐ์ฒด์ SigningPubKey๋ฅผ "๋ฏธ๋ฆฌ" ๋ฃ๋๋ค
const toSign = {
...prepared,
SigningPubKey: user.publicKey, // ๋ณดํต 'ED...' 33๋ฐ์ดํธ(hex)
}
// 2) seed๋ก keypair ํ์ (โ
Wallet.privateKey ๋์ , seedโderive ์ฌ์ฉ)
const { privateKey, publicKey } = deriveKeypair(USER_SEED)
// 3) ์๋ช
(ํ๋ฆฌํฝ์ค ์๋ฅด์ง ๋ง๊ณ ๊ทธ๋๋ก ์ ๋ฌ)
const signingData = encodeForSigning(toSign as any)
const signature = kpSign(signingData, privateKey)
// 4) ์ต์ข
์ธ์ฝ๋ฉ & ์ ์ถ
const signedTx = { ...toSign, TxnSignature: signature }
const tx_blob = encode(signedTx)
const result = await client.submitAndWait(tx_blob)
TypeScript
๋ณต์ฌ
โข
์ฝ์ ์ถ๋ ฅ ์์
EscrowCreate(MPT UserโUser2) -> Owner=<User์ฃผ์>, OfferSequence=123456
Bash
๋ณต์ฌ
Step 2. ์์คํฌ๋ก ํด์ (escrowFinish.ts)
โข
์ฃผ์ฒด: ๋๊ตฌ๋(์กฐ๊ฑด ์ถฉ์กฑ ์)
โข
ํ๋: EscrowFinish ํธ๋์ญ์
์ ์ก
โข
ํ์ ์กฐ๊ฑด:
โฆ
FinishAfter ์๊ฐ ๊ฒฝ๊ณผ
โฆ
Destination์ด RequireAuth์ด๋ฉด ์ฌ์ ์น์ธ ํ์
โฆ
IOU โ Trustline ์กด์ฌ / MPT โ ํ ํฐ ๋ณด์ ๋๋ ์๋ ์์ฑ ๊ฐ๋ฅ
โฆ
Freeze/Lock ์ํ ์ฒดํฌ
โข
์ํ ๋ณํ:
โฆ
์์ก ์ด๋: ๋ฐํ์/์์ ์ ์กฐํฉ์ ๋ฐ๋ผ EscrowedAmount, OutstandingAmount ์ฒ๋ฆฌ ๊ท์น ๋ค๋ฆ
โฆ
Escrow ๊ฐ์ฒด ์ญ์
import { Client, Wallet, Transaction } from "xrpl"
import path from "path"
import dotenv from "dotenv"
dotenv.config({ path: path.join(__dirname, "..", ".env") })
export async function escrowFinish() {
const client = new Client("wss://s.devnet.rippletest.net:51233")
await client.connect()
// ๐ ์ง๊ฐ ๋ก๋
const USER_SEED = process.env.USER_SEED! // Escrow ์์ค(User)
const USER2_SEED = process.env.USER2_SEED! // ๋ชฉ์ ์ง(User2, Finisher)
const ownerWallet = Wallet.fromSeed(USER_SEED) // Escrow ์์ค
const finisherWallet = Wallet.fromSeed(USER2_SEED) // Finish ์คํ์
try {
// โ ๏ธ EscrowCreate ์คํ ํ ์ฝ์์ ์ถ๋ ฅ๋ Sequence ๊ฐ์ผ๋ก ๊ต์ฒด
const OFFER_SEQUENCE = 4836805
// ๐ EscrowFinish ํธ๋์ญ์
const tx: Transaction = {
TransactionType: "EscrowFinish",
Account: finisherWallet.address, // ์คํ์(๋ณดํต ์ํ์ User2)
Owner: ownerWallet.address, // Escrow ์ ์์ค ์ฃผ์
OfferSequence: OFFER_SEQUENCE // EscrowCreate ์ํ์ค ๋ฒํธ
}
// ์๋ ํ๋ ์ฑ์ โ ์๋ช
โ ์ ์ถ
const prepared = await client.autofill(tx)
const signed = finisherWallet.sign(prepared)
const result = await client.submitAndWait(signed.tx_blob)
console.log(JSON.stringify(result, null, 2))
console.log(`โ
EscrowFinish ์ฑ๊ณต: Owner=${ownerWallet.address}, OfferSequence=${OFFER_SEQUENCE}`)
return result
} finally {
await client.disconnect()
console.log("๐ ์ฐ๊ฒฐ ์ข
๋ฃ")
}
}
// ์ง์ ์คํ
if (require.main === module) {
escrowFinish().catch(e => { console.error(e); process.exit(1) })
}
TypeScript
๋ณต์ฌ
Step 3. ์์คํฌ๋ก ์ทจ์ (escrowCancel.ts)
โข
์ฃผ์ฒด: ๋๊ตฌ๋(์กฐ๊ฑด ์ถฉ์กฑ ์)
โข
ํ๋: EscrowCancel ํธ๋์ญ์
์ ์ก
โข
ํ์ ์กฐ๊ฑด:
โฆ
CancelAfter ๊ฒฝ๊ณผ
โฆ
Source AuthorizeยทTrustline/MPT ๋ณด์ ์ํ ์ ์
โฆ
Freeze/Lock ์ํ๋ ์ทจ์ ๊ฐ๋ฅ
โข
์ํ ๋ณํ:
โฆ
์์ก ๋ฐํ
โฆ
Escrow ๊ฐ์ฒด ์ญ์
import { Client, Wallet, Transaction } from "xrpl"
import path from "path"
import dotenv from "dotenv"
dotenv.config({ path: path.join(__dirname, "..", ".env") })
export async function escrowCancel() {
const client = new Client("wss://s.devnet.rippletest.net:51233")
await client.connect()
// ๐ ํ๊ฒฝ๋ณ์์์ ์๋ ๋ก๋
const USER_SEED = process.env.USER_SEED! // Escrow ์์ค(User)
const USER2_SEED = process.env.USER2_SEED! // Cancel ์คํ์(User2)
// ๐ชช Escrow ์์ค(Owner) / Cancel ์คํ์ ๊ณ์ ์์ฑ
const ownerWallet = Wallet.fromSeed(USER_SEED)
const cancellerWallet = Wallet.fromSeed(USER2_SEED)
try {
/**
* โ ๏ธ EscrowCreate ์คํ ํ ๋ฐํ๋ "Sequence"๋ฅผ OFFER_SEQUENCE์ ๋ฃ์ด์ผ ํจ
* - EscrowCancel์ Escrow๋ฅผ ์์ฑํ ์๋ ํธ๋์ญ์
(Sequence)์ Owner ์ฃผ์๋ฅผ ์ง์ ํด์ผ ์ ํจ
*/
const OFFER_SEQUENCE = 4836805
const tx: Transaction = {
TransactionType: "EscrowCancel",
Account: cancellerWallet.address, // Cancel ์คํ์(= ํธ๋์ญ์
์๋ช
์ฃผ์ฒด)
Owner: ownerWallet.address, // Escrow ์์ค(์๋ Escrow ๋ง๋ ๊ณ์ )
OfferSequence: OFFER_SEQUENCE // EscrowCreate์ Sequence
}
// ํธ๋์ญ์
์ค๋น โ ์๋ช
โ ์ ์ถ
const prepared = await client.autofill(tx)
const signed = cancellerWallet.sign(prepared)
const result = await client.submitAndWait(signed.tx_blob)
console.log(JSON.stringify(result, null, 2))
return result
} finally {
await client.disconnect()
console.log("๐ ์ฐ๊ฒฐ ์ข
๋ฃ")
}
}
// ์ง์ ์คํ
if (require.main === module) {
escrowCancel().catch(e => { console.error(e); process.exit(1) })
}
TypeScript
๋ณต์ฌ

