Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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,61 @@ 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,
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};

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 +141,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.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"recurring-delegation",
"subscription-plan",
"close-subscription-authority",
"revoke-abandoned",
"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,120 @@
---
title: Revoke Abandoned Delegations And Subscriptions
description:
Guide for reclaiming rent from a delegation or subscription whose Subscription
Authority is gone.
---

Delegations and subscriptions hold rent that normally returns to their recorded
payer on revoke. If a sponsor funded the account and the user later closes or
replaces their Subscription Authority, the account is stranded and its rent
stuck. `RevokeAbandonedDelegation` and `RevokeAbandonedSubscription` let the
recorded payer reclaim it.

## When It Applies

Allowed only when the account's Subscription Authority is provably terminal —
closed, or closed and recreated (so `init_id` no longer matches). A live
delegation or subscription can never be closed this way.

| | `RevokeAbandonedDelegation` | `RevokeAbandonedSubscription` |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| Closes | Fixed or recurring delegation PDA | Subscription PDA |
| Signer | Recorded payer | Recorded payer |
| Accounts | Delegation + recorded authority | Subscription + recorded authority + plan |
| Eligible when | Authority closed, or recreated (`init_id` mismatch) | Same, for the plan's mint — the mint is read from the bound plan, so an unrelated authority cannot be substituted to force a close |
| Use ordinary revoke instead when | Delegation is [expired or fully spent](/docs/payments/subscriptions/recurring-delegation#revoke-the-delegation) | Subscription is [cancelled and expired](/docs/payments/subscriptions/subscription-plan#cancel-and-revoke) |

`RevokeAbandonedDelegation` is also the recorded payer's only recovery path for
a never-expiring delegation (`expiry_ts == 0`) that isn't fully spent: ordinary
revoke fires for the payer only on expiry or, for a fixed delegation, full
spend.

## Revoke An Abandoned Delegation

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>

## Revoke An Abandoned Subscription

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 delegator, delegatee, or subscriber.
- Both instructions fail if the Subscription Authority is still live for the
account — use ordinary revoke instead.
- `RevokeAbandonedDelegation` works for both fixed and recurring delegations.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ resulting subscription PDA.
<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 @@ -133,6 +133,16 @@ The merchant can update mutable plan fields after creation. Existing subscribers
keep the terms they accepted, while new subscribers accept the current plan
terms.

A few rules apply:

- A finite `endTs` can only be shortened, never extended or cleared
(`PlanEndTsCannotExtend`).
- On a `Sunset` plan you can remove pullers (the new set must be a subset of the
current one) to revoke a compromised puller; status, `endTs`, and metadata
stay frozen.
- Edits work during a plan's final billing period as long as `endTs` is
unchanged.

<Tabs items={["TypeScript", "Rust"]}>
<Tab value="TypeScript">
```ts
Expand Down Expand Up @@ -165,9 +175,13 @@ terms.
let updated_metadata_bytes = b"https://example.com/updated-plan.json";
updated_metadata_uri[..updated_metadata_bytes.len()].copy_from_slice(updated_metadata_bytes);

let (event_authority, _) =
Address::find_program_address(&[b"event_authority"], &SUBSCRIPTIONS_ID);

let update_plan_ix = UpdatePlanBuilder::new()
.owner(merchant)
.plan_pda(plan_pda)
.event_authority(event_authority)
.update_plan_data(UpdatePlanData {
status: PlanStatus::Active as u8,
end_ts: 0,
Expand Down Expand Up @@ -402,6 +416,47 @@ transactions.
</Tab>
</Tabs>

## Resume A Subscription

A cancelled subscription can be reactivated before it is revoked. Once revoked,
the subscription account is closed and the subscriber must
[subscribe](#subscribe) again. Resume requires the subscriber's
`SubscriptionAuthority` for the plan's mint, which the program validates (owner,
mint, and `init_id`) and rejects if it is stale or re-initialized.

<Tabs items={["TypeScript", "Rust"]}>
<Tab value="TypeScript">
```ts
await subscriberClient.subscriptions.instructions
.resumeSubscription({
subscriber: subscriberSigner,
planPda,
tokenMint,
})
.sendTransaction();
```

</Tab>

<Tab value="Rust">
```rust
use subscriptions::generated::instructions::*;

let (event_authority, _) =
Address::find_program_address(&[b"event_authority"], &SUBSCRIPTIONS_ID);

let resume_ix = ResumeSubscriptionBuilder::new()
.subscriber(subscriber)
.plan_pda(plan_pda)
.subscription_pda(subscription_pda)
.subscription_authority(subscription_authority)
.event_authority(event_authority)
.instruction();
```

</Tab>
</Tabs>

## Notes

- `amount` is in base units. For a 6-decimal token, `5_000_000` means `5`
Expand Down
Loading