templates.fa2_lib

FA2 library with the new SmartPy syntax.

Warning: Work in progress. Currently the metadata cannot be generated. It means that you cannot originate your contract with the metadata (offchain-views, contract's name and mandatory metadata keys)

  1"""
  2FA2 library with the new SmartPy syntax.
  3
  4Warning:
  5    Work in progress.
  6    Currently the metadata cannot be generated.
  7    It means that you cannot originate your contract with the metadata
  8    (offchain-views, contract's name and mandatory metadata keys)
  9"""
 10
 11import smartpy as sp
 12
 13
 14@sp.module
 15def t():
 16    operator_permission: type = sp.record(
 17        owner=sp.address, operator=sp.address, token_id=sp.nat
 18    ).layout(("owner", ("operator", "token_id")))
 19
 20    update_operators_params: type = list[
 21        sp.variant(
 22            add_operator=operator_permission, remove_operator=operator_permission
 23        )
 24    ]
 25
 26    tx: type = sp.record(
 27        to_=sp.address,
 28        token_id=sp.nat,
 29        amount=sp.nat,
 30    ).layout(("to_", ("token_id", "amount")))
 31
 32    transfer_batch: type = sp.record(
 33        from_=sp.address,
 34        txs=list[tx],
 35    ).layout(("from_", "txs"))
 36
 37    transfer_params: type = list[transfer_batch]
 38
 39    balance_of_request: type = sp.record(owner=sp.address, token_id=sp.nat).layout(
 40        ("owner", "token_id")
 41    )
 42
 43    balance_of_response: type = sp.record(
 44        request=balance_of_request, balance=sp.nat
 45    ).layout(("request", "balance"))
 46
 47    balance_of_params: type = sp.record(
 48        callback=sp.contract[list[balance_of_response]],
 49        requests=list[balance_of_request],
 50    ).layout(("requests", "callback"))
 51
 52    balance_params: type = sp.pair[
 53        sp.lambda_(sp.nat, sp.bool, with_storage="read-only"), balance_of_request
 54    ]
 55
 56    token_metadata: type = sp.big_map[
 57        sp.nat,
 58        sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]).layout(
 59            ("token_id", "token_info")
 60        ),
 61    ]
 62
 63    metadata: type = sp.big_map[sp.string, sp.bytes]
 64
 65    ledger_nft: type = sp.big_map[sp.nat, sp.address]
 66
 67    ledger_fungible: type = sp.big_map[sp.pair[sp.address, sp.nat], sp.nat]
 68
 69    ledger_single_asset: type = sp.big_map[sp.address, sp.nat]
 70
 71    supply_fungible: type = sp.big_map[sp.nat, sp.nat]
 72
 73    tx_transfer_permission: type = sp.record(
 74        from_=sp.address, to_=sp.address, token_id=sp.nat
 75    )
 76
 77
 78@sp.module
 79def main():
 80    ############
 81    # Policies #
 82    ############
 83
 84    class NoTransfer(sp.Contract):
 85        """No transfer allowed."""
 86
 87        def __init__(self):
 88            self.private.policy = sp.record(
 89                name="no-transfer",
 90                supports_transfer=False,
 91                supports_operator=False,
 92            )
 93
 94        @sp.private()
 95        def check_operator_update_permissions_(self, operator_permission):
 96            sp.cast(operator_permission, t.operator_permission)
 97            raise "FA2_OPERATORS_UNSUPPORTED"
 98            return ()
 99
100        @sp.private()
101        def check_tx_transfer_permissions_(self, params):
102            sp.cast(params, t.tx_transfer_permission)
103            raise "FA2_TX_DENIED"
104            return ()
105
106        @sp.private()
107        def is_operator_(self, operator_permission):
108            sp.cast(operator_permission, t.operator_permission)
109            return False
110
111    class OwnerTransfer(sp.Contract):
112        """Only owner are allowed to transfer, no operators."""
113
114        def __init__(self):
115            self.private.policy = sp.record(
116                name="owner-transfer",
117                supports_transfer=True,
118                supports_operator=False,
119            )
120
121        @sp.private()
122        def check_operator_update_permissions_(self, operator_permission):
123            sp.cast(operator_permission, t.operator_permission)
124            raise "FA2_OPERATORS_UNSUPPORTED"
125            return ()
126
127        @sp.private()
128        def check_tx_transfer_permissions_(self, params):
129            sp.cast(params, t.tx_transfer_permission)
130            assert sp.sender == params.from_, "FA2_NOT_OWNER"
131
132        @sp.private()
133        def is_operator_(self, operator_permission):
134            sp.cast(operator_permission, t.operator_permission)
135            return False
136
137    class OwnerOrOperatorTransfer(sp.Contract):
138        """Owner or operator are allowed to transfer."""
139
140        def __init__(self):
141            self.private.policy = sp.record(
142                name="owner-or-operator-transfer",
143                supports_transfer=True,
144                supports_operator=True,
145            )
146            self.data.operators = sp.cast(
147                sp.big_map(), sp.big_map[t.operator_permission, sp.unit]
148            )
149
150        @sp.private()
151        def check_operator_update_permissions_(self, operator_permission):
152            sp.cast(operator_permission, t.operator_permission)
153            assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER"
154
155        @sp.private(with_storage="read-only")
156        def check_tx_transfer_permissions_(self, params):
157            sp.cast(params, t.tx_transfer_permission)
158            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
159            assert (sp.sender == params.from_) or self.data.operators.contains(
160                sp.record(
161                    owner=params.from_, operator=sp.sender, token_id=params.token_id
162                )
163            ), "FA2_NOT_OPERATOR"
164
165        @sp.private(with_storage="read-only")
166        def is_operator_(self, operator_permission):
167            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
168            return self.data.operators.contains(operator_permission)
169
170    ##########
171    # Common #
172    ##########
173
174    class CommonInterface(OwnerOrOperatorTransfer):
175        def __init__(self):
176            OwnerOrOperatorTransfer.__init__(self)
177            self.data.token_metadata = sp.cast(sp.big_map(), t.token_metadata)
178            self.data.metadata = sp.cast(sp.big_map(), t.metadata)
179            self.data.next_token_id = 0
180
181        @sp.private()
182        def balance_(self, params):
183            """Return the balance of an account.
184            Must be redefined in child"""
185            sp.cast(params, t.balance_params)
186            raise "NotImplemented"
187            return 0
188
189        @sp.private()
190        def is_defined_(self, token_id):
191            """Return True if the token is defined, else otherwise.
192            Must be redefined in child"""
193            sp.cast(token_id, sp.nat)
194            raise "NotImplemented"
195            return False
196
197        @sp.private()
198        def transfer_tx_(self, params):
199            """Perform the transfer action.
200            Must be redefined in child"""
201            sp.cast(params, sp.record(from_=sp.address, tx=t.tx))
202            raise "NotImplemented"
203            return ()
204
205        @sp.private()
206        def supply_(self, params):
207            """Return the supply of a token.
208            Must be redefined in child"""
209            (is_defined, token_id) = params
210            sp.cast(token_id, sp.nat)
211            raise "NotImplemented"
212            return 0
213
214    class Common(CommonInterface):
215        """Common logic between Nft, Fungible and SingleAsset."""
216
217        def __init__(self, metadata):
218            CommonInterface.__init__(self)
219            self.data.metadata = sp.cast(metadata, t.metadata)
220
221        @sp.private(with_storage="read-only")
222        def is_defined_(self, token_id):
223            """Return True if the token is defined, else otherwise."""
224            return self.data.token_metadata.contains(token_id)
225
226        # Entrypoints
227
228        @sp.entrypoint
229        def update_operators(self, batch):
230            """Accept a list of variants to add or remove operators who can perform
231            transfers on behalf of the owner."""
232            sp.cast(batch, t.update_operators_params)
233            if self.private.policy.supports_operator:
234                for action in batch:
235                    with sp.match(action):
236                        with sp.case.add_operator as operator:
237                            _ = self.check_operator_update_permissions_(operator)
238                            self.data.operators[operator] = ()
239                        with sp.case.remove_operator as operator:
240                            _ = self.check_operator_update_permissions_(operator)
241                            del self.data.operators[operator]
242            else:
243                raise "FA2_OPERATORS_UNSUPPORTED"
244
245        @sp.entrypoint
246        def balance_of(self, params):
247            """Send the balance of multiple account / token pairs to a callback
248            address.
249
250            `balance_` and `is_defined_` must be defined in the child class.
251            """
252            sp.cast(params, t.balance_of_params)
253
254            @sp.effects(with_storage="read-write")
255            def f_process_request(param):
256                (req, is_defined, balance) = param
257                sp.cast(req, t.balance_of_request)
258                return sp.cast(
259                    sp.record(
260                        request=req,
261                        balance=balance((is_defined, req)),
262                    ),
263                    t.balance_of_response,
264                )
265
266            answers = [
267                f_process_request((x, self.is_defined_, self.balance_))
268                for x in params.requests
269            ]
270            sp.transfer(answers, sp.mutez(0), params.callback)
271
272        @sp.entrypoint
273        def transfer(self, batch):
274            """Accept a list of transfer operations between a source and multiple
275            destinations.
276
277            `transfer_tx_` and `is_defined_` must be defined in the child class.
278            """
279            sp.cast(batch, t.transfer_params)
280            if self.private.policy.supports_transfer:
281                for transfer in batch:
282                    for tx in transfer.txs:
283                        # The ordering of assert is important:
284                        # 1) token_undefined, 2) transfer permission 3) balance
285                        assert self.is_defined_(tx.token_id), "FA2_TOKEN_UNDEFINED"
286                        _ = self.check_tx_transfer_permissions_(
287                            sp.record(
288                                from_=transfer.from_, to_=tx.to_, token_id=tx.token_id
289                            )
290                        )
291                        if tx.amount > 0:
292                            tx_params = sp.record(from_=transfer.from_, tx=tx)
293                            self.transfer_tx_(tx_params)
294            else:
295                raise "FA2_TX_DENIED"
296
297        # Offchain views
298
299        @sp.offchain_view()
300        def all_tokens(self):
301            """Return the list of all the token IDs known to the contract."""
302            return range(0, self.data.next_token_id)
303
304        @sp.offchain_view()
305        def is_operator(self, operator_permission):
306            """Return whether `operator` is allowed to transfer `token_id` tokens
307            owned by `owner`."""
308            sp.cast(operator_permission, t.operator_permission)
309            return self.is_operator_(operator_permission)
310
311        @sp.offchain_view
312        def get_balance(self, params):
313            """Return the balance of an address for the specified `token_id`."""
314            sp.cast(params, t.balance_of_request)
315            return self.balance_((self.is_defined_, params))
316
317        @sp.offchain_view()
318        def total_supply(self, params):
319            """Return the total number of tokens for the given `token_id`."""
320            supply = self.supply_((self.is_defined_, params.token_id))
321            return sp.cast(supply, sp.nat)
322
323    ################
324    # Base classes #
325    ################
326
327    class NftInterface(sp.Contract):
328        def __init__(self):
329            self.data.ledger = sp.cast(sp.big_map(), t.ledger_nft)
330            self.private.ledger_type = "NFT"
331
332    class Nft(NftInterface, Common):
333        def __init__(self, metadata, ledger, token_metadata):
334            Common.__init__(self, metadata)
335            NftInterface.__init__(self)
336
337            for token_info in token_metadata:
338                self.data.token_metadata[self.data.next_token_id] = sp.record(
339                    token_id=self.data.next_token_id, token_info=token_info
340                )
341                self.data.next_token_id += 1
342
343            for token in ledger.items():
344                assert self.data.token_metadata.contains(
345                    token.key
346                ), "The `ledger` parameter contains a key no contained in `token_metadata`."
347                self.data.ledger[token.key] = token.value
348
349        @sp.private(with_storage="read-only")
350        def balance_(self, params):
351            (is_defined, balance_params) = params
352            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
353            return (
354                1
355                if self.data.ledger[balance_params.token_id] == balance_params.owner
356                else 0
357            )
358
359        @sp.private(with_storage="read-write")
360        def transfer_tx_(self, params):
361            assert (
362                params.tx.amount == 1
363                and self.data.ledger[params.tx.token_id] == params.from_
364            ), "FA2_INSUFFICIENT_BALANCE"
365
366            # Makes the transfer
367            self.data.ledger[params.tx.token_id] = params.tx.to_
368
369        @sp.private(with_storage="read-only")
370        def supply_(self, params):
371            (is_defined, token_id) = params
372            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
373            return sp.cast(1, sp.nat)
374
375    class FungibleInterface(sp.Contract):
376        def __init__(self):
377            self.private.ledger_type = "Fungible"
378            self.data.ledger = sp.cast(sp.big_map(), t.ledger_fungible)
379            self.data.supply = sp.cast(sp.big_map(), t.supply_fungible)
380
381    class Fungible(FungibleInterface, Common):
382        def __init__(self, metadata, ledger, token_metadata):
383            Common.__init__(self, metadata)
384            FungibleInterface.__init__(self)
385
386            for token_info in token_metadata:
387                token_id = self.data.next_token_id
388                self.data.token_metadata[token_id] = sp.record(
389                    token_id=token_id, token_info=token_info
390                )
391                self.data.supply[token_id] = 0
392                self.data.next_token_id += 1
393
394            for token in ledger.items():
395                token_id = sp.snd(token.key)
396                assert self.data.token_metadata.contains(
397                    token_id
398                ), "The `ledger` parameter contains a key no contained in `token_metadata`."
399                self.data.supply[token_id] += token.value
400                self.data.ledger[token.key] = token.value
401
402        @sp.private(with_storage="read-only")
403        def balance_(self, params):
404            (is_defined, balance_params) = params
405            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
406            return self.data.ledger.get(
407                (balance_params.owner, balance_params.token_id), default=0
408            )
409
410        @sp.private(with_storage="read-write")
411        def transfer_tx_(self, params):
412            # Makes the transfer
413            from_ = (params.from_, params.tx.token_id)
414            self.data.ledger[from_] = sp.as_nat(
415                self.data.ledger.get(from_, default=0) - params.tx.amount,
416                error="FA2_INSUFFICIENT_BALANCE",
417            )
418            to_ = (params.tx.to_, params.tx.token_id)
419            self.data.ledger[to_] = (
420                self.data.ledger.get(to_, default=0) + params.tx.amount
421            )
422
423        @sp.private(with_storage="read-only")
424        def supply_(self, params):
425            (is_defined, token_id) = params
426            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
427            return self.data.supply[token_id]
428
429    class SingleAssetInterface(sp.Contract):
430        def __init__(self):
431            self.private.ledger_type = "SingleAsset"
432            self.data.ledger = sp.cast(sp.big_map(), t.ledger_single_asset)
433            self.data.supply = sp.cast(0, sp.nat)
434
435    class SingleAsset(SingleAssetInterface, Common):
436        def __init__(self, metadata, ledger, token_metadata):
437            Common.__init__(self, metadata)
438            SingleAssetInterface.__init__(self)
439
440            self.data.token_metadata[0] = sp.record(
441                token_id=0, token_info=token_metadata
442            )
443            for token in ledger.items():
444                self.data.supply += token.value
445                self.data.ledger[token.key] = token.value
446
447        @sp.private(with_storage="read-only")
448        def balance_(self, params):
449            (is_defined, balance_params) = params
450            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
451            return self.data.ledger.get(balance_params.owner, default=0)
452
453        @sp.private(with_storage="read-write")
454        def transfer_tx_(self, params):
455            # Makes the transfer
456            self.data.ledger[params.from_] = sp.as_nat(
457                self.data.ledger.get(params.from_, default=0) - params.tx.amount,
458                error="FA2_INSUFFICIENT_BALANCE",
459            )
460            self.data.ledger[params.tx.to_] = (
461                self.data.ledger.get(params.tx.to_, default=0) + params.tx.amount
462            )
463
464        @sp.private(with_storage="read-only")
465        def supply_(self, params):
466            (is_defined, token_id) = params
467            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
468            return self.data.supply
469
470    ##########
471    # Mixins #
472    ##########
473
474    class AdminInterface(sp.Contract):
475        def __init__(self):
476            self.data.administrator = sp.address("")
477
478        @sp.private()
479        def is_administrator_(self):
480            raise "NotImplemented"
481            return False
482
483    class Admin(sp.Contract):
484        """(Mixin) Provide the basics for having an administrator in the contract.
485
486        Adds an `administrator` attribute in the storage. Provides a
487        `set_administrator` entrypoint.
488        """
489
490        def __init__(self, administrator):
491            self.data.administrator = administrator
492
493        @sp.private(with_storage="read-only")
494        def is_administrator_(self):
495            return sp.sender == self.data.administrator
496
497        @sp.entrypoint
498        def set_administrator(self, administrator):
499            """(Admin only) Set the contract administrator."""
500            assert self.is_administrator_(), "FA2_NOT_ADMIN"
501            self.data.administrator = administrator
502
503    class ChangeMetadata(AdminInterface):
504        """(Mixin) Provide an entrypoint to change contract metadata.
505
506        Requires the `Admin` mixin.
507        """
508
509        def __init__(self):
510            AdminInterface.__init__(self)
511            self.data.metadata = sp.cast(sp.big_map(), sp.big_map[sp.string, sp.bytes])
512
513        @sp.entrypoint
514        def set_metadata(self, metadata):
515            """(Admin only) Set the contract metadata."""
516            assert self.is_administrator_(), "FA2_NOT_ADMIN"
517            self.data.metadata = metadata
518
519    class WithdrawMutez(AdminInterface):
520        """(Mixin) Provide an entrypoint to withdraw mutez that are in the
521        contract's balance.
522
523        Requires the `Admin` mixin.
524        """
525
526        def __init__(self):
527            AdminInterface.__init__(self)
528
529        @sp.entrypoint
530        def withdraw_mutez(self, destination, amount):
531            """(Admin only) Transfer `amount` mutez to `destination`."""
532            assert self.is_administrator_(), "FA2_NOT_ADMIN"
533            sp.send(destination, amount)
534
535    class OffchainviewTokenMetadata(CommonInterface):
536        """(Mixin) If present indexers use it to retrieve the token's metadata.
537
538        Warning: If someone can change the contract's metadata they can change how
539        indexers see every token metadata.
540        """
541
542        def __init__(self):
543            CommonInterface.__init__(self)
544
545        @sp.offchain_view()
546        def token_metadata(self, token_id):
547            """Returns the token-metadata URI for the given token."""
548            assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED"
549            return self.data.token_metadata[token_id]
550
551    class OnchainviewBalanceOf(sp.Contract):
552        """(Mixin) Non-standard onchain view equivalent to `balance_of`.
553
554        Before onchain views were introduced in Michelson, the standard way
555        of getting value from a contract was through a callback. Now that
556        views are here we can create a view for the old style one.
557        """
558
559        @sp.private(with_storage="read-write")
560        def balance_(self, params):
561            raise "NotImplemented"
562            return 0
563
564        @sp.private(with_storage="read-write")
565        def is_defined_(self, params):
566            sp.cast(params, sp.nat)
567            raise "NotImplemented"
568            return False
569
570        @sp.onchain_view()
571        def get_balance_of(self, requests):
572            """Onchain view equivalent to the `balance_of` entrypoint."""
573            sp.cast(requests, sp.list[t.balance_of_request])
574
575            @sp.effects(with_storage="read-write")
576            def f_process_request_(param):
577                (req, is_defined, balance) = param
578                return sp.cast(
579                    sp.record(
580                        request=req,
581                        balance=balance((is_defined, req)),
582                    ),
583                    t.balance_of_response,
584                )
585
586            return [
587                f_process_request_((x, self.is_defined_, self.balance_))
588                for x in requests
589            ]
590
591    class MintNft(AdminInterface, NftInterface, CommonInterface):
592        """(Mixin) Non-standard `mint` entrypoint for FA2Nft with incrementing id.
593
594        Requires the `Admin` mixin.
595        """
596
597        def __init__(self):
598            CommonInterface.__init__(self)
599            NftInterface.__init__(self)
600            AdminInterface.__init__(self)
601
602        @sp.entrypoint
603        def mint(self, batch):
604            """Admin can mint new or existing tokens."""
605            sp.cast(
606                batch,
607                sp.list[
608                    sp.record(
609                        to_=sp.address,
610                        metadata=sp.map[sp.string, sp.bytes],
611                    ).layout(("to_", "metadata"))
612                ],
613            )
614            assert self.is_administrator_(), "FA2_NOT_ADMIN"
615            for action in batch:
616                token_id = self.data.next_token_id
617                self.data.token_metadata[token_id] = sp.record(
618                    token_id=token_id, token_info=action.metadata
619                )
620                self.data.ledger[token_id] = action.to_
621                self.data.next_token_id += 1
622
623    class MintFungible(AdminInterface, FungibleInterface, CommonInterface):
624        """(Mixin) Non-standard `mint` entrypoint for FA2Fungible with incrementing
625        id.
626
627        Requires the `Admin` mixin.
628        """
629
630        def __init__(self):
631            CommonInterface.__init__(self)
632            FungibleInterface.__init__(self)
633            AdminInterface.__init__(self)
634
635        @sp.entrypoint
636        def mint(self, batch):
637            """Admin can mint tokens."""
638            sp.cast(
639                batch,
640                sp.list[
641                    sp.record(
642                        to_=sp.address,
643                        token=sp.variant(
644                            new=sp.map[sp.string, sp.bytes], existing=sp.nat
645                        ),
646                        amount=sp.nat,
647                    ).layout(("to_", ("token", "amount")))
648                ],
649            )
650            assert self.is_administrator_(), "FA2_NOT_ADMIN"
651            for action in batch:
652                with sp.match(action.token):
653                    with sp.case.new as metadata:
654                        token_id = self.data.next_token_id
655                        self.data.token_metadata[token_id] = sp.record(
656                            token_id=token_id, token_info=metadata
657                        )
658                        self.data.supply[token_id] = action.amount
659                        self.data.ledger[(action.to_, token_id)] = action.amount
660                        self.data.next_token_id += 1
661                    with sp.case.existing as token_id:
662                        assert self.is_defined_(token_id), "FA2_TOKEN_UNDEFINED"
663                        self.data.supply[token_id] += action.amount
664                        from_ = (action.to_, token_id)
665                        self.data.ledger[from_] = (
666                            self.data.ledger.get(from_, default=0) + action.amount
667                        )
668
669    class MintSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface):
670        """(Mixin) Non-standard `mint` entrypoint for FA2SingleAsset.
671
672        Requires the `Admin` mixin.
673        """
674
675        def __init__(self):
676            CommonInterface.__init__(self)
677            SingleAssetInterface.__init__(self)
678            AdminInterface.__init__(self)
679
680        @sp.entrypoint
681        def mint(self, batch):
682            """Admin can mint tokens."""
683            sp.cast(
684                batch,
685                sp.list[
686                    sp.record(to_=sp.address, amount=sp.nat).layout(("to_", "amount"))
687                ],
688            )
689            assert self.is_administrator_(), "FA2_NOT_ADMIN"
690            for action in batch:
691                assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED"
692                self.data.supply += action.amount
693                self.data.ledger[action.to_] = (
694                    self.data.ledger.get(action.to_, default=0) + action.amount
695                )
696
697    class BurnNft(AdminInterface, NftInterface, CommonInterface):
698        """(Mixin) Non-standard `burn` entrypoint for FA2Nft that uses the transfer
699        policy permission."""
700
701        def __init__(self):
702            CommonInterface.__init__(self)
703            NftInterface.__init__(self)
704            AdminInterface.__init__(self)
705
706        @sp.entrypoint
707        def burn(self, batch):
708            """Users can burn tokens if they have the transfer policy permission.
709
710            Burning an nft destroys its metadata.
711            """
712            sp.cast(
713                batch,
714                sp.list[
715                    sp.record(
716                        from_=sp.address,
717                        token_id=sp.nat,
718                        amount=sp.nat,
719                    ).layout(("from_", ("token_id", "amount")))
720                ],
721            )
722            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
723            for action in batch:
724                assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED"
725                self.check_tx_transfer_permissions_(
726                    sp.record(
727                        from_=action.from_, to_=action.from_, token_id=action.token_id
728                    )
729                )
730                if action.amount > 0:
731                    assert (action.amount == 1) and (
732                        self.data.ledger[action.token_id] == action.from_
733                    ), "FA2_INSUFFICIENT_BALANCE"
734                    # Burn the token
735                    del self.data.ledger[action.token_id]
736                    del self.data.token_metadata[action.token_id]
737
738    class BurnFungible(AdminInterface, FungibleInterface, CommonInterface):
739        """(Mixin) Non-standard `burn` entrypoint for FA2Fungible that uses the
740        transfer policy permission."""
741
742        def __init__(self):
743            CommonInterface.__init__(self)
744            FungibleInterface.__init__(self)
745            AdminInterface.__init__(self)
746
747        @sp.entrypoint
748        def burn(self, batch):
749            """Users can burn tokens if they have the transfer policy
750            permission."""
751            sp.cast(
752                batch,
753                sp.list[
754                    sp.record(
755                        from_=sp.address,
756                        token_id=sp.nat,
757                        amount=sp.nat,
758                    ).layout(("from_", ("token_id", "amount")))
759                ],
760            )
761            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
762            for action in batch:
763                assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED"
764                self.check_tx_transfer_permissions_(
765                    sp.record(
766                        from_=action.from_, to_=action.from_, token_id=action.token_id
767                    )
768                )
769                from_ = (action.from_, action.token_id)
770                # Burn the tokens
771                self.data.ledger[from_] = sp.as_nat(
772                    self.data.ledger.get(from_, default=0) - action.amount,
773                    error="FA2_INSUFFICIENT_BALANCE",
774                )
775
776                is_supply = sp.is_nat(
777                    self.data.supply.get(action.token_id, default=0) - action.amount
778                )
779                with sp.match(is_supply):
780                    with sp.case.Some as supply:
781                        self.data.supply[action.token_id] = supply
782                    with None:
783                        self.data.supply[action.token_id] = 0
784
785    class BurnSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface):
786        """(Mixin) Non-standard `burn` entrypoint for FA2SingleAsset that uses the
787        transfer policy permission."""
788
789        def __init__(self):
790            CommonInterface.__init__(self)
791            SingleAssetInterface.__init__(self)
792            AdminInterface.__init__(self)
793
794        @sp.entrypoint
795        def burn(self, batch):
796            """Users can burn tokens if they have the transfer policy
797            permission."""
798            sp.cast(
799                batch,
800                sp.list[
801                    sp.record(
802                        from_=sp.address,
803                        token_id=sp.nat,
804                        amount=sp.nat,
805                    ).layout(("from_", ("token_id", "amount")))
806                ],
807            )
808            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
809            for action in batch:
810                assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED"
811                self.check_tx_transfer_permissions_(
812                    sp.record(
813                        from_=action.from_, to_=action.from_, token_id=action.token_id
814                    )
815                )
816                # Burn the tokens
817                self.data.ledger[action.from_] = sp.as_nat(
818                    self.data.ledger.get(action.from_, default=0) - action.amount,
819                    error="FA2_INSUFFICIENT_BALANCE",
820                )
821
822                is_supply = sp.is_nat(self.data.supply - action.amount)
823                with sp.match(is_supply):
824                    with sp.case.Some as supply:
825                        self.data.supply = supply
826                    with None:
827                        self.data.supply = 0
828
829    #########################
830    # Non standard policies #
831    #########################
832
833    class PauseOwnerOrOperatorTransfer(AdminInterface):
834        """Owner or operator can transfers. Transfers can be paused by the admin.
835
836        Adds a `set_pause` operator.
837
838        Requires the `Admin` mixin."""
839
840        def __init__(self):
841            AdminInterface.__init__(self)
842            self.private.policy = sp.record(
843                name="pauseable-owner-or-operator-transfer",
844                supports_transfer=True,
845                supports_operator=True,
846            )
847            self.data.paused = False
848            self.data.operators = sp.cast(
849                sp.big_map(), sp.big_map[t.operator_permission, sp.unit]
850            )
851
852        @sp.private(with_storage="read-only")
853        def check_operator_update_permissions_(self, operator_permission):
854            sp.cast(operator_permission, t.operator_permission)
855            assert not self.data.paused, ("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED")
856            assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER"
857
858        @sp.private(with_storage="read-only")
859        def check_tx_transfer_permissions_(self, params):
860            sp.cast(
861                params,
862                sp.record(
863                    from_=sp.address,
864                    to_=sp.address,
865                    token_id=sp.nat,
866                ),
867            )
868            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
869            assert not self.data.paused, ("FA2_TX_DENIED", "FA2_PAUSED")
870            assert (sp.sender == params.from_) or self.data.operators.contains(
871                sp.record(
872                    owner=params.from_, operator=sp.sender, token_id=params.token_id
873                )
874            ), "FA2_NOT_OPERATOR"
875
876        @sp.private(with_storage="read-only")
877        def is_operator(self, operator_permission):
878            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
879            return self.data.operators.contains(operator_permission)
880
881        @sp.entrypoint
882        def set_pause(self, params):
883            assert self.is_administrator_(), "FA2_NOT_ADMIN"
884            self.data.paused = params
885
886
887###########
888# Helpers #
889###########
890
891
892@sp.module
893def Helpers():
894    class TestReceiverBalanceOf(sp.Contract):
895        """Helper used to test the `balance_of` entrypoint.
896
897        Don't use it on-chain as it can be gas locked.
898        """
899
900        def __init__(self):
901            self.last_known_balances = sp.big_map()
902
903        @sp.entrypoint
904        def receive_balances(self, params):
905            sp.cast(params, list[t.balance_of_response])
906            for resp in params:
907                owner = (resp.request.owner, resp.request.token_id)
908                if self.data.last_known_balances.contains(sp.sender):
909                    self.data.last_known_balances[sp.sender][owner] = resp.balance
910                else:
911                    self.data.last_known_balances[sp.sender] = {owner: resp.balance}
912
913
914def make_metadata(symbol, name, decimals):
915    """Helper function to build metadata JSON bytes values."""
916    return sp.map(
917        l={
918            "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals),
919            "name": sp.scenario_utils.bytes_of_string(name),
920            "symbol": sp.scenario_utils.bytes_of_string(symbol),
921        }
922    )
def make_metadata(symbol, name, decimals):
915def make_metadata(symbol, name, decimals):
916    """Helper function to build metadata JSON bytes values."""
917    return sp.map(
918        l={
919            "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals),
920            "name": sp.scenario_utils.bytes_of_string(name),
921            "symbol": sp.scenario_utils.bytes_of_string(symbol),
922        }
923    )

Helper function to build metadata JSON bytes values.