item.gno
6.40 Kb · 251 lines
1package item
2
3import (
4 "chain/runtime/unsafe"
5 "strconv"
6
7 "gno.land/p/akkadia/beta/accesscontrol"
8 "gno.land/p/akkadia/beta/grc1155"
9 "gno.land/p/nt/bptree/v0"
10 "gno.land/r/akkadia/beta/admin"
11)
12
13const (
14 maxInt64 = int64(9223372036854775807)
15)
16
17var (
18 itemToken = grc1155.NewBasicGRC1155Token("")
19 itemSupplies *bptree.BPTree
20 ownerTokenIDs *bptree.BPTree
21)
22
23func init() { resetItemState() }
24
25func resetItemState() {
26 itemToken = grc1155.NewBasicGRC1155Token("")
27 itemSupplies = bptree.NewBPTree32()
28 ownerTokenIDs = bptree.NewBPTree32()
29 minterRealms = bptree.NewBPTree32()
30 listLimit = 100
31}
32
33func SafeMint(cur realm, recipient address, tokenID string, amount int64) {
34 executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
35 assertItemNoCoins()
36 mintItem(cross(cur), executor, recipient, tokenID, amount)
37}
38
39func SafeMintFromRealm(cur realm, recipient address, tokenID string, amount int64) {
40 executor := mustGetMinterRealmCaller(0, cur)
41 if unsafe.OriginCaller() != recipient {
42 panic("item recipient must be origin caller")
43 }
44 mintItem(cross(cur), executor, recipient, tokenID, amount)
45}
46
47func Uri() string { return itemToken.Uri() }
48func IsApprovedForAll(owner, operator address) bool {
49 return itemToken.IsApprovedForAll(owner, operator)
50}
51
52func SetApprovalForAll(cur realm, operator address, approved bool) {
53 caller := accesscontrol.MustGetUserCaller(0, cur)
54 assertItemNoCoins()
55 if err := itemToken.SetApprovalForAll(caller, operator, approved); err != nil {
56 panic("item operator approval failed: " + err.Error())
57 }
58}
59
60func SafeTransferFrom(cur realm, from, to address, tokenID string, amount int64) {
61 caller := accesscontrol.MustGetUserCaller(0, cur)
62 assertItemNoCoins()
63 assertItemTokenID(tokenID)
64 assertItemAmount(amount, "transfer")
65 if err := itemToken.SafeTransferFrom(caller, from, to, grc1155.TokenID(tokenID), amount); err != nil {
66 panic("item transfer failed: " + err.Error())
67 }
68 if from != to {
69 addOwnerTokenID(to, tokenID)
70 if BalanceOf(from, tokenID) == 0 {
71 removeOwnerTokenID(from, tokenID)
72 }
73 }
74}
75
76func BalanceOf(owner address, tokenID string) int64 {
77 if !owner.IsValid() {
78 panic("invalid item owner")
79 }
80 assertItemTokenID(tokenID)
81 balance, err := itemToken.BalanceOf(owner, grc1155.TokenID(tokenID))
82 if err != nil {
83 panic("item balance query failed: " + err.Error())
84 }
85 return balance
86}
87
88func TotalSupply(tokenID string) int64 {
89 assertItemTokenID(tokenID)
90 value := itemSupplies.Get(tokenID)
91 if value == nil {
92 return 0
93 }
94 return value.(int64)
95}
96
97func GetOwnedCount(owner address) int {
98 if !owner.IsValid() {
99 panic("invalid item owner")
100 }
101 ids, found := getOwnerTokenIDs(owner)
102 if !found {
103 return 0
104 }
105 return ids.Size()
106}
107
108func ListByOwner(owner address, page, count int) []map[string]string {
109 if !owner.IsValid() {
110 panic("invalid item owner")
111 }
112 assertItemListPageCount(page, count)
113 ids, found := getOwnerTokenIDs(owner)
114 if !found {
115 return []map[string]string{}
116 }
117 result := []map[string]string{}
118 ids.IterateByOffset((page-1)*count, count, func(tokenID string, _ any) bool {
119 result = append(result, map[string]string{
120 "tokenId": tokenID,
121 "amount": strconv.FormatInt(BalanceOf(owner, tokenID), 10),
122 })
123 return false
124 })
125 return result
126}
127
128func Burn(cur realm, from address, tokenID string, amount int64) {
129 caller := accesscontrol.MustGetUserCaller(0, cur)
130 assertItemNoCoins()
131 assertItemTokenID(tokenID)
132 if caller != from && !itemToken.IsApprovedForAll(from, caller) {
133 panic("item owner or approved operator required")
134 }
135 assertItemAmount(amount, "burn")
136 burnItem(cross(cur), caller, from, tokenID, amount)
137}
138
139func BurnFromRealm(cur realm, from address, tokenID string, amount int64) {
140 caller := mustGetMinterRealmCaller(0, cur)
141 if unsafe.OriginCaller() != from {
142 panic("item owner must be origin caller")
143 }
144 assertItemTokenID(tokenID)
145 assertItemAmount(amount, "burn")
146 burnItem(cross(cur), caller, from, tokenID, amount)
147}
148
149func mintItem(cur realm, executor, recipient address, tokenID string, amount int64) {
150 assertItemTokenID(tokenID)
151 if !recipient.IsValid() || recipient == cur.Address() {
152 panic("invalid item recipient")
153 }
154 if amount <= 0 {
155 panic("item amount must be positive")
156 }
157 supply := TotalSupply(tokenID)
158 if supply > maxInt64-amount {
159 panic("item amount overflow")
160 }
161 if err := itemToken.SafeMint(executor, recipient, grc1155.TokenID(tokenID), amount); err != nil {
162 panic("item mint failed: " + err.Error())
163 }
164 itemSupplies.Set(tokenID, supply+amount)
165 addOwnerTokenID(recipient, tokenID)
166}
167
168func burnItem(cur realm, operator, from address, tokenID string, amount int64) {
169 supply := TotalSupply(tokenID)
170 if supply < amount {
171 panic("item burn amount exceeds supply")
172 }
173 if err := itemToken.Burn(operator, from, grc1155.TokenID(tokenID), amount); err != nil {
174 panic("item burn failed: " + err.Error())
175 }
176 itemSupplies.Set(tokenID, supply-amount)
177 if BalanceOf(from, tokenID) == 0 {
178 removeOwnerTokenID(from, tokenID)
179 }
180}
181
182func mustGetMinterRealmCaller(_ int, rlm realm) address {
183 accesscontrol.AssertCurrentRealm(0, rlm)
184 prev := rlm.Previous()
185 if prev.IsUserCall() {
186 panic("user calls not allowed: function must be called by a realm")
187 }
188 caller := prev.Address()
189 if !IsMinterRealm(caller) {
190 panic("minter realm required")
191 }
192 return caller
193}
194
195func assertItemNoCoins() {
196 if len(unsafe.OriginSend()) != 0 {
197 panic("attached coins not allowed")
198 }
199}
200
201func assertItemTokenID(tokenID string) {
202 if tokenID == "" {
203 panic("token id is required")
204 }
205}
206
207func assertItemAmount(amount int64, action string) {
208 if amount <= 0 {
209 panic("item " + action + " amount must be positive")
210 }
211}
212
213func assertItemListPageCount(page, count int) {
214 if page < 1 {
215 panic("page must be at least 1")
216 }
217 if count < 1 {
218 panic("count must be at least 1")
219 }
220 if count > listLimit {
221 panic("count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")")
222 }
223}
224
225func addOwnerTokenID(owner address, tokenID string) {
226 value := ownerTokenIDs.Get(owner.String())
227 var ids *bptree.BPTree
228 if value != nil {
229 ids = value.(*bptree.BPTree)
230 } else {
231 ids = bptree.NewBPTree32()
232 ownerTokenIDs.Set(owner.String(), ids)
233 }
234 ids.Set(tokenID, true)
235}
236
237func removeOwnerTokenID(owner address, tokenID string) {
238 ids, found := getOwnerTokenIDs(owner)
239 if !found {
240 return
241 }
242 ids.Remove(tokenID)
243}
244
245func getOwnerTokenIDs(owner address) (*bptree.BPTree, bool) {
246 value := ownerTokenIDs.Get(owner.String())
247 if value == nil {
248 return nil, false
249 }
250 return value.(*bptree.BPTree), true
251}