Module 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)

Expand source code
"""
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)
"""

import smartpy as sp


@sp.module
def t():
    operator_permission: type = sp.record(
        owner=sp.address, operator=sp.address, token_id=sp.nat
    ).layout(("owner", ("operator", "token_id")))

    update_operators_params: type = list[
        sp.variant(
            add_operator=operator_permission, remove_operator=operator_permission
        )
    ]

    tx: type = sp.record(
        to_=sp.address,
        token_id=sp.nat,
        amount=sp.nat,
    ).layout(("to_", ("token_id", "amount")))

    transfer_batch: type = sp.record(
        from_=sp.address,
        txs=list[tx],
    ).layout(("from_", "txs"))

    transfer_params: type = list[transfer_batch]

    balance_of_request: type = sp.record(owner=sp.address, token_id=sp.nat).layout(
        ("owner", "token_id")
    )

    balance_of_response: type = sp.record(
        request=balance_of_request, balance=sp.nat
    ).layout(("request", "balance"))

    balance_of_params: type = sp.record(
        callback=sp.contract[list[balance_of_response]],
        requests=list[balance_of_request],
    ).layout(("requests", "callback"))

    balance_params: type = sp.pair[
        sp.lambda_(sp.nat, sp.bool, with_storage="read-only"), balance_of_request
    ]

    token_metadata: type = sp.big_map[
        sp.nat,
        sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]).layout(
            ("token_id", "token_info")
        ),
    ]

    metadata: type = sp.big_map[sp.string, sp.bytes]

    ledger_nft: type = sp.big_map[sp.nat, sp.address]

    ledger_fungible: type = sp.big_map[sp.pair[sp.address, sp.nat], sp.nat]

    ledger_single_asset: type = sp.big_map[sp.address, sp.nat]

    supply_fungible: type = sp.big_map[sp.nat, sp.nat]

    tx_transfer_permission: type = sp.record(
        from_=sp.address, to_=sp.address, token_id=sp.nat
    )


@sp.module
def main():
    ############
    # Policies #
    ############

    class NoTransfer(sp.Contract):
        """No transfer allowed."""

        def __init__(self):
            self.private.policy = sp.record(
                name="no-transfer",
                supports_transfer=False,
                supports_operator=False,
            )

        @sp.private()
        def check_operator_update_permissions_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            raise "FA2_OPERATORS_UNSUPPORTED"
            return ()

        @sp.private()
        def check_tx_transfer_permissions_(self, params):
            sp.cast(params, t.tx_transfer_permission)
            raise "FA2_TX_DENIED"
            return ()

        @sp.private()
        def is_operator_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            return False

    class OwnerTransfer(sp.Contract):
        """Only owner are allowed to transfer, no operators."""

        def __init__(self):
            self.private.policy = sp.record(
                name="owner-transfer",
                supports_transfer=True,
                supports_operator=False,
            )

        @sp.private()
        def check_operator_update_permissions_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            raise "FA2_OPERATORS_UNSUPPORTED"
            return ()

        @sp.private()
        def check_tx_transfer_permissions_(self, params):
            sp.cast(params, t.tx_transfer_permission)
            assert sp.sender == params.from_, "FA2_NOT_OWNER"

        @sp.private()
        def is_operator_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            return False

    class OwnerOrOperatorTransfer(sp.Contract):
        """Owner or operator are allowed to transfer."""

        def __init__(self):
            self.private.policy = sp.record(
                name="owner-or-operator-transfer",
                supports_transfer=True,
                supports_operator=True,
            )
            self.data.operators = sp.cast(
                sp.big_map(), sp.big_map[t.operator_permission, sp.unit]
            )

        @sp.private()
        def check_operator_update_permissions_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER"

        @sp.private(with_storage="read-only")
        def check_tx_transfer_permissions_(self, params):
            sp.cast(params, t.tx_transfer_permission)
            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
            assert (sp.sender == params.from_) or self.data.operators.contains(
                sp.record(
                    owner=params.from_, operator=sp.sender, token_id=params.token_id
                )
            ), "FA2_NOT_OPERATOR"

        @sp.private(with_storage="read-only")
        def is_operator_(self, operator_permission):
            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
            return self.data.operators.contains(operator_permission)

    ##########
    # Common #
    ##########

    class CommonInterface(OwnerOrOperatorTransfer):
        def __init__(self):
            OwnerOrOperatorTransfer.__init__(self)
            self.data.token_metadata = sp.cast(sp.big_map(), t.token_metadata)
            self.data.metadata = sp.cast(sp.big_map(), t.metadata)
            self.data.next_token_id = 0

        @sp.private()
        def balance_(self, params):
            """Return the balance of an account.
            Must be redefined in child"""
            sp.cast(params, t.balance_params)
            raise "NotImplemented"
            return 0

        @sp.private()
        def is_defined_(self, token_id):
            """Return True if the token is defined, else otherwise.
            Must be redefined in child"""
            sp.cast(token_id, sp.nat)
            raise "NotImplemented"
            return False

        @sp.private()
        def transfer_tx_(self, params):
            """Perform the transfer action.
            Must be redefined in child"""
            sp.cast(params, sp.record(from_=sp.address, tx=t.tx))
            raise "NotImplemented"
            return ()

        @sp.private()
        def supply_(self, params):
            """Return the supply of a token.
            Must be redefined in child"""
            (is_defined, token_id) = params
            sp.cast(token_id, sp.nat)
            raise "NotImplemented"
            return 0

    class Common(CommonInterface):
        """Common logic between Nft, Fungible and SingleAsset."""

        def __init__(self, metadata):
            CommonInterface.__init__(self)
            self.data.metadata = sp.cast(metadata, t.metadata)

        @sp.private(with_storage="read-only")
        def is_defined_(self, token_id):
            """Return True if the token is defined, else otherwise."""
            return self.data.token_metadata.contains(token_id)

        # Entrypoints

        @sp.entrypoint
        def update_operators(self, batch):
            """Accept a list of variants to add or remove operators who can perform
            transfers on behalf of the owner."""
            sp.cast(batch, t.update_operators_params)
            if self.private.policy.supports_operator:
                for action in batch:
                    with sp.match(action):
                        with sp.case.add_operator as operator:
                            _ = self.check_operator_update_permissions_(operator)
                            self.data.operators[operator] = ()
                        with sp.case.remove_operator as operator:
                            _ = self.check_operator_update_permissions_(operator)
                            del self.data.operators[operator]
            else:
                raise "FA2_OPERATORS_UNSUPPORTED"

        @sp.entrypoint
        def balance_of(self, params):
            """Send the balance of multiple account / token pairs to a callback
            address.

            `balance_` and `is_defined_` must be defined in the child class.
            """
            sp.cast(params, t.balance_of_params)

            @sp.effects(with_storage="read-write")
            def f_process_request(param):
                (req, is_defined, balance) = param
                sp.cast(req, t.balance_of_request)
                return sp.cast(
                    sp.record(
                        request=req,
                        balance=balance((is_defined, req)),
                    ),
                    t.balance_of_response,
                )

            answers = [
                f_process_request((x, self.is_defined_, self.balance_))
                for x in params.requests
            ]
            sp.transfer(answers, sp.mutez(0), params.callback)

        @sp.entrypoint
        def transfer(self, batch):
            """Accept a list of transfer operations between a source and multiple
            destinations.

            `transfer_tx_` and `is_defined_` must be defined in the child class.
            """
            sp.cast(batch, t.transfer_params)
            if self.private.policy.supports_transfer:
                for transfer in batch:
                    for tx in transfer.txs:
                        # The ordering of assert is important:
                        # 1) token_undefined, 2) transfer permission 3) balance
                        assert self.is_defined_(tx.token_id), "FA2_TOKEN_UNDEFINED"
                        _ = self.check_tx_transfer_permissions_(
                            sp.record(
                                from_=transfer.from_, to_=tx.to_, token_id=tx.token_id
                            )
                        )
                        if tx.amount > 0:
                            tx_params = sp.record(from_=transfer.from_, tx=tx)
                            self.transfer_tx_(tx_params)
            else:
                raise "FA2_TX_DENIED"

        # Offchain views

        @sp.offchain_view()
        def all_tokens(self):
            """Return the list of all the token IDs known to the contract."""
            return sp.range(0, self.data.next_token_id)

        @sp.offchain_view()
        def is_operator(self, operator_permission):
            """Return whether `operator` is allowed to transfer `token_id` tokens
            owned by `owner`."""
            sp.cast(operator_permission, t.operator_permission)
            return self.is_operator_(operator_permission)

        @sp.offchain_view
        def get_balance(self, params):
            """Return the balance of an address for the specified `token_id`."""
            sp.cast(params, t.balance_of_request)
            return self.balance_((self.is_defined_, params))

        @sp.offchain_view()
        def total_supply(self, params):
            """Return the total number of tokens for the given `token_id`."""
            supply = self.supply_((self.is_defined_, params.token_id))
            return sp.cast(supply, sp.nat)

    ################
    # Base classes #
    ################

    class NftInterface(sp.Contract):
        def __init__(self):
            self.data.ledger = sp.cast(sp.big_map(), t.ledger_nft)
            self.private.ledger_type = "NFT"

    class Nft(NftInterface, Common):
        def __init__(self, metadata, ledger, token_metadata):
            Common.__init__(self, metadata)
            NftInterface.__init__(self)

            for token_info in token_metadata:
                self.data.token_metadata[self.data.next_token_id] = sp.record(
                    token_id=self.data.next_token_id, token_info=token_info
                )
                self.data.next_token_id += 1

            for token in ledger.items():
                assert self.data.token_metadata.contains(
                    token.key
                ), "The `ledger` parameter contains a key no contained in `token_metadata`."
                self.data.ledger[token.key] = token.value

        @sp.private(with_storage="read-only")
        def balance_(self, params):
            (is_defined, balance_params) = params
            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
            return (
                1
                if self.data.ledger[balance_params.token_id] == balance_params.owner
                else 0
            )

        @sp.private(with_storage="read-write")
        def transfer_tx_(self, params):
            assert (
                params.tx.amount == 1
                and self.data.ledger[params.tx.token_id] == params.from_
            ), "FA2_INSUFFICIENT_BALANCE"

            # Makes the transfer
            self.data.ledger[params.tx.token_id] = params.tx.to_

        @sp.private(with_storage="read-only")
        def supply_(self, params):
            (is_defined, token_id) = params
            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
            return sp.cast(1, sp.nat)

    class FungibleInterface(sp.Contract):
        def __init__(self):
            self.private.ledger_type = "Fungible"
            self.data.ledger = sp.cast(sp.big_map(), t.ledger_fungible)
            self.data.supply = sp.cast(sp.big_map(), t.supply_fungible)

    class Fungible(FungibleInterface, Common):
        def __init__(self, metadata, ledger, token_metadata):
            Common.__init__(self, metadata)
            FungibleInterface.__init__(self)

            for token_info in token_metadata:
                token_id = self.data.next_token_id
                self.data.token_metadata[token_id] = sp.record(
                    token_id=token_id, token_info=token_info
                )
                self.data.supply[token_id] = 0
                self.data.next_token_id += 1

            for token in ledger.items():
                token_id = sp.snd(token.key)
                assert self.data.token_metadata.contains(
                    token_id
                ), "The `ledger` parameter contains a key no contained in `token_metadata`."
                self.data.supply[token_id] += token.value
                self.data.ledger[token.key] = token.value

        @sp.private(with_storage="read-only")
        def balance_(self, params):
            (is_defined, balance_params) = params
            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
            return self.data.ledger.get(
                (balance_params.owner, balance_params.token_id), default=0
            )

        @sp.private(with_storage="read-write")
        def transfer_tx_(self, params):
            # Makes the transfer
            from_ = (params.from_, params.tx.token_id)
            self.data.ledger[from_] = sp.as_nat(
                self.data.ledger.get(from_, default=0) - params.tx.amount,
                error="FA2_INSUFFICIENT_BALANCE",
            )
            to_ = (params.tx.to_, params.tx.token_id)
            self.data.ledger[to_] = (
                self.data.ledger.get(to_, default=0) + params.tx.amount
            )

        @sp.private(with_storage="read-only")
        def supply_(self, params):
            (is_defined, token_id) = params
            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
            return self.data.supply[token_id]

    class SingleAssetInterface(sp.Contract):
        def __init__(self):
            self.private.ledger_type = "SingleAsset"
            self.data.ledger = sp.cast(sp.big_map(), t.ledger_single_asset)
            self.data.supply = sp.cast(0, sp.nat)

    class SingleAsset(SingleAssetInterface, Common):
        def __init__(self, metadata, ledger, token_metadata):
            Common.__init__(self, metadata)
            SingleAssetInterface.__init__(self)

            self.data.token_metadata[0] = sp.record(
                token_id=0, token_info=token_metadata
            )
            for token in ledger.items():
                self.data.supply += token.value
                self.data.ledger[token.key] = token.value

        @sp.private(with_storage="read-only")
        def balance_(self, params):
            (is_defined, balance_params) = params
            assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED"
            return self.data.ledger.get(balance_params.owner, default=0)

        @sp.private(with_storage="read-write")
        def transfer_tx_(self, params):
            # Makes the transfer
            self.data.ledger[params.from_] = sp.as_nat(
                self.data.ledger.get(params.from_, default=0) - params.tx.amount,
                error="FA2_INSUFFICIENT_BALANCE",
            )
            self.data.ledger[params.tx.to_] = (
                self.data.ledger.get(params.tx.to_, default=0) + params.tx.amount
            )

        @sp.private(with_storage="read-only")
        def supply_(self, params):
            (is_defined, token_id) = params
            assert is_defined(token_id), "FA2_TOKEN_UNDEFINED"
            return self.data.supply

    ##########
    # Mixins #
    ##########

    class AdminInterface(sp.Contract):
        def __init__(self):
            self.data.administrator = sp.address("")

        @sp.private()
        def is_administrator_(self):
            raise "NotImplemented"
            return False

    class Admin(sp.Contract):
        """(Mixin) Provide the basics for having an administrator in the contract.

        Adds an `administrator` attribute in the storage. Provides a
        `set_administrator` entrypoint.
        """

        def __init__(self, administrator):
            self.data.administrator = administrator

        @sp.private(with_storage="read-only")
        def is_administrator_(self):
            return sp.sender == self.data.administrator

        @sp.entrypoint
        def set_administrator(self, administrator):
            """(Admin only) Set the contract administrator."""
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            self.data.administrator = administrator

    class ChangeMetadata(AdminInterface):
        """(Mixin) Provide an entrypoint to change contract metadata.

        Requires the `Admin` mixin.
        """

        def __init__(self):
            AdminInterface.__init__(self)
            self.data.metadata = sp.cast(sp.big_map(), sp.big_map[sp.string, sp.bytes])

        @sp.entrypoint
        def set_metadata(self, metadata):
            """(Admin only) Set the contract metadata."""
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            self.data.metadata = metadata

    class WithdrawMutez(AdminInterface):
        """(Mixin) Provide an entrypoint to withdraw mutez that are in the
        contract's balance.

        Requires the `Admin` mixin.
        """

        def __init__(self):
            AdminInterface.__init__(self)

        @sp.entrypoint
        def withdraw_mutez(self, destination, amount):
            """(Admin only) Transfer `amount` mutez to `destination`."""
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            sp.send(destination, amount)

    class OffchainviewTokenMetadata(CommonInterface):
        """(Mixin) If present indexers use it to retrieve the token's metadata.

        Warning: If someone can change the contract's metadata they can change how
        indexers see every token metadata.
        """

        def __init__(self):
            CommonInterface.__init__(self)

        @sp.offchain_view()
        def token_metadata(self, token_id):
            """Returns the token-metadata URI for the given token."""
            assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED"
            return self.data.token_metadata[token_id]

    class OnchainviewBalanceOf(sp.Contract):
        """(Mixin) Non-standard onchain view equivalent to `balance_of`.

        Before onchain views were introduced in Michelson, the standard way
        of getting value from a contract was through a callback. Now that
        views are here we can create a view for the old style one.
        """

        @sp.private(with_storage="read-write")
        def balance_(self, params):
            raise "NotImplemented"
            return 0

        @sp.private(with_storage="read-write")
        def is_defined_(self, params):
            sp.cast(params, sp.nat)
            raise "NotImplemented"
            return False

        @sp.onchain_view()
        def get_balance_of(self, requests):
            """Onchain view equivalent to the `balance_of` entrypoint."""
            sp.cast(requests, sp.list[t.balance_of_request])

            @sp.effects(with_storage="read-write")
            def f_process_request_(param):
                (req, is_defined, balance) = param
                return sp.cast(
                    sp.record(
                        request=req,
                        balance=balance((is_defined, req)),
                    ),
                    t.balance_of_response,
                )

            return [
                f_process_request_((x, self.is_defined_, self.balance_))
                for x in requests
            ]

    class MintNft(AdminInterface, NftInterface, CommonInterface):
        """(Mixin) Non-standard `mint` entrypoint for FA2Nft with incrementing id.

        Requires the `Admin` mixin.
        """

        def __init__(self):
            CommonInterface.__init__(self)
            NftInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def mint(self, batch):
            """Admin can mint new or existing tokens."""
            sp.cast(
                batch,
                sp.list[
                    sp.record(
                        to_=sp.address,
                        metadata=sp.map[sp.string, sp.bytes],
                    ).layout(("to_", "metadata"))
                ],
            )
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            for action in batch:
                token_id = self.data.next_token_id
                self.data.token_metadata[token_id] = sp.record(
                    token_id=token_id, token_info=action.metadata
                )
                self.data.ledger[token_id] = action.to_
                self.data.next_token_id += 1

    class MintFungible(AdminInterface, FungibleInterface, CommonInterface):
        """(Mixin) Non-standard `mint` entrypoint for FA2Fungible with incrementing
        id.

        Requires the `Admin` mixin.
        """

        def __init__(self):
            CommonInterface.__init__(self)
            FungibleInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def mint(self, batch):
            """Admin can mint tokens."""
            sp.cast(
                batch,
                sp.list[
                    sp.record(
                        to_=sp.address,
                        token=sp.variant(
                            new=sp.map[sp.string, sp.bytes], existing=sp.nat
                        ),
                        amount=sp.nat,
                    ).layout(("to_", ("token", "amount")))
                ],
            )
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            for action in batch:
                with sp.match(action.token):
                    with sp.case.new as metadata:
                        token_id = self.data.next_token_id
                        self.data.token_metadata[token_id] = sp.record(
                            token_id=token_id, token_info=metadata
                        )
                        self.data.supply[token_id] = action.amount
                        self.data.ledger[(action.to_, token_id)] = action.amount
                        self.data.next_token_id += 1
                    with sp.case.existing as token_id:
                        assert self.is_defined_(token_id), "FA2_TOKEN_UNDEFINED"
                        self.data.supply[token_id] += action.amount
                        from_ = (action.to_, token_id)
                        self.data.ledger[from_] = (
                            self.data.ledger.get(from_, default=0) + action.amount
                        )

    class MintSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface):
        """(Mixin) Non-standard `mint` entrypoint for FA2SingleAsset.

        Requires the `Admin` mixin.
        """

        def __init__(self):
            CommonInterface.__init__(self)
            SingleAssetInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def mint(self, batch):
            """Admin can mint tokens."""
            sp.cast(
                batch,
                sp.list[
                    sp.record(to_=sp.address, amount=sp.nat).layout(("to_", "amount"))
                ],
            )
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            for action in batch:
                assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED"
                self.data.supply += action.amount
                self.data.ledger[action.to_] = (
                    self.data.ledger.get(action.to_, default=0) + action.amount
                )

    class BurnNft(AdminInterface, NftInterface, CommonInterface):
        """(Mixin) Non-standard `burn` entrypoint for FA2Nft that uses the transfer
        policy permission."""

        def __init__(self):
            CommonInterface.__init__(self)
            NftInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def burn(self, batch):
            """Users can burn tokens if they have the transfer policy permission.

            Burning an nft destroys its metadata.
            """
            sp.cast(
                batch,
                sp.list[
                    sp.record(
                        from_=sp.address,
                        token_id=sp.nat,
                        amount=sp.nat,
                    ).layout(("from_", ("token_id", "amount")))
                ],
            )
            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
            for action in batch:
                assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED"
                self.check_tx_transfer_permissions_(
                    sp.record(
                        from_=action.from_, to_=action.from_, token_id=action.token_id
                    )
                )
                if action.amount > 0:
                    assert (action.amount == 1) and (
                        self.data.ledger[action.token_id] == action.from_
                    ), "FA2_INSUFFICIENT_BALANCE"
                    # Burn the token
                    del self.data.ledger[action.token_id]
                    del self.data.token_metadata[action.token_id]

    class BurnFungible(AdminInterface, FungibleInterface, CommonInterface):
        """(Mixin) Non-standard `burn` entrypoint for FA2Fungible that uses the
        transfer policy permission."""

        def __init__(self):
            CommonInterface.__init__(self)
            FungibleInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def burn(self, batch):
            """Users can burn tokens if they have the transfer policy
            permission."""
            sp.cast(
                batch,
                sp.list[
                    sp.record(
                        from_=sp.address,
                        token_id=sp.nat,
                        amount=sp.nat,
                    ).layout(("from_", ("token_id", "amount")))
                ],
            )
            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
            for action in batch:
                assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED"
                self.check_tx_transfer_permissions_(
                    sp.record(
                        from_=action.from_, to_=action.from_, token_id=action.token_id
                    )
                )
                from_ = (action.from_, action.token_id)
                # Burn the tokens
                self.data.ledger[from_] = sp.as_nat(
                    self.data.ledger.get(from_, default=0) - action.amount,
                    error="FA2_INSUFFICIENT_BALANCE",
                )

                is_supply = sp.is_nat(
                    self.data.supply.get(action.token_id, default=0) - action.amount
                )
                with sp.match(is_supply):
                    with sp.case.Some as supply:
                        self.data.supply[action.token_id] = supply
                    with None:
                        self.data.supply[action.token_id] = 0

    class BurnSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface):
        """(Mixin) Non-standard `burn` entrypoint for FA2SingleAsset that uses the
        transfer policy permission."""

        def __init__(self):
            CommonInterface.__init__(self)
            SingleAssetInterface.__init__(self)
            AdminInterface.__init__(self)

        @sp.entrypoint
        def burn(self, batch):
            """Users can burn tokens if they have the transfer policy
            permission."""
            sp.cast(
                batch,
                sp.list[
                    sp.record(
                        from_=sp.address,
                        token_id=sp.nat,
                        amount=sp.nat,
                    ).layout(("from_", ("token_id", "amount")))
                ],
            )
            assert self.private.policy.supports_transfer, "FA2_TX_DENIED"
            for action in batch:
                assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED"
                self.check_tx_transfer_permissions_(
                    sp.record(
                        from_=action.from_, to_=action.from_, token_id=action.token_id
                    )
                )
                # Burn the tokens
                self.data.ledger[action.from_] = sp.as_nat(
                    self.data.ledger.get(action.from_, default=0) - action.amount,
                    error="FA2_INSUFFICIENT_BALANCE",
                )

                is_supply = sp.is_nat(self.data.supply - action.amount)
                with sp.match(is_supply):
                    with sp.case.Some as supply:
                        self.data.supply = supply
                    with None:
                        self.data.supply = 0

    #########################
    # Non standard policies #
    #########################

    class PauseOwnerOrOperatorTransfer(AdminInterface):
        """Owner or operator can transfers. Transfers can be paused by the admin.

        Adds a `set_pause` operator.

        Requires the `Admin` mixin."""

        def __init__(self):
            AdminInterface.__init__(self)
            self.private.policy = sp.record(
                name="pauseable-owner-or-operator-transfer",
                supports_transfer=True,
                supports_operator=True,
            )
            self.data.paused = False
            self.data.operators = sp.cast(
                sp.big_map(), sp.big_map[t.operator_permission, sp.unit]
            )

        @sp.private(with_storage="read-only")
        def check_operator_update_permissions_(self, operator_permission):
            sp.cast(operator_permission, t.operator_permission)
            assert not self.data.paused, ("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED")
            assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER"

        @sp.private(with_storage="read-only")
        def check_tx_transfer_permissions_(self, params):
            sp.cast(
                params,
                sp.record(
                    from_=sp.address,
                    to_=sp.address,
                    token_id=sp.nat,
                ),
            )
            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
            assert not self.data.paused, ("FA2_TX_DENIED", "FA2_PAUSED")
            assert (sp.sender == params.from_) or self.data.operators.contains(
                sp.record(
                    owner=params.from_, operator=sp.sender, token_id=params.token_id
                )
            ), "FA2_NOT_OPERATOR"

        @sp.private(with_storage="read-only")
        def is_operator(self, operator_permission):
            sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit])
            return self.data.operators.contains(operator_permission)

        @sp.entrypoint
        def set_pause(self, params):
            assert self.is_administrator_(), "FA2_NOT_ADMIN"
            self.data.paused = params


###########
# Helpers #
###########


@sp.module
def Helpers():
    class TestReceiverBalanceOf(sp.Contract):
        """Helper used to test the `balance_of` entrypoint.

        Don't use it on-chain as it can be gas locked.
        """

        def __init__(self):
            self.last_known_balances = sp.big_map()

        @sp.entrypoint
        def receive_balances(self, params):
            sp.cast(params, list[t.balance_of_response])
            for resp in params:
                owner = (resp.request.owner, resp.request.token_id)
                if self.data.last_known_balances.contains(sp.sender):
                    self.data.last_known_balances[sp.sender][owner] = resp.balance
                else:
                    self.data.last_known_balances[sp.sender] = {owner: resp.balance}


def make_metadata(symbol, name, decimals):
    """Helper function to build metadata JSON bytes values."""
    return sp.map(
        l={
            "decimals": sp.utils.bytes_of_string("%d" % decimals),
            "name": sp.utils.bytes_of_string(name),
            "symbol": sp.utils.bytes_of_string(symbol),
        }
    )

Functions

def make_metadata(symbol, name, decimals)

Helper function to build metadata JSON bytes values.

Expand source code
def make_metadata(symbol, name, decimals):
    """Helper function to build metadata JSON bytes values."""
    return sp.map(
        l={
            "decimals": sp.utils.bytes_of_string("%d" % decimals),
            "name": sp.utils.bytes_of_string(name),
            "symbol": sp.utils.bytes_of_string(symbol),
        }
    )