Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

vault.gno

7.25 Kb · 255 lines
  1package data
  2
  3import (
  4	"chain"
  5	"chain/banker"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/nym-alexiscolin000/gnoradio/role/v0"
 10	"gno.land/p/nym-alexiscolin000/gnoradio/store/v0"
 11	bptree "gno.land/p/nt/bptree/v0"
 12)
 13
 14// The vault holds the coins of promo budgets, at this realm's address, so
 15// they never move across a release. It lives in unexported state, not in a
 16// collection, so no writer can forge a balance. An account ("catalog/<id>")
 17// has a funder, a balance and holds: amounts reserved until a UTC day, which
 18// lapse by themselves after it. A role's writer credits, holds, releases,
 19// pays from holds and refunds; the funder alone withdraws what is not held,
 20// whatever the writer, a pause or a renounced owner. Holds lapse on the
 21// vault's clock (VaultDay), which a pause stops.
 22//
 23// Invariant: the realm's ugnot >= VaultTotal. A credit is checked against
 24// the coins already received; every payout lowers the total first.
 25
 26const (
 27	denom       = "ugnot"
 28	daySecs     = 24 * 3600
 29	maxHoldDays = 31 // a hold lapses at most this many days ahead
 30	maxAcct     = 64
 31)
 32
 33var (
 34	vault      = bptree.NewBPTree32() // account -> store.Rec(funder, balance, holds)
 35	vaultTotal int64
 36)
 37
 38type account struct {
 39	funder  address
 40	balance int64  // held for the account, holds included
 41	holds   string // "day:ugnot,..." reserved until the end of day
 42}
 43
 44func getAcct(name string) account {
 45	if len(name) > maxAcct || !strings.Contains(name, "/") {
 46		panic("data: an account is <role>/<name>, at most 64 bytes")
 47	}
 48	v := vault.Get(name)
 49	if v == nil {
 50		return account{}
 51	}
 52	f := store.Fields(v.(string))
 53	return account{address(f[0]), atoi(f[1]), f[2]}
 54}
 55
 56func putAcct(name string, a account) {
 57	vault.Set(name, store.Rec(a.funder.String(), strconv.FormatInt(a.balance, 10), a.holds))
 58}
 59
 60func atoi(s string) int64 {
 61	n, err := strconv.ParseInt(s, 10, 64)
 62	if err != nil {
 63		panic("data: bad number")
 64	}
 65	return n
 66}
 67
 68// VaultDay is the UTC day of the vault's clock, which stands still while
 69// any role is paused: a pause never makes a hold lapse, it lapses that much
 70// later. The clock lags real time by every past pause, so a hold made
 71// after one also lasts that much longer.
 72func VaultDay() int64 {
 73	if frozeAt != 0 {
 74		return (frozeAt - frozen) / daySecs
 75	}
 76	return (now() - frozen) / daySecs
 77}
 78
 79func today() int64 { return VaultDay() }
 80
 81// held reads the holds still live today, and the one lapsing on day e.
 82func held(list string, e int64) (live, atE int64) {
 83	t := today()
 84	for _, p := range strings.Split(list, ",") {
 85		d, v, ok := strings.Cut(p, ":")
 86		if ok && atoi(d) >= t {
 87			live += atoi(v)
 88			if atoi(d) == e {
 89				atE += atoi(v)
 90			}
 91		}
 92	}
 93	return live, atE
 94}
 95
 96// hold adds delta to the hold lapsing on day e and drops lapsed ones.
 97func hold(list string, e, delta int64) string {
 98	t := today()
 99	var out []string
100	for _, p := range strings.Split(list, ",") {
101		d, v, ok := strings.Cut(p, ":")
102		if !ok || atoi(d) < t {
103			continue
104		}
105		n := atoi(v)
106		if atoi(d) == e {
107			n, delta = n+delta, 0
108		}
109		if n > 0 {
110			out = append(out, d+":"+strconv.FormatInt(n, 10))
111		}
112	}
113	if delta > 0 {
114		out = append(out, strconv.FormatInt(e, 10)+":"+strconv.FormatInt(delta, 10))
115	}
116	return strings.Join(out, ",")
117}
118
119func (a account) free() int64 {
120	live, _ := held(a.holds, -1)
121	return a.balance - live
122}
123
124func positive(amt int64) {
125	if amt <= 0 {
126		panic("data: the amount must be positive")
127	}
128}
129
130// send pays out of the vault: the total goes down before the coins leave.
131func send(cur realm, acct string, to address, amt int64, event string) {
132	vaultTotal -= amt
133	banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(cur.Address(), to, chain.Coins{{denom, amt}})
134	chain.Emit(event, "account", acct, "to", to.String(), "amount", strconv.FormatInt(amt, 10))
135}
136
137// VaultCredit adds amt, which the writer has just sent to this realm, to an
138// account funded by funder. A new funder needs an empty account
139// (VaultRefund first).
140func VaultCredit(cur realm, acct string, funder address, amt int64) {
141	gate(cur, acct)
142	positive(amt)
143	if !role.Canonical(funder) || IsGnoRadio(funder) {
144		panic("data: a funder is an account outside GnoRadio")
145	}
146	a := getAcct(acct)
147	if a.balance > 0 && a.funder != funder {
148		panic("data: refund the previous funder first")
149	}
150	// GetCoin >= vaultTotal holds, so the subtraction cannot wrap (an addition
151	// could, with a huge amt).
152	if banker.NewReadonlyBanker().GetCoin(cur.Address(), denom)-vaultTotal < amt {
153		panic("data: send the coins before crediting them")
154	}
155	if a.balance == 0 {
156		a.holds = ""
157	}
158	a.funder, a.balance = funder, a.balance+amt
159	vaultTotal += amt
160	putAcct(acct, a)
161	chain.Emit("VaultCredited", "account", acct, "funder", funder.String(), "amount", strconv.FormatInt(amt, 10))
162}
163
164// VaultHold reserves amt of the free balance until the end of day on the
165// vault's clock (from VaultDay to 31 days from now).
166func VaultHold(cur realm, acct string, amt, day int64) {
167	gate(cur, acct)
168	positive(amt)
169	if day < today() || day > now()/daySecs+maxHoldDays {
170		panic("data: a hold lapses within 31 days")
171	}
172	a := getAcct(acct)
173	if a.free() < amt {
174		panic("data: not enough free in this account")
175	}
176	a.holds = hold(a.holds, day, amt)
177	putAcct(acct, a)
178}
179
180// VaultRelease gives back up to amt of the hold lapsing on day and returns
181// what it released (less when part of it lapsed or was refunded).
182func VaultRelease(cur realm, acct string, amt, day int64) int64 {
183	gate(cur, acct)
184	positive(amt)
185	a := getAcct(acct)
186	if _, atE := held(a.holds, day); atE < amt {
187		amt = atE
188	}
189	if amt > 0 {
190		a.holds = hold(a.holds, day, -amt)
191		putAcct(acct, a)
192	}
193	return amt
194}
195
196// VaultPay pays amt from the live hold lapsing on day to an account outside
197// GnoRadio.
198func VaultPay(cur realm, acct string, to address, amt, day int64) {
199	gate(cur, acct)
200	positive(amt)
201	if !role.Canonical(to) || IsGnoRadio(to) {
202		panic("data: pay an account outside GnoRadio")
203	}
204	a := getAcct(acct)
205	if _, atE := held(a.holds, day); atE < amt {
206		panic("data: no such hold")
207	}
208	a.holds, a.balance = hold(a.holds, day, -amt), a.balance-amt
209	putAcct(acct, a)
210	send(cur, acct, to, amt, "VaultPaid")
211}
212
213// VaultRefund pays the whole balance back to the funder and drops the holds
214// (the profile changed hands). It returns the amount.
215func VaultRefund(cur realm, acct string) int64 {
216	gate(cur, acct)
217	a := getAcct(acct)
218	amt := a.balance
219	if amt == 0 {
220		return 0
221	}
222	a.balance, a.holds = 0, ""
223	putAcct(acct, a)
224	send(cur, acct, a.funder, amt, "VaultRefunded")
225	return amt
226}
227
228// VaultWithdraw pays the caller what is free in an account they funded. It
229// works whoever the writer is, while paused and after Renounce.
230func VaultWithdraw(cur realm, acct string) {
231	noPayment()
232	a := getAcct(acct)
233	free := a.free()
234	if a.funder != cur.Previous().Address() || free <= 0 {
235		panic("data: nothing of yours to withdraw here")
236	}
237	a.balance -= free
238	putAcct(acct, a)
239	send(cur, acct, a.funder, free, "VaultWithdrawn")
240}
241
242// VaultInfo is an account's funder, balance and free part (not held).
243func VaultInfo(acct string) (funder address, balance, free int64) {
244	a := getAcct(acct)
245	return a.funder, a.balance, a.free()
246}
247
248// VaultHeld is the live hold of an account lapsing on day.
249func VaultHeld(acct string, day int64) int64 {
250	_, atE := held(getAcct(acct).holds, day)
251	return atE
252}
253
254// VaultTotal is every account's balance: what this realm holds for them.
255func VaultTotal() int64 { return vaultTotal }