成为商户
商户是市场的供给方:你把稳定币存入由履约托管程序控制的资金库,公布价格与你接受的支付方式,并在法币到账后释放资金。你的币只会通过程序自身的指令流转——无论是交易对手还是节点,都无法拿走它们。
里程碑 2 / 2 · 挂单与吃单
开始之前
- 一个 Solana 钱包,以及 devnet 上足够支付交易手续费与账户租金的 SOL
- 用于质押的 OPEN。devnet 上 OPEN 铸币权限已被永久移除,任何人都无法再增发——在 devnet 上只能来自预售份额或某个已持有者
- 你打算出售的稳定币,存放在由同一钱包持有的 Token-2022 账户中
- 可访问某个 OpenFiat 节点的 JSON-RPC 端点,用于发布挂单本身
- 发布与管理挂单需要一个使用 OpenFiat SDK 的脚本;目前只有质押环节提供浏览器操作界面
操作步骤
- 01
以商户身份质押 OPEN
质押的作用是让刷单变得昂贵,并让协议在出现不当行为时有东西可以罚没。在链上,质押程序对除仲裁者以外的所有角色只设一个统一的最低额,部署时采用 OFS-4100 §4 建议的 1,000 OPEN——参考应用质押页面上显示的 5,000 是模拟展示数据,并非实际部署的配置。如果你需要真正被强制执行的数值,请直接读取 StakingConfig 账户。质押是唯一提供真实浏览器操作流程的环节:应用的质押页面构建并提交的正是下面这两条指令。
TypeScriptimport { PublicKey, Transaction } from "@solana/web3.js"; import { getAssociatedTokenAddressSync, TOKEN_2022_PROGRAM_ID } from "@solana/spl-token"; import { onchain } from "@openfiat/sdk"; // OPEN has 9 decimals (OFS-4100 §1). const amount = 1_000n * 1_000_000_000n; const from = getAssociatedTokenAddressSync(openMint, owner, false, TOKEN_2022_PROGRAM_ID); // The stake account is per (wallet, role): one wallet can hold separate // merchant and node-operator stakes, each its own PDA. const instructions = [ onchain.staking.initializeStakeAccountIx(owner, onchain.Role.Merchant), onchain.staking.stakeIx(owner, openMint, onchain.Role.Merchant, from, amount), ]; const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash(); const tx = new Transaction({ feePayer: owner, blockhash, lastValidBlockHeight }).add(...instructions); await wallet.signAndSendTransaction(tx); - 02
开设流动性资金库并注资
资金库是按(商户,稳定币)一一对应的账户,其代币余额只能由履约托管程序自身的指令来变动。它分别记录 total、reserved、available、pending_settlement 与 settled,因此一笔预订可以先占用你挂单对应的库存,而代币尚未真正转出——只有在交易履约托管被实际注资时才会转移。请注意,资金库与质押在链上是相互独立的:存入资金时并不会检查你是否已质押,所以如果你打算发布挂单,请先完成质押。
TypeScriptimport { onchain } from "@openfiat/sdk"; // One vault per stablecoin you sell. Creating it also creates the // program-owned token account that actually holds the balance. const create = onchain.escrow.createLiquidityVaultIx(merchant, usdcMint); // `from` is your own Token-2022 account for that mint. const deposit = onchain.escrow.depositLiquidityIx( merchant, usdcMint, from, 200_000n * 1_000_000n, // 200,000 USDC, 6 decimals ); - 03
发布挂单
挂单是一条链下、通过 gossip 传播的记录:由你签名后提交给任意节点,网络会负责传播。固定价格目前可以正常使用。浮动价格可以填写预言机提供方与以基点表示的溢价,但这条记录只承载该项配置——根据实时预言机中间价进行价格解析尚未实现,因此除非你是有意测试这条尚未完成的路径,否则请使用固定价格。
TypeScriptimport { Client, advertisements, peerIdFromPublicKey, toBytes, type AdvertisementCreate } from "@openfiat/sdk"; const client = new Client({ endpoint: "http://localhost:7080", timeoutMs: 30_000 }); const create: AdvertisementCreate = { id: "my-usdt-kes-sell-1", merchant: toBytes(peerIdFromPublicKey(keypair.publicKey)), merchant_public_key: toBytes(keypair.publicKey), asset: "USDT", direction: "Sell", fiat_currency: "KES", // Amounts are { base_units, decimals } — 10.00 to 500.00 KES-priced trades. min_trade: { base_units: 1_000, decimals: 2 }, max_trade: { base_units: 50_000, decimals: 2 }, initial_liquidity: { base_units: 200_000, decimals: 2 }, pricing: { Fixed: { price: { base_units: 12_950, decimals: 2 } } }, payment_methods: ["M-Pesa"], timestamp: Date.now(), }; const adId = await advertisements.sendAdvertisementCreate(client, create, keypair);Verify it propagatedcurl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getAdvertisements","params":{}}' - 04
改价与暂停——目前尚未打通
在基于此开发之前请先了解这一点。advertisements crate 中定义了带签名的 AdvertisementDisable 与 AdvertisementPriceUpdate 事件,记录本身也具备 Active、Disabled、Vacation 与 Deleted 四种状态——但目前没有任何 JSON-RPC 方法可以提交这两类事件。sendAdvertisementCreate 是节点当前唯一接受的挂单变更操作,因此实际上挂单只能发布、无法修改。可用流动性是个例外:它由预订与结算活动自动调整,而不需要你为每笔交易重新签名。
- 05
确认法币到账后再批准
当买方标记已付款时,请核对你自己的银行或移动支付账户——绝不要仅凭对方的说法,也不要凭截图。批准发生在两个地方:一是推动结算自身状态机的链下签名事件,二是链上的 approve_settlement,它会将履约托管的 approved 标志置为真。批准本身并不转移资金;release_escrow 是唯一会转移资金的指令,并会在同一步骤中计算并分配手续费。
Off-chain — the settlement state machinecurl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \ -d '{"jsonrpc":"2.0","id":1,"method":"getSettlements","params":{}}' # Then submit a signed SettlementApproved for the one you have verified — # see the SDK's send_settlement_approved / sendSettlementApproved.On-chain — approve, then releaseimport { onchain } from "@openfiat/sdk"; // Only valid while the escrow is AwaitingFiatSettlement, and only from // the seller's own wallet — both are enforced by the program. const approve = onchain.escrow.approveSettlementIx(merchant, reservationId); // Permissionless once approved: it pays the buyer and splits the // settlement fee across the four treasuries in FeeConfig. const release = onchain.escrow.releaseEscrowIx(usdcMint, merchant, reservationId, { buyerTokenAccount, devTreasury, ecosystemTreasury, infraTreasury, emergencyReserve, }); - 06
取回流动性
提取资金只能动用资金库中 available 的余额,因此任何已被未完成交易占用的部分都会保留在库中,直到该交易完成结算、被取消或过期。解除质押是另一回事,而且有意设计得更慢:先发起申请,等待解绑期结束,再提取。有效质押在你发起申请的那一刻就会减少,而不是等到计时结束——因此一旦开始解绑,你会立即失去相应资格。
TypeScriptimport { onchain } from "@openfiat/sdk"; const pullOut = onchain.escrow.withdrawLiquidityIx(merchant, usdcMint, to, 50_000n * 1_000_000n); // Unbonding: request, wait out StakingConfig.unbonding_period_secs, withdraw. const request = onchain.staking.requestUnstakeIx(owner, onchain.Role.Merchant, 1_000n * 1_000_000_000n); const collect = onchain.staking.withdrawUnstakedIx(owner, openMint, onchain.Role.Merchant, to);