-
Notifications
You must be signed in to change notification settings - Fork 78
/
nft.go
279 lines (243 loc) · 8.38 KB
/
nft.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
/*
Package nft contains non-divisible non-fungible NEP11-compatible token
implementation. This token can be minted with GAS transfer to contract address,
it will hash some data (including data provided in transfer) and produce
base64-encoded string that is your NFT. Since it's based on hashing and basically
you own a hash it's HASHY.
*/
package nft
import (
"github.com/nspcc-dev/neo-go/pkg/interop"
"github.com/nspcc-dev/neo-go/pkg/interop/contract"
"github.com/nspcc-dev/neo-go/pkg/interop/iterator"
"github.com/nspcc-dev/neo-go/pkg/interop/native/crypto"
"github.com/nspcc-dev/neo-go/pkg/interop/native/gas"
"github.com/nspcc-dev/neo-go/pkg/interop/native/management"
"github.com/nspcc-dev/neo-go/pkg/interop/native/std"
"github.com/nspcc-dev/neo-go/pkg/interop/runtime"
"github.com/nspcc-dev/neo-go/pkg/interop/storage"
"github.com/nspcc-dev/neo-go/pkg/interop/util"
)
// Prefixes used for contract data storage.
const (
totalSupplyPrefix = "s"
// balancePrefix contains map from addresses to balances.
balancePrefix = "b"
// accountPrefix contains map from address + token id to tokens
accountPrefix = "a"
// tokenPrefix contains map from token id to it's owner.
tokenPrefix = "t"
)
var (
// contractOwner is a special address that can perform some management
// functions on this contract like updating/destroying it and can also
// be used for contract address verification.
contractOwner = util.FromAddress("NbrUYaZgyhSkNoRo9ugRyEMdUZxrhkNaWB")
)
// Symbol returns token symbol, it's HASHY.
func Symbol() string {
return "HASHY"
}
// Decimals returns token decimals, this NFT is non-divisible, so it's 0.
func Decimals() int {
return 0
}
// TotalSupply is a contract method that returns the number of tokens minted.
func TotalSupply() int {
return totalSupply(storage.GetReadOnlyContext())
}
// totalSupply is an internal implementation of TotalSupply operating with
// given context. The number itself is stored raw in the DB with totalSupplyPrefix
// key.
func totalSupply(ctx storage.Context) int {
var res int
val := storage.Get(ctx, []byte(totalSupplyPrefix))
if val != nil {
res = val.(int)
}
return res
}
// mkAccountPrefix creates DB key-prefix for account tokens specified
// by concatenating accountPrefix and account address.
func mkAccountPrefix(holder interop.Hash160) []byte {
res := []byte(accountPrefix)
return append(res, holder...)
}
// mkBalanceKey creates DB key for account specified by concatenating balancePrefix
// and account address.
func mkBalanceKey(holder interop.Hash160) []byte {
res := []byte(balancePrefix)
return append(res, holder...)
}
// mkTokenKey creates DB key for token specified by concatenating tokenPrefix
// and token ID.
func mkTokenKey(tokenID []byte) []byte {
res := []byte(tokenPrefix)
return append(res, tokenID...)
}
// BalanceOf returns the number of tokens owned by specified address.
func BalanceOf(holder interop.Hash160) int {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
return getBalanceOf(ctx, mkBalanceKey(holder))
}
// getBalanceOf returns balance of the account using database key.
func getBalanceOf(ctx storage.Context, balanceKey []byte) int {
val := storage.Get(ctx, balanceKey)
if val != nil {
return val.(int)
}
return 0
}
// addToBalance adds amount to the account balance. Amount can be negative.
func addToBalance(ctx storage.Context, holder interop.Hash160, amount int) {
key := mkBalanceKey(holder)
old := getBalanceOf(ctx, key)
old += amount
if old > 0 {
storage.Put(ctx, key, old)
} else {
storage.Delete(ctx, key)
}
}
// addToken adds token to the account.
func addToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Put(ctx, append(key, token...), token)
}
// removeToken removes token from the account.
func removeToken(ctx storage.Context, holder interop.Hash160, token []byte) {
key := mkAccountPrefix(holder)
storage.Delete(ctx, append(key, token...))
}
// Tokens returns an iterator that contains all of the tokens minted by the contract.
func Tokens() iterator.Iterator {
ctx := storage.GetReadOnlyContext()
key := []byte(tokenPrefix)
iter := storage.Find(ctx, key, storage.RemovePrefix|storage.KeysOnly)
return iter
}
// TokensOf returns an iterator with all tokens held by specified address.
func TokensOf(holder interop.Hash160) iterator.Iterator {
if len(holder) != 20 {
panic("bad owner address")
}
ctx := storage.GetReadOnlyContext()
key := mkAccountPrefix(holder)
iter := storage.Find(ctx, key, storage.ValuesOnly)
return iter
}
// getOwnerOf returns current owner of the specified token or panics if token
// ID is invalid. Owner is stored as value of the token key (prefix + token ID).
func getOwnerOf(ctx storage.Context, token []byte) interop.Hash160 {
key := mkTokenKey(token)
val := storage.Get(ctx, key)
if val == nil {
panic("no token found")
}
return val.(interop.Hash160)
}
// setOwnerOf writes current owner of the specified token into the DB.
func setOwnerOf(ctx storage.Context, token []byte, holder interop.Hash160) {
key := mkTokenKey(token)
storage.Put(ctx, key, holder)
}
// OwnerOf returns owner of specified token.
func OwnerOf(token []byte) interop.Hash160 {
ctx := storage.GetReadOnlyContext()
return getOwnerOf(ctx, token)
}
// Transfer token from its owner to another user, notice that it only has three
// parameters because token owner can be deduced from token ID itself.
func Transfer(to interop.Hash160, token []byte, data interface{}) bool {
if len(to) != 20 {
panic("invalid 'to' address")
}
ctx := storage.GetContext()
owner := getOwnerOf(ctx, token)
// Note that although calling script hash is not checked explicitly in
// this contract it is in fact checked for in `CheckWitness` itself.
if !runtime.CheckWitness(owner) {
return false
}
if string(owner) != string(to) {
addToBalance(ctx, owner, -1)
removeToken(ctx, owner, token)
addToBalance(ctx, to, 1)
addToken(ctx, to, token)
setOwnerOf(ctx, token, to)
}
postTransfer(owner, to, token, data)
return true
}
// postTransfer emits Transfer event and calls onNEP11Payment if needed.
func postTransfer(from interop.Hash160, to interop.Hash160, token []byte, data interface{}) {
runtime.Notify("Transfer", from, to, 1, token)
if management.GetContract(to) != nil {
contract.Call(to, "onNEP11Payment", contract.All, from, 1, token, data)
}
}
// OnNEP17Payment mints tokens if at least 10 GAS is provided. You don't call
// this method directly, instead it's called by GAS contract when you transfer
// GAS from your address to the address of this NFT contract.
func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) {
if string(runtime.GetCallingScriptHash()) != gas.Hash {
panic("only GAS is accepted")
}
if amount < 10_00000000 {
panic("minting HASHY costs at least 10 GAS")
}
var tokIn = []byte{}
var ctx = storage.GetContext()
total := totalSupply(ctx)
tokIn = append(tokIn, []byte(std.Itoa(total, 10))...)
tokIn = append(tokIn, []byte(std.Itoa(amount, 10))...)
tokIn = append(tokIn, from...)
tx := runtime.GetScriptContainer()
tokIn = append(tokIn, tx.Hash...)
if data != nil {
tokIn = append(tokIn, std.Serialize(data)...)
}
tokenHash := crypto.Ripemd160(tokIn)
token := std.Base64Encode(tokenHash)
addToken(ctx, from, []byte(token))
setOwnerOf(ctx, []byte(token), from)
addToBalance(ctx, from, 1)
total++
storage.Put(ctx, []byte(totalSupplyPrefix), total)
postTransfer(nil, from, []byte(token), nil) // no `data` during minting
}
// Verify allows owner to manage contract's address, including earned GAS
// transfer from contract's address to somewhere else. It just checks for transaction
// to also be signed by contract owner, so contract's witness should be empty.
func Verify() bool {
return runtime.CheckWitness(contractOwner)
}
// Destroy destroys the contract, only owner can do that.
func Destroy() {
if !Verify() {
panic("only owner can destroy")
}
management.Destroy()
}
// Update updates the contract, only owner can do that.
func Update(nef, manifest []byte) {
if !Verify() {
panic("only owner can update")
}
management.Update(nef, manifest)
}
// Properties returns properties of the given NFT.
func Properties(id []byte) map[string]string {
ctx := storage.GetReadOnlyContext()
owner := storage.Get(ctx, mkTokenKey(id)).(interop.Hash160)
if owner == nil {
panic("unknown token")
}
result := map[string]string{
"name": "HASHY " + string(id),
}
return result
}