支付订阅
订阅计划允许商家发布用户可以接受的计费条款。用户订阅后,商家或经授权的拉取方可以在每个计费周期内收取最高不超过计划金额的款项。
本指南以构建模块的形式展示完整流程。商家创建计划,订阅者接受计划,然后商家或拉取方从生成的订阅 PDA 中收取付款。
安装
pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token
创建计划
商家拥有该计划。计划 PDA 由商家地址和 planId 派生而来。
import { address, createClient } from '@solana/kit';import { solanaLocalRpc } from '@solana/kit-plugin-rpc';import { signer } from '@solana/kit-plugin-signer';import { findPlanPda, subscriptionsProgram } from '@solana/subscriptions';const merchantClient = createClient().use(signer(merchantSigner)).use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' })).use(subscriptionsProgram());const planId = 1n;const tokenMint = address('TOKEN_MINT_ADDRESS_HERE');const amount = 5_000_000n;const periodHours = 720n;const metadataUri = 'https://example.com/plan.json';const destinations = [merchantSigner.address];const pullers = [address('PULLER_WALLET_ADDRESS_HERE')];await merchantClient.subscriptions.instructions.createPlan({planId,mint: tokenMint,amount,periodHours,endTs: 0n,destinations,pullers,metadataUri,}).sendTransaction();const [planPda] = await findPlanPda({owner: merchantSigner.address,planId,});
更新计划
商家可以在创建后更新可变的计划字段。现有订阅者保留他们接受的条款,而新订阅者则接受当前的计划条款。
以下规则适用:
- 有限的
endTs只能缩短,不能延长或清除 (PlanEndTsCannotExtend)。 - 在
Sunset计划中,您可以移除拉取方(新集合必须是当前集合的子集)以撤销已泄露的拉取方;状态、endTs和元数据将保持冻结。 - 只要
endTs未更改,在计划最终计费周期内均可进行编辑。
import { PlanStatus } from '@solana/subscriptions';const updatedMetadataUri = 'https://example.com/updated-plan.json';const updatedPullers = [address('NEW_PULLER_WALLET_ADDRESS_HERE')];await merchantClient.subscriptions.instructions.updatePlan({owner: merchantSigner,planPda,status: PlanStatus.Active,endTs: 0n,pullers: updatedPullers,metadataUri: updatedMetadataUri,}).sendTransaction();
订阅
订阅者接受当前计划条款。订阅 PDA 由计划 PDA 和订阅者地址派生而来。
import { createClient } from '@solana/kit';import { solanaLocalRpc } from '@solana/kit-plugin-rpc';import { signer } from '@solana/kit-plugin-signer';import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';import {fetchMaybeSubscriptionAuthority,findSubscriptionAuthorityPda,findSubscriptionDelegationPda,subscriptionsProgram,} from '@solana/subscriptions';const subscriberClient = createClient().use(signer(subscriberSigner)).use(solanaLocalRpc({ rpcUrl: 'http://127.0.0.1:8899' })).use(subscriptionsProgram());const [subscriberAta] = await findAssociatedTokenPda({mint: tokenMint,owner: subscriberSigner.address,tokenProgram: TOKEN_PROGRAM_ADDRESS,});const [subscriptionAuthorityPda] = await findSubscriptionAuthorityPda({user: subscriberSigner.address,tokenMint,});const subscriptionAuthority = await fetchMaybeSubscriptionAuthority(subscriberClient.rpc,subscriptionAuthorityPda,);if (!subscriptionAuthority.exists) {await subscriberClient.subscriptions.instructions.initSubscriptionAuthority({tokenMint,tokenProgram: TOKEN_PROGRAM_ADDRESS,userAta: subscriberAta,}).sendTransaction();}await subscriberClient.subscriptions.instructions.subscribe({merchant: merchantSigner.address,planId,tokenMint,}).sendTransaction();const [subscriptionPda] = await findSubscriptionDelegationPda({planPda,subscriber: subscriberSigner.address,});
收取付款
商户或已列入白名单的拉取方签署收款操作。当计划使用目标地址白名单时,接收方 token
account 的所有者必须列于 destinations 中。
const receiverAta = address('MERCHANT_TOKEN_ACCOUNT_ADDRESS_HERE');await merchantClient.subscriptions.instructions.transferSubscription({caller: merchantOrPullerSigner,delegator: subscriberSigner.address,tokenMint,subscriptionPda,planPda,amount: 200_000n,receiverAta,tokenProgram: TOKEN_PROGRAM_ADDRESS,}).sendTransaction();
取消与撤销
取消操作将订阅标记为即将结束。撤销操作则在取消到期后关闭订阅的 PDA。订阅者需对两笔交易进行签名。
await subscriberClient.subscriptions.instructions.cancelSubscription({subscriber: subscriberSigner,planPda,subscriptionPda,}).sendTransaction();// Run this after the cancelled subscription's expiresAtTs has elapsed.await subscriberClient.subscriptions.instructions.revokeSubscription({authority: subscriberSigner,planPda,subscriptionPda,}).sendTransaction();
恢复订阅
已取消的订阅在被撤销之前可以重新激活。一旦撤销,订阅账户将被关闭,订阅者必须重新
订阅。恢复订阅需要订阅者提供该计划铸币的
SubscriptionAuthority,程序将对其进行验证(所有者、铸币及
init_id),若已过期或被重新初始化则拒绝操作。
await subscriberClient.subscriptions.instructions.resumeSubscription({subscriber: subscriberSigner,planPda,tokenMint,}).sendTransaction();
注意事项
amount以基本单位表示。对于 6 位小数的代币,5_000_000表示5个代币。- TypeScript SDK 在调用
subscribe时,若未传入计划条款,将自动获取最新条款。 - Rust 的
SubscribeBuilder需要预期的计划条款。请先获取并解码计划账户,再通过SubscribeData传入相应字段。 - 只有商户或在
pullers中列出的钱包才能收款。 - 订阅者签署设置、取消和撤销交易;商户或已授权的拉取方签署收款交易。
Is this page helpful?