Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: p/subscription #2116

Merged
merged 17 commits into from
Aug 22, 2024
Merged
66 changes: 66 additions & 0 deletions examples/gno.land/p/demo/subscription/doc.gno
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
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/subscription/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/subscription
10 changes: 10 additions & 0 deletions examples/gno.land/p/demo/subscription/lifetime/errors.gno
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")
)
8 changes: 8 additions & 0 deletions examples/gno.land/p/demo/subscription/lifetime/gno.mod
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 examples/gno.land/p/demo/subscription/lifetime/lifetime.gno
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 examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno
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 examples/gno.land/p/demo/subscription/recurring/errors.gno
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")
)
8 changes: 8 additions & 0 deletions examples/gno.land/p/demo/subscription/recurring/gno.mod
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
)
Loading
Loading