运行通知网关
一笔交易是有时限的:预订会过期、付款会被标记为已发送、履约托管会释放。没有一直盯着应用的人需要被告知。通知网关做的正是这件事——你注册自己能够投递的渠道,投递钱包已订阅的内容,并回报每一次投递尝试的结果。
里程碑 6 / 6 · 运行基础设施
开始之前
- 一个可用于实际投递的账号——SMTP 或邮件 API、短信服务商、Telegram 机器人、推送服务等,协议本身不提供任何一种
- 如果你注册的是 Webhook 渠道,还需要一个公网可达的端点,供应用向你发送投递请求
- 可访问某个 OpenFiat 节点的 JSON-RPC 端点,可以是你自己的节点,也可以是他人的
- 一份持久化的 wallet.json 作为网关身份——投递回执用它签名,你的运营信誉也绑定在它上面
操作步骤
- 01
创建网关身份
网关由一对 Ed25519 密钥标识,采用 Solana CLI 的 wallet.json 格式,与其他各类提供方相同。生成一次后请妥善保存:你发布的每一条投递回执都用它签名,§18 中的提供方信誉也正是由这些回执推导而来。
shellsolana-keygen new --outfile /etc/openfiat/gateway-wallet.json chmod 600 /etc/openfiat/gateway-wallet.json - 02
注册你所支持的投递渠道
通知网关没有专属的注册事件——你通过通用的服务注册表(OFS-1500)注册,服务类型为 Notifications,并指明一个渠道:Email、Telegram、Sms、Push 或 Webhook。`endpoints` 是应用实际联系你的地址,因此对 Webhook 网关而言就是你对外提供的 URL。注册无需任何人批准;你发布它,网络会通过 gossip 传播。
Rustuse 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?;TypeScriptimport { 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); - 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 } } - 04
编写渠道适配器
这一部分需要你自己实现。`openfiat-notifications` 只定义了插件接口,并未附带任何渠道的适配器——邮件、短信、Telegram 等都需要你在该 trait 之上自行实现。有两条规则约束着你的实现:网关从不创建协议事件,只负责投递已在上游完成验证的事件(§17);网关只会收到投递所必需的信息——目标地址与已渲染的内容,绝不包括产生该通知的交易详情、账户余额或争议证据(§19)。
Rustuse 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) } } - 05
回报投递结果
每一次投递尝试都要提交一条经签名的投递回执。状态取值为 Queued、Sent、Delivered、Read、Failed、Retried 或 Expired 之一,每种状态对应各自的 gossip 事件——如实上报 Failed,才能让应用绕开出问题的网关,也才能让 §18 的提供方信誉保持其意义。回执以通知 ID 为键,最新一条生效,因此你可以先上报 Sent,随后再为同一个 ID 上报 Delivered。
Rustuse 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?;TypeScriptimport { 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); - 06
确认回执已被记录
按钱包读取回执,是确认你的上报已被接受并完成传播的最快方式。应用会依据信誉、投递延迟、地理邻近度、支持渠道与成本来选择提供方——协议刻意不规定具体的路由算法——因此一份干净、如实的回执记录,就是你全部对外可见的履约记录。
TypeScriptconst receipts = await notifications.getDeliveryReceiptsByWallet(client, walletId); console.log(`delivery receipts for this wallet: ${receipts.length}`); - 07
质押能带来什么,又不能带来什么
最低质押额是真实生效的:在已部署的 devnet 配置中,质押程序对 NotificationProvider 角色强制要求 5,000 OPEN,并会拒绝任何会使你低于该门槛的质押或解押操作。但它并不会据此限制其他环节——注册表与通知相关的 crate 都不会读取你的质押数据,因此无论是否质押,注册与投递都照常可用。收益部分则完全没有实现:协议中不存在任何通知费,也就无从分成。联系方式验证同理——身份 crate 定义了声明及其 Verified 状态,但协议层并没有 OTP 流程,实际验证由钱包应用在协议之外完成,再发布结果。