This document provides a specification for subscription extension.
When installing, wallet must perform the following steps:
- Add the extension address to the list of allowed extensions.
- Deploy the extension contract.
State init must be constructed as follows:
import { beginCell } from '@ton/core';
const stateInit = beginCell()
.storeMaybeRef(null)
.storeUint(0, 32) // lastRequestTime
.storeUint(0, 32) // chargeDate
.storeUint(0, 2) // subscription_state
.storeRef(beginCell().storeCoins(0).storeRef(beginCell().endCell())) // precompiled
.storeUint(0, 32) // gracePeriod
.storeCoins(0) // callerFee
.storeAddress(wallet)
.storeUint(walletVersion, 8)
.storeAddress(beneficiary)
.storeUint(subscriptionId, 32)
.storeCoins(0) // paymentPerPeriod
.storeUint(0, 32) // period
.storeRef(beginCell().storeAddress(null).storeRef(beginCell())) // withdrawInfo
.storeRef(beginCell());TL-B:
state$_
reward_address_ref: Maybe ^[MsgAddressInt]
last_request_time: uint32
charge_date: uint32
subscription_state: uint2
precompiled: ^[precompiled_fwd_amount: Coins precompiled_c5: ^Cell]
grace_period:uint32
caller_fee: Coins
wallet: MsgAddressInt
wallet_version: WalletVersion
beneficiary: MsgAddressInt
subscription_id: uint32
payment_per_period: Coins
period: uint32
withdraw_info: ^[withdraw_address: MsgAddressInt withdraw_msg_body: ^Cell]
metadata: ^Cell
= SubscriptionState;
wallet_version_v4#4 = WalletVersion;
wallet_version_v5r1#33 = WalletVersion;
where:
walletis the address of the wallet associated with the subscription.walletVersionis the version of the wallet:0x04for v4,0x33for v5r1. Contract will fail if the different value is provided.beneficiaryis the address that can cancel the subscription and receive the remaining balance.subscriptionIdis the subscription identifier (in case if few subscriptions with the same beneficiary are required).
- When deploying, it should call the
op::deploymethod with the following scheme:
deploy#f71783cb
query_id:uint64
first_charging_date:uint32
payment_per_period:Coins
period:uint32
grace_period:uint32
caller_fee:Coins
withdraw_address: MsgAddressInt
withdraw_msg_body: ^Cell
metadata:^Cell
= InternalMsg;
where:
first_charging_dateis the timestamp of the first charging date. This should be specified in case the subscription has a free trial period until the specified date. If the first payment should be charged immediately, this value should be set to0.payment_per_periodis the amount of coins to be withdrawn per subscription period. This value must be greater than the reserved amount of coins (can be received usingget_reserved_amountget method).periodis the duration of the subscription period in seconds.grace_periodis the grace period in seconds. If the subscription is not paid within this period, it is considered expired. Three attempts to pay the subscription are allowed within the grace period.grace_periodmust be lesser thanperiod.caller_feeis the fee that the caller charges for calling the prolongation request. If set to0, the fee is not charged.withdraw_addressis the address of the contract that will receive the subscription payment.withdraw_msg_bodyis the message body that will be sent to thewithdraw_address.metadatais a cell containing additional data related to the subscription. JSON cyphered bytes.
destruct#64737472 query_id:uint64 = MessageInternal: Destroys the subscription contract.subscription_stateis set to2.- If called by the wallet, and the remaining balance is transferred to the beneficiary.
- If called by the beneficiary, the remaining balance is transferred to the beneficiary, and the subscription's address is removed from the wallet's list of authorized extensions.
- Contract remains in the blockchain and can be used for indexing.
Subscriptions can be prolonged by calling an external method on the contract using the following scheme:
cron_trigger#2114702d reward_address:MsgAddress salt:uint32 = ExternalMsgBody;
where:
reward_addressis the address to which the caller fee will be sent if the prolongation request is successful.saltis any value used to prevent liteserver from caching the message.
When this method is called, the contract checks if it is permissible to make the next write-off. If permissible, a request is sent to the wallet to proceed with the subscription extension.
Subscription prolongation requests must be sent after charging_date, during grace_period which by default equals
to 3 days. Three attempts are possible, and delay equals to the grace_period / 3 is required between attempts.
If attempt was successful:
- Exact amount of
payment_per_periodTON will be charged from the wallet. caller_feeamount of TON will be sent to thereward_address.- Extension contract balance amount will be replenished to the minimum storage amount.
- Remainder will be sent to the
withdraw_addresswith the message bodywithdraw_msg_body. charging_datewill be set to theprevious_charging_date+period.
The caller_fee is paid to reward_address only if the prolongation request is successful (i.e., if the
wallet has sufficient balance to cover the subscription cost). This encourages callers to verify the user’s wallet
balance before spamming requests, thereby saving the beneficiary unnecessary fees.
If attempt was unsuccessful:
- Next attempt can be made only after
grace_period / 3seconds from the previous attempt.
If three attempts were unsuccessful, the Caller may call the op::prolong fourth time after the grace period is over,
with the following results:
subscription_stateis set to2.caller_feeis sent to thereward_address.- The subscription's address is removed from the wallet's list of authorized extensions.
- The remaining balance is transferred to the beneficiary.
- The contract remains in the blockchain and can be used for indexing.
All on-chain fees are covered by the beneficiary using the funds the user initially provided. Consequently, from
the user’s perspective, the amount deducted from their wallet each period is always exactly payment_per_period
(except the first payment).
-
get_subscription_info()returns(slice wallet, int wallet_version, slice beneficiary, int subscription_id, slice withdraw_address, cell withdraw_msg_body, cell metadata)wallet: Address of the wallet associated with the subscription.wallet_version: Version of the wallet.beneficiary: Address that can cancel the subscription and receive the remaining balance.subscription_id: Subscription identifier.withdraw_address: Address of the contract that will receive the subscription payment.withdraw_msg_body: Message body that will be sent to thewithdraw_address.metadata: A cell containing additional data related to the subscription.
-
get_payment_info()returns(int contract_state, int payment_per_period, int period, int charge_date, int grace_period, int last_request_time, int caller_fee)contract_state: Contract state. Note that it is contract state, not the state of the subscription. Subscription may be already inactive due to grace period expiration, but the contract itself may containactivestate. Possible values:0- contract is deployed but not yet initialized.1- contract is deployed and initialized.2- subscription is cancelled and the contract cannot be used anymore.
payment_per_period: Amount of coins to be withdrawn per subscription period.period: Duration of the subscription period in seconds.charge_date: Timestamp of the next charging date.grace_period: Grace period in seconds.last_request_time: Timestamp of the last prolongation request.caller_fee: Fee that the caller charges for calling the prolongation request.
-
get_reserved_amount()returnsint reserved_amountreserved_amount: Amount of coins reserved in the contract.
-
get_is_subscription_active()returnsint is_activeis_active:1if the subscription is active,0otherwise. Subscription is considered active if current time is less thancharge_date + grace_periodand the subscription state is1.
-
get_cron_info()returns(int next_call_time, int caller_fee, int balance_after_minus_amounts, int period)next_call_time: Timestamp of the next time when prolongation method can be called.caller_fee: Fee that the caller charges for calling the prolongation request.balance_after_minus_amounts: Balance after the call.period: Period after which the prolongation request can be made.
Metadata fields (shortened keys in parentheses):
logo(l) - URL to the subscription’s logo image.name(n) - Name of the subscription (text, max 80 characters).description(d) - Description of the subscription (text, max 255 characters).link(L) - URL to the source or service the user is subscribing to (e.g., a Telegram channel).tos(t) - URL to the Terms of Service.merchant(m) - Name of the merchant (text, max 80 characters).website(w) - URL to the merchant’s website.category(optional) (c) - Tier or category of the subscription (text, max 80 characters).
Metadata stored in the cell in the encrypted form. Encryption algorithm is implementation-defined and therefore not covered by this specification.