跳到主要内容

完成你的第一笔交易

一笔交易包含四个动作:你预订商户挂单中的一部分,稳定币进入链上履约托管,你通过平时使用的银行或移动支付渠道付款,商户确认后履约托管即释放给你。中间没有任何人能拿走这笔资金,双方也都不需要信任对方的善意。

里程碑 1 / 2 · 挂单与吃单

开始之前

  • 一个你自己掌控的 Solana 钱包,其中包含履约托管将要打款到的代币账户
  • 当地货币以及商户接受的付款方式;如果你是卖方,则需要相应的稳定币
  • 可访问某个 OpenFiat 节点的 JSON-RPC 端点——可以是你自己的节点,也可以是你信任其代为转发签名事件的他人节点
  • 一个能够签署协议事件的客户端:Rust SDK 覆盖下面的每一个步骤;TypeScript SDK 覆盖浏览挂单与预订,其余步骤需要通过原始 JSON-RPC 调用完成
  • 请注意:参考网页应用的交易界面目前仍使用模拟数据,因此下面这些步骤才是当前真实可行的路径,而不是在浏览器里点几下就能完成

操作步骤

  1. 01

    先弄清楚究竟是什么在保护你

    这里涉及两个彼此独立的层,把它们混为一谈是唯一可能让你蒙受损失的错误。点对点层负责协调交易:它记录你预订了某个挂单的一部分,并把该预订标记为 EscrowLocked。这个状态只是各节点之间的账目记录——它本身并不能证明 Solana 上真的存在什么。真正锁住稳定币的,是由卖方钱包在链上创建、并以你的预订 ID 为索引的交易履约托管金库。在你汇出任何法币之前,请先确认该金库确实存在且已注资。

  2. 02

    寻找一个挂单

    向任意节点查询它已同步的挂单。该方法不接受任何筛选参数,会返回整个挂单簿,因此需要你自行筛选:用 fiat_currency 匹配你的货币,用 direction 匹配你想要的交易方向,用 payment_methods 匹配你实际能使用的支付渠道,并确认你的金额落在 min_trade 与 max_trade 之间、且背后有足够的 available_liquidity。只有 status 为 Active 的挂单才能被预订。

    shell
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getAdvertisements","params":{}}'
    
    # Returns every synchronized ad. Filter client-side on fiat_currency,
    # direction, payment_methods, min_trade/max_trade and available_liquidity.
  3. 03

    预订你想要的金额

    预订是一条经过签名的请求,其中指明挂单与金额。节点会依据挂单的限额与剩余流动性进行校验,要么将其存储为 EscrowLocked,要么直接拒绝——校验失败的请求不会留下任何记录。从这一刻起你有 30 分钟:若届时交易仍未推进,该预订会在每个节点上自动过期,流动性也会退回挂单。在预订仍处于 EscrowLocked 状态期间,你也可以自行取消。

    Rust
    use openfiat_reservations::events::ReservationRequest;
    use openfiat_reservations::ReservationId;
    use openfiat_types::{Amount, Timestamp};
    
    let request = ReservationRequest {
        id: ReservationId::new("res-1"),
        advertisement_id: ad.id.clone(),
        requester: my_peer_id.clone(),
        requester_public_key: keypair.public_key(),
        // Base units and decimals must match the advertised asset.
        amount: Amount::new(2_000_000, 6),
        timestamp: Timestamp::now(),
    };
    let reservation_id = client.send_reservation_request(request, &keypair).await?;
    TypeScript
    import { reservations, toBytes, type ReservationRequest } from "@openfiat/sdk";
    
    const request: ReservationRequest = {
      id: "res-1",
      advertisement_id: ad.id,
      requester: toBytes(peerId),
      requester_public_key: toBytes(keypair.publicKey),
      amount: { base_units: 2_000_000, decimals: 6 },
      timestamp: Date.now(),
    };
    const reservationId = await reservations.sendReservationRequest(client, request, keypair);
  4. 04

    付款前先确认履约托管

    这一步正是保护你的关键,也是任何软件都无法代你完成的一步。卖方会创建交易履约托管金库,并从其流动性金库中为之注资——这是履约托管程序上的两条指令,均由其钱包签名,而金库地址是由你的预订 ID 推导出来的。请在 Solana 上读取该账户,确认其中确实持有你预期的金额。如果账户不存在或余额为零,就不要汇出法币:取消预订,或者干脆让它自然过期。

    shell
    # The vault is a PDA of the escrow program, derived from your reservation id.
    # Devnet escrow program: HaPpM1QYM3dKp3sX7zhEdft9hB6ncu6xfALAbkyQChQP
    #
    # The node's own read-only join over reservation + settlement:
    curl -s -X POST http://localhost:7080/rpc -H 'content-type: application/json' \
      -d '{"jsonrpc":"2.0","id":1,"method":"getTrade","params":{"id":"res-1"}}'
  5. 05

    通过约定渠道付款,然后声明已付款

    严格按照挂单指定的方式汇出当地货币——相同的付款方式、相同的收款账户,以及商户要求的任何附言。尽可能使用你本人名下的账户付款,因为一旦出现问题,这正是仲裁者会审阅的证据。随后向网络声明你已付款,并附上转账凭证号,以便商户查找。从履约托管锁定起你有 30 分钟时间声明付款,之后商户有 30 分钟时间批准或拒绝。

    Rust
    use openfiat_settlement::events::PaymentSubmitted;
    
    let payment = PaymentSubmitted {
        settlement_id: settlement_id.clone(),
        buyer: my_peer_id.clone(),
        // Whatever lets the merchant find the transfer on their side.
        payment_reference: Some("MPESA-QK12345678".to_string()),
        timestamp: Timestamp::now(),
    };
    client.send_payment_submitted(payment, &keypair).await?;
    TypeScript
    // The TypeScript SDK has no typed settlement helper yet, so sign and
    // send the event through the client's own signed-call path.
    const action = {
      settlement_id: settlementId,
      buyer: toBytes(peerId),
      payment_reference: "MPESA-QK12345678",
      timestamp: Date.now(),
    };
    const bytes = new TextEncoder().encode(JSON.stringify(action));
    const signature = await sign(keypair, bytes);
    await client.sendSigned("sendPaymentSubmitted", { action, signature: toBytes(signature) });
  6. 06

    商户批准,履约托管随即释放

    商户看到款项到账后即批准结算,链上金库随之转为已批准状态。从这一刻起,释放是无需许可的:商户从来不会亲手把稳定币交给你,批准之后也无法再扣留——你自己、某个转发节点,或任何其他人都可以触发 release_escrow,程序会向你的代币账户打款并扣除协议费用。持续查询 getTrade,直到结算状态变为 Completed;只有在释放交易于链上确认之后,这一状态才会被记录。

  7. 07

    如果出了问题,发起争议

    只有该结算的买方或卖方才能发起争议——节点会拒绝其他任何人。请如实说明发生了什么,并保留好你的付款凭证。随后会有三名仲裁者质押自己的 OPEN 来接手该案件,以「先承诺后揭示」的方式投票,因此没有人能跟风他人,裁决结果最终在链上针对同一个履约托管金库执行。有一个值得了解的缺口:相关 crate 支持双方协商达成和解,但该路径尚未通过 JSON-RPC 暴露,因此目前发起争议就意味着走仲裁流程。

    Rust
    use openfiat_disputes::events::DisputeOpen;
    use openfiat_disputes::DisputeId;
    
    let open = DisputeOpen {
        id: DisputeId::new("dsp-1"),
        settlement_id: settlement_id.clone(),
        opener: my_peer_id.clone(),
        opener_public_key: keypair.public_key(),
        reason: "Paid in full on 12 March, reference MPESA-QK12345678; not released.".to_string(),
        timestamp: Timestamp::now(),
    };
    client.send_dispute_open(open, &keypair).await?;