subtoken.gno
5.01 Kb · 185 lines
1// Package subtoken is spikes (b) and (c) of tasarim-v6 §5.5:
2//
3// (b) a grc721 token created with a sub-realm identity cur.Sub("c/1"):
4// does NewToken accept it, what is Token.ID(), and can the host spend
5// coins that land on a sub-address ("roy/1") with a banker bound to
6// cur.Sub("roy/1")?
7// (c) a grc721 Extension that vetoes every transfer and burn until a
8// reveal flag flips, while mints keep working.
9//
10// One token, one collection, no owner: anyone may call every function.
11// Never part of the product.
12package subtoken
13
14import (
15 "chain"
16 "chain/banker"
17 "strconv"
18
19 "gno.land/p/nt/grc721/royalty/v0"
20 "gno.land/p/nt/grc721/v0"
21)
22
23const (
24 tokenSub = "c/1"
25 royaltySub = "roy/1"
26 royaltyBps = 500
27)
28
29// lockExt is declared in this realm, so its hooks run with this realm's
30// authority (borrow rule #1) even when another realm moves the token
31// through a RealmTeller.
32type lockExt struct {
33 revealed bool
34 moves int
35}
36
37func (e *lockExt) ExtensionKind() string { return "spike-lock" }
38
39func (e *lockExt) OnMint(to address, tid grc721.TokenID) {}
40
41func (e *lockExt) OnTransfer(from, to address, tid grc721.TokenID) {
42 if !e.revealed {
43 panic("spike-lock: transfers are locked until reveal")
44 }
45 e.moves++
46}
47
48func (e *lockExt) OnBurn(tid grc721.TokenID) {
49 if !e.revealed {
50 panic("spike-lock: burns are locked until reveal")
51 }
52}
53
54var (
55 tok *grc721.Token
56 ledger *grc721.PrivateLedger
57 roy *royalty.Royalty
58 lock = &lockExt{}
59 nextTID int64 = 1
60)
61
62// Create builds the token once. The rlm handed to NewToken is a sub-realm
63// token, so the hypothesis is Token.ID() == "<this pkgpath>#c/1.SPK.<seq>".
64func Create(cur realm) string {
65 if tok != nil {
66 panic("already created")
67 }
68 t, led := grc721.NewToken("Spike", "SPK", 1, cur.Sub(tokenSub))
69 led.RegisterExtension(lock)
70 r, royLed := royalty.NewRoyalty(led, 1000)
71 if err := royLed.SetDefaultRoyalty(chain.DerivePkgSubAddr(cur.PkgPath(), royaltySub), royaltyBps); err != nil {
72 panic(err)
73 }
74 tok, ledger, roy = t, led, r
75 return tok.ID()
76}
77
78// Mint mints the next token to the caller; allowed while locked.
79func Mint(cur realm) string {
80 mustCreated()
81 tid := strconv.FormatInt(nextTID, 10)
82 nextTID++
83 if err := ledger.Mint(cur.Previous().Address(), grc721.TokenID(tid)); err != nil {
84 panic(err)
85 }
86 return tid
87}
88
89func Approve(cur realm, tid string, to address) {
90 mustCreated()
91 if err := ledger.Approve(cur.Previous().Address(), to, grc721.TokenID(tid)); err != nil {
92 panic(err)
93 }
94}
95
96func Transfer(cur realm, tid string, to address) {
97 mustCreated()
98 caller := cur.Previous().Address()
99 if err := ledger.TransferFrom(caller, caller, to, grc721.TokenID(tid)); err != nil {
100 panic(err)
101 }
102}
103
104func Burn(cur realm, tid string) {
105 mustCreated()
106 owner, err := tok.OwnerOf(grc721.TokenID(tid))
107 if err != nil {
108 panic(err)
109 }
110 if owner != cur.Previous().Address() {
111 panic("only the owner can burn")
112 }
113 if err := ledger.Burn(grc721.TokenID(tid)); err != nil {
114 panic(err)
115 }
116}
117
118// Reveal lifts the lock forever.
119func Reveal(cur realm) {
120 lock.revealed = true
121}
122
123// Sweep spends the whole roy/1 sub-treasury to `to` with a banker bound to
124// the sub-realm identity (interrealm v2 §5.5 example).
125func Sweep(cur realm, to address) int64 {
126 sub := cur.Sub(royaltySub)
127 bal := banker.NewReadonlyBanker().GetCoin(sub.Address(), "ugnot")
128 if bal > 0 {
129 banker.NewBanker(banker.BankerTypeRealmSend, sub).
130 SendCoins(sub.Address(), to, chain.NewCoins(chain.NewCoin("ugnot", bal)))
131 }
132 return bal
133}
134
135// --- reads -----------------------------------------------------------------
136
137func Token() *grc721.Token { return tok }
138
139func TokenID() string {
140 mustCreated()
141 return tok.ID()
142}
143
144func OwnerOf(tid string) (address, error) {
145 mustCreated()
146 return tok.OwnerOf(grc721.TokenID(tid))
147}
148
149func Moves() int { return lock.moves }
150
151func IsRevealed() bool { return lock.revealed }
152
153// RoyaltyAddress is computed without cur, the way an off-chain tool or
154// another realm would; it must equal the receiver stored in the royalty.
155func RoyaltyAddress() address {
156 return chain.DerivePkgSubAddr("gno.land/r/g19dzk54hhsdc3gllpksuj6gu4a3e6fl8cr6rn4w/minhwa/spike/subtoken", royaltySub)
157}
158
159func RoyaltyInfo(tid string, price int64) (address, int64, error) {
160 mustCreated()
161 return roy.RoyaltyInfo(grc721.TokenID(tid), price)
162}
163
164func RoyaltyBalance() int64 {
165 return banker.NewReadonlyBanker().GetCoin(RoyaltyAddress(), "ugnot")
166}
167
168// Render also answers spike §5.5-6: does gnoweb display a data: SVG image?
169func Render(path string) string {
170 out := "# spike/subtoken\n\n"
171 if tok == nil {
172 return out + "not created yet\n"
173 }
174 out += "- token id: `" + tok.ID() + "`\n"
175 out += "- revealed: " + strconv.FormatBool(lock.revealed) + ", moves: " + strconv.Itoa(lock.moves) + "\n"
176 out += "- royalty address: `" + RoyaltyAddress().String() + "`\n\n"
177 out += "\n"
178 return out
179}
180
181func mustCreated() {
182 if tok == nil {
183 panic("call Create first")
184 }
185}