-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
I've created this `subscription` package based on this [PR](#1025) from @moul, I've integrated two types of subscription, a recurring payment subscription and a lifetime subscription. Do you have any ideas for other types of subscription that would be relevant to this package? <details><summary>Contributors' checklist...</summary> - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md). </details> --------- Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
- Loading branch information
Showing
11 changed files
with
540 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,66 @@ | ||
// Package subscription provides a flexible system for managing both recurring and | ||
// lifetime subscriptions in Gno applications. It enables developers to handle | ||
// payment-based access control for services or products. The library supports | ||
// both subscriptions requiring periodic payments (recurring) and one-time payments | ||
// (lifetime). Subscriptions are tracked using an AVL tree for efficient management | ||
// of subscription statuses. | ||
// | ||
// Usage: | ||
// | ||
// Import the required sub-packages (`recurring` and/or `lifetime`) to manage specific | ||
// subscription types. The methods provided allow users to subscribe, check subscription | ||
// status, and manage payments. | ||
// | ||
// Recurring Subscription: | ||
// | ||
// Recurring subscriptions require periodic payments to maintain access. | ||
// Users pay to extend their access for a specific duration. | ||
// | ||
// Example: | ||
// | ||
// // Create a recurring subscription requiring 100 ugnot every 30 days | ||
// recSub := recurring.NewRecurringSubscription(time.Hour * 24 * 30, 100) | ||
// | ||
// // Process payment for the recurring subscription | ||
// recSub.Subscribe() | ||
// | ||
// // Gift a recurring subscription to another user | ||
// recSub.GiftSubscription(recipientAddress) | ||
// | ||
// // Check if a user has a valid subscription | ||
// recSub.HasValidSubscription(addr) | ||
// | ||
// // Get the expiration date of the subscription | ||
// recSub.GetExpiration(caller) | ||
// | ||
// // Update the subscription amount to 200 ugnot | ||
// recSub.UpdateAmount(200) | ||
// | ||
// // Get the current subscription amount | ||
// recSub.GetAmount() | ||
// | ||
// Lifetime Subscription: | ||
// | ||
// Lifetime subscriptions require a one-time payment for permanent access. | ||
// Once paid, users have indefinite access without further payments. | ||
// | ||
// Example: | ||
// | ||
// // Create a lifetime subscription costing 500 ugnot | ||
// lifeSub := lifetime.NewLifetimeSubscription(500) | ||
// | ||
// // Process payment for lifetime access | ||
// lifeSub.Subscribe() | ||
// | ||
// // Gift a lifetime subscription to another user | ||
// lifeSub.GiftSubscription(recipientAddress) | ||
// | ||
// // Check if a user has a valid subscription | ||
// lifeSub.HasValidSubscription(addr) | ||
// | ||
// // Update the lifetime subscription amount to 1000 ugnot | ||
// lifeSub.UpdateAmount(1000) | ||
// | ||
// // Get the current lifetime subscription amount | ||
// lifeSub.GetAmount() | ||
package subscription |
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 @@ | ||
module gno.land/p/demo/subscription |
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,10 @@ | ||
package lifetime | ||
|
||
import "errors" | ||
|
||
var ( | ||
ErrNoSub = errors.New("lifetime subscription: no active subscription found") | ||
ErrAmt = errors.New("lifetime subscription: payment amount does not match the required subscription amount") | ||
ErrAlreadySub = errors.New("lifetime subscription: this address already has an active lifetime subscription") | ||
ErrNotAuthorized = errors.New("lifetime subscription: action not authorized") | ||
) |
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,8 @@ | ||
module gno.land/p/demo/subscription/lifetime | ||
|
||
require ( | ||
gno.land/p/demo/avl v0.0.0-latest | ||
gno.land/p/demo/ownable v0.0.0-latest | ||
gno.land/p/demo/testutils v0.0.0-latest | ||
gno.land/p/demo/uassert v0.0.0-latest | ||
) |
81 changes: 81 additions & 0 deletions
81
examples/gno.land/p/demo/subscription/lifetime/lifetime.gno
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,81 @@ | ||
package lifetime | ||
|
||
import ( | ||
"std" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/ownable" | ||
) | ||
|
||
// LifetimeSubscription represents a subscription that requires only a one-time payment. | ||
// It grants permanent access to a service or product. | ||
type LifetimeSubscription struct { | ||
ownable.Ownable | ||
amount int64 | ||
subs *avl.Tree // std.Address -> bool | ||
} | ||
|
||
// NewLifetimeSubscription creates and returns a new lifetime subscription. | ||
func NewLifetimeSubscription(amount int64) *LifetimeSubscription { | ||
return &LifetimeSubscription{ | ||
Ownable: *ownable.New(), | ||
amount: amount, | ||
subs: avl.NewTree(), | ||
} | ||
} | ||
|
||
// processSubscription handles the subscription process for a given receiver. | ||
func (ls *LifetimeSubscription) processSubscription(receiver std.Address) error { | ||
amount := std.GetOrigSend() | ||
|
||
if amount.AmountOf("ugnot") != ls.amount { | ||
return ErrAmt | ||
} | ||
|
||
_, exists := ls.subs.Get(receiver.String()) | ||
|
||
if exists { | ||
return ErrAlreadySub | ||
} | ||
|
||
ls.subs.Set(receiver.String(), true) | ||
|
||
return nil | ||
} | ||
|
||
// Subscribe processes the payment for a lifetime subscription. | ||
func (ls *LifetimeSubscription) Subscribe() error { | ||
caller := std.PrevRealm().Addr() | ||
return ls.processSubscription(caller) | ||
} | ||
|
||
// GiftSubscription allows the caller to pay for a lifetime subscription for another user. | ||
func (ls *LifetimeSubscription) GiftSubscription(receiver std.Address) error { | ||
return ls.processSubscription(receiver) | ||
} | ||
|
||
// HasValidSubscription checks if the given address has an active lifetime subscription. | ||
func (ls *LifetimeSubscription) HasValidSubscription(addr std.Address) error { | ||
_, exists := ls.subs.Get(addr.String()) | ||
|
||
if !exists { | ||
return ErrNoSub | ||
} | ||
|
||
return nil | ||
} | ||
|
||
// UpdateAmount allows the owner of the LifetimeSubscription contract to update the subscription price. | ||
func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { | ||
if err := ls.CallerIsOwner(); err != nil { | ||
return ErrNotAuthorized | ||
} | ||
|
||
ls.amount = newAmount | ||
return nil | ||
} | ||
|
||
// GetAmount returns the current subscription price. | ||
func (ls *LifetimeSubscription) GetAmount() int64 { | ||
return ls.amount | ||
} |
105 changes: 105 additions & 0 deletions
105
examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno
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,105 @@ | ||
package lifetime | ||
|
||
import ( | ||
"std" | ||
"testing" | ||
|
||
"gno.land/p/demo/testutils" | ||
"gno.land/p/demo/uassert" | ||
) | ||
|
||
var ( | ||
alice = testutils.TestAddress("alice") | ||
bob = testutils.TestAddress("bob") | ||
charlie = testutils.TestAddress("charlie") | ||
) | ||
|
||
func TestLifetimeSubscription(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) | ||
err := ls.Subscribe() | ||
uassert.NoError(t, err, "Expected ProcessPayment to succeed") | ||
|
||
err = ls.HasValidSubscription(std.PrevRealm().Addr()) | ||
uassert.NoError(t, err, "Expected Alice to have access") | ||
} | ||
|
||
func TestLifetimeSubscriptionGift(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) | ||
err := ls.GiftSubscription(bob) | ||
uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") | ||
|
||
err = ls.HasValidSubscription(bob) | ||
uassert.NoError(t, err, "Expected Bob to have access") | ||
|
||
err = ls.HasValidSubscription(charlie) | ||
uassert.Error(t, err, "Expected Charlie to fail access check") | ||
} | ||
|
||
func TestUpdateAmountAuthorization(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
err := ls.UpdateAmount(2000) | ||
uassert.NoError(t, err, "Expected Alice to succeed in updating amount") | ||
|
||
std.TestSetOrigCaller(bob) | ||
|
||
err = ls.UpdateAmount(3000) | ||
uassert.Error(t, err, "Expected Bob to fail when updating amount") | ||
} | ||
|
||
func TestIncorrectPaymentAmount(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) | ||
err := ls.Subscribe() | ||
uassert.Error(t, err, "Expected payment to fail with incorrect amount") | ||
} | ||
|
||
func TestMultipleSubscriptionAttempts(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) | ||
err := ls.Subscribe() | ||
uassert.NoError(t, err, "Expected first subscription to succeed") | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) | ||
err = ls.Subscribe() | ||
uassert.Error(t, err, "Expected second subscription to fail as Alice is already subscribed") | ||
} | ||
|
||
func TestGiftSubscriptionWithIncorrectAmount(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) | ||
err := ls.GiftSubscription(bob) | ||
uassert.Error(t, err, "Expected gift subscription to fail with incorrect amount") | ||
|
||
err = ls.HasValidSubscription(bob) | ||
uassert.Error(t, err, "Expected Bob to not have access after incorrect gift subscription") | ||
} | ||
|
||
func TestUpdateAmountEffectiveness(t *testing.T) { | ||
std.TestSetRealm(std.NewUserRealm(alice)) | ||
ls := NewLifetimeSubscription(1000) | ||
|
||
err := ls.UpdateAmount(2000) | ||
uassert.NoError(t, err, "Expected Alice to succeed in updating amount") | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) | ||
err = ls.Subscribe() | ||
uassert.Error(t, err, "Expected subscription to fail with old amount after update") | ||
|
||
std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 2000}}, nil) | ||
err = ls.Subscribe() | ||
uassert.NoError(t, err, "Expected subscription to succeed with new amount") | ||
} |
11 changes: 11 additions & 0 deletions
11
examples/gno.land/p/demo/subscription/recurring/errors.gno
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,11 @@ | ||
package recurring | ||
|
||
import "errors" | ||
|
||
var ( | ||
ErrNoSub = errors.New("recurring subscription: no active subscription found") | ||
ErrSubExpired = errors.New("recurring subscription: your subscription has expired") | ||
ErrAmt = errors.New("recurring subscription: payment amount does not match the required subscription amount") | ||
ErrAlreadySub = errors.New("recurring subscription: this address already has an active subscription") | ||
ErrNotAuthorized = errors.New("recurring subscription: action not authorized") | ||
) |
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,8 @@ | ||
module gno.land/p/demo/subscription/recurring | ||
|
||
require ( | ||
gno.land/p/demo/avl v0.0.0-latest | ||
gno.land/p/demo/ownable v0.0.0-latest | ||
gno.land/p/demo/testutils v0.0.0-latest | ||
gno.land/p/demo/uassert v0.0.0-latest | ||
) |
Oops, something went wrong.