-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* demo: referral * update considerations * remove unused * global getter * update getter * time guard
- Loading branch information
Showing
10 changed files
with
1,150 additions
and
1 deletion.
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,109 @@ | ||
// Package referral implements a referral system on Gno. It allows | ||
// any authorized caller to register, update, or remove referral | ||
// information. A referral link is defined as a mapping from one | ||
// address (the "user") to another address (the "referrer"). | ||
// | ||
// ## Overview | ||
// | ||
// The referral package is composed of the following components: | ||
// | ||
// 1. **errors.gno**: Provides custom error types (ReferralError) with | ||
// specific error codes and messages. | ||
// 2. **utils.gno**: Contains utility functions for permission checks, | ||
// especially isValidCaller, which ensures only specific, pre-authorized | ||
// callers (e.g., governance or router addresses) can invoke the core | ||
// functions. | ||
// 3. **types.gno**: Defines core constants for event types, attributes, | ||
// and the ReferralKeeper interface, which outlines the fundamental | ||
// methods of the referral system (Register, Update, Remove, etc.). | ||
// 4. **keeper.gno**: Implements the actual business logic behind the | ||
// ReferralKeeper interface. It uses an AVL Tree (avl.Tree) to store | ||
// referral data (address -> referrer). The keeper methods emit events | ||
// when a new referral is registered, updated, or removed. | ||
// 5. **referral.gno**: Exposes a public API (the Referral struct) | ||
// that delegates to the keeper, providing external contracts or | ||
// applications a straightforward way to interact with the system. | ||
// | ||
// ## Workflow | ||
// | ||
// Typical usage of this contract follows these steps: | ||
// | ||
// 1. A caller with valid permissions invokes Register, Update, or Remove | ||
// through the Referral struct. | ||
// 2. The Referral struct forwards the request to the internal keeper | ||
// methods. | ||
// 3. The keeper checks caller permission (via isValidCaller), validates | ||
// addresses, and stores or removes data in the AVL Tree. | ||
// 4. An event is emitted for off-chain or cross-module notifications. | ||
// | ||
// ## Integration with Other Contracts | ||
// | ||
// Other contracts can leverage the referral system in two major ways: | ||
// | ||
// 1. **Direct Calls**: If you wish to directly call this contract, | ||
// instantiate the Referral object (via NewReferral) and invoke its | ||
// methods, assuming you meet the authorized-caller criteria. | ||
// | ||
// 2. **Embedded or Extended**: If you have a complex module that includes | ||
// referral features, import this package and embed a Referral instance | ||
// in your own keeper. This way, you can handle additional validations | ||
// or custom logic before delegating to the existing referral functions. | ||
// | ||
// ## Error Handling | ||
// | ||
// The package defines several error types through ReferralError: | ||
// - `ErrInvalidAddress`: Returned when an address format is invalid | ||
// - `ErrUnauthorized`: Returned when the caller lacks permission | ||
// - `ErrNotFound`: Returned when attempting to get a non-existent referral | ||
// - `ErrZeroAddress`: Returned when attempting operations with zero address | ||
// | ||
// ## Example: Integration with a Staking Contract | ||
// | ||
// Suppose you have a staking contract that wants to reward referrers | ||
// when a new user stakes tokens: | ||
// | ||
// ```go | ||
// | ||
// import ( | ||
// "std" | ||
// "gno.land/r/gnoswap/v1/referral" | ||
// "gno.land/p/demo/mystaking" // example staking contract | ||
// ) | ||
// | ||
// func rewardReferrerOnStake(user std.Address, amount int) { | ||
// // 1) Access the referral system | ||
// r := referral.NewReferral() | ||
// | ||
// // 2) Get the user's referrer | ||
// refAddr, err := r.GetReferral(user) | ||
// if err != nil { | ||
// // handle error or skip if not found | ||
// return | ||
// } | ||
// | ||
// // 3) Reward the referrer | ||
// mystaking.AddReward(refAddr, calculateReward(amount)) | ||
// } | ||
// | ||
// ``` | ||
// | ||
// In this simple example, the staking contract checks if the user has | ||
// a referrer by calling `GetReferral`. If a referrer is found, it then | ||
// calculates a reward based on the staked amount. | ||
// | ||
// ## Limitations and Constraints | ||
// | ||
// - A user can have only one referrer at a time | ||
// - Once a referral is removed, it cannot be automatically restored | ||
// - Only authorized contracts can modify referral relationships | ||
// - Address validation is strict and requires proper Bech32 format | ||
// | ||
// # Notes | ||
// | ||
// - The contract strictly enforces caller restrictions via isValidCaller. | ||
// Make sure to configure it to permit only the addresses or roles that | ||
// should be able to register or update referrals. | ||
// - Zero addresses are treated as a trigger for removing a referral record. | ||
// - The system emits events (register_referral, update_referral, remove_referral) | ||
// which can be consumed by other on-chain or off-chain services. | ||
package referral |
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,63 @@ | ||
package referral | ||
|
||
import ( | ||
"gno.land/p/demo/ufmt" | ||
) | ||
|
||
const ( | ||
ErrNone = iota | ||
ErrInvalidAddress | ||
ErrZeroAddress | ||
ErrSelfReferral | ||
ErrUnauthorized | ||
ErrInvalidCaller | ||
ErrCyclicReference | ||
ErrTooManyRequests | ||
ErrNotFound | ||
) | ||
|
||
var errorMessages = map[int]string{ | ||
ErrInvalidAddress: "invalid address format", | ||
ErrZeroAddress: "zero address is not allowed", | ||
ErrSelfReferral: "self referral is not allowed", | ||
ErrUnauthorized: "unauthorized caller", | ||
ErrInvalidCaller: "invalid caller", | ||
ErrCyclicReference: "cyclic reference is not allowed", | ||
ErrTooManyRequests: "too many requests: operations allowed once per 24 hours for each address", | ||
ErrNotFound: "referral not found", | ||
} | ||
|
||
type ReferralError struct { | ||
Code int | ||
Message string | ||
Err error | ||
} | ||
|
||
func (e *ReferralError) Error() string { | ||
// TODO: format error message to follow previous error message format | ||
if e.Err != nil { | ||
return ufmt.Sprintf("code: %d, message: %s, error: %v", e.Code, e.Message, e.Err) | ||
} | ||
return ufmt.Sprintf("code: %d, message: %s", e.Code, e.Message) | ||
} | ||
|
||
func (e *ReferralError) Unwrap() error { | ||
return e.Err | ||
} | ||
|
||
func NewError(code int, args ...interface{}) *ReferralError { | ||
msg := errorMessages[code] | ||
var err error | ||
|
||
if len(args) > 0 { | ||
if lastArg, ok := args[len(args)-1].(error); ok { | ||
err = lastArg | ||
} | ||
} | ||
|
||
return &ReferralError{ | ||
Code: code, | ||
Message: msg, | ||
Err: err, | ||
} | ||
} |
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,41 @@ | ||
package referral | ||
|
||
import "std" | ||
|
||
// gReferralKeeper is the global instance of the referral keeper | ||
var gReferralKeeper ReferralKeeper | ||
|
||
func init() { | ||
gReferralKeeper = NewKeeper() | ||
} | ||
|
||
// GetKeeper returns the global instance of the referral keeper | ||
// | ||
// Example: | ||
// | ||
// // In other packages: | ||
// keeper := referral.GetKeeper() | ||
// keeper.register(addr, refAddr) | ||
func GetKeeper() ReferralKeeper { | ||
return gReferralKeeper | ||
} | ||
|
||
func GetReferral(addr string) string { | ||
referral, err := gReferralKeeper.get(std.Address(addr)) | ||
if err != nil { | ||
panic(err) | ||
} | ||
return referral.String() | ||
} | ||
|
||
func HasReferral(addr string) bool { | ||
referral, err := gReferralKeeper.get(std.Address(addr)) | ||
if err != nil { | ||
return false | ||
} | ||
return referral != zeroAddress | ||
} | ||
|
||
func IsEmpty() bool { | ||
return gReferralKeeper.isEmpty() | ||
} |
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/r/gnoswap/v1/referral |
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,177 @@ | ||
package referral | ||
|
||
import ( | ||
"std" | ||
"time" | ||
|
||
"gno.land/p/demo/avl" | ||
"gno.land/p/demo/ufmt" | ||
) | ||
|
||
const ( | ||
// MinTimeBetweenUpdates represents minimum duration between operations | ||
MinTimeBetweenUpdates int64 = 24 * 60 * 60 | ||
) | ||
|
||
// keeper implements the `ReferralKeeper` interface using an AVL tree for storage. | ||
type keeper struct { | ||
store *avl.Tree | ||
lastOps map[string]int64 | ||
} | ||
|
||
// check interface implementation at compile time | ||
var _ ReferralKeeper = &keeper{} | ||
|
||
// NewKeeper creates and returns a new instance of ReferralKeeper. | ||
// The keeper is initialized with an empty AVL tree for storing referral relationships. | ||
func NewKeeper() ReferralKeeper { | ||
return &keeper{ | ||
store: avl.NewTree(), | ||
lastOps: make(map[string]int64), | ||
} | ||
} | ||
|
||
// register implements the `register` method of the `ReferralKeeper` interface. | ||
// It sets a new referral relationship between the given address and referral address. | ||
func (k *keeper) register(addr, refAddr std.Address) error { | ||
return k.setReferral(addr, refAddr, EventTypeRegister) | ||
} | ||
|
||
// update implements the `update` method of the `ReferralKeeper` interface. | ||
// It updates the referral address for a given address. | ||
func (k *keeper) update(addr, newRefAddr std.Address) error { | ||
return k.setReferral(addr, newRefAddr, EventTypeUpdate) | ||
} | ||
|
||
// setReferral handles the common logic for registering and updating referrals. | ||
// It validates the addresses and caller, then stores the referral relationship. | ||
// | ||
// Note: The current implementation allows circular references, but since it only manages | ||
// simple reference relationships, cycles are not a significant issue. However, when introducing | ||
// a referral-based reward system in the future or adding business logic where cycles could cause problems, | ||
// it will be necessary to implement validation checks. | ||
// | ||
// TODO: need to discuss what values to emit as event | ||
func (k *keeper) setReferral(addr, refAddr std.Address, eventType string) error { | ||
if err := isValidCaller(std.PrevRealm().Addr()); err != nil { | ||
return err | ||
} | ||
|
||
if err := k.validateAddresses(addr, refAddr); err != nil { | ||
return err | ||
} | ||
|
||
if err := k.checkRateLimit(addr.String()); err != nil { | ||
// XXX: because of the weird type-related errors, this is the only | ||
// part where we need to use `ufmt.Errorf` to build error message. | ||
return ufmt.Errorf("too many requests") | ||
} | ||
|
||
if refAddr == zeroAddress { | ||
std.Emit( | ||
EventTypeRemove, | ||
"removedAddress", addr.String(), | ||
) | ||
return k.remove(addr) | ||
} | ||
|
||
k.store.Set(addr.String(), refAddr.String()) | ||
std.Emit( | ||
eventType, | ||
"myAddress", addr.String(), | ||
"refAddress", refAddr.String(), | ||
) | ||
|
||
return nil | ||
} | ||
|
||
// validateAddresses checks if the given addresses are valid for referral | ||
func (k *keeper) validateAddresses(addr, refAddr std.Address) error { | ||
if !addr.IsValid() || (refAddr != zeroAddress && !refAddr.IsValid()) { | ||
return NewError(ErrInvalidAddress) | ||
} | ||
if addr == refAddr { | ||
return NewError(ErrSelfReferral) | ||
} | ||
return nil | ||
} | ||
|
||
// remove implements the `remove` method of the `ReferralKeeper` interface. | ||
// It validates the caller and address before removing the referral relationship. | ||
// | ||
// TODO: need to discuss what values to emit as event | ||
func (k *keeper) remove(target std.Address) error { | ||
if err := isValidCaller(std.PrevRealm().Addr()); err != nil { | ||
return err | ||
} | ||
|
||
if !target.IsValid() { | ||
return NewError(ErrInvalidAddress) | ||
} | ||
|
||
if err := k.checkRateLimit(target.String()); err != nil { | ||
return err | ||
} | ||
|
||
k.store.Remove(target.String()) | ||
|
||
// TODO: update event | ||
std.Emit( | ||
EventTypeRemove, | ||
"removedAddress", target.String(), | ||
) | ||
|
||
return nil | ||
} | ||
|
||
// has implements the `has` method of the `ReferralKeeper` interface. | ||
// It checks if a referral relationship exists for a given address. | ||
func (k *keeper) has(addr std.Address) bool { | ||
_, exists := k.store.Get(addr.String()) | ||
return exists | ||
} | ||
|
||
// get implements the `get` method of the `ReferralKeeper` interface. | ||
// It retrieves the referral address for a given address. | ||
func (k *keeper) get(addr std.Address) (std.Address, error) { | ||
if !addr.IsValid() { | ||
return zeroAddress, NewError(ErrInvalidAddress) | ||
} | ||
|
||
val, ok := k.store.Get(addr.String()) | ||
if !ok { | ||
return zeroAddress, NewError(ErrNotFound) | ||
} | ||
|
||
refAddr, ok := val.(string) | ||
if !ok { | ||
return zeroAddress, NewError(ErrInvalidAddress) | ||
} | ||
|
||
return std.Address(refAddr), nil | ||
} | ||
|
||
func (k *keeper) isEmpty() bool { | ||
empty := true | ||
k.store.Iterate("", "", func(key string, value interface{}) bool { | ||
empty = false | ||
return true // stop iteration on first item | ||
}) | ||
return empty | ||
} | ||
|
||
// checkRateLimit verifies if enough time has passed since the last operation | ||
func (k *keeper) checkRateLimit(addr string) error { | ||
now := time.Now().Unix() | ||
|
||
if lastOpTime, exists := k.lastOps[addr]; exists { | ||
timeSinceLastOp := now - lastOpTime | ||
|
||
if timeSinceLastOp < MinTimeBetweenUpdates { | ||
return NewError(ErrTooManyRequests) | ||
} | ||
} | ||
|
||
k.lastOps[addr] = now | ||
return nil | ||
} |
Oops, something went wrong.