PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
feeledger
Namespace
g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39
Files
3 (gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/g1qp5pt8cdq2f6kfdamn3yuxwg6quqnfpyewzz39/feeledger
gno
0.9

Files (3)

  • gnomod.tomltoml
  • feeledger.gnogno
  • feeledger_test.gnogno
feeledger.gnogno
1// Package feeledger is a pure accounting primitive for realms that hold2// coins on behalf of users and charge an explicit protocol fee on3// deposits. It tracks per-account balances, the sum of user liabilities,4// and a separately-accrued fee pot. It never touches coins itself: the5// importing realm moves coins and drives this ledger, keeping the two in6// lock-step so that7//8//	coins held by realm == UsersTotal() + FeesAccrued() + surplus9//10// where surplus is coins pushed to the realm outside the ledger's flow11// (always >= 0, and 0 if every coin movement goes through the ledger).12//13// Fee model:14//   - Fees are expressed in basis points (1 bps = 0.01%); BpsDenominator15//     is 10000, so bps == 10000 means the whole deposit is fee.16//   - fee = floor(amount * bps / 10000). Rounding always favors the17//     DEPOSITOR: any fractional fee is dropped, the account is credited18//     amount - fee. A small deposit may therefore pay zero fee.19//   - There is no minimum fee. The maximum fee is bounded by the20//     per-ledger cap set at construction (maxFeeBps <= 10000), so a fee21//     can never exceed its deposit.22//   - bps == 0 is a valid configuration: fee is exactly 0 and the full23//     amount is credited.24//25// All failures are returned as errors and leave the ledger COMPLETELY26// UNCHANGED; callers in realms typically wrap calls so an error panics27// and aborts the transaction. Must* wrappers are provided and are the28// only functions in this package that panic.29//30// The ledger is address-agnostic: accounts are non-empty strings. Realms31// normally use address.String().32package feeledger3334import (35	"errors"3637	"gno.land/p/nt/avl/v0"38)3940// BpsDenominator is the fee basis: fees are bps/BpsDenominator of the41// deposited amount.42const BpsDenominator = int64(10000)4344// Errors returned by Ledger operations.45var (46	ErrEmptyAccount  = errors.New("feeledger: empty account key")47	ErrInvalidAmount = errors.New("feeledger: amount must be positive")48	ErrInvalidBps    = errors.New("feeledger: fee bps out of range")49	ErrInsufficient  = errors.New("feeledger: insufficient balance")50	ErrOverflow      = errors.New("feeledger: int64 overflow")51)5253// Ledger tracks per-account balances, their sum (user liabilities), and54// separately-accrued protocol fees. The zero value is not usable;55// construct with New.56type Ledger struct {57	maxFeeBps   int6458	balances    *avl.Tree // account string -> int64 (always > 0)59	usersTotal  int64     // == sum of all balances60	feesAccrued int64     // fees charged but not yet withdrawn61}6263// New returns an empty ledger that rejects deposit fees above maxFeeBps.64// maxFeeBps must be in [0, BpsDenominator].65func New(maxFeeBps int64) (*Ledger, error) {66	if maxFeeBps < 0 || maxFeeBps > BpsDenominator {67		return nil, ErrInvalidBps68	}69	return &Ledger{70		maxFeeBps: maxFeeBps,71		balances:  avl.NewTree(),72	}, nil73}7475// MustNew is New but panics on error.76func MustNew(maxFeeBps int64) *Ledger {77	l, err := New(maxFeeBps)78	if err != nil {79		panic(err)80	}81	return l82}8384// MaxFeeBps returns the ledger's hard fee cap.85func (l *Ledger) MaxFeeBps() int64 {86	return l.maxFeeBps87}8889// FeeFor returns the fee charged on amount at bps, using the ledger's90// rounding rule: floor(amount * bps / BpsDenominator). It is a pure91// preview — no state is read or written beyond validation against92// BpsDenominator (NOT the ledger cap; use it to inspect any policy).93// amount must be >= 0 and bps in [0, BpsDenominator].94func FeeFor(amount, bps int64) (int64, error) {95	if amount < 0 {96		return 0, ErrInvalidAmount97	}98	if bps < 0 || bps > BpsDenominator {99		return 0, ErrInvalidBps100	}101	return feeFor(amount, bps), nil102}103104// feeFor computes floor(amount*bps/BpsDenominator) without intermediate105// overflow: amount = q*BpsDenominator + r, so the fee is106// q*bps + floor(r*bps/BpsDenominator). q*bps <= amount because107// bps <= BpsDenominator, and r*bps < BpsDenominator^2 (10^8), so every108// intermediate fits comfortably in int64. Inputs must be pre-validated.109func feeFor(amount, bps int64) int64 {110	q := amount / BpsDenominator111	r := amount % BpsDenominator112	return q*bps + r*bps/BpsDenominator113}114115// Deposit credits account with amount minus the fee at feeBps, accruing116// the fee to the fee pot. Returns the credited amount and the fee117// (credited + fee == amount always). Fails with ErrEmptyAccount,118// ErrInvalidAmount (amount <= 0), ErrInvalidBps (feeBps outside119// [0, MaxFeeBps]), or ErrOverflow if the account balance or total120// liabilities would leave int64 range. On error nothing is modified.121func (l *Ledger) Deposit(account string, amount, feeBps int64) (credited, fee int64, err error) {122	if account == "" {123		return 0, 0, ErrEmptyAccount124	}125	if amount <= 0 {126		return 0, 0, ErrInvalidAmount127	}128	if feeBps < 0 || feeBps > l.maxFeeBps {129		return 0, 0, ErrInvalidBps130	}131132	fee = feeFor(amount, feeBps)133	credited = amount - fee // >= 0; > 0 whenever feeBps < BpsDenominator134135	newBalance, ok := checkedAdd(l.BalanceOf(account), credited)136	if !ok {137		return 0, 0, ErrOverflow138	}139	newUsersTotal, ok := checkedAdd(l.usersTotal, credited)140	if !ok {141		return 0, 0, ErrOverflow142	}143	newFeesAccrued, ok := checkedAdd(l.feesAccrued, fee)144	if !ok {145		return 0, 0, ErrOverflow146	}147	// Total liabilities must stay representable as one int64, since they148	// mirror a single coin balance held by the importing realm.149	if _, ok := checkedAdd(newUsersTotal, newFeesAccrued); !ok {150		return 0, 0, ErrOverflow151	}152153	if newBalance > 0 {154		l.balances.Set(account, newBalance)155	}156	l.usersTotal = newUsersTotal157	l.feesAccrued = newFeesAccrued158	return credited, fee, nil159}160161// MustDeposit is Deposit but panics on error.162func (l *Ledger) MustDeposit(account string, amount, feeBps int64) (credited, fee int64) {163	credited, fee, err := l.Deposit(account, amount, feeBps)164	if err != nil {165		panic(err)166	}167	return credited, fee168}169170// Withdraw debits amount from account. Fails with ErrEmptyAccount,171// ErrInvalidAmount (amount <= 0), or ErrInsufficient if the balance is172// smaller than amount. A balance drained to zero is removed from173// storage. On error nothing is modified.174func (l *Ledger) Withdraw(account string, amount int64) error {175	if account == "" {176		return ErrEmptyAccount177	}178	if amount <= 0 {179		return ErrInvalidAmount180	}181	balance := l.BalanceOf(account)182	if balance < amount {183		return ErrInsufficient184	}185	if balance == amount {186		l.balances.Remove(account)187	} else {188		l.balances.Set(account, balance-amount)189	}190	// usersTotal >= balance >= amount by the sum invariant, so this191	// cannot underflow.192	l.usersTotal -= amount193	return nil194}195196// MustWithdraw is Withdraw but panics on error.197func (l *Ledger) MustWithdraw(account string, amount int64) {198	if err := l.Withdraw(account, amount); err != nil {199		panic(err)200	}201}202203// WithdrawAll drains account's entire balance and returns it. Returns204// (0, nil) if the account holds nothing. Fails only with205// ErrEmptyAccount.206func (l *Ledger) WithdrawAll(account string) (int64, error) {207	if account == "" {208		return 0, ErrEmptyAccount209	}210	balance := l.BalanceOf(account)211	if balance == 0 {212		return 0, nil213	}214	l.balances.Remove(account)215	l.usersTotal -= balance216	return balance, nil217}218219// WithdrawFees drains the accrued fee pot and returns the amount220// (0 if nothing has accrued). Never fails.221func (l *Ledger) WithdrawFees() int64 {222	fees := l.feesAccrued223	l.feesAccrued = 0224	return fees225}226227// BalanceOf returns account's balance, or 0 if absent.228func (l *Ledger) BalanceOf(account string) int64 {229	v := l.balances.Get(account)230	if v == nil {231		return 0232	}233	return v.(int64)234}235236// UsersTotal returns the sum of all account balances (user liabilities).237func (l *Ledger) UsersTotal() int64 {238	return l.usersTotal239}240241// FeesAccrued returns the fee pot: charged but not yet withdrawn.242func (l *Ledger) FeesAccrued() int64 {243	return l.feesAccrued244}245246// Liabilities returns UsersTotal() + FeesAccrued() — everything the247// importing realm owes. Deposit guarantees this sum fits in int64.248func (l *Ledger) Liabilities() int64 {249	return l.usersTotal + l.feesAccrued250}251252// Accounts returns the number of accounts with a non-zero balance.253func (l *Ledger) Accounts() int {254	return l.balances.Size()255}256257// Iterate calls fn for each account in sorted order with its balance.258// Iteration stops early when fn returns true.259func (l *Ledger) Iterate(fn func(account string, balance int64) bool) {260	l.balances.Iterate("", "", func(key string, value any) bool {261		return fn(key, value.(int64))262	})263}264265// checkedAdd returns a+b and reports whether the addition did not266// overflow int64.267func checkedAdd(a, b int64) (int64, bool) {268	sum := a + b269	if (b > 0 && sum < a) || (b < 0 && sum > a) {270		return 0, false271	}272	return sum, true273}274

Functions

not supported for pure packages by the node (vm/qfuncs)

Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.