-
Notifications
You must be signed in to change notification settings - Fork 350
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
Solution: - add one - use crontab syntax to specify intervals
- Loading branch information
Showing
7 changed files
with
286 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
<!-- | ||
order: 1 | ||
--> | ||
|
||
# Concepts | ||
|
||
## Subscription | ||
|
||
- Subscription plan owners create/stop plans on Chain, the plan prices are defined for specified token denominations, subscribers should do necessary token conversions in other ways, e.g. exchange. | ||
- Users subscribe/unsubscribe to plan. | ||
- Payments are collected automatically at the start of their specified intervals, the results of collections are written into block events. | ||
- Create plan transaction will consume a gas fee to cover the computational cost of future automatic payment collections. | ||
- If the automatic collection mechanism fails on some subscribers, Chain won't automatically retry later, the corresponding plan owner can either configure their plan to automatically cancel these subscribers or manually retry failed collections later. | ||
|
||
### Intervals | ||
|
||
Intervals are specified using [crontab syntax](https://crontab.guru/), for example: | ||
|
||
- `* * * * *`: At *every minute* | ||
- `0 8 * * *`: At *08**:**00* | ||
- `0 8 1 * *`: At *08**:**00* *on day-of-month 1* | ||
- `0 8 * * 1`: At *08**:**00* *on Monday* | ||
|
||
Convenient shortcuts: | ||
|
||
- `@monthly`: `0 0 * * *` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<!-- | ||
order: 2 | ||
--> | ||
|
||
# State | ||
|
||
The `x/subscription` stores the plans and subscriptions on chain: | ||
|
||
```golang | ||
type SubscriptionPlan struct { | ||
id int, // auto-increasing unique identifier | ||
title string, | ||
description string, | ||
owner Address, // beneficial owner of the plan | ||
price Coins, // price to pay for each period, Coins contains both amount and denomination | ||
subscription_duration int, // duration of subscriptions | ||
cron_spec CronSpec, // Configure time intervals, parsed from crontab syntax | ||
cron_tz Timezone, // timezone for cron_spec | ||
} | ||
|
||
type Subscription struct { | ||
plan_id int, | ||
subscriber Address, | ||
create_time int, // the block time when subscription was created | ||
// the timestamp of last successful collection, | ||
// default to the current block time round-down against cron spec | ||
// subscribers don't pay for the period it gets created in | ||
last_collected_time int, | ||
payment_failures int, // times of failed payment collection | ||
} | ||
|
||
type GenesisState struct { | ||
subscription_plans [SubscriptionPlan]; | ||
subscriptions [Subscription]; | ||
} | ||
|
||
func (s *Subscription) next_collection_time() int { | ||
var plan = get_plan(s.plan_id) | ||
return round_up_time(s.last_collected_time, plan.cron_spec, plan.cron_tz) | ||
} | ||
|
||
func (s *Subscription) expiration_time() int { | ||
var plan = get_plan(s.plan_id) | ||
return s.create_time + plan.subscription_duration | ||
} | ||
|
||
// Parsed crontab syntax | ||
type CronSpec struct { | ||
minute [CronValue] | ||
hour [CronValue] | ||
day [CronValue] | ||
month [CronValue] | ||
wday [CronValue] | ||
} | ||
CronValue = Any | Range(start, end, step) | Value(v) | ||
``` | ||
|
||
## Round time | ||
|
||
We define two functions to round timestamp to the boundary of periods: | ||
|
||
```python | ||
def round_down_time(timestamp, cron_spec, cron_tx): | ||
# return the largest timestamp which matches cron_spec and less or equal than timestamp | ||
|
||
def round_up_time(timestamp, cron_spec, cron_tx): | ||
# return the smallest timestamp which matches cron_spec and greater than timestamp | ||
|
||
def count_period(begin_time, end_time, cron_spec, cron_tz): | ||
# return the number of periods between two timestamps | ||
count = 0 | ||
while True: | ||
begin_time = round_up_time(begin_time, cron_spec, cron_tz) | ||
if begin_time >= end_time: | ||
break | ||
count += 1 | ||
return count | ||
``` | ||
|
||
To keep payment collection idempotent (no duplicated collection in same collection period), we record the last round-down timestamp of collection: | ||
|
||
```python | ||
period_index = round_down_time(block_time, cron_spec) | ||
if period_index > last_period_index: | ||
# do the collection | ||
last_period_index = period_index | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<!-- | ||
order: 3 | ||
--> | ||
|
||
# Messages | ||
|
||
## MsgCreateSubscriptionPlan | ||
|
||
Plan owners create plans, the message signer is the plan owner. | ||
|
||
```protobuf | ||
message MsgCreateSubscriptionPlan { | ||
string title; | ||
string description; | ||
Coins price, // amount + denomination, amount + denomination, ... | ||
int32 subscription_duration, | ||
repeated int32 collection_timestamps, | ||
} | ||
``` | ||
|
||
## MsgStopSubscriptionPlan | ||
|
||
Sent by plan owners, also removes all corresponding subscriptions. | ||
|
||
```protobuf | ||
message MsgStopSubscriptionPlan { | ||
int32 plan_id, | ||
} | ||
``` | ||
|
||
## MsgCreateSubscription | ||
|
||
The message signer subscribe to the plan. | ||
|
||
```protobuf | ||
message MsgCreateSubscription { | ||
int32 plan_id, | ||
} | ||
``` | ||
|
||
It'll consume some gases for each collection period: | ||
|
||
```python | ||
ConsumeGas( count_period(create_time, expiration_time, plan.cron_spec, plan.cron_tz) * GasPerCollection ) | ||
``` | ||
|
||
## MsgStopSubscription | ||
|
||
Both the subscriber and the plan owner can stop a subscription. | ||
|
||
```protobuf | ||
message MsgStopSubscription { | ||
int32 plan_id, | ||
Address subscriber, | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
<!-- | ||
order: 4 | ||
--> | ||
|
||
# Events | ||
|
||
The `x/subscription` module emits the following events: | ||
|
||
## BeginBlocker | ||
|
||
| Type | Attribute Key | Attribute Value | | ||
| ----------------- | ------------- | --------------- | | ||
| collect_payment | subscriber | {Address} | | ||
| collect_payment | amount | {Coins} | | ||
| collect_payment | plan_id | {int} | | ||
| stop_subscription | plan_id | {int} | | ||
| stop_subscription | subscriber | {Address} | | ||
|
||
|
||
## MsgStopSubscription | ||
|
||
| Type | Attribute Key | Attribute Value | | ||
| ----------------- | ------------- | --------------- | | ||
| stop_subscription | plan_id | {int} | | ||
| stop_subscription | subscriber | {Address} | | ||
|
||
## MsgCreateSubscription | ||
|
||
| Type | Attribute Key | Attribute Value | | ||
| ------------------- | ------------- | --------------- | | ||
| create_subscription | plan_id | {int} | | ||
| create_subscription | subscriber | {Address} | | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
<!-- | ||
order: 5 | ||
--> | ||
|
||
# Parameters | ||
|
||
The subscription module contains the following parameters: | ||
|
||
| Key | Type | Example | | ||
| ------------------- | ---- | ------- | | ||
| GasPerCollection | int | "10000" | | ||
| SubscriptionEnabled | bool | true | | ||
| FailureTolerance | int | 30 | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
<!-- | ||
order: 6 | ||
--> | ||
|
||
# BeginBlock | ||
|
||
## Remove expired subscriptions | ||
|
||
```python | ||
for plan_id, subscriber, expiration_time in get_subscriptions_sorted_by_expiration_time(): | ||
if block_time >= expiration_time: | ||
stop_subscription(plan_id, subscriber) | ||
else: | ||
break | ||
``` | ||
|
||
## Collect payments | ||
|
||
`x/subscription` module automatically collect payments from subscribers at begin block, and write collection results into events. | ||
|
||
```python | ||
for subscription in get_subscriptions_sorted_by_next_collection_time(): | ||
plan = get_plan(subscription.plan_id) | ||
if block_time >= subscription.next_collection_time(): | ||
current_period = round_down_time(block_time, plan.cron_spec, plan.cron_tz) | ||
if current_period > subscription.last_collected_time: | ||
if transfer(subscription.subscriber, plan.owner, plan.price): | ||
subscription.payment_failures = 0 | ||
emit Event( | ||
'collect_payment', | ||
plan_id=plan.id, | ||
subscriber=subscription.subscriber, | ||
amount=amount, | ||
) | ||
subscription.last_collected_time = current_period | ||
else: | ||
subscription.payment_failures += 1 | ||
if subscription.payment_failures > FailureTolerance: | ||
stop_subscription(plan.id, subscription.subscriber) | ||
emit Event( | ||
'stop_subscription', | ||
plan_id=plan.id, | ||
subscriber=subscription.subscriber, | ||
) | ||
else: | ||
break | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<!-- | ||
order: 0 | ||
title: Subscription Overview | ||
parent: | ||
title: "subscription" | ||
--> | ||
|
||
# `x/subscription` | ||
|
||
## Table of Contents | ||
|
||
<!-- TOC --> | ||
|
||
1. **[Concepts](01_concepts.md)** | ||
2. **[State](02_state.md)** | ||
3. **[Messages](03_messages.md)** | ||
4. **[Events](04_events.md)** | ||
5. **[Params](05_params.md)** | ||
6. **[BeginBlock](06_begin_block.md)** | ||
|
||
## Abstract | ||
|
||
`x/subscription` is an implementation of a Cosmos SDK module that allows for billing customers for goods or services at | ||
regular intervals. |