1import smartpy as sp
2
3# FA2 standard: https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md
4# Documentation: https://smartpy.io/guides/FA2-lib/overview
5
6
7@sp.module
8def main():
9 balance_of_args: type = sp.record(
10 requests=sp.list[sp.record(owner=sp.address, token_id=sp.nat)],
11 callback=sp.contract[
12 sp.list[
13 sp.record(
14 request=sp.record(owner=sp.address, token_id=sp.nat), balance=sp.nat
15 ).layout(("request", "balance"))
16 ]
17 ],
18 ).layout(("requests", "callback"))
19
20 class Fa2NftMinimal(sp.Contract):
21 """Minimal FA2 contract for NFTs.
22
23 This is a minimal self contained implementation example showing how to
24 implement an NFT contract following the FA2 standard in SmartPy. It is for
25 illustrative purposes only. For a more flexible toolbox aimed at real world
26 applications please refer to FA2_lib."
27 """
28
29 def __init__(self, administrator, metadata):
30 self.data.administrator = administrator
31 self.data.ledger = sp.cast(sp.big_map(), sp.big_map[sp.nat, sp.address])
32 self.data.metadata = metadata
33 self.data.next_token_id = sp.nat(0)
34 self.data.operators = sp.cast(
35 sp.big_map(),
36 sp.big_map[
37 sp.record(
38 owner=sp.address,
39 operator=sp.address,
40 token_id=sp.nat,
41 ).layout(("owner", ("operator", "token_id"))),
42 sp.unit,
43 ],
44 )
45 self.data.token_metadata = sp.cast(
46 sp.big_map(),
47 sp.big_map[
48 sp.nat,
49 sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
50 ],
51 )
52
53 # TODO: pass metadata_base as an argument
54 # metadata_base["views"] = [
55 # self.all_tokens,
56 # self.get_balance,
57 # self.is_operator,
58 # self.total_supply,
59 # ]
60 # self.init_metadata("metadata_base", metadata_base)
61
62 @sp.entrypoint
63 def transfer(self, batch):
64 """Accept a list of transfer operations.
65
66 Each transfer operation specifies a source: `from_` and a list
67 of transactions. Each transaction specifies the destination: `to_`,
68 the `token_id` and the `amount` to be transferred.
69
70 Args:
71 batch: List of transfer operations.
72 Raises:
73 `FA2_TOKEN_UNDEFINED`, `FA2_NOT_OPERATOR`, `FA2_INSUFFICIENT_BALANCE`
74 """
75 for transfer in batch:
76 for tx in transfer.txs:
77 sp.cast(
78 tx,
79 sp.record(
80 to_=sp.address, token_id=sp.nat, amount=sp.nat
81 ).layout(("to_", ("token_id", "amount"))),
82 )
83 assert tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
84 assert transfer.from_ == sp.sender or self.data.operators.contains(
85 sp.record(
86 owner=transfer.from_,
87 operator=sp.sender,
88 token_id=tx.token_id,
89 )
90 ), "FA2_NOT_OPERATOR"
91 if tx.amount > 0:
92 assert (
93 tx.amount == 1
94 and self.data.ledger[tx.token_id] == transfer.from_
95 ), "FA2_INSUFFICIENT_BALANCE"
96 self.data.ledger[tx.token_id] = tx.to_
97
98 @sp.entrypoint
99 def update_operators(self, actions):
100 """Accept a list of variants to add or remove operators.
101
102 Operators can perform transfer on behalf of the owner.
103 Owner is a Tezos address which can hold tokens.
104
105 Only the owner can change its operators.
106
107 Args:
108 actions: List of operator update actions.
109 Raises:
110 `FA2_NOT_OWNER`
111 """
112 for action in actions:
113 with sp.match(action):
114 with sp.case.add_operator as operator:
115 assert operator.owner == sp.sender, "FA2_NOT_OWNER"
116 self.data.operators[operator] = ()
117 with sp.case.remove_operator as operator:
118 assert operator.owner == sp.sender, "FA2_NOT_OWNER"
119 del self.data.operators[operator]
120
121 @sp.entrypoint
122 def balance_of(self, param):
123 """Send the balance of multiple account / token pairs to a callback
124 address.
125
126 transfer 0 mutez to `callback` with corresponding response.
127
128 Args:
129 callback (contract): Where to callback the answer.
130 requests: List of requested balances.
131 Raises:
132 `FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND`
133 """
134 sp.cast(param, balance_of_args)
135 balances = []
136 for req in param.requests:
137 assert req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
138 balances.push(
139 sp.record(
140 request=sp.record(owner=req.owner, token_id=req.token_id),
141 balance=(
142 1 if self.data.ledger[req.token_id] == req.owner else 0
143 ),
144 )
145 )
146
147 sp.transfer(reversed(balances), sp.mutez(0), param.callback)
148
149 @sp.entrypoint
150 def mint(self, to_, metadata):
151 """(Admin only) Create a new token with an incremented id and assign
152 it. to `to_`.
153
154 Args:
155 to_ (address): Receiver of the tokens.
156 metadata (map of string bytes): Metadata of the token.
157 Raises:
158 `FA2_NOT_ADMIN`
159 """
160 assert sp.sender == self.data.administrator, "FA2_NOT_ADMIN"
161 token_id = self.data.next_token_id
162 self.data.token_metadata[token_id] = sp.record(
163 token_id=token_id, token_info=metadata
164 )
165 self.data.ledger[token_id] = to_
166 self.data.next_token_id += 1
167
168 @sp.offchain_view
169 def all_tokens(self):
170 """Return the list of all the `token_id` known to the contract."""
171 return range(0, self.data.next_token_id)
172
173 @sp.offchain_view
174 def get_balance(self, params):
175 """Return the balance of an address for the specified `token_id`."""
176 sp.cast(
177 params,
178 sp.record(owner=sp.address, token_id=sp.nat).layout(
179 ("owner", "token_id")
180 ),
181 )
182 assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
183 return (
184 sp.nat(1)
185 if self.data.ledger[params.token_id] == params.owner
186 else sp.nat(0)
187 )
188
189 @sp.offchain_view
190 def total_supply(self, params):
191 """Return the total number of tokens for the given `token_id` if known
192 or fail if not."""
193 assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
194 return 1
195
196 @sp.offchain_view
197 def is_operator(self, params):
198 """Return whether `operator` is allowed to transfer `token_id` tokens
199 owned by `owner`."""
200 return self.data.operators.contains(params)
201
202 class Fa2NftMinimalTest(Fa2NftMinimal):
203 def __init__(
204 self, administrator, metadata, ledger, token_metadata, next_token_id
205 ):
206 Fa2NftMinimal.__init__(self, administrator, metadata)
207
208 self.data.next_token_id = next_token_id
209 self.data.ledger = ledger
210 self.data.token_metadata = token_metadata
211
212
213# metadata_base = {
214# "name": "FA2 NFT minimal",
215# "version": "1.0.0",
216# "description": "This is a minimal implementation of FA2 (TZIP-012) using SmartPy.",
217# "interfaces": ["TZIP-012", "TZIP-016"],
218# "authors": ["SmartPy <https://smartpy.io/#contact>"],
219# "homepage": "https://smartpy.io/ide?template=fa2_nft_minimal.py",
220# "source": {
221# "tools": ["SmartPy"],
222# "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/fa2_nft_minimal.py",
223# },
224# "permissions": {
225# "operator": "owner-or-operator-transfer",
226# "receiver": "owner-no-hook",
227# "sender": "owner-no-hook",
228# },
229# }
230
231if "main" in __name__:
232
233 def make_metadata(symbol, name, decimals):
234 """Helper function to build metadata JSON bytes values."""
235 return sp.map(
236 l={
237 "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals),
238 "name": sp.scenario_utils.bytes_of_string(name),
239 "symbol": sp.scenario_utils.bytes_of_string(symbol),
240 }
241 )
242
243 admin = sp.test_account("Administrator")
244 alice = sp.test_account("Alice")
245 tok0_md = make_metadata(name="Token Zero", decimals=1, symbol="Tok0")
246 tok1_md = make_metadata(name="Token One", decimals=1, symbol="Tok1")
247 tok2_md = make_metadata(name="Token Two", decimals=1, symbol="Tok2")
248
249 @sp.add_test()
250 def test():
251 scenario = sp.test_scenario("Minimal test", main)
252 c1 = main.Fa2NftMinimal(
253 admin.address, sp.scenario_utils.metadata_of_url("https://example.com")
254 )
255 scenario += c1
256
257 from smartpy.templates import fa2_lib_testing as testing
258
259 kwargs = {
260 "class_": main.Fa2NftMinimalTest,
261 "kwargs": {
262 "administrator": admin.address,
263 "metadata": sp.scenario_utils.metadata_of_url("https://example.com"),
264 "ledger": sp.big_map(
265 {0: alice.address, 1: alice.address, 2: alice.address}
266 ),
267 "token_metadata": sp.big_map(
268 {
269 0: sp.record(token_id=0, token_info=tok0_md),
270 1: sp.record(token_id=1, token_info=tok1_md),
271 2: sp.record(token_id=2, token_info=tok2_md),
272 }
273 ),
274 "next_token_id": 3,
275 },
276 "ledger_type": "NFT",
277 "test_name": "",
278 "modules": main,
279 }
280
281 testing.test_core_interfaces(**kwargs)
282 testing.test_transfer(**kwargs)
283 testing.test_balance_of(**kwargs)
284 testing.test_owner_or_operator_transfer(**kwargs)