templates.fa2_fungible_minimal

  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)