Gói Đăng Ký

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 endTs hữ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, endTs và 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à endTs khô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àm updatePlan củ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 Ý

  • amount tính theo đơn vị cơ sở. Với token có 6 chữ số thập phân, 5_000_000 có nghĩa là 5 token.
  • TypeScript SDK lấy các điều khoản gói trực tiếp trong quá trình subscribe khi bạn bỏ qua chúng.
  • SubscribeBuilder củ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 đó qua SubscribeData.
  • Chỉ merchant hoặc ví được liệt kê trong pullers mớ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?

Mục lục

Chỉnh sửa trang