access.gno
3.54 Kb · 126 lines
1// GnoPulse access: on-chain subscriptions + prepaid credits that gate the GnoPulse API.
2// Users pay from their own wallet; the realm holds only what they pay it.
3package access
4
5import (
6 "chain"
7 "chain/banker"
8 "chain/runtime"
9 "chain/runtime/unsafe"
10 "strconv"
11 "time"
12)
13
14var (
15 owner = address("g1jsl3x46uhk75lvws0f3mjnc49g84m2hu4rpuyt")
16
17 priceUgnot int64 = 5_000_000
18 periodSec int64 = 30 * 24 * 60 * 60
19 creditRate int64 = 1
20
21 subs = map[address]int64{} // addr -> expiry (unix)
22 credits = map[address]int64{} // addr -> credit balance
23)
24
25// Subscribe buys pro time for the caller; each price unit sent buys one period.
26func Subscribe(cur realm) {
27 runtime.AssertOriginCall()
28 payer := cur.Previous().Address()
29 paid := int64(unsafe.OriginSend().AmountOf("ugnot"))
30 if paid < priceUgnot {
31 panic("payment below price")
32 }
33 base := subs[payer]
34 if now := time.Now().Unix(); base < now {
35 base = now
36 }
37 subs[payer] = base + (paid/priceUgnot)*periodSec
38 chain.Emit("Subscribe", "addr", payer.String(), "expiresAt", strconv.FormatInt(subs[payer], 10))
39}
40
41// Login does nothing. Sign-In-With-Gno signs (never broadcasts) a call to it so the wallet shows
42// a "Login" to GnoPulse rather than a token transfer; the challenge nonce rides in the tx memo.
43func Login(cur realm) {}
44
45// TopUp buys prepaid credits for the caller.
46func TopUp(cur realm) {
47 runtime.AssertOriginCall()
48 payer := cur.Previous().Address()
49 paid := int64(unsafe.OriginSend().AmountOf("ugnot"))
50 if paid <= 0 {
51 panic("no payment")
52 }
53 credits[payer] += paid * creditRate
54 chain.Emit("TopUp", "addr", payer.String(), "credits", strconv.FormatInt(credits[payer], 10))
55}
56
57func HasAccess(addr address) bool { return subs[addr] > time.Now().Unix() || credits[addr] > 0 }
58
59func TierOf(addr address) string {
60 if subs[addr] > time.Now().Unix() {
61 return "pro"
62 }
63 return "free"
64}
65
66func CreditsOf(addr address) int64 { return credits[addr] }
67func ExpiresAt(addr address) int64 { return subs[addr] }
68func Owner() address { return owner }
69func Config() (int64, int64, int64) { return priceUgnot, periodSec, creditRate }
70
71// Consume debits credits (owner settles metered usage in batches). Floors at zero.
72func Consume(cur realm, addr address, n int64) {
73 assertOwner(cur)
74 if n < 0 {
75 panic("negative")
76 }
77 if credits[addr] <= n {
78 delete(credits, addr)
79 } else {
80 credits[addr] -= n
81 }
82}
83
84func SetConfig(cur realm, price, period, rate int64) {
85 assertOwner(cur)
86 if price <= 0 || period <= 0 || rate <= 0 {
87 panic("must be positive")
88 }
89 priceUgnot, periodSec, creditRate = price, period, rate
90}
91
92func Withdraw(cur realm, amount int64) {
93 assertOwner(cur)
94 if amount <= 0 {
95 panic("must be positive")
96 }
97 banker.NewBanker(banker.BankerTypeRealmSend, cur).SendCoins(
98 cur.Address(), owner, chain.Coins{{"ugnot", amount}})
99}
100
101func TransferOwnership(cur realm, newOwner address) {
102 assertOwner(cur)
103 if !newOwner.IsValid() {
104 panic("invalid address")
105 }
106 owner = newOwner
107}
108
109func assertOwner(cur realm) {
110 runtime.AssertOriginCall()
111 if cur.Previous().Address() != owner {
112 panic("owner only")
113 }
114}
115
116func Render(path string) string {
117 if len(path) > 5 && path[:5] == "addr/" {
118 a := address(path[5:])
119 return "# " + a.String() + "\n\n- tier: " + TierOf(a) +
120 "\n- expires: " + strconv.FormatInt(subs[a], 10) +
121 "\n- credits: " + strconv.FormatInt(credits[a], 10) + "\n"
122 }
123 return "# GnoPulse Access\n\nSubscribe() or TopUp() from your wallet.\n\n- price: " +
124 strconv.FormatInt(priceUgnot, 10) + " ugnot / " + strconv.FormatInt(periodSec, 10) + "s\n" +
125 "- credits: " + strconv.FormatInt(creditRate, 10) + " per ugnot\n"
126}