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