跳到主要内容

运行通知网关

一笔交易是有时限的:预订会过期、付款会被标记为已发送、履约托管会释放。没有一直盯着应用的人需要被告知。通知网关做的正是这件事——你注册自己能够投递的渠道,投递钱包已订阅的内容,并回报每一次投递尝试的结果。

里程碑 6 / 6 · 运行基础设施

开始之前

  • 一个可用于实际投递的账号——SMTP 或邮件 API、短信服务商、Telegram 机器人、推送服务等,协议本身不提供任何一种
  • 如果你注册的是 Webhook 渠道,还需要一个公网可达的端点,供应用向你发送投递请求
  • 可访问某个 OpenFiat 节点的 JSON-RPC 端点,可以是你自己的节点,也可以是他人的
  • 一份持久化的 wallet.json 作为网关身份——投递回执用它签名,你的运营信誉也绑定在它上面

操作步骤

  1. 01

    创建网关身份

    网关由一对 Ed25519 密钥标识,采用 Solana CLI 的 wallet.json 格式,与其他各类提供方相同。生成一次后请妥善保存:你发布的每一条投递回执都用它签名,§18 中的提供方信誉也正是由这些回执推导而来。

    shell
    solana-keygen new --outfile /etc/openfiat/gateway-wallet.json
    chmod 600 /etc/openfiat/gateway-wallet.json
  2. 02

    注册你所支持的投递渠道

    通知网关没有专属的注册事件——你通过通用的服务注册表(OFS-1500)注册,服务类型为 Notifications,并指明一个渠道:Email、Telegram、Sms、Push 或 Webhook。`endpoints` 是应用实际联系你的地址,因此对 Webhook 网关而言就是你对外提供的 URL。注册无需任何人批准;你发布它,网络会通过 gossip 传播。

    Rust
    use openfiat_registry::Registration;
    use openfiat_sdk::{Client, ClientConfig};
    use openfiat_types::{NotificationChannel, ServiceId, ServiceType, Timestamp};
    
    let client = Client::new(ClientConfig {
        endpoint: "http://localhost:7080".to_string(),
        ..ClientConfig::default()
    });
    
    let registration = Registration {
        service_id: ServiceId::new("my-notification-gateway-1"),
        service_type: ServiceType::Notifications(NotificationChannel::Webhook),
        provider: provider_peer_id.clone(),
        provider_public_key: provider.public_key(),
        // Where applications send you delivery requests.
        endpoints: vec!["https://notify.example.com/openfiat".to_string()],
        supported_ofs: vec![1500, 6000],
        region: None,
        capabilities: vec!["Webhook".to_string()],
        pricing: None,
        timestamp: Timestamp::now(),
    };
    client.send_provider_register(registration, &provider).await?;
    TypeScript
    import { Client, providers, toBytes, type Registration } from "@openfiat/sdk";
    
    const client = new Client({ endpoint: "http://localhost:7080", timeoutMs: 30_000 });
    
    const registration: Registration = {
      service_id: "my-notification-gateway-1",
      service_type: { Notifications: "Webhook" },
      provider: toBytes(providerId),
      provider_public_key: toBytes(provider.publicKey),
      endpoints: ["https://notify.example.com/openfiat"],
      supported_ofs: [1500, 6000],
      region: null,
      capabilities: ["Webhook"],
      pricing: null,
      timestamp: Date.now(),
    };
    await providers.sendProviderRegister(client, registration, provider);
  3. 03

    只投递钱包主动订阅的内容

    订阅属于钱包,而不属于你——钱包会发布一条经签名的记录,列出它希望接收的类别,最新一条会完全覆盖此前的记录。共有五个类别(Trading、Marketplace、Disputes、Governance、Infrastructure),下辖十六种触发事件;因此一个只启用了 Trading 的钱包,会收到从 ReservationCreated 到 TradeCompleted 的通知,而不会收到任何 Governance 相关内容。投递前请先读取订阅信息,不要凭假设行事。

    Rust
    // What has this wallet opted into?
    let subscription = client.get_subscription(&recipient_wallet).await?;
    
    // `Subscription::wants` maps a trigger to its category for you.
    if let Some(subscription) = subscription {
        if subscription.wants(NotificationTrigger::TradeCompleted) {
            // ... deliver
        }
    }
  4. 04

    编写渠道适配器

    这一部分需要你自己实现。`openfiat-notifications` 只定义了插件接口,并未附带任何渠道的适配器——邮件、短信、Telegram 等都需要你在该 trait 之上自行实现。有两条规则约束着你的实现:网关从不创建协议事件,只负责投递已在上游完成验证的事件(§17);网关只会收到投递所必需的信息——目标地址与已渲染的内容,绝不包括产生该通知的交易详情、账户余额或争议证据(§19)。

    Rust
    use openfiat_notifications::{NotificationPayload, NotificationProvider};
    use openfiat_notifications::NotificationError;
    use openfiat_types::NotificationChannel;
    
    struct EmailGateway { /* your SMTP or API client */ }
    
    impl NotificationProvider for EmailGateway {
        fn channel(&self) -> NotificationChannel {
            NotificationChannel::Email
        }
    
        fn send(&self, payload: &NotificationPayload) -> Result<(), NotificationError> {
            // `destination` is the address; `subject` and `body` are already
            // rendered. Nothing else about the trade is available here, by design.
            self.deliver(&payload.destination, &payload.subject, &payload.body)
        }
    }
  5. 05

    回报投递结果

    每一次投递尝试都要提交一条经签名的投递回执。状态取值为 Queued、Sent、Delivered、Read、Failed、Retried 或 Expired 之一,每种状态对应各自的 gossip 事件——如实上报 Failed,才能让应用绕开出问题的网关,也才能让 §18 的提供方信誉保持其意义。回执以通知 ID 为键,最新一条生效,因此你可以先上报 Sent,随后再为同一个 ID 上报 Delivered。

    Rust
    use openfiat_notifications::events::DeliveryReport;
    use openfiat_notifications::{DeliveryStatus, NotificationId, NotificationTrigger};
    
    let report = DeliveryReport {
        notification_id: NotificationId::new("notification-1"),
        service_id: service_id.clone(),
        provider: provider_peer_id.clone(),
        provider_public_key: provider.public_key(),
        recipient_wallet: recipient_wallet.clone(),
        trigger: NotificationTrigger::TradeCompleted,
        status: DeliveryStatus::Delivered,
        timestamp: Timestamp::now(),
    };
    client.send_delivery_report(report, &provider).await?;
    TypeScript
    import { notifications, type DeliveryReport } from "@openfiat/sdk";
    
    const report: DeliveryReport = {
      notification_id: "notification-1",
      service_id: serviceId,
      provider: toBytes(providerId),
      provider_public_key: toBytes(provider.publicKey),
      recipient_wallet: toBytes(walletId),
      trigger: "TradeCompleted",
      status: "Delivered",
      timestamp: Date.now(),
    };
    await notifications.sendDeliveryReport(client, report, provider);
  6. 06

    确认回执已被记录

    按钱包读取回执,是确认你的上报已被接受并完成传播的最快方式。应用会依据信誉、投递延迟、地理邻近度、支持渠道与成本来选择提供方——协议刻意不规定具体的路由算法——因此一份干净、如实的回执记录,就是你全部对外可见的履约记录。

    TypeScript
    const receipts = await notifications.getDeliveryReceiptsByWallet(client, walletId);
    console.log(`delivery receipts for this wallet: ${receipts.length}`);
  7. 07

    质押能带来什么,又不能带来什么

    最低质押额是真实生效的:在已部署的 devnet 配置中,质押程序对 NotificationProvider 角色强制要求 5,000 OPEN,并会拒绝任何会使你低于该门槛的质押或解押操作。但它并不会据此限制其他环节——注册表与通知相关的 crate 都不会读取你的质押数据,因此无论是否质押,注册与投递都照常可用。收益部分则完全没有实现:协议中不存在任何通知费,也就无从分成。联系方式验证同理——身份 crate 定义了声明及其 Verified 状态,但协议层并没有 OTP 流程,实际验证由钱包应用在协议之外完成,再发布结果。