From 57c8d968bfe590a6d3e67922e3b1397127d31cc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaza=C3=AF?= <149690535+kazai777@users.noreply.github.com> Date: Wed, 15 May 2024 16:10:08 +0200 Subject: [PATCH 01/16] add p/subscription --- examples/gno.land/p/demo/subscription/gno.mod | 6 + .../p/demo/subscription/subscription.gno | 123 +++++++++++++++++ .../p/demo/subscription/subscription_test.gno | 126 ++++++++++++++++++ 3 files changed, 255 insertions(+) create mode 100644 examples/gno.land/p/demo/subscription/gno.mod create mode 100644 examples/gno.land/p/demo/subscription/subscription.gno create mode 100644 examples/gno.land/p/demo/subscription/subscription_test.gno diff --git a/examples/gno.land/p/demo/subscription/gno.mod b/examples/gno.land/p/demo/subscription/gno.mod new file mode 100644 index 00000000000..ad93c3cdfda --- /dev/null +++ b/examples/gno.land/p/demo/subscription/gno.mod @@ -0,0 +1,6 @@ +module gno.land/p/demo/subscription + +require ( + gno.land/p/demo/ufmt v0.0.0-latest + gno.land/p/demo/avl v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno new file mode 100644 index 00000000000..e2af05f879d --- /dev/null +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -0,0 +1,123 @@ +// Package subscription provides a library for managing different types of +// subscriptions in Gno applications. It supports both recurring and +// lifetime subscriptions, enabling users to access services based on their +// subscription status. This package uses a tree-based data structure to +// efficiently track and manage subscription statuses. + +// Example Usage: +// +// import "gno.land/p/demo/subscription" +// +// Create a recurring subscription for 30 days costing 100 units. +// recurringSub := subscription.NewRecurringSubscription(time.Hour*24*30, 100) +// +// Create a lifetime subscription for a one-time payment of 500 units. +// lifetimeSub := subscription.NewLifetimeSubscription(500) +// +// func HandleRequest(caller std.Address) { +// Check access for a recurring subscription. +// recurringSub.CheckAccess(caller) +// +// Perform payment for a lifetime subscription. +// lifetimeSub.ProcessPayment(caller, 500) +// } + +package subscription // import "gno.land/p/demo/subscription" + +import ( + "gno.land/p/demo/ufmt" + "gno.land/p/demo/avl" + "time" + "std" +) + +// Subscription interface defines standard methods that all subscription types must implement. +type Subscription interface { + CheckAccess(std.Address) + ProcessPayment(std.Address, int64) +} + +// RecurringSubscription represents a subscription that requires periodic payments. +// It includes the duration of the subscription and the amount required per period. +type RecurringSubscription struct { + duration time.Duration + amount int64 + subs *avl.Tree // std.Address -> time.Time +} + +// LifetimeSubscription represents a subscription that requires only a one-time payment. +// It grants permanent access to a service or product. +type LifetimeSubscription struct { + amount int64 + subs *avl.Tree // std.Address -> bool +} + +// NewRecurringSubscription creates and returns a new recurring subscription. +func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription { + return &RecurringSubscription{ + duration: duration, + amount: amount, + subs: avl.NewTree(), + } +} + +// CheckAccess verifies if the caller has an active recurring subscription. +func (rs *RecurringSubscription) CheckAccess(caller std.Address) { + send := std.GetOrigSend() + sendAmount := send.AmountOf("ugnot") + + if sendAmount < rs.amount { + panic(ufmt.Sprintf("you need to send at least %d units to access this feature", rs.amount)) + } + + expTime, exists := rs.subs.Get(caller.String()) + if !exists || time.Now().After(expTime.(time.Time)) { + panic("your subscription has expired or does not exist") + } +} + +// ProcessPayment processes the payment for a recurring subscription and extends its validity. +func (rs *RecurringSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { + if paymentAmount < rs.amount { + panic("insufficient payment") + } + + expiration := time.Now().Add(rs.duration) + rs.subs.Set(caller.String(), expiration) +} + +// GetExpiration returns the expiration date of the recurring subscription for a given caller. +func (rs *RecurringSubscription) GetExpiration(caller std.Address) time.Time { + expTime, exists := rs.subs.Get(caller.String()) + if !exists { + panic("no subscription found") + } + + return expTime.(time.Time) +} + +// NewLifetimeSubscription creates and returns a new lifetime subscription. +func NewLifetimeSubscription(amount int64) *LifetimeSubscription { + return &LifetimeSubscription{ + amount: amount, + subs: avl.NewTree(), + } +} + +// ProcessPayment processes the payment for a lifetime subscription. +func (ls *LifetimeSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { + if paymentAmount < ls.amount { + panic(ufmt.Sprintf("insufficient payment, required %d units", ls.amount)) + } + + ls.subs.Set(caller.String(), true) +} + +// CheckAccess verifies if the caller has a lifetime subscription. +func (ls *LifetimeSubscription) CheckAccess(caller std.Address) { + _, exists := ls.subs.Get(caller.String()) + + if !exists { + panic("you do not have a lifetime subscription") + } +} diff --git a/examples/gno.land/p/demo/subscription/subscription_test.gno b/examples/gno.land/p/demo/subscription/subscription_test.gno new file mode 100644 index 00000000000..b15013af03a --- /dev/null +++ b/examples/gno.land/p/demo/subscription/subscription_test.gno @@ -0,0 +1,126 @@ +package subscription + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/avl" +) + +// Test the initialization of a recurring subscription. +func TestNewRecurringSubscription(t *testing.T) { + duration := time.Hour * 24 * 30 // 30 days + amount := int64(100) + rs := NewRecurringSubscription(duration, amount) + if rs.duration != duration { + t.Errorf("Expected duration %v, got %v", duration, rs.duration) + } + if rs.amount != amount { + t.Errorf("Expected amount %d, got %d", amount, rs.amount) + } +} + +// Test access check for an active subscription with better error handling. +func TestRecurringSubscriptionCheckAccessActive(t *testing.T) { + rs := NewRecurringSubscription(time.Hour*24*30, 100) + caller := std.Address("test-address") + // Assuming the subscription expects a previous payment, simulate it: + rs.subs.Set(caller.String(), time.Now().Add(time.Hour*24*31)) // Set expiration in the future + + defer func() { + if r := recover(); r != nil { + t.Errorf("Access check failed unexpectedly: %v", r) + } + }() + // Simulate sending the correct amount to access the feature + std.TestSetOrigSend(std.Coins{{"ugnot", 100}}, nil) + rs.CheckAccess(caller) +} + + +// Test access check for an expired subscription. +func TestRecurringSubscriptionCheckAccessExpired(t *testing.T) { + rs := NewRecurringSubscription(time.Hour*24*30, 100) + caller := std.Address("test-address") + rs.subs.Set(caller.String(), time.Now().Add(-time.Hour)) // Set expiration in the past + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected access check to fail for expired subscription") + } + }() + rs.CheckAccess(caller) +} + +// Test processing payments correctly extends the subscription. +func TestRecurringSubscriptionProcessPayment(t *testing.T) { + rs := NewRecurringSubscription(time.Hour*24*30, 100) + caller := std.Address("test-address") + // Initial payment processing + rs.ProcessPayment(caller, 100) + expiration := rs.GetExpiration(caller) + if time.Now().After(expiration) { + t.Errorf("Payment did not extend subscription as expected") + } +} + + +// Test the initialization of a lifetime subscription. +func TestNewLifetimeSubscription(t *testing.T) { + amount := int64(500) + ls := NewLifetimeSubscription(amount) + if ls.amount != amount { + t.Errorf("Expected amount %d, got %d", amount, ls.amount) + } +} + +// Test processing payments for lifetime subscription. +func TestLifetimeSubscriptionProcessPayment(t *testing.T) { + ls := NewLifetimeSubscription(500) + caller := std.Address("test-address") + + // Test insufficient payment. + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected payment error for insufficient amount") + } + }() + ls.ProcessPayment(caller, 400) + + // Test exact payment. + defer func() { + if r := recover(); r != nil { + t.Errorf("Payment failed unexpectedly: %v", r) + } + }() + ls.ProcessPayment(caller, 500) + + _, exists := ls.subs.Get(caller.String()) + if !exists { + t.Errorf("Lifetime subscription was not recorded") + } +} + +// Test access check for lifetime subscription. +func TestLifetimeSubscriptionCheckAccess(t *testing.T) { + ls := NewLifetimeSubscription(500) + caller := std.Address("test-address") + + // Simulate payment and check access. + ls.subs.Set(caller.String(), true) + defer func() { + if r := recover(); r != nil { + t.Errorf("Access denied unexpectedly") + } + }() + ls.CheckAccess(caller) + + // Check access without payment. + ls.subs = avl.NewTree() // Reset the tree to simulate no payment + defer func() { + if r := recover(); r == nil { + t.Errorf("Expected access to be denied for unpaid subscription") + } + }() + ls.CheckAccess(caller) +} \ No newline at end of file From 3fc895617e488406cee00129316ecf324318abe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaza=C3=AF?= <149690535+kazai777@users.noreply.github.com> Date: Wed, 15 May 2024 16:29:17 +0200 Subject: [PATCH 02/16] make tidy --- examples/gno.land/p/demo/subscription/gno.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/gno.mod b/examples/gno.land/p/demo/subscription/gno.mod index ad93c3cdfda..a7ca40f727a 100644 --- a/examples/gno.land/p/demo/subscription/gno.mod +++ b/examples/gno.land/p/demo/subscription/gno.mod @@ -1,6 +1,6 @@ module gno.land/p/demo/subscription require ( - gno.land/p/demo/ufmt v0.0.0-latest gno.land/p/demo/avl v0.0.0-latest -) \ No newline at end of file + gno.land/p/demo/ufmt v0.0.0-latest +) From bbecab25a57c03132ac018f2749fd8c96b14244e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kaza=C3=AF?= <149690535+kazai777@users.noreply.github.com> Date: Tue, 2 Jul 2024 07:22:44 +0200 Subject: [PATCH 03/16] Update examples/gno.land/p/demo/subscription/subscription.gno Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com> --- examples/gno.land/p/demo/subscription/subscription.gno | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno index e2af05f879d..3446f76e243 100644 --- a/examples/gno.land/p/demo/subscription/subscription.gno +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -63,7 +63,7 @@ func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSu // CheckAccess verifies if the caller has an active recurring subscription. func (rs *RecurringSubscription) CheckAccess(caller std.Address) { - send := std.GetOrigSend() + send := std.PrevRealm().Addr() sendAmount := send.AmountOf("ugnot") if sendAmount < rs.amount { From 8f8706fd3a3c8e1490d65e19dd482b6331f88c7d Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Thu, 15 Aug 2024 16:31:53 +0200 Subject: [PATCH 04/16] replace panic by error and create errors.gno file for manage errors messages --- .../gno.land/p/demo/subscription/errors.gno | 8 ++++++++ .../p/demo/subscription/subscription.gno | 18 ++++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 examples/gno.land/p/demo/subscription/errors.gno diff --git a/examples/gno.land/p/demo/subscription/errors.gno b/examples/gno.land/p/demo/subscription/errors.gno new file mode 100644 index 00000000000..6b9d455312b --- /dev/null +++ b/examples/gno.land/p/demo/subscription/errors.gno @@ -0,0 +1,8 @@ +package subscription + +import "errors" + +var ( + ErrPayment = errors.New("subscription: insufficient payment amount") + ErrNoSub = errors.New("subscription: no subscription found") +) diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno index 3446f76e243..95b1944a1a8 100644 --- a/examples/gno.land/p/demo/subscription/subscription.gno +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -22,13 +22,15 @@ // lifetimeSub.ProcessPayment(caller, 500) // } -package subscription // import "gno.land/p/demo/subscription" +package subscription import ( - "gno.land/p/demo/ufmt" - "gno.land/p/demo/avl" - "time" + "errors" "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ufmt" ) // Subscription interface defines standard methods that all subscription types must implement. @@ -40,16 +42,16 @@ type Subscription interface { // RecurringSubscription represents a subscription that requires periodic payments. // It includes the duration of the subscription and the amount required per period. type RecurringSubscription struct { - duration time.Duration + duration time.Duration amount int64 - subs *avl.Tree // std.Address -> time.Time + subs *avl.Tree // std.Address -> time.Time } // LifetimeSubscription represents a subscription that requires only a one-time payment. // It grants permanent access to a service or product. type LifetimeSubscription struct { amount int64 - subs *avl.Tree // std.Address -> bool + subs *avl.Tree // std.Address -> bool } // NewRecurringSubscription creates and returns a new recurring subscription. @@ -107,7 +109,7 @@ func NewLifetimeSubscription(amount int64) *LifetimeSubscription { // ProcessPayment processes the payment for a lifetime subscription. func (ls *LifetimeSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { if paymentAmount < ls.amount { - panic(ufmt.Sprintf("insufficient payment, required %d units", ls.amount)) + return ErrPayment } ls.subs.Set(caller.String(), true) From d16d0b4252c8d61bbe9e13646995cbf712c668a5 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Thu, 15 Aug 2024 16:45:53 +0200 Subject: [PATCH 05/16] fix checkaccess method --- .../gno.land/p/demo/subscription/errors.gno | 5 +++-- .../p/demo/subscription/subscription.gno | 20 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/errors.gno b/examples/gno.land/p/demo/subscription/errors.gno index 6b9d455312b..1d5af78ca5e 100644 --- a/examples/gno.land/p/demo/subscription/errors.gno +++ b/examples/gno.land/p/demo/subscription/errors.gno @@ -3,6 +3,7 @@ package subscription import "errors" var ( - ErrPayment = errors.New("subscription: insufficient payment amount") - ErrNoSub = errors.New("subscription: no subscription found") + ErrPayment = errors.New("subscription: insufficient payment amount") + ErrNoSub = errors.New("subscription: no subscription found") + ErrSubExpired = errors.New("subscription: subscription expired") ) diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno index 95b1944a1a8..ce2cd5986df 100644 --- a/examples/gno.land/p/demo/subscription/subscription.gno +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -25,7 +25,6 @@ package subscription import ( - "errors" "std" "time" @@ -65,23 +64,20 @@ func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSu // CheckAccess verifies if the caller has an active recurring subscription. func (rs *RecurringSubscription) CheckAccess(caller std.Address) { - send := std.PrevRealm().Addr() - sendAmount := send.AmountOf("ugnot") - - if sendAmount < rs.amount { - panic(ufmt.Sprintf("you need to send at least %d units to access this feature", rs.amount)) + expTime, exists := rs.subs.Get(caller.String()) + if !exists { + return ErrNoSub } - expTime, exists := rs.subs.Get(caller.String()) - if !exists || time.Now().After(expTime.(time.Time)) { - panic("your subscription has expired or does not exist") + if time.Now().After(expTime.(time.Time)) { + return ErrSubExpired } } // ProcessPayment processes the payment for a recurring subscription and extends its validity. func (rs *RecurringSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { if paymentAmount < rs.amount { - panic("insufficient payment") + return ErrPayment } expiration := time.Now().Add(rs.duration) @@ -92,7 +88,7 @@ func (rs *RecurringSubscription) ProcessPayment(caller std.Address, paymentAmoun func (rs *RecurringSubscription) GetExpiration(caller std.Address) time.Time { expTime, exists := rs.subs.Get(caller.String()) if !exists { - panic("no subscription found") + return ErrNoSub } return expTime.(time.Time) @@ -120,6 +116,6 @@ func (ls *LifetimeSubscription) CheckAccess(caller std.Address) { _, exists := ls.subs.Get(caller.String()) if !exists { - panic("you do not have a lifetime subscription") + return ErrNoSub } } From 5224d15afcf45a43b755154b683f54e3a8bcdf10 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Tue, 20 Aug 2024 19:24:49 +0200 Subject: [PATCH 06/16] refactor lifetime, error to be fixed IsEqual --- .../p/demo/subscription/lifetime/errors.gno | 9 ++ .../p/demo/subscription/lifetime/gno.mod | 1 + .../p/demo/subscription/lifetime/lifetime.gno | 82 +++++++++++++++++++ .../subscription/lifetime/lifetime_test.gno | 38 +++++++++ 4 files changed, 130 insertions(+) create mode 100644 examples/gno.land/p/demo/subscription/lifetime/errors.gno create mode 100644 examples/gno.land/p/demo/subscription/lifetime/gno.mod create mode 100644 examples/gno.land/p/demo/subscription/lifetime/lifetime.gno create mode 100644 examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno diff --git a/examples/gno.land/p/demo/subscription/lifetime/errors.gno b/examples/gno.land/p/demo/subscription/lifetime/errors.gno new file mode 100644 index 00000000000..7d8e35446ae --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/errors.gno @@ -0,0 +1,9 @@ +package lifetime + +import "errors" + +var ( + ErrNoSub = errors.New("subscription lifetime: no subscription found") + ErrAmt = errors.New("subscription lifetime: the amount does not correspond") + ErrAlreadySub = errors.New("subscription lifetime: this address already has a lifetime subscription") +) diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod new file mode 100644 index 00000000000..59b6c1cf001 --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/subscription/lifetime diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno new file mode 100644 index 00000000000..59d27b7512c --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -0,0 +1,82 @@ +package lifetime + +import ( + "std" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" + "gno.land/p/demo/subscription" +) + +// 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(), + } +} + +func (ls *LifetimeSubscription) validatePayment(receiver std.Address) error { + amount := std.GetOrigSend() + expAmt := std.Coins{{Denom: "ugnot", Amount: ls.amount}} + + if !amount.IsEqual(expAmt) { + return ErrAmt + } + + _, exists := ls.subs.Get(receiver.String()) + + if exists { + return ErrAlreadySub + } + + ls.subs.Set(receiver.String(), true) + + return nil +} + +// ProcessPayment processes the payment for a lifetime subscription. +func (ls *LifetimeSubscription) ProcessPayment() error { + caller := std.PrevRealm().Addr() + return ls.validatePayment(caller) +} + +// ProcessPaymentGift processes the payement for offer a lifetime subscription +func (ls *LifetimeSubscription) ProcessPaymentGift(receiver std.Address) error { + return ls.validatePayment(receiver) +} + +// CheckAccess verifies if the caller has a lifetime subscription. +func (ls *LifetimeSubscription) CheckAccess(caller std.Address) error { + _, exists := ls.subs.Get(caller.String()) + + if !exists { + return ErrNoSub + } + + return nil +} + +func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { + caller := std.PrevRealm().Addr() + + if err := ls.CallerIsOwner(); err != nil { + return subscription.ErrNotAuthorized + } + + ls.amount = newAmount + return nil +} + +func (ls *LifetimeSubscription) GetAmount() int64 { + return ls.amount +} diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno new file mode 100644 index 00000000000..0e6ec68c455 --- /dev/null +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno @@ -0,0 +1,38 @@ +package lifetime + +import ( + "std" + "testing" + + "gno.land/p/demo/uassert" +) + +var ( + alice = std.Address("alice") + bob = std.Address("bob") +) + +func TestLifetimeSubscription(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend(std.Coins{{Denom: "ugnot", Amount: 1000}}) + err := ls.ProcessPayment() + uassert.NoError(t, err, "Expected ProcessPayment to succeed") + + err = ls.CheckAccess(alice) + uassert.NoError(t, err, "Expected Alice to have access") + + std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigSend(std.Coins{{Denom: "ugnot", Amount: 1000}}) + err = ls.ProcessPaymentGift(bob) + uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed") + + err = ls.CheckAccess(bob) + uassert.NoError(t, err, "Expected Bob to have access") + + std.TestSetRealm(std.NewUserRealm(alice)) + err = ls.UpdateAmount(2000) + uassert.NoError(t, err, "Expected UpdateAmount to succeed") +} From bc6b0f8d1ee3680ed29cbf5b9b9fbf66ff5cf96c Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Tue, 20 Aug 2024 19:26:36 +0200 Subject: [PATCH 07/16] remove subcritpion test file --- .../p/demo/subscription/subscription_test.gno | 126 ------------------ 1 file changed, 126 deletions(-) delete mode 100644 examples/gno.land/p/demo/subscription/subscription_test.gno diff --git a/examples/gno.land/p/demo/subscription/subscription_test.gno b/examples/gno.land/p/demo/subscription/subscription_test.gno deleted file mode 100644 index b15013af03a..00000000000 --- a/examples/gno.land/p/demo/subscription/subscription_test.gno +++ /dev/null @@ -1,126 +0,0 @@ -package subscription - -import ( - "std" - "testing" - "time" - - "gno.land/p/demo/avl" -) - -// Test the initialization of a recurring subscription. -func TestNewRecurringSubscription(t *testing.T) { - duration := time.Hour * 24 * 30 // 30 days - amount := int64(100) - rs := NewRecurringSubscription(duration, amount) - if rs.duration != duration { - t.Errorf("Expected duration %v, got %v", duration, rs.duration) - } - if rs.amount != amount { - t.Errorf("Expected amount %d, got %d", amount, rs.amount) - } -} - -// Test access check for an active subscription with better error handling. -func TestRecurringSubscriptionCheckAccessActive(t *testing.T) { - rs := NewRecurringSubscription(time.Hour*24*30, 100) - caller := std.Address("test-address") - // Assuming the subscription expects a previous payment, simulate it: - rs.subs.Set(caller.String(), time.Now().Add(time.Hour*24*31)) // Set expiration in the future - - defer func() { - if r := recover(); r != nil { - t.Errorf("Access check failed unexpectedly: %v", r) - } - }() - // Simulate sending the correct amount to access the feature - std.TestSetOrigSend(std.Coins{{"ugnot", 100}}, nil) - rs.CheckAccess(caller) -} - - -// Test access check for an expired subscription. -func TestRecurringSubscriptionCheckAccessExpired(t *testing.T) { - rs := NewRecurringSubscription(time.Hour*24*30, 100) - caller := std.Address("test-address") - rs.subs.Set(caller.String(), time.Now().Add(-time.Hour)) // Set expiration in the past - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected access check to fail for expired subscription") - } - }() - rs.CheckAccess(caller) -} - -// Test processing payments correctly extends the subscription. -func TestRecurringSubscriptionProcessPayment(t *testing.T) { - rs := NewRecurringSubscription(time.Hour*24*30, 100) - caller := std.Address("test-address") - // Initial payment processing - rs.ProcessPayment(caller, 100) - expiration := rs.GetExpiration(caller) - if time.Now().After(expiration) { - t.Errorf("Payment did not extend subscription as expected") - } -} - - -// Test the initialization of a lifetime subscription. -func TestNewLifetimeSubscription(t *testing.T) { - amount := int64(500) - ls := NewLifetimeSubscription(amount) - if ls.amount != amount { - t.Errorf("Expected amount %d, got %d", amount, ls.amount) - } -} - -// Test processing payments for lifetime subscription. -func TestLifetimeSubscriptionProcessPayment(t *testing.T) { - ls := NewLifetimeSubscription(500) - caller := std.Address("test-address") - - // Test insufficient payment. - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected payment error for insufficient amount") - } - }() - ls.ProcessPayment(caller, 400) - - // Test exact payment. - defer func() { - if r := recover(); r != nil { - t.Errorf("Payment failed unexpectedly: %v", r) - } - }() - ls.ProcessPayment(caller, 500) - - _, exists := ls.subs.Get(caller.String()) - if !exists { - t.Errorf("Lifetime subscription was not recorded") - } -} - -// Test access check for lifetime subscription. -func TestLifetimeSubscriptionCheckAccess(t *testing.T) { - ls := NewLifetimeSubscription(500) - caller := std.Address("test-address") - - // Simulate payment and check access. - ls.subs.Set(caller.String(), true) - defer func() { - if r := recover(); r != nil { - t.Errorf("Access denied unexpectedly") - } - }() - ls.CheckAccess(caller) - - // Check access without payment. - ls.subs = avl.NewTree() // Reset the tree to simulate no payment - defer func() { - if r := recover(); r == nil { - t.Errorf("Expected access to be denied for unpaid subscription") - } - }() - ls.CheckAccess(caller) -} \ No newline at end of file From 314ea88f062de469245c6809ef2541560759119d Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Tue, 20 Aug 2024 19:27:45 +0200 Subject: [PATCH 08/16] subscription interface --- .../p/demo/subscription/subscription.gno | 115 +----------------- 1 file changed, 3 insertions(+), 112 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno index ce2cd5986df..53aff89a804 100644 --- a/examples/gno.land/p/demo/subscription/subscription.gno +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -1,121 +1,12 @@ -// Package subscription provides a library for managing different types of -// subscriptions in Gno applications. It supports both recurring and -// lifetime subscriptions, enabling users to access services based on their -// subscription status. This package uses a tree-based data structure to -// efficiently track and manage subscription statuses. - -// Example Usage: -// -// import "gno.land/p/demo/subscription" -// -// Create a recurring subscription for 30 days costing 100 units. -// recurringSub := subscription.NewRecurringSubscription(time.Hour*24*30, 100) -// -// Create a lifetime subscription for a one-time payment of 500 units. -// lifetimeSub := subscription.NewLifetimeSubscription(500) -// -// func HandleRequest(caller std.Address) { -// Check access for a recurring subscription. -// recurringSub.CheckAccess(caller) -// -// Perform payment for a lifetime subscription. -// lifetimeSub.ProcessPayment(caller, 500) -// } - package subscription import ( "std" - "time" - - "gno.land/p/demo/avl" - "gno.land/p/demo/ufmt" ) // Subscription interface defines standard methods that all subscription types must implement. type Subscription interface { - CheckAccess(std.Address) - ProcessPayment(std.Address, int64) -} - -// RecurringSubscription represents a subscription that requires periodic payments. -// It includes the duration of the subscription and the amount required per period. -type RecurringSubscription struct { - duration time.Duration - amount int64 - subs *avl.Tree // std.Address -> time.Time -} - -// LifetimeSubscription represents a subscription that requires only a one-time payment. -// It grants permanent access to a service or product. -type LifetimeSubscription struct { - amount int64 - subs *avl.Tree // std.Address -> bool -} - -// NewRecurringSubscription creates and returns a new recurring subscription. -func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription { - return &RecurringSubscription{ - duration: duration, - amount: amount, - subs: avl.NewTree(), - } -} - -// CheckAccess verifies if the caller has an active recurring subscription. -func (rs *RecurringSubscription) CheckAccess(caller std.Address) { - expTime, exists := rs.subs.Get(caller.String()) - if !exists { - return ErrNoSub - } - - if time.Now().After(expTime.(time.Time)) { - return ErrSubExpired - } -} - -// ProcessPayment processes the payment for a recurring subscription and extends its validity. -func (rs *RecurringSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { - if paymentAmount < rs.amount { - return ErrPayment - } - - expiration := time.Now().Add(rs.duration) - rs.subs.Set(caller.String(), expiration) -} - -// GetExpiration returns the expiration date of the recurring subscription for a given caller. -func (rs *RecurringSubscription) GetExpiration(caller std.Address) time.Time { - expTime, exists := rs.subs.Get(caller.String()) - if !exists { - return ErrNoSub - } - - return expTime.(time.Time) -} - -// NewLifetimeSubscription creates and returns a new lifetime subscription. -func NewLifetimeSubscription(amount int64) *LifetimeSubscription { - return &LifetimeSubscription{ - amount: amount, - subs: avl.NewTree(), - } -} - -// ProcessPayment processes the payment for a lifetime subscription. -func (ls *LifetimeSubscription) ProcessPayment(caller std.Address, paymentAmount int64) { - if paymentAmount < ls.amount { - return ErrPayment - } - - ls.subs.Set(caller.String(), true) -} - -// CheckAccess verifies if the caller has a lifetime subscription. -func (ls *LifetimeSubscription) CheckAccess(caller std.Address) { - _, exists := ls.subs.Get(caller.String()) - - if !exists { - return ErrNoSub - } + CheckAccess(std.Address) error + ProcessPayment() error + UpdateAmount(newAmount int64) error } From 1aa0796b41cb901141ab0b7de86ef458ba515d17 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Tue, 20 Aug 2024 21:41:53 +0200 Subject: [PATCH 09/16] update test --- .../p/demo/subscription/lifetime/errors.gno | 7 ++-- .../p/demo/subscription/lifetime/lifetime.gno | 7 +--- .../subscription/lifetime/lifetime_test.gno | 39 +++++++++++++------ 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/lifetime/errors.gno b/examples/gno.land/p/demo/subscription/lifetime/errors.gno index 7d8e35446ae..f14e967529c 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/errors.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/errors.gno @@ -3,7 +3,8 @@ package lifetime import "errors" var ( - ErrNoSub = errors.New("subscription lifetime: no subscription found") - ErrAmt = errors.New("subscription lifetime: the amount does not correspond") - ErrAlreadySub = errors.New("subscription lifetime: this address already has a lifetime subscription") + ErrNoSub = errors.New("subscription lifetime: no subscription found") + ErrAmt = errors.New("subscription lifetime: the amount does not correspond") + ErrAlreadySub = errors.New("subscription lifetime: this address already has a lifetime subscription") + ErrNotAuthorized = errors.New("subscription lifetime: action not authorized") ) diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno index 59d27b7512c..ce52cfffca3 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -27,9 +27,8 @@ func NewLifetimeSubscription(amount int64) *LifetimeSubscription { func (ls *LifetimeSubscription) validatePayment(receiver std.Address) error { amount := std.GetOrigSend() - expAmt := std.Coins{{Denom: "ugnot", Amount: ls.amount}} - if !amount.IsEqual(expAmt) { + if amount.AmountOf("ugnot") != ls.amount { return ErrAmt } @@ -50,12 +49,10 @@ func (ls *LifetimeSubscription) ProcessPayment() error { return ls.validatePayment(caller) } -// ProcessPaymentGift processes the payement for offer a lifetime subscription func (ls *LifetimeSubscription) ProcessPaymentGift(receiver std.Address) error { return ls.validatePayment(receiver) } -// CheckAccess verifies if the caller has a lifetime subscription. func (ls *LifetimeSubscription) CheckAccess(caller std.Address) error { _, exists := ls.subs.Get(caller.String()) @@ -70,7 +67,7 @@ func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { caller := std.PrevRealm().Addr() if err := ls.CallerIsOwner(); err != nil { - return subscription.ErrNotAuthorized + return ErrNotAuthorized } ls.amount = newAmount diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno index 0e6ec68c455..db2802a0af0 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno @@ -4,35 +4,52 @@ import ( "std" "testing" + "gno.land/p/demo/testutils" "gno.land/p/demo/uassert" ) var ( - alice = std.Address("alice") - bob = std.Address("bob") + 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.Coins{{Denom: "ugnot", Amount: 1000}}) + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) err := ls.ProcessPayment() uassert.NoError(t, err, "Expected ProcessPayment to succeed") - err = ls.CheckAccess(alice) + err = ls.CheckAccess(std.PrevRealm().Addr()) uassert.NoError(t, err, "Expected Alice to have access") +} - std.TestSetRealm(std.NewUserRealm(bob)) - std.TestSetOrigSend(std.Coins{{Denom: "ugnot", Amount: 1000}}) - err = ls.ProcessPaymentGift(bob) - uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed") +func TestLifetimeSubscriptionGift(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + ls := NewLifetimeSubscription(1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := ls.ProcessPaymentGift(bob) + uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") err = ls.CheckAccess(bob) uassert.NoError(t, err, "Expected Bob to have access") + err = ls.CheckAccess(charlie) + uassert.Error(t, err, "Expected Charlie to fail access check") +} + +func TestUpdateAmountAuthorization(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) - err = ls.UpdateAmount(2000) - uassert.NoError(t, err, "Expected UpdateAmount to succeed") + ls := NewLifetimeSubscription(1000) + + err := ls.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + std.TestSetRealm(std.NewUserRealm(bob)) + + err = ls.UpdateAmount(3000) + uassert.Error(t, err, "Expected Bob to fail when updating amount") } From e84726d0f07f7f9083ec6afc4af803848ec7c7f7 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 03:23:46 +0200 Subject: [PATCH 10/16] lifetime complete --- examples/gno.land/p/demo/subscription/lifetime/gno.mod | 7 +++++++ .../gno.land/p/demo/subscription/lifetime/lifetime.gno | 3 --- .../p/demo/subscription/lifetime/lifetime_test.gno | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/lifetime/gno.mod b/examples/gno.land/p/demo/subscription/lifetime/gno.mod index 59b6c1cf001..0084aa714c5 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/gno.mod +++ b/examples/gno.land/p/demo/subscription/lifetime/gno.mod @@ -1 +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 +) diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno index ce52cfffca3..df8c9e832ff 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -5,7 +5,6 @@ import ( "gno.land/p/demo/avl" "gno.land/p/demo/ownable" - "gno.land/p/demo/subscription" ) // LifetimeSubscription represents a subscription that requires only a one-time payment. @@ -64,8 +63,6 @@ func (ls *LifetimeSubscription) CheckAccess(caller std.Address) error { } func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { - caller := std.PrevRealm().Addr() - if err := ls.CallerIsOwner(); err != nil { return ErrNotAuthorized } diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno index db2802a0af0..00f49d57e43 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno @@ -48,7 +48,7 @@ func TestUpdateAmountAuthorization(t *testing.T) { err := ls.UpdateAmount(2000) uassert.NoError(t, err, "Expected Alice to succeed in updating amount") - std.TestSetRealm(std.NewUserRealm(bob)) + std.TestSetOrigCaller(bob) err = ls.UpdateAmount(3000) uassert.Error(t, err, "Expected Bob to fail when updating amount") From 1412f820029d7dae7b9aa53f250f227cb7f06e2c Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:25:19 +0200 Subject: [PATCH 11/16] doc udpate --- examples/gno.land/p/demo/subscription/doc.gno | 65 +++++++++++++++++++ .../gno.land/p/demo/subscription/errors.gno | 9 --- 2 files changed, 65 insertions(+), 9 deletions(-) create mode 100644 examples/gno.land/p/demo/subscription/doc.gno delete mode 100644 examples/gno.land/p/demo/subscription/errors.gno diff --git a/examples/gno.land/p/demo/subscription/doc.gno b/examples/gno.land/p/demo/subscription/doc.gno new file mode 100644 index 00000000000..878d3f1873d --- /dev/null +++ b/examples/gno.land/p/demo/subscription/doc.gno @@ -0,0 +1,65 @@ +// 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() diff --git a/examples/gno.land/p/demo/subscription/errors.gno b/examples/gno.land/p/demo/subscription/errors.gno deleted file mode 100644 index 1d5af78ca5e..00000000000 --- a/examples/gno.land/p/demo/subscription/errors.gno +++ /dev/null @@ -1,9 +0,0 @@ -package subscription - -import "errors" - -var ( - ErrPayment = errors.New("subscription: insufficient payment amount") - ErrNoSub = errors.New("subscription: no subscription found") - ErrSubExpired = errors.New("subscription: subscription expired") -) From d1d464cd1edeaf51f1e41cb13a0e2b34ef4a4039 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:26:19 +0200 Subject: [PATCH 12/16] gno.mod update --- examples/gno.land/p/demo/subscription/gno.mod | 5 ----- 1 file changed, 5 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/gno.mod b/examples/gno.land/p/demo/subscription/gno.mod index a7ca40f727a..ea60a4c628a 100644 --- a/examples/gno.land/p/demo/subscription/gno.mod +++ b/examples/gno.land/p/demo/subscription/gno.mod @@ -1,6 +1 @@ module gno.land/p/demo/subscription - -require ( - gno.land/p/demo/avl v0.0.0-latest - gno.land/p/demo/ufmt v0.0.0-latest -) From d646d2cfc3d2e5b6a72a8dfc37eab00f61f35c02 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:27:04 +0200 Subject: [PATCH 13/16] interface update --- examples/gno.land/p/demo/subscription/subscription.gno | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/subscription.gno b/examples/gno.land/p/demo/subscription/subscription.gno index 53aff89a804..cc52a2c0e2d 100644 --- a/examples/gno.land/p/demo/subscription/subscription.gno +++ b/examples/gno.land/p/demo/subscription/subscription.gno @@ -6,7 +6,7 @@ import ( // Subscription interface defines standard methods that all subscription types must implement. type Subscription interface { - CheckAccess(std.Address) error - ProcessPayment() error + HasValidSubscription(std.Address) error + Subscribe() error UpdateAmount(newAmount int64) error } From 04f84c4f030e1fc6757af692087de21f4a9bce04 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:30:09 +0200 Subject: [PATCH 14/16] complete lifetime --- .../p/demo/subscription/lifetime/errors.gno | 8 +-- .../p/demo/subscription/lifetime/lifetime.gno | 21 ++++--- .../subscription/lifetime/lifetime_test.gno | 60 +++++++++++++++++-- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/lifetime/errors.gno b/examples/gno.land/p/demo/subscription/lifetime/errors.gno index f14e967529c..faeda4cd9fe 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/errors.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/errors.gno @@ -3,8 +3,8 @@ package lifetime import "errors" var ( - ErrNoSub = errors.New("subscription lifetime: no subscription found") - ErrAmt = errors.New("subscription lifetime: the amount does not correspond") - ErrAlreadySub = errors.New("subscription lifetime: this address already has a lifetime subscription") - ErrNotAuthorized = errors.New("subscription lifetime: action not authorized") + 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") ) diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno index df8c9e832ff..8a4c10b687b 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime.gno @@ -24,7 +24,8 @@ func NewLifetimeSubscription(amount int64) *LifetimeSubscription { } } -func (ls *LifetimeSubscription) validatePayment(receiver std.Address) error { +// 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 { @@ -42,18 +43,20 @@ func (ls *LifetimeSubscription) validatePayment(receiver std.Address) error { return nil } -// ProcessPayment processes the payment for a lifetime subscription. -func (ls *LifetimeSubscription) ProcessPayment() error { +// Subscribe processes the payment for a lifetime subscription. +func (ls *LifetimeSubscription) Subscribe() error { caller := std.PrevRealm().Addr() - return ls.validatePayment(caller) + return ls.processSubscription(caller) } -func (ls *LifetimeSubscription) ProcessPaymentGift(receiver std.Address) error { - return ls.validatePayment(receiver) +// 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) } -func (ls *LifetimeSubscription) CheckAccess(caller std.Address) error { - _, exists := ls.subs.Get(caller.String()) +// 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 @@ -62,6 +65,7 @@ func (ls *LifetimeSubscription) CheckAccess(caller std.Address) error { 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 @@ -71,6 +75,7 @@ func (ls *LifetimeSubscription) UpdateAmount(newAmount int64) error { return nil } +// GetAmount returns the current subscription price. func (ls *LifetimeSubscription) GetAmount() int64 { return ls.amount } diff --git a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno index 00f49d57e43..efbae90c11c 100644 --- a/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno +++ b/examples/gno.land/p/demo/subscription/lifetime/lifetime_test.gno @@ -19,10 +19,10 @@ func TestLifetimeSubscription(t *testing.T) { ls := NewLifetimeSubscription(1000) std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) - err := ls.ProcessPayment() + err := ls.Subscribe() uassert.NoError(t, err, "Expected ProcessPayment to succeed") - err = ls.CheckAccess(std.PrevRealm().Addr()) + err = ls.HasValidSubscription(std.PrevRealm().Addr()) uassert.NoError(t, err, "Expected Alice to have access") } @@ -31,13 +31,13 @@ func TestLifetimeSubscriptionGift(t *testing.T) { ls := NewLifetimeSubscription(1000) std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) - err := ls.ProcessPaymentGift(bob) + err := ls.GiftSubscription(bob) uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") - err = ls.CheckAccess(bob) + err = ls.HasValidSubscription(bob) uassert.NoError(t, err, "Expected Bob to have access") - err = ls.CheckAccess(charlie) + err = ls.HasValidSubscription(charlie) uassert.Error(t, err, "Expected Charlie to fail access check") } @@ -53,3 +53,53 @@ func TestUpdateAmountAuthorization(t *testing.T) { 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") +} From d98cacf9a07f8ffec8ed408ffd81e4fa2f691773 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:31:40 +0200 Subject: [PATCH 15/16] complete recurring --- .../p/demo/subscription/recurring/errors.gno | 11 ++ .../p/demo/subscription/recurring/gno.mod | 8 ++ .../demo/subscription/recurring/recurring.gno | 104 ++++++++++++++ .../subscription/recurring/recurring_test.gno | 134 ++++++++++++++++++ 4 files changed, 257 insertions(+) create mode 100644 examples/gno.land/p/demo/subscription/recurring/errors.gno create mode 100644 examples/gno.land/p/demo/subscription/recurring/gno.mod create mode 100644 examples/gno.land/p/demo/subscription/recurring/recurring.gno create mode 100644 examples/gno.land/p/demo/subscription/recurring/recurring_test.gno diff --git a/examples/gno.land/p/demo/subscription/recurring/errors.gno b/examples/gno.land/p/demo/subscription/recurring/errors.gno new file mode 100644 index 00000000000..76a55e069bf --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/errors.gno @@ -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") +) diff --git a/examples/gno.land/p/demo/subscription/recurring/gno.mod b/examples/gno.land/p/demo/subscription/recurring/gno.mod new file mode 100644 index 00000000000..d3cf8a044f8 --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/gno.mod @@ -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 +) diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring.gno b/examples/gno.land/p/demo/subscription/recurring/recurring.gno new file mode 100644 index 00000000000..b5277bd716e --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/recurring.gno @@ -0,0 +1,104 @@ +package recurring + +import ( + "std" + "time" + + "gno.land/p/demo/avl" + "gno.land/p/demo/ownable" +) + +// RecurringSubscription represents a subscription that requires periodic payments. +// It includes the duration of the subscription and the amount required per period. +type RecurringSubscription struct { + ownable.Ownable + duration time.Duration + amount int64 + subs *avl.Tree // std.Address -> time.Time +} + +// NewRecurringSubscription creates and returns a new recurring subscription. +func NewRecurringSubscription(duration time.Duration, amount int64) *RecurringSubscription { + return &RecurringSubscription{ + Ownable: *ownable.New(), + duration: duration, + amount: amount, + subs: avl.NewTree(), + } +} + +// HasValidSubscription verifies if the caller has an active recurring subscription. +func (rs *RecurringSubscription) HasValidSubscription(addr std.Address) error { + expTime, exists := rs.subs.Get(addr.String()) + if !exists { + return ErrNoSub + } + + if time.Now().After(expTime.(time.Time)) { + return ErrSubExpired + } + + return nil +} + +// processSubscription processes the payment for a given receiver and renews or adds their subscription. +func (rs *RecurringSubscription) processSubscription(receiver std.Address) error { + amount := std.GetOrigSend() + + if amount.AmountOf("ugnot") != rs.amount { + return ErrAmt + } + + expTime, exists := rs.subs.Get(receiver.String()) + + // If the user is already a subscriber but his subscription has expired, authorize renewal + if exists { + expiration := expTime.(time.Time) + if time.Now().Before(expiration) { + return ErrAlreadySub + } + } + + // Renew or add subscription + newExpiration := time.Now().Add(rs.duration) + rs.subs.Set(receiver.String(), newExpiration) + + return nil +} + +// Subscribe handles the payment for the caller's subscription. +func (rs *RecurringSubscription) Subscribe() error { + caller := std.PrevRealm().Addr() + + return rs.processSubscription(caller) +} + +// GiftSubscription allows the user to pay for a subscription for another user (receiver). +func (rs *RecurringSubscription) GiftSubscription(receiver std.Address) error { + return rs.processSubscription(receiver) +} + +// GetExpiration returns the expiration date of the recurring subscription for a given caller. +func (rs *RecurringSubscription) GetExpiration(addr std.Address) (time.Time, error) { + expTime, exists := rs.subs.Get(addr.String()) + if !exists { + return time.Time{}, ErrNoSub + } + + return expTime.(time.Time), nil +} + +// UpdateAmount allows the owner of the subscription contract to change the required subscription amount. +func (rs *RecurringSubscription) UpdateAmount(newAmount int64) error { + if err := rs.CallerIsOwner(); err != nil { + return ErrNotAuthorized + } + + rs.amount = newAmount + return nil +} + +// GetAmount returns the current amount required for each subscription period. +func (rs *RecurringSubscription) GetAmount() int64 { + return rs.amount +} diff --git a/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno new file mode 100644 index 00000000000..e8bca15c0bf --- /dev/null +++ b/examples/gno.land/p/demo/subscription/recurring/recurring_test.gno @@ -0,0 +1,134 @@ +package recurring + +import ( + "std" + "testing" + "time" + + "gno.land/p/demo/testutils" + "gno.land/p/demo/uassert" +) + +var ( + alice = testutils.TestAddress("alice") + bob = testutils.TestAddress("bob") + charlie = testutils.TestAddress("charlie") +) + +func TestRecurringSubscription(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access") + + expiration, err := rs.GetExpiration(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected to get expiration for Alice") +} + +func TestRecurringSubscriptionGift(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.GiftSubscription(bob) + uassert.NoError(t, err, "Expected ProcessPaymentGift to succeed for Bob") + + err = rs.HasValidSubscription(bob) + uassert.NoError(t, err, "Expected Bob to have access") + + err = rs.HasValidSubscription(charlie) + uassert.Error(t, err, "Expected Charlie to fail access check") +} + +func TestRecurringSubscriptionExpiration(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access") + + expiration := time.Now().Add(-time.Hour * 2) + rs.subs.Set(std.PrevRealm().Addr().String(), expiration) + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.Error(t, err, "Expected Alice's subscription to be expired") +} + +func TestUpdateAmountAuthorization(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + err := rs.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + std.TestSetOrigCaller(bob) + err = rs.UpdateAmount(3000) + uassert.Error(t, err, "Expected Bob to fail when updating amount") +} + +func TestGetAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + amount := rs.GetAmount() + uassert.Equal(t, amount, int64(1000), "Expected the initial amount to be 1000 ugnot") + + err := rs.UpdateAmount(2000) + uassert.NoError(t, err, "Expected Alice to succeed in updating amount") + + amount = rs.GetAmount() + uassert.Equal(t, amount, int64(2000), "Expected the updated amount to be 2000 ugnot") +} + +func TestIncorrectPaymentAmount(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 500}}, nil) + err := rs.Subscribe() + uassert.Error(t, err, "Expected payment with incorrect amount to fail") +} + +func TestMultiplePaymentsForSameUser(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour*24, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected first ProcessPayment to succeed for Alice") + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = rs.Subscribe() + uassert.Error(t, err, "Expected second ProcessPayment to fail for Alice due to existing subscription") +} + +func TestRecurringSubscriptionWithMultiplePayments(t *testing.T) { + std.TestSetRealm(std.NewUserRealm(alice)) + rs := NewRecurringSubscription(time.Hour, 1000) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err := rs.Subscribe() + uassert.NoError(t, err, "Expected first ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access after first payment") + + expiration := time.Now().Add(-time.Hour * 2) + rs.subs.Set(std.PrevRealm().Addr().String(), expiration) + + std.TestSetOrigSend([]std.Coin{{Denom: "ugnot", Amount: 1000}}, nil) + err = rs.Subscribe() + uassert.NoError(t, err, "Expected second ProcessPayment to succeed for Alice") + + err = rs.HasValidSubscription(std.PrevRealm().Addr()) + uassert.NoError(t, err, "Expected Alice to have access after second payment") +} From 53e11f6777eafb81c3aac1880f7a1b6128c451c8 Mon Sep 17 00:00:00 2001 From: Kazai777 Date: Wed, 21 Aug 2024 12:50:31 +0200 Subject: [PATCH 16/16] fix CI --- examples/gno.land/p/demo/subscription/doc.gno | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/examples/gno.land/p/demo/subscription/doc.gno b/examples/gno.land/p/demo/subscription/doc.gno index 878d3f1873d..9cc102fcc9a 100644 --- a/examples/gno.land/p/demo/subscription/doc.gno +++ b/examples/gno.land/p/demo/subscription/doc.gno @@ -1,19 +1,19 @@ -// 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 +// 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 +// 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. +// Recurring subscriptions require periodic payments to maintain access. // Users pay to extend their access for a specific duration. // // Example: @@ -41,7 +41,7 @@ // // Lifetime Subscription: // -// Lifetime subscriptions require a one-time payment for permanent access. +// Lifetime subscriptions require a one-time payment for permanent access. // Once paid, users have indefinite access without further payments. // // Example: @@ -63,3 +63,4 @@ // // // Get the current lifetime subscription amount // lifeSub.GetAmount() +package subscription