-
Notifications
You must be signed in to change notification settings - Fork 3
/
NFT.aes
265 lines (234 loc) · 13.2 KB
/
NFT.aes
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
@compiler >= 6
include "List.aes"
include "Option.aes"
include "String.aes"
// ak_11111111111111111111111111111111273Yts
contract interface Versioner =
entrypoint version : () => (string * list(string))
contract interface NFTReceiver =
entrypoint onNFTReceived : (address, address, int, string) => bool
contract NFT =
entrypoint version() : (string * list(string)) =
("NFT", ["0.1.0"])
/// Events.
/// TransferEvent(_from, _to, _tokenID)
/// ApprovalEvent(_owner, _approved, _tokenID)
/// ApprovalForAllEvent(_owner, _operator, _approved)
datatype event
= TransferEvent(indexed address, indexed address, indexed int)
| ApprovalEvent(indexed address, indexed address, indexed int)
| ApprovalForAllEvent(indexed address, indexed address, bool)
/// Data structure and init.
/// state represents the contract's state.
/// @property name is a human readable name for the contract
/// @property symbol is a short code that represents the contract
/// @property tokenData allocates metadata to a token. Metadata is in the form of
/// a list of which the elements are type(e.g. uri, object_id, etc), value
/// @property owners maps a tokenID to the owner's address
/// @property balances stores the balance (amount of tokens held) for an address
/// @property approvals holds approved accounts to do transactions on behalf
/// of the owner for a single tokenID
/// @property operators holds a list of authorizations to do transactions on
/// behalf of the owner for all tokens held
record state = {
name: string,
symbol: string,
owners: map(int, address),
balances: map(address, int),
approvals: map(int, address),
operators: map(address, map(address, bool)),
tokenData: map(int, (string * string)) }
/// init Initialize the contract
/// @param _name is a human readable name for the contract
/// @param _symbol is a short code that represents the contract
stateful entrypoint init(_name: string, _symbol: string) = {
name = _name,
symbol = _symbol,
tokenData = {},
owners = {},
balances = {},
approvals = {},
operators = {} }
/// getTokenData provides the token data for the requested token (if any)
/// @param _tokenID is the token id for which the uri is requested
/// @return Some(list(type, value)) or None if no uri has been set for this token
entrypoint getTokenData(_tokenID: int): option((string * string)) =
Map.lookup(_tokenID, state.tokenData)
/// getURI provides the URI (if any) for a token
/// @param _tokenID is the token id for which the uri is requested
/// @return Some(uri) or None if no uri has been set for this token
entrypoint getURI(_tokenID: int) : option(string) =
switch(Map.lookup(_tokenID, state.tokenData))
None => None
Some((t, v)) =>
if(t != "uri")
None
else
Some(v)
/// mint issues a new token to the provided address
/// @param _to is the address of the new token's owner
/// @param _tokenID is the id of the minted token
/// @dev throws if already minted
/// @return true after completion
stateful entrypoint mint(_to: address, _tokenID: int) : bool =
// TODO: Option to limit who may mint
require(Map.lookup(_tokenID, state.owners) == None, "Already minted")
put( state { balances[_to = 0] @ b = b + 1, owners[_tokenID] = _to } )
Chain.event(TransferEvent(Contract.address, _to, _tokenID))
true
/// mintWithTokenData issues a new token with metadata to the provided address
/// @param _to is the address of the new token's owner
/// @param _tokenID is the id of the minted token
/// @param _tokenDataType is the type of data the value represents, e.g. uri, object_id
/// @param _tokenDataValue is the data's value
/// @dev throws if already minted
/// @return true after completion
stateful entrypoint mintWithTokenData(_to: address, _tokenID: int, _tokenDataType: string, _tokenDataValue: string) : bool =
// TODO: Option to limit who may mint
require(Map.lookup(_tokenID, state.owners) == None, "Already minted")
put( state { balances[_to = 0] @ b = b + 1, owners[_tokenID] = _to, tokenData[_tokenID] = (String.to_lower(_tokenDataType), _tokenDataValue) } )
Chain.event(TransferEvent(Contract.address, _to, _tokenID))
true
/// safeMint wraps around mint and offers a safe way to mint to contract recipients
/// by checking whether the NFTReceiver interface is implemented on the receiving
/// contract.
/// @param _to is the address of the new token's owner
/// @param _tokenID is the id of the minted token
/// @param _data is data that will be forwarded to contact recipients
/// @dev throws if already minted
/// @return true after completion
stateful entrypoint safeMint(_to: address, _tokenID: int, _data: string) : bool =
// TODO: Option to limit who may mint
require(Map.lookup(_tokenID, state.owners) == None, "Already minted")
_invokeNFTReceiver(Contract.address, _to, _tokenID, _data)
mint(_to, _tokenID)
stateful entrypoint safeMintWithTokenData(_to: address, _tokenID: int, _tokenDataType: string, _tokenDataValue: string, _data: string) : bool =
// TODO: Option to limit who may mint
require(Map.lookup(_tokenID, state.owners) == None, "Already minted")
_invokeNFTReceiver(Contract.address, _to, _tokenID, _data)
mintWithTokenData(_to, _tokenID, _tokenDataType, _tokenDataValue)
/// burn will burn a token by setting the owner's address to the contract's address.
/// @param _tokenID is the id of the token
stateful entrypoint burn(_tokenID: int) : bool =
switch(Map.lookup(_tokenID, state.owners))
None => abort("invalid tokenID")
Some(owner) =>
require(Call.caller == owner, "caller is not owner")
removeApproval(_tokenID)
put(state { owners[_tokenID] = Contract.address, balances[owner] @b = b - 1 })
Chain.event(TransferEvent(owner, Contract.address, _tokenID))
true
/// balanceOf provides all tokens assigned to an owner
/// @param _owner is the address for whom to query the balance
/// @return the number of tokens owned by `_owner` or 0
entrypoint balanceOf(_owner: address) : int =
state.balances[_owner = 0]
/// ownerOf provides the owner of an NFT
/// @param _tokenId The identifier for an NFT
/// @return Some(address) or None
entrypoint ownerOf(_tokenID: int) : option(address) =
Map.lookup(_tokenID, state.owners)
/// safeTransferFromWithData transfers the ownership of an NFT from one address to another address
/// @dev Throws unless `msg.sender` is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if `_to` is the zero address. Throws if
/// `_tokenId` is not a valid NFT. When transfer is complete, this function
/// checks if `_to` is a smart contract. If so, it calls `onNFTReceived` on `_to`.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenID The token to transfer
/// @param data Additional data with no specified format, sent in call to `_to`
stateful entrypoint safeTransferFromWithData(_from: address, _to: address, _tokenID: int, _data: string) =
transferFrom(_from, _to, _tokenID)
_invokeNFTReceiver(_from, _to, _tokenID, _data)
function _invokeNFTReceiver(_from: address, _to: address, _tokenID: int, _data: string) : bool =
if(Address.is_contract(_to))
let c = Address.to_contract(_to)
switch(c.onNFTReceived(_from, _to, _tokenID, _data, protected = true) : option(bool))
None => false
Some(val) => val
else
false
/// safeTransferFrom transfers the ownership of an NFT from one address to another address
/// works identically to safeTransferFromWithData with that difference that the data
/// parameter is set to an empty string
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
stateful entrypoint safeTransferFrom(_from: address, _to: address, _tokenID: int) =
safeTransferFromWithData(_from, _to, _tokenID, "")
/// transferFrom transfers ownership of an NFT without any safety measures.
/// @dev Throws unless caller is the current owner, an authorized
/// operator, or the approved address for this NFT. Throws if `_from` is
/// not the current owner. Throws if _tokenId` is not a valid NFT.
/// @param _from The current owner of the NFT
/// @param _to The new owner
/// @param _tokenId The NFT to transfer
stateful entrypoint transferFrom(_from: address, _to: address, _tokenID: int) =
require(Call.caller == _from || isApproved(_tokenID, Call.caller) || isApprovedForAll(_from, Call.caller), "caller is not owner, approved or operator")
require(Map.member(_tokenID, state.owners), "invalid tokenID")
require(state.owners[_tokenID] == _from, "from address is not the owner of the token")
removeApproval(_tokenID)
put( state { balances[_from] @b = b - 1, balances[_to = 0] @nb = nb + 1, owners[_tokenID] = _to } )
Chain.event(TransferEvent(_from, _to, _tokenID))
/// approve sets or reaffirms the approved address for an NFT
/// @dev The zero address indicates there is no approved address.
/// @dev Throws unless caller is the current NFT owner, or an authorized
/// operator of the current owner.
/// @param _approved The new approved NFT controller
/// @param _tokenId The NFT to approve
stateful entrypoint approve(_approved: address, _tokenID: int) =
let owner = state.owners[_tokenID]
require(Call.caller == owner || isApprovedForAll(owner, Call.caller), "caller is not owner or operator")
// TODO: check for zero address and forward to remove approval for compatibility reasons
put( state { approvals[_tokenID] = _approved })
Chain.event(ApprovalEvent(state.owners[_tokenID], _approved, _tokenID))
stateful entrypoint removeApproval(_tokenID: int) =
switch(Map.lookup(_tokenID, state.owners))
None => abort("invalid tokenID")
Some(owner) =>
require(Call.caller == owner || Call.caller == state.approvals[_tokenID = ak_11111111111111111111111111111111273Yts] || isApprovedForAll(owner, Call.caller), "caller is not owner, approved or operator")
if(Map.member(_tokenID, state.approvals))
put( state { approvals = Map.delete(_tokenID, state.approvals)})
/// setApprovalForAll enables or disable approval for a manager (operator) to manage
/// all of the caller's assets.
/// @dev Emits the ApprovalForAll event. The contract allows
/// multiple operators per owner.
/// @param _operator Address to add to the set of authorized operators.
/// @param _approved True if the operator is approved, false to revoke approval
stateful entrypoint setApprovalForAll(_operator: address, _approved: bool) : bool =
require(Call.caller != _operator, "caller can't update own status")
put( state { operators = { [Call.caller] = { [_operator] = true }} } )
Chain.event(ApprovalForAllEvent(Call.caller, _operator, _approved))
true
/// getApproved provides the approved address for a token
/// @dev Throws if `_tokenId` is not a valid token
/// @param _tokenId The NFT to find the approved address for
/// @return The approved address for this NFT, or None if none is set
entrypoint getApproved(_tokenID: int) : option(address) =
require(Map.member(_tokenID, state.owners), "invalid tokenID")
Map.lookup(_tokenID, state.approvals)
entrypoint isApproved(_tokenID: int, _approved: address) : bool =
switch(getApproved(_tokenID))
None => false
Some(approved) => approved == _approved
/// isApprovedForAll shows wether an address is an authorized operator for another address
/// @param _owner The address that owns the NFTs
/// @param _operator The address that acts on behalf of the owner
/// @return True or false to indicate whether `_operator` is an approved operator or not
entrypoint isApprovedForAll(_owner: address, _operator: address) : bool =
// TODO: Check for a more efficient way
switch(Map.lookup(_owner, state.operators))
None => false
Some(approvals) =>
switch(Map.lookup(_operator, approvals))
None => false
Some(val) => val
/// HELPER FUNCTIONS
function _isNFTReceiver(_contract: NFTReceiver) : bool =
true
function _isCompatibleContract(_contract: Versioner) : bool =
switch(_contract.version(protected = true) : option((string * list(string))))
None => false
Some((t, v)) =>
t == "NFT" && List.contains("0.1.0", v)