Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ delegate for a user's token account. Close it when the user no longer has active
fixed delegations, recurring delegations, or subscription delegations for that
mint.

Closing the account returns its rent to the account's lamport funder and revokes
the token delegate approval owned by the program. The funder is usually the
user, but it can be any payer that funded the Subscription Authority account. If
any active delegation still depends on the authority, close those delegations
first.
Closing returns the account's rent to its funder (the user, or whichever payer
funded it). Close any active delegations first.

Closing does **not** clear the SPL Token delegate approval. Once the PDA is gone
the approval is inert, but it stays on the token account until revoked — see
[Revoke The Delegate Approval](#revoke-the-delegate-approval).

## Close The Authority

Expand Down Expand Up @@ -76,6 +77,63 @@ first.
</Tab>
</Tabs>

## Revoke The Delegate Approval

`RevokeSubscriptionAuthority` clears the leftover delegate approval from the
user's token account. It calls the token program's `Revoke`, signed by the user,
so it works whether or not the Subscription Authority still exists. Send it
standalone or alongside `CloseSubscriptionAuthority`.

Pass the token program that owns the mint: `TOKEN_PROGRAM_ADDRESS` for SPL Token
mints, or `TOKEN_2022_PROGRAM_ADDRESS` (from `@solana-program/token-2022`) for
Token-2022 mints.

<Tabs items={["TypeScript", "Rust"]}>
<Tab value="TypeScript">
```ts
import { TOKEN_PROGRAM_ADDRESS } from '@solana-program/token';

await client.subscriptions.instructions
.revokeSubscriptionAuthority({
user: userSigner,
tokenMint,
// Use TOKEN_2022_PROGRAM_ADDRESS for Token-2022 mints.
Comment thread
dev-jodee marked this conversation as resolved.
Outdated
tokenProgram: TOKEN_PROGRAM_ADDRESS,
})
.sendTransaction();
```
Comment thread
dev-jodee marked this conversation as resolved.

</Tab>

<Tab value="Rust">
```rust
use solana_address::{address, Address};
use subscriptions::{generated::instructions::*, SUBSCRIPTIONS_ID};

// For Token-2022 mints use TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.
Comment thread
dev-jodee marked this conversation as resolved.
Outdated
const TOKEN_PROGRAM_ID: Address = address!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");

let user: Address = address!("USER_WALLET_ADDRESS_HERE");
let user_ata: Address = address!("USER_TOKEN_ACCOUNT_ADDRESS_HERE");
let token_mint: Address = address!("TOKEN_MINT_ADDRESS_HERE");

let (subscription_authority, _) = Address::find_program_address(
&[b"SubscriptionAuthority", user.as_ref(), token_mint.as_ref()],
&SUBSCRIPTIONS_ID,
);

let revoke_ix = RevokeSubscriptionAuthorityBuilder::new()
.user(user)
.user_ata(user_ata)
.token_mint(token_mint)
.token_program(TOKEN_PROGRAM_ID)
.subscription_authority(subscription_authority)
.instruction();
```

</Tab>
</Tabs>

## Notes

- The user signs the close transaction.
Expand All @@ -85,3 +143,5 @@ first.
Subscription Authority.
- If the user creates a new delegation later, initialize a new Subscription
Authority for the same mint.
- Closing does not clear the token delegate approval. Use
`RevokeSubscriptionAuthority` to remove it from the user's token account.
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,10 @@ token account.
- Create one Subscription Authority per user and token mint pair.
- Reuse the existing Subscription Authority for later delegations on the same
mint.
- A sponsor can fund the account: pass an optional `payer` to
`initSubscriptionAuthority` (also supported on `createFixedDelegation`,
`createRecurringDelegation`, and `subscribe`), or configure one with
`.use(payer(sponsorSigner))`. Rent then returns to that sponsor when you close
with a matching `receiver`. Omit it for the self-funded default.
- Close the Subscription Authority only after every delegation that depends on
it has been revoked or closed.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ signer pays or authorizes each step.
<Tab value="Rust">
```toml
[dependencies]
subscriptions = "^0.1"
solana-instruction = "^2"
subscriptions = "0.4.0"
solana-instruction = "^3"
solana-address = { version = "^2", features = ["curve25519"] }
```
</Tab>
Expand Down Expand Up @@ -246,4 +246,6 @@ delegation PDA and returns its rent to the signer.
- Amounts are in base units. For a 6-decimal token, `1_000_000` means `1` token.
- Run `initSubscriptionAuthority` only when the Subscription Authority account
does not already exist.
- `expiryTs` is a hard stop: once it passes, transfers fail and the sponsor can
recover rent. There is no spend-time grace period.
- The user signs setup and revoke transactions. The delegatee signs transfers.
2 changes: 2 additions & 0 deletions apps/docs/content/docs/en/payments/subscriptions/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"recurring-delegation",
"subscription-plan",
"close-subscription-authority",
"revoke-abandoned-delegation",
"revoke-abandoned-subscription",
"pay-sh"
],
"defaultOpen": false
Expand Down
30 changes: 23 additions & 7 deletions apps/docs/content/docs/en/payments/subscriptions/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,16 +46,32 @@ The program supports three authorization models:

## Supported Tokens

The program supports tokens created with both SPL Token and Token-2022. The only
Token-2022 extension it rejects is a configured `TransferHook` (where the hook
`authority` or `program_id` is set). An inert `TransferHook` (both unset, and
therefore permanently immutable) and all other extensions are allowed.
Supports all mints from both SPL Token and Token-2022.

For Token-2022 mints with a configured `TransferHook`, the program forwards the
mint's hook accounts (hook program, `ExtraAccountMetaList` PDA, extras) into the
`TransferChecked` CPI on each delegated transfer, so the hook policy is
enforced. The SDK resolves them for you:

- Plugin client (`transferFixed`, `transferRecurring`, `transferSubscription`) —
appended automatically.
- Building manually — call `resolveTransferHookAccounts`, pass
`transferHookAccounts`.

## On-Chain Events

The program emits on-chain events so indexers and applications can track
important activity. These events cover subscription changes and transfers made
through fixed, recurring, and subscription-plan flows.
The program emits on-chain events via self-CPI so indexers and applications can
track activity. All events are registered in the published IDL, so generated
clients can decode them. The emitted events are:

- `FixedTransferEvent`, `RecurringTransferEvent`, and
`SubscriptionTransferEvent` — one per delegated transfer. Each carries the
credited `receiverTokenAccount`; `SubscriptionTransferEvent` also carries the
`puller`.
- `SubscriptionCreatedEvent`, `SubscriptionCancelledEvent`, and
`SubscriptionResumedEvent` — subscription lifecycle.
`SubscriptionCreatedEvent` carries the `payer`.
- `PlanUpdatedEvent` — emitted when a plan is updated.

## Versioning

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ delegation, then use that delegation PDA for transfers.
<Tab value="Rust">
```toml
[dependencies]
subscriptions = "^0.1"
solana-instruction = "^2"
subscriptions = "0.4.0"
solana-instruction = "^3"
solana-address = { version = "^2", features = ["curve25519"] }
```
</Tab>
Expand Down Expand Up @@ -260,4 +260,10 @@ the delegation PDA and returns its rent to the signer.
- The program rejects transfers that exceed the current period's remaining
allowance.
- Once the next period starts, the pulled amount resets.
- `expiryTs` is a hard stop: once it passes, transfers fail and the sponsor can
recover rent. There is no spend-time grace period.
- Set `startTs` to `0` to start the delegation when the transaction lands on
chain instead of at a fixed time. This widens the window to get the user to
sign and onboard. When you do this, `expiryTs` must be non-zero — a
start-on-landing delegation cannot also be set to never expire.
- The user signs setup and revoke transactions. The delegatee signs transfers.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Revoke Abandoned Delegation
description:
Guide for reclaiming rent from a fixed or recurring delegation whose
Subscription Authority is gone.
---

A fixed or recurring delegation holds rent that normally returns to its recorded
payer on revoke. If a sponsor funded it and the user later closes or replaces
their Subscription Authority, the delegation is stranded and its rent stuck.
`RevokeAbandonedDelegation` lets the recorded payer reclaim it.

## When It Applies

Allowed only when the delegation is provably dead — its Subscription Authority
was closed, or closed and recreated (so `init_id` no longer matches). A live
delegation can never be closed this way.

It's the recorded payer's only recovery path for a never-expiring delegation
(`expiry_ts == 0`) that isn't fully spent: ordinary
[revoke](/docs/payments/subscriptions/recurring-delegation#revoke-the-delegation)
fires for the payer only on expiry or, for a fixed delegation, full spend.

## Reclaim The Rent

The recorded payer signs. Pass the dead delegation PDA and its recorded
Subscription Authority (which may already be closed). Rent returns to the payer.

<Tabs items={["TypeScript", "Rust"]}>
<Tab value="TypeScript">
```ts
import { getRevokeAbandonedDelegationInstruction } from '@solana/subscriptions';

// Build the instruction and send it in a transaction like any other.
const revokeAbandonedIx = getRevokeAbandonedDelegationInstruction({
payer: payerSigner,
delegationAccount: delegationPda,
subscriptionAuthority: subscriptionAuthorityPda,
});
```

</Tab>

<Tab value="Rust">
```rust
use solana_address::{address, Address};
use subscriptions::generated::instructions::*;

let payer: Address = address!("PAYER_THAT_FUNDED_RENT_HERE");
let delegation_pda: Address = address!("DELEGATION_PDA_ADDRESS_HERE");
let subscription_authority: Address = address!("RECORDED_SUBSCRIPTION_AUTHORITY_HERE");

let revoke_abandoned_ix = RevokeAbandonedDelegationBuilder::new()
.payer(payer)
.delegation_account(delegation_pda)
.subscription_authority(subscription_authority)
.instruction();
```

</Tab>
</Tabs>

## Notes

- The recorded payer signs, not the delegator or delegatee.
- The instruction fails if the Subscription Authority is still live and matches
the delegation — use ordinary revoke for delegations that can still expire.
- It works for both fixed and recurring delegations.
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
title: Revoke Abandoned Subscription
description:
Guide for reclaiming rent from a subscription whose Subscription Authority is
gone.
---

A subscription PDA holds rent that normally returns to its recorded payer when
the subscriber revokes it. If a sponsor funded the subscription and the user
later closes or replaces their Subscription Authority, the subscription is
stranded and its rent stuck. `RevokeAbandonedSubscription` lets the recorded
payer reclaim it.

## When It Applies

Allowed only when the subscriber's Subscription Authority for the plan's mint is
provably terminal — closed, or closed and recreated (so `init_id` no longer
matches). The mint is read from the bound plan, so an unrelated authority cannot
be substituted to force a close. A live subscription can never be closed this
way.

This is the subscription-plan counterpart to
[Revoke Abandoned Delegation](/docs/payments/subscriptions/revoke-abandoned-delegation),
which covers fixed and recurring delegations.

## Reclaim The Rent

The recorded payer signs. Pass the abandoned subscription PDA, its recorded
Subscription Authority (which may already be closed), and the plan the
subscription belongs to. Rent returns to the payer.

<Tabs items={["TypeScript", "Rust"]}>
<Tab value="TypeScript">
```ts
import { getRevokeAbandonedSubscriptionInstruction } from '@solana/subscriptions';

// Build the instruction and send it in a transaction like any other.
const revokeAbandonedIx = getRevokeAbandonedSubscriptionInstruction({
payer: payerSigner,
subscriptionAccount: subscriptionPda,
subscriptionAuthority: subscriptionAuthorityPda,
planPda,
});
```

</Tab>

<Tab value="Rust">
```rust
use solana_address::{address, Address};
use subscriptions::generated::instructions::*;

let payer: Address = address!("PAYER_THAT_FUNDED_RENT_HERE");
let subscription_pda: Address = address!("SUBSCRIPTION_PDA_ADDRESS_HERE");
let subscription_authority: Address = address!("RECORDED_SUBSCRIPTION_AUTHORITY_HERE");
let plan_pda: Address = address!("PLAN_PDA_ADDRESS_HERE");

let revoke_abandoned_ix = RevokeAbandonedSubscriptionBuilder::new()
.payer(payer)
.subscription_account(subscription_pda)
.subscription_authority(subscription_authority)
.plan_pda(plan_pda)
.instruction();
```

</Tab>
</Tabs>

## Notes

- The recorded payer signs, not the subscriber.
- The mint is read from the bound plan, blocking abandonment spoofing with an
unrelated authority.
- The instruction fails if the subscriber's Subscription Authority is still live
for the plan's mint — use ordinary cancel and revoke for active subscriptions.
Loading
Loading