Gói đăng ký cho phép người bán công bố các điều khoản thanh toán mà người dùng có thể chấp nhận. Sau khi người dùng đăng ký, người bán hoặc bên được ủy quyền có thể thu tiền lên đến số tiền trong gói cho mỗi kỳ thanh toán.
Hướng dẫn này trình bày toàn bộ quy trình dưới dạng các khối xây dựng. Người bán tạo gói, người đăng ký chấp nhận gói đó, và người bán hoặc bên được ủy quyền thu tiền từ PDA đăng ký được tạo ra.
Cài Đặt
pnpm add @solana/subscriptions @solana/kit @solana/kit-plugin-rpc @solana/kit-plugin-signer @solana-program/token
Tạo Gói
Người bán sở hữu gói. PDA của gói được tạo từ địa chỉ người bán và planId.
Nhà tài trợ có thể tài trợ rent của plan bằng cách truyền tham số tùy chọn payer trong khi
người bán vẫn là chủ sở hữu plan. Việc xóa plan sẽ hoàn tiền cho chủ sở hữu, không phải
payer, vì vậy hãy kiểm soát việc tài trợ ở ngoài chuỗi (off-chain).
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,});
Cập Nhật Gói
Người bán có thể cập nhật các trường có thể thay đổi của gói sau khi tạo. Những người đăng ký hiện tại vẫn giữ nguyên các điều khoản họ đã chấp nhận, trong khi người đăng ký mới sẽ chấp nhận các điều khoản hiện tại của gói.
Một số quy tắc áp dụng:
- Một
endTshữu hạn chỉ có thể được rút ngắn, không bao giờ được gia hạn hoặc xóa (PlanEndTsCannotExtend). - Trong gói
Sunset, bạn có thể xóa các puller (tập hợp mới phải là tập con của tập hợp hiện tại) để thu hồi quyền của một puller bị xâm phạm; trạng thái,endTsvà siêu dữ liệu vẫn bị đóng băng. - Các chỉnh sửa có hiệu lực trong kỳ thanh toán cuối cùng của gói miễn là
endTskhông thay đổi. - Lệnh này mang theo trạng thái plan được quan sát tại thời điểm ký
(
expectedCreatedAt,expectedEndTs,expectedPullers,expectedMetadataUri). Nếu plan hiện tại không còn khớp, bản cập nhật sẽ bị từ chối (StalePlanApproval), do đó một bản cập nhật đã ký lỗi thời không thể khôi phục các puller đã bị xóa hoặc hoàn nguyên các chỉnh sửa sau này. HàmupdatePlancủa plugin client sẽ tự động lấy trạng thái hiện tại cho bạn; khi xây dựng thủ công, hãy lấy plan và tự truyền các trường vào.
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();
Đăng Ký
Người đăng ký chấp nhận các điều khoản gói hiện tại. PDA đăng ký được suy ra từ PDA của gói và địa chỉ người đăng ký.
Để gộp việc khởi tạo authority và đăng ký vào một giao dịch duy nhất, hãy truyền
giá trị sentinel UNKNOWN_INIT_ID (được xuất bởi TypeScript SDK) làm
expectedSubscriptionAuthorityInitId. Chương trình chỉ chấp nhận authority nếu
nó được tạo trong slot hiện tại, vì vậy sentinel hoạt động cho các đăng ký mới; một
người dùng quay lại có authority được tạo trong một slot trước đó phải truyền
initId thực sự (plugin client sẽ lấy cho bạn) nếu không lệnh gọi sẽ thất bại với
StaleSubscriptionAuthority.
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,});
Thu Thanh Toán
Người bán hoặc một puller được đưa vào danh sách trắng sẽ ký xác nhận thu tiền.
Khi gói sử dụng danh sách cho phép đích, chủ sở hữu của token account nhận phải
được liệt kê trong 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();
Hủy Và Thu Hồi
Hủy đánh dấu gói đăng ký là sắp kết thúc. Thu hồi đóng PDA của gói đăng ký sau khi thời hạn hủy đã hết. Người đăng ký ký cả hai giao dịch.
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();
Hủy Ngay Lập Tức
Khi cả người đăng ký và chủ sở hữu plan hiện tại cùng ký,
cancelSubscriptionNow sẽ cho hết hạn đăng ký tại thời điểm hủy thay vì
vào cuối kỳ thanh toán. Nó cũng có thể rút ngắn một lần hủy đang trong giai đoạn ân hạn. Phê duyệt được ràng buộc với thời điểm bắt đầu kỳ được quan sát tại lúc ký; nếu
đăng ký đã thay đổi kể từ đó, giao dịch sẽ bị từ chối
(StaleSubscriptionApproval).
import { fetchSubscriptionDelegation } from '@solana/subscriptions';const subscription = await fetchSubscriptionDelegation(subscriberClient.rpc,subscriptionPda,);await subscriberClient.subscriptions.instructions.cancelSubscriptionNow({subscriber: subscriberSigner,merchant: merchantSigner,planPda,expectedCurrentPeriodStartTs: subscription.data.currentPeriodStartTs,}).sendTransaction();
Tiếp Tục Gói Đăng Ký
Một gói đăng ký đã hủy có thể được kích hoạt lại trước khi bị thu hồi. Sau khi
bị thu hồi, tài khoản đăng ký sẽ bị đóng và người đăng ký phải
đăng ký lại. Tiếp tục yêu cầu SubscriptionAuthority của người
đăng ký cho mint của gói, mà chương trình xác thực (chủ sở hữu, mint và
init_id) và từ chối nếu đã cũ hoặc được khởi tạo lại.
Lệnh này mang theo thời hạn hết hạn mà người đăng ký quan sát được tại thời điểm ký
(expectedExpiresAtTs); nếu không khớp sẽ bị từ chối (StaleSubscriptionApproval),
do đó một bản resume đã ký lỗi thời không thể xóa lần hủy sau mà người đăng ký chưa bao giờ
chấp thuận.
import { fetchSubscriptionDelegation } from '@solana/subscriptions';const subscription = await fetchSubscriptionDelegation(subscriberClient.rpc,subscriptionPda,);await subscriberClient.subscriptions.instructions.resumeSubscription({subscriber: subscriberSigner,planPda,tokenMint,expectedExpiresAtTs: subscription.data.expiresAtTs,}).sendTransaction();
Lưu Ý
amounttính theo đơn vị cơ sở. Với token có 6 chữ số thập phân,5_000_000có nghĩa là5token.- TypeScript SDK lấy các điều khoản gói trực tiếp trong quá trình
subscribekhi bạn bỏ qua chúng. SubscribeBuildercủa Rust cần các điều khoản gói dự kiến. Tải và giải mã tài khoản gói trước, sau đó truyền các trường đó quaSubscribeData.- Chỉ merchant hoặc ví được liệt kê trong
pullersmới có thể thu thanh toán. - Người đăng ký ký các giao dịch thiết lập, hủy và thu hồi. Merchant hoặc puller được phê duyệt ký các giao dịch thu tiền.
Is this page helpful?