Module fa2_lib

FA2 standard: https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md.
Documentation: FA2 lib.

Multiple mixins and several standard policies are supported.

Expand source code
"""
FA2 standard: https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md. <br/>
Documentation: [FA2 lib](/docs/guides/FA/FA2_lib).

Multiple mixins and several standard [policies](https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/permissions-policy.md#operator-permission-behavior) are supported.
"""

import smartpy as sp


#########
# Types #
#########


t_operator_permission = sp.TRecord(
    owner=sp.TAddress, operator=sp.TAddress, token_id=sp.TNat
).layout(("owner", ("operator", "token_id")))

t_update_operators_params = sp.TList(
    sp.TVariant(
        add_operator=t_operator_permission, remove_operator=t_operator_permission
    )
)

t_transfer_batch = sp.TRecord(
    from_=sp.TAddress,
    txs=sp.TList(
        sp.TRecord(
            to_=sp.TAddress,
            token_id=sp.TNat,
            amount=sp.TNat,
        ).layout(("to_", ("token_id", "amount")))
    ),
).layout(("from_", "txs"))

t_transfer_params = sp.TList(t_transfer_batch)

t_balance_of_request = sp.TRecord(owner=sp.TAddress, token_id=sp.TNat).layout(
    ("owner", "token_id")
)

t_balance_of_response = sp.TRecord(
    request=t_balance_of_request, balance=sp.TNat
).layout(("request", "balance"))

t_balance_of_params = sp.TRecord(
    callback=sp.TContract(sp.TList(t_balance_of_response)),
    requests=sp.TList(t_balance_of_request),
).layout(("requests", "callback"))


############
# Policies #
############


class NoTransfer:
    """(Transfer Policy) No transfer allowed."""

    def init_policy(self, contract):
        self.name = "no-transfer"
        self.supports_transfer = False
        self.supports_operator = False

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        pass

    def check_operator_update_permissions(self, contract, operator_permission):
        pass

    def is_operator(self, contract, operator_permission):
        return False


class OwnerTransfer:
    """(Transfer Policy) Only owner can transfer tokens, no operator
    allowed."""

    def init_policy(self, contract):
        self.name = "owner-transfer"
        self.supports_transfer = True
        self.supports_operator = False

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(sp.sender == from_, "FA2_NOT_OWNER")

    def check_operator_update_permissions(self, contract, operator_permission):
        pass

    def is_operator(self, contract, operator_permission):
        return False


class OwnerOrOperatorTransfer:
    """(Transfer Policy) Only owner and operators can transfer tokens.

    Operators allowed.
    """

    def init_policy(self, contract):
        self.name = "owner-or-operator-transfer"
        self.supports_transfer = True
        self.supports_operator = True
        contract.update_initial_storage(
            operators=sp.big_map(tkey=t_operator_permission, tvalue=sp.TUnit)
        )

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(
            (sp.sender == from_)
            | contract.data.operators.contains(
                sp.record(owner=from_, operator=sp.sender, token_id=token_id)
            ),
            message="FA2_NOT_OPERATOR",
        )

    def check_operator_update_permissions(self, contract, operator_permission):
        sp.verify(operator_permission.owner == sp.sender, "FA2_NOT_OWNER")

    def is_operator(self, contract, operator_permission):
        return contract.data.operators.contains(operator_permission)


class PauseTransfer:
    """(Transfer Policy) Decorate any policy to add a pause mechanism.

    Adds a `set_pause` entrypoint. Checks that contract.data.paused is
    `False` before accepting transfers and operator updates.

    Needs the `Admin` mixin in order to work.
    """

    def __init__(self, policy=None):
        if policy is None:
            self.policy = OwnerOrOperatorTransfer()
        else:
            self.policy = policy

    def init_policy(self, contract):
        self.policy.init_policy(contract)
        self.name = "pauseable-" + self.policy.name
        self.supports_transfer = self.policy.supports_transfer
        self.supports_operator = self.policy.supports_operator
        contract.update_initial_storage(paused=False)

        # Add a set_pause entrypoint
        def set_pause(self, params):
            sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
            self.data.paused = params

        contract.set_pause = sp.entrypoint(set_pause)

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(~contract.data.paused, message=sp.pair("FA2_TX_DENIED", "FA2_PAUSED"))
        self.policy.check_tx_transfer_permissions(contract, from_, to_, token_id)

    def check_operator_update_permissions(self, contract, operator_param):
        sp.verify(
            ~contract.data.paused,
            message=sp.pair("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED"),
        )
        self.policy.check_operator_update_permissions(contract, operator_param)

    def is_operator(self, contract, operator_param):
        return self.policy.is_operator(contract, operator_param)


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


class Common(sp.Contract):
    """Common logic between Fa2Nft, Fa2Fungible and Fa2SingleAsset."""

    def __init__(self, policy=None, metadata_base=None, token_metadata={}):
        if policy is None:
            self.policy = OwnerOrOperatorTransfer()
        else:
            self.policy = policy
        self.update_initial_storage(
            token_metadata=sp.big_map(
                token_metadata,
                tkey=sp.TNat,
                tvalue=sp.TRecord(
                    token_id=sp.TNat, token_info=sp.TMap(sp.TString, sp.TBytes)
                ).layout(("token_id", "token_info")),
            )
        )
        self.policy.init_policy(self)
        self.generate_contract_metadata("metadata_base", metadata_base)

    def is_defined(self, token_id):
        return self.data.token_metadata.contains(token_id)

    def generate_contract_metadata(self, filename, metadata_base=None):
        """Generate a metadata json file with all the contract's offchain views
        and standard TZIP-126 and TZIP-016 key/values."""
        if metadata_base is None:
            metadata_base = {
                "name": "FA2 contract",
                "version": "1.0.0",
                "description": "This implements FA2 (TZIP-012) using SmartPy.",
                "interfaces": ["TZIP-012", "TZIP-016"],
                "authors": ["SmartPy <https://legacy.smartpy.io/#contact>"],
                "homepage": "https://legacy.smartpy.io/ide?template=FA2.py",
                "source": {
                    "tools": ["SmartPy"],
                    "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/FA2.py",
                },
                "permissions": {"receiver": "owner-no-hook", "sender": "owner-no-hook"},
            }
        offchain_views = []
        for f in dir(self):
            attr = getattr(self, f)
            if isinstance(attr, sp.OnOffchainView):
                if attr.kind == "offchain":
                    offchain_views.append(attr)
        metadata_base["views"] = offchain_views
        metadata_base["permissions"]["operator"] = self.policy.name
        self.init_metadata(filename, metadata_base)

    def balance_of_batch(self, requests):
        """Mapping of balances."""
        sp.set_type(requests, sp.TList(t_balance_of_request))

        def f_process_request(req):
            sp.result(
                sp.record(
                    request=req,
                    balance=self.balance_(req.owner, req.token_id),
                )
            )

        return requests.map(f_process_request)

    # 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.set_type(batch, t_update_operators_params)
        if self.policy.supports_operator:
            with sp.for_("action", batch) as action:
                with action.match_cases() as arg:
                    with arg.match("add_operator") as operator:
                        self.policy.check_operator_update_permissions(self, operator)
                        self.data.operators[operator] = sp.unit
                    with arg.match("remove_operator") as operator:
                        self.policy.check_operator_update_permissions(self, operator)
                        del self.data.operators[operator]
        else:
            sp.failwith("FA2_OPERATORS_UNSUPPORTED")

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

        `balance_of_batch` must be defined in the child class.
        """
        sp.set_type(params, t_balance_of_params)
        sp.transfer(
            self.balance_of_batch(params.requests), 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_` must be defined in the child class.
        """
        sp.set_type(batch, t_transfer_params)
        if self.policy.supports_transfer:
            with sp.for_("transfer", batch) as transfer:
                with sp.for_("tx", transfer.txs) as tx:
                    # The ordering of sp.verify is important: 1) token_undefined, 2) transfer permission 3) balance
                    sp.verify(self.is_defined(tx.token_id), "FA2_TOKEN_UNDEFINED")
                    self.policy.check_tx_transfer_permissions(
                        self, transfer.from_, tx.to_, tx.token_id
                    )
                    with sp.if_(tx.amount > 0):
                        self.transfer_tx_(transfer.from_, tx)
        else:
            sp.failwith("FA2_TX_DENIED")

    # Offchain views

    @sp.offchain_view(pure=True)
    def all_tokens(self):
        """OffchainView: Return the list of all the token IDs known to the contract."""
        sp.result(sp.range(0, self.data.last_token_id))

    @sp.offchain_view(pure=True)
    def is_operator(self, params):
        """Return whether `operator` is allowed to transfer `token_id` tokens
        owned by `owner`."""
        sp.set_type(params, t_operator_permission)
        sp.result(self.policy.is_operator(self, params))

    @sp.offchain_view(pure=True)
    def get_balance(self, params):
        """Return the balance of an address for the specified `token_id`."""
        sp.set_type(
            params,
            sp.TRecord(owner=sp.TAddress, token_id=sp.TNat).layout(
                ("owner", "token_id")
            ),
        )
        sp.result(self.balance_(params.owner, params.token_id))

    @sp.offchain_view(pure=True)
    def total_supply(self, params):
        """Return the total number of tokens for the given `token_id`."""
        sp.verify(self.is_defined(params.token_id), "FA2_TOKEN_UNDEFINED")
        sp.result(sp.set_type_expr(self.supply_(params.token_id), sp.TNat))


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


class Fa2Nft(Common):
    """Base class for a FA2 NFT contract.

    Respects the FA2 standard.
    """

    ledger_type = "NFT"

    def __init__(
        self, metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None
    ):
        ledger, token_metadata = self.initial_mint(token_metadata, ledger)
        self.init(
            ledger=sp.big_map(ledger, tkey=sp.TNat, tvalue=sp.TAddress),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def initial_mint(self, token_metadata=[], ledger={}):
        """Perform a mint before the origination.

        Returns `ledger` and `token_metadata`.
        """
        token_metadata_dict = {}
        for token_id, metadata in enumerate(token_metadata):
            token_metadata_dict[token_id] = sp.record(
                token_id=token_id, token_info=metadata
            )
        for token_id, address in ledger.items():
            if token_id not in token_metadata_dict:
                raise Exception(
                    "Ledger contains token_id with no corresponding metadata"
                )
        return (ledger, token_metadata_dict)

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return sp.eif(self.data.ledger[token_id] == owner, 1, 0)

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return sp.nat(1)

    def transfer_tx_(self, from_, tx):
        sp.verify(
            (tx.amount == 1) & (self.data.ledger[tx.token_id] == from_),
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        self.data.ledger[tx.token_id] = tx.to_


class Fa2Fungible(Common):
    """Base class for a FA2 fungible contract.

    Respects the FA2 standard.
    """

    ledger_type = "Fungible"

    def __init__(
        self, metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None
    ):
        ledger, supply, token_metadata = self.initial_mint(token_metadata, ledger)
        self.init(
            ledger=sp.big_map(
                ledger, tkey=sp.TPair(sp.TAddress, sp.TNat), tvalue=sp.TNat
            ),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
            supply=sp.big_map(supply, tkey=sp.TNat, tvalue=sp.TNat),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def initial_mint(self, token_metadata=[], ledger={}):
        """Perform a mint before the origination.

        Returns `ledger`, `supply` and `token_metadata`.
        """
        token_metadata_dict = {}
        supply = {}
        for token_id, metadata in enumerate(token_metadata):
            metadata = sp.record(token_id=token_id, token_info=metadata)
            token_metadata_dict[token_id] = metadata
            # Token that are in token_metadata and not in ledger exist with supply = 0
            supply[token_id] = 0
        for (address, token_id), amount in ledger.items():
            if token_id not in token_metadata_dict:
                raise Exception("Ledger contains a token_id with no metadata")
            supply[token_id] += amount
        return (ledger, supply, token_metadata_dict)

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.ledger.get((owner, token_id), 0)

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.supply.get(token_id, sp.nat(0))

    def transfer_tx_(self, from_, tx):
        from_ = (from_, tx.token_id)
        self.data.ledger[from_] = sp.as_nat(
            self.data.ledger.get(from_, 0) - tx.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        to_ = (tx.to_, tx.token_id)
        self.data.ledger[to_] = self.data.ledger.get(to_, 0) + tx.amount


class Fa2SingleAsset(Common):
    """Base class for a FA2 contract with single (fungible) asset.

    Respects the FA2 standard.
    """

    ledger_type = "SingleAsset"

    def __init__(
        self, metadata, token_metadata=None, ledger={}, policy=None, metadata_base=None
    ):
        if token_metadata is None:
            if len(ledger) > 0:
                raise Exception("Ledger not empty while metadata is None")
            token_metadata = {}
        else:
            token_metadata = {0: sp.record(token_id=0, token_info=token_metadata)}
        self.init(
            ledger=sp.big_map(ledger, tkey=sp.TAddress, tvalue=sp.TNat),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
            supply=sum(ledger.values()),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.ledger.get(owner, 0)

    def transfer_tx_(self, from_, tx):
        self.data.ledger[from_] = sp.as_nat(
            self.data.ledger.get(from_, 0) - tx.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        self.data.ledger[tx.to_] = self.data.ledger.get(tx.to_, 0) + tx.amount

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.supply


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


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

    Adds an `administrator` attribute in the storage record. Provides a
    `set_administrator` entrypoint. Provides a `is_administrator` meta-
    programming function.
    """

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

    def is_administrator(self, sender):
        return sender == self.data.administrator

    @sp.entrypoint
    def set_administrator(self, params):
        """(Admin only) Set the contract administrator."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        self.data.administrator = params


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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def set_metadata(self, metadata):
        """(Admin only) Set the contract metadata."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        self.data.metadata = metadata


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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def withdraw_mutez(self, destination, amount):
        """(Admin only) Transfer `amount` mutez to `destination`."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        sp.send(destination, amount)


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

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

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


class OnchainviewBalanceOf:
    """(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.onchain_view()
    def get_balance_of(self, requests):
        """Onchain view equivalent to the `balance_of` entrypoint."""
        sp.set_type(requests, sp.TList(t_balance_of_request))
        sp.result(
            sp.set_type_expr(
                self.balance_of_batch(requests), sp.TList(t_balance_of_response)
            )
        )


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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint new or existing tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    to_=sp.TAddress,
                    metadata=sp.TMap(sp.TString, sp.TBytes),
                ).layout(("to_", "metadata"))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            token_id = sp.compute(self.data.last_token_id)
            metadata = sp.record(token_id=token_id, token_info=action.metadata)
            self.data.token_metadata[token_id] = metadata
            self.data.ledger[token_id] = action.to_
            self.data.last_token_id += 1


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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    to_=sp.TAddress,
                    token=sp.TVariant(
                        new=sp.TMap(sp.TString, sp.TBytes), existing=sp.TNat
                    ),
                    amount=sp.TNat,
                ).layout(("to_", ("token", "amount")))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            with action.token.match_cases() as arg:
                with arg.match("new") as metadata:
                    token_id = sp.compute(self.data.last_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.last_token_id += 1
                with arg.match("existing") as token_id:
                    sp.verify(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_, 0) + action.amount
                    )


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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(to_=sp.TAddress, amount=sp.TNat).layout(("to_", "amount"))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(0), "FA2_TOKEN_UNDEFINED")
            self.data.supply += action.amount
            self.data.ledger[action.to_] = (
                self.data.ledger.get(action.to_, 0) + action.amount
            )


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

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

        Burning an nft destroys its metadata.
        """
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            with sp.if_(action.amount > 0):
                sp.verify(
                    (action.amount == sp.nat(1))
                    & (self.data.ledger[action.token_id] == action.from_),
                    message="FA2_INSUFFICIENT_BALANCE",
                )
                # Burn the token
                del self.data.ledger[action.token_id]
                del self.data.token_metadata[action.token_id]


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

    @sp.entrypoint
    def burn(self, batch):
        """Users can burn tokens if they have the transfer policy
        permission."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            from_ = (action.from_, action.token_id)
            # Burn the tokens
            self.data.ledger[from_] = sp.as_nat(
                self.data.ledger.get(from_, 0) - action.amount,
                message="FA2_INSUFFICIENT_BALANCE",
            )

            supply = sp.compute(
                sp.is_nat(self.data.supply.get(action.token_id, 0) - action.amount)
            )
            with supply.match_cases() as arg:
                with arg.match("Some") as nat_supply:
                    self.data.supply[action.token_id] = nat_supply
                with arg.match("None"):
                    self.data.supply[action.token_id] = 0


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

    @sp.entrypoint
    def burn(self, batch):
        """Users can burn tokens if they have the transfer policy
        permission."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            # Burn the tokens
            self.data.ledger[action.from_] = sp.as_nat(
                self.data.ledger.get(action.from_, 0) - action.amount,
                message="FA2_INSUFFICIENT_BALANCE",
            )

            supply = sp.compute(sp.is_nat(self.data.supply - action.amount))
            with supply.match_cases() as arg:
                with arg.match("Some") as nat_supply:
                    self.data.supply = nat_supply
                with arg.match("None"):
                    self.data.supply = 0


###########
# 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.init(last_known_balances=sp.big_map())

    @sp.entrypoint
    def receive_balances(self, params):
        sp.set_type(params, sp.TList(t_balance_of_response))
        with sp.for_("resp", params) as resp:
            owner = (resp.request.owner, resp.request.token_id)
            with sp.if_(self.data.last_known_balances.contains(sp.sender)):
                self.data.last_known_balances[sp.sender][owner] = resp.balance
            with sp.else_():
                self.data.last_known_balances[sp.sender] = sp.map({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),
        }
    )

Classes

class NoTransfer

(Transfer Policy) No transfer allowed.

Expand source code
class NoTransfer:
    """(Transfer Policy) No transfer allowed."""

    def init_policy(self, contract):
        self.name = "no-transfer"
        self.supports_transfer = False
        self.supports_operator = False

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        pass

    def check_operator_update_permissions(self, contract, operator_permission):
        pass

    def is_operator(self, contract, operator_permission):
        return False

Methods

def init_policy(self, contract)
Expand source code
def init_policy(self, contract):
    self.name = "no-transfer"
    self.supports_transfer = False
    self.supports_operator = False
def check_tx_transfer_permissions(self, contract, from_, to_, token_id)
Expand source code
def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
    pass
def check_operator_update_permissions(self, contract, operator_permission)
Expand source code
def check_operator_update_permissions(self, contract, operator_permission):
    pass
def is_operator(self, contract, operator_permission)
Expand source code
def is_operator(self, contract, operator_permission):
    return False
class OwnerTransfer

(Transfer Policy) Only owner can transfer tokens, no operator allowed.

Expand source code
class OwnerTransfer:
    """(Transfer Policy) Only owner can transfer tokens, no operator
    allowed."""

    def init_policy(self, contract):
        self.name = "owner-transfer"
        self.supports_transfer = True
        self.supports_operator = False

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(sp.sender == from_, "FA2_NOT_OWNER")

    def check_operator_update_permissions(self, contract, operator_permission):
        pass

    def is_operator(self, contract, operator_permission):
        return False

Methods

def init_policy(self, contract)
Expand source code
def init_policy(self, contract):
    self.name = "owner-transfer"
    self.supports_transfer = True
    self.supports_operator = False
def check_tx_transfer_permissions(self, contract, from_, to_, token_id)
Expand source code
def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
    sp.verify(sp.sender == from_, "FA2_NOT_OWNER")
def check_operator_update_permissions(self, contract, operator_permission)
Expand source code
def check_operator_update_permissions(self, contract, operator_permission):
    pass
def is_operator(self, contract, operator_permission)
Expand source code
def is_operator(self, contract, operator_permission):
    return False
class OwnerOrOperatorTransfer

(Transfer Policy) Only owner and operators can transfer tokens.

Operators allowed.

Expand source code
class OwnerOrOperatorTransfer:
    """(Transfer Policy) Only owner and operators can transfer tokens.

    Operators allowed.
    """

    def init_policy(self, contract):
        self.name = "owner-or-operator-transfer"
        self.supports_transfer = True
        self.supports_operator = True
        contract.update_initial_storage(
            operators=sp.big_map(tkey=t_operator_permission, tvalue=sp.TUnit)
        )

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(
            (sp.sender == from_)
            | contract.data.operators.contains(
                sp.record(owner=from_, operator=sp.sender, token_id=token_id)
            ),
            message="FA2_NOT_OPERATOR",
        )

    def check_operator_update_permissions(self, contract, operator_permission):
        sp.verify(operator_permission.owner == sp.sender, "FA2_NOT_OWNER")

    def is_operator(self, contract, operator_permission):
        return contract.data.operators.contains(operator_permission)

Methods

def init_policy(self, contract)
Expand source code
def init_policy(self, contract):
    self.name = "owner-or-operator-transfer"
    self.supports_transfer = True
    self.supports_operator = True
    contract.update_initial_storage(
        operators=sp.big_map(tkey=t_operator_permission, tvalue=sp.TUnit)
    )
def check_tx_transfer_permissions(self, contract, from_, to_, token_id)
Expand source code
def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
    sp.verify(
        (sp.sender == from_)
        | contract.data.operators.contains(
            sp.record(owner=from_, operator=sp.sender, token_id=token_id)
        ),
        message="FA2_NOT_OPERATOR",
    )
def check_operator_update_permissions(self, contract, operator_permission)
Expand source code
def check_operator_update_permissions(self, contract, operator_permission):
    sp.verify(operator_permission.owner == sp.sender, "FA2_NOT_OWNER")
def is_operator(self, contract, operator_permission)
Expand source code
def is_operator(self, contract, operator_permission):
    return contract.data.operators.contains(operator_permission)
class PauseTransfer (policy=None)

(Transfer Policy) Decorate any policy to add a pause mechanism.

Adds a set_pause entrypoint. Checks that contract.data.paused is False before accepting transfers and operator updates.

Needs the Admin mixin in order to work.

Expand source code
class PauseTransfer:
    """(Transfer Policy) Decorate any policy to add a pause mechanism.

    Adds a `set_pause` entrypoint. Checks that contract.data.paused is
    `False` before accepting transfers and operator updates.

    Needs the `Admin` mixin in order to work.
    """

    def __init__(self, policy=None):
        if policy is None:
            self.policy = OwnerOrOperatorTransfer()
        else:
            self.policy = policy

    def init_policy(self, contract):
        self.policy.init_policy(contract)
        self.name = "pauseable-" + self.policy.name
        self.supports_transfer = self.policy.supports_transfer
        self.supports_operator = self.policy.supports_operator
        contract.update_initial_storage(paused=False)

        # Add a set_pause entrypoint
        def set_pause(self, params):
            sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
            self.data.paused = params

        contract.set_pause = sp.entrypoint(set_pause)

    def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
        sp.verify(~contract.data.paused, message=sp.pair("FA2_TX_DENIED", "FA2_PAUSED"))
        self.policy.check_tx_transfer_permissions(contract, from_, to_, token_id)

    def check_operator_update_permissions(self, contract, operator_param):
        sp.verify(
            ~contract.data.paused,
            message=sp.pair("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED"),
        )
        self.policy.check_operator_update_permissions(contract, operator_param)

    def is_operator(self, contract, operator_param):
        return self.policy.is_operator(contract, operator_param)

Methods

def init_policy(self, contract)
Expand source code
def init_policy(self, contract):
    self.policy.init_policy(contract)
    self.name = "pauseable-" + self.policy.name
    self.supports_transfer = self.policy.supports_transfer
    self.supports_operator = self.policy.supports_operator
    contract.update_initial_storage(paused=False)

    # Add a set_pause entrypoint
    def set_pause(self, params):
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        self.data.paused = params

    contract.set_pause = sp.entrypoint(set_pause)
def check_tx_transfer_permissions(self, contract, from_, to_, token_id)
Expand source code
def check_tx_transfer_permissions(self, contract, from_, to_, token_id):
    sp.verify(~contract.data.paused, message=sp.pair("FA2_TX_DENIED", "FA2_PAUSED"))
    self.policy.check_tx_transfer_permissions(contract, from_, to_, token_id)
def check_operator_update_permissions(self, contract, operator_param)
Expand source code
def check_operator_update_permissions(self, contract, operator_param):
    sp.verify(
        ~contract.data.paused,
        message=sp.pair("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED"),
    )
    self.policy.check_operator_update_permissions(contract, operator_param)
def is_operator(self, contract, operator_param)
Expand source code
def is_operator(self, contract, operator_param):
    return self.policy.is_operator(contract, operator_param)
class Common (policy=None, metadata_base=None, token_metadata={})

Common logic between Fa2Nft, Fa2Fungible and Fa2SingleAsset.

Expand source code
class Common(sp.Contract):
    """Common logic between Fa2Nft, Fa2Fungible and Fa2SingleAsset."""

    def __init__(self, policy=None, metadata_base=None, token_metadata={}):
        if policy is None:
            self.policy = OwnerOrOperatorTransfer()
        else:
            self.policy = policy
        self.update_initial_storage(
            token_metadata=sp.big_map(
                token_metadata,
                tkey=sp.TNat,
                tvalue=sp.TRecord(
                    token_id=sp.TNat, token_info=sp.TMap(sp.TString, sp.TBytes)
                ).layout(("token_id", "token_info")),
            )
        )
        self.policy.init_policy(self)
        self.generate_contract_metadata("metadata_base", metadata_base)

    def is_defined(self, token_id):
        return self.data.token_metadata.contains(token_id)

    def generate_contract_metadata(self, filename, metadata_base=None):
        """Generate a metadata json file with all the contract's offchain views
        and standard TZIP-126 and TZIP-016 key/values."""
        if metadata_base is None:
            metadata_base = {
                "name": "FA2 contract",
                "version": "1.0.0",
                "description": "This implements FA2 (TZIP-012) using SmartPy.",
                "interfaces": ["TZIP-012", "TZIP-016"],
                "authors": ["SmartPy <https://legacy.smartpy.io/#contact>"],
                "homepage": "https://legacy.smartpy.io/ide?template=FA2.py",
                "source": {
                    "tools": ["SmartPy"],
                    "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/FA2.py",
                },
                "permissions": {"receiver": "owner-no-hook", "sender": "owner-no-hook"},
            }
        offchain_views = []
        for f in dir(self):
            attr = getattr(self, f)
            if isinstance(attr, sp.OnOffchainView):
                if attr.kind == "offchain":
                    offchain_views.append(attr)
        metadata_base["views"] = offchain_views
        metadata_base["permissions"]["operator"] = self.policy.name
        self.init_metadata(filename, metadata_base)

    def balance_of_batch(self, requests):
        """Mapping of balances."""
        sp.set_type(requests, sp.TList(t_balance_of_request))

        def f_process_request(req):
            sp.result(
                sp.record(
                    request=req,
                    balance=self.balance_(req.owner, req.token_id),
                )
            )

        return requests.map(f_process_request)

    # 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.set_type(batch, t_update_operators_params)
        if self.policy.supports_operator:
            with sp.for_("action", batch) as action:
                with action.match_cases() as arg:
                    with arg.match("add_operator") as operator:
                        self.policy.check_operator_update_permissions(self, operator)
                        self.data.operators[operator] = sp.unit
                    with arg.match("remove_operator") as operator:
                        self.policy.check_operator_update_permissions(self, operator)
                        del self.data.operators[operator]
        else:
            sp.failwith("FA2_OPERATORS_UNSUPPORTED")

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

        `balance_of_batch` must be defined in the child class.
        """
        sp.set_type(params, t_balance_of_params)
        sp.transfer(
            self.balance_of_batch(params.requests), 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_` must be defined in the child class.
        """
        sp.set_type(batch, t_transfer_params)
        if self.policy.supports_transfer:
            with sp.for_("transfer", batch) as transfer:
                with sp.for_("tx", transfer.txs) as tx:
                    # The ordering of sp.verify is important: 1) token_undefined, 2) transfer permission 3) balance
                    sp.verify(self.is_defined(tx.token_id), "FA2_TOKEN_UNDEFINED")
                    self.policy.check_tx_transfer_permissions(
                        self, transfer.from_, tx.to_, tx.token_id
                    )
                    with sp.if_(tx.amount > 0):
                        self.transfer_tx_(transfer.from_, tx)
        else:
            sp.failwith("FA2_TX_DENIED")

    # Offchain views

    @sp.offchain_view(pure=True)
    def all_tokens(self):
        """OffchainView: Return the list of all the token IDs known to the contract."""
        sp.result(sp.range(0, self.data.last_token_id))

    @sp.offchain_view(pure=True)
    def is_operator(self, params):
        """Return whether `operator` is allowed to transfer `token_id` tokens
        owned by `owner`."""
        sp.set_type(params, t_operator_permission)
        sp.result(self.policy.is_operator(self, params))

    @sp.offchain_view(pure=True)
    def get_balance(self, params):
        """Return the balance of an address for the specified `token_id`."""
        sp.set_type(
            params,
            sp.TRecord(owner=sp.TAddress, token_id=sp.TNat).layout(
                ("owner", "token_id")
            ),
        )
        sp.result(self.balance_(params.owner, params.token_id))

    @sp.offchain_view(pure=True)
    def total_supply(self, params):
        """Return the total number of tokens for the given `token_id`."""
        sp.verify(self.is_defined(params.token_id), "FA2_TOKEN_UNDEFINED")
        sp.result(sp.set_type_expr(self.supply_(params.token_id), sp.TNat))

Ancestors

  • smartpy.Contract

Subclasses

Methods

def is_defined(self, token_id)
Expand source code
def is_defined(self, token_id):
    return self.data.token_metadata.contains(token_id)
def generate_contract_metadata(self, filename, metadata_base=None)

Generate a metadata json file with all the contract's offchain views and standard TZIP-126 and TZIP-016 key/values.

Expand source code
def generate_contract_metadata(self, filename, metadata_base=None):
    """Generate a metadata json file with all the contract's offchain views
    and standard TZIP-126 and TZIP-016 key/values."""
    if metadata_base is None:
        metadata_base = {
            "name": "FA2 contract",
            "version": "1.0.0",
            "description": "This implements FA2 (TZIP-012) using SmartPy.",
            "interfaces": ["TZIP-012", "TZIP-016"],
            "authors": ["SmartPy <https://legacy.smartpy.io/#contact>"],
            "homepage": "https://legacy.smartpy.io/ide?template=FA2.py",
            "source": {
                "tools": ["SmartPy"],
                "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/FA2.py",
            },
            "permissions": {"receiver": "owner-no-hook", "sender": "owner-no-hook"},
        }
    offchain_views = []
    for f in dir(self):
        attr = getattr(self, f)
        if isinstance(attr, sp.OnOffchainView):
            if attr.kind == "offchain":
                offchain_views.append(attr)
    metadata_base["views"] = offchain_views
    metadata_base["permissions"]["operator"] = self.policy.name
    self.init_metadata(filename, metadata_base)
def balance_of_batch(self, requests)

Mapping of balances.

Expand source code
def balance_of_batch(self, requests):
    """Mapping of balances."""
    sp.set_type(requests, sp.TList(t_balance_of_request))

    def f_process_request(req):
        sp.result(
            sp.record(
                request=req,
                balance=self.balance_(req.owner, req.token_id),
            )
        )

    return requests.map(f_process_request)
def update_operators(self, batch)

Entrypoint. Accept a list of variants to add or remove operators who can perform transfers on behalf of the owner.

Expand source code
@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.set_type(batch, t_update_operators_params)
    if self.policy.supports_operator:
        with sp.for_("action", batch) as action:
            with action.match_cases() as arg:
                with arg.match("add_operator") as operator:
                    self.policy.check_operator_update_permissions(self, operator)
                    self.data.operators[operator] = sp.unit
                with arg.match("remove_operator") as operator:
                    self.policy.check_operator_update_permissions(self, operator)
                    del self.data.operators[operator]
    else:
        sp.failwith("FA2_OPERATORS_UNSUPPORTED")
def balance_of(self, params)

Entrypoint. Send the balance of multiple account / token pairs to a callback address.

balance_of_batch must be defined in the child class.

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

    `balance_of_batch` must be defined in the child class.
    """
    sp.set_type(params, t_balance_of_params)
    sp.transfer(
        self.balance_of_batch(params.requests), sp.mutez(0), params.callback
    )
def transfer(self, batch)

Entrypoint. Accept a list of transfer operations between a source and multiple destinations.

transfer_tx_ must be defined in the child class.

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

    `transfer_tx_` must be defined in the child class.
    """
    sp.set_type(batch, t_transfer_params)
    if self.policy.supports_transfer:
        with sp.for_("transfer", batch) as transfer:
            with sp.for_("tx", transfer.txs) as tx:
                # The ordering of sp.verify is important: 1) token_undefined, 2) transfer permission 3) balance
                sp.verify(self.is_defined(tx.token_id), "FA2_TOKEN_UNDEFINED")
                self.policy.check_tx_transfer_permissions(
                    self, transfer.from_, tx.to_, tx.token_id
                )
                with sp.if_(tx.amount > 0):
                    self.transfer_tx_(transfer.from_, tx)
    else:
        sp.failwith("FA2_TX_DENIED")
def all_tokens(self)

Offchain view. OffchainView: Return the list of all the token IDs known to the contract.

Expand source code
@sp.offchain_view(pure=True)
def all_tokens(self):
    """OffchainView: Return the list of all the token IDs known to the contract."""
    sp.result(sp.range(0, self.data.last_token_id))
def is_operator(self, params)

Offchain view. Return whether operator is allowed to transfer token_id tokens owned by owner.

Expand source code
@sp.offchain_view(pure=True)
def is_operator(self, params):
    """Return whether `operator` is allowed to transfer `token_id` tokens
    owned by `owner`."""
    sp.set_type(params, t_operator_permission)
    sp.result(self.policy.is_operator(self, params))
def get_balance(self, params)

Offchain view. Return the balance of an address for the specified token_id.

Expand source code
@sp.offchain_view(pure=True)
def get_balance(self, params):
    """Return the balance of an address for the specified `token_id`."""
    sp.set_type(
        params,
        sp.TRecord(owner=sp.TAddress, token_id=sp.TNat).layout(
            ("owner", "token_id")
        ),
    )
    sp.result(self.balance_(params.owner, params.token_id))
def total_supply(self, params)

Offchain view. Return the total number of tokens for the given token_id.

Expand source code
@sp.offchain_view(pure=True)
def total_supply(self, params):
    """Return the total number of tokens for the given `token_id`."""
    sp.verify(self.is_defined(params.token_id), "FA2_TOKEN_UNDEFINED")
    sp.result(sp.set_type_expr(self.supply_(params.token_id), sp.TNat))
class Fa2Nft (metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None)

Base class for a FA2 NFT contract.

Respects the FA2 standard.

Expand source code
class Fa2Nft(Common):
    """Base class for a FA2 NFT contract.

    Respects the FA2 standard.
    """

    ledger_type = "NFT"

    def __init__(
        self, metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None
    ):
        ledger, token_metadata = self.initial_mint(token_metadata, ledger)
        self.init(
            ledger=sp.big_map(ledger, tkey=sp.TNat, tvalue=sp.TAddress),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def initial_mint(self, token_metadata=[], ledger={}):
        """Perform a mint before the origination.

        Returns `ledger` and `token_metadata`.
        """
        token_metadata_dict = {}
        for token_id, metadata in enumerate(token_metadata):
            token_metadata_dict[token_id] = sp.record(
                token_id=token_id, token_info=metadata
            )
        for token_id, address in ledger.items():
            if token_id not in token_metadata_dict:
                raise Exception(
                    "Ledger contains token_id with no corresponding metadata"
                )
        return (ledger, token_metadata_dict)

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return sp.eif(self.data.ledger[token_id] == owner, 1, 0)

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return sp.nat(1)

    def transfer_tx_(self, from_, tx):
        sp.verify(
            (tx.amount == 1) & (self.data.ledger[tx.token_id] == from_),
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        self.data.ledger[tx.token_id] = tx.to_

Ancestors

Class variables

var ledger_type

Methods

def initial_mint(self, token_metadata=[], ledger={})

Perform a mint before the origination.

Returns ledger and token_metadata.

Expand source code
def initial_mint(self, token_metadata=[], ledger={}):
    """Perform a mint before the origination.

    Returns `ledger` and `token_metadata`.
    """
    token_metadata_dict = {}
    for token_id, metadata in enumerate(token_metadata):
        token_metadata_dict[token_id] = sp.record(
            token_id=token_id, token_info=metadata
        )
    for token_id, address in ledger.items():
        if token_id not in token_metadata_dict:
            raise Exception(
                "Ledger contains token_id with no corresponding metadata"
            )
    return (ledger, token_metadata_dict)
def balance_(self, owner, token_id)
Expand source code
def balance_(self, owner, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return sp.eif(self.data.ledger[token_id] == owner, 1, 0)
def supply_(self, token_id)
Expand source code
def supply_(self, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return sp.nat(1)
def transfer_tx_(self, from_, tx)
Expand source code
def transfer_tx_(self, from_, tx):
    sp.verify(
        (tx.amount == 1) & (self.data.ledger[tx.token_id] == from_),
        message="FA2_INSUFFICIENT_BALANCE",
    )
    # Do the transfer
    self.data.ledger[tx.token_id] = tx.to_

Inherited members

class Fa2Fungible (metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None)

Base class for a FA2 fungible contract.

Respects the FA2 standard.

Expand source code
class Fa2Fungible(Common):
    """Base class for a FA2 fungible contract.

    Respects the FA2 standard.
    """

    ledger_type = "Fungible"

    def __init__(
        self, metadata, token_metadata=[], ledger={}, policy=None, metadata_base=None
    ):
        ledger, supply, token_metadata = self.initial_mint(token_metadata, ledger)
        self.init(
            ledger=sp.big_map(
                ledger, tkey=sp.TPair(sp.TAddress, sp.TNat), tvalue=sp.TNat
            ),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
            supply=sp.big_map(supply, tkey=sp.TNat, tvalue=sp.TNat),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def initial_mint(self, token_metadata=[], ledger={}):
        """Perform a mint before the origination.

        Returns `ledger`, `supply` and `token_metadata`.
        """
        token_metadata_dict = {}
        supply = {}
        for token_id, metadata in enumerate(token_metadata):
            metadata = sp.record(token_id=token_id, token_info=metadata)
            token_metadata_dict[token_id] = metadata
            # Token that are in token_metadata and not in ledger exist with supply = 0
            supply[token_id] = 0
        for (address, token_id), amount in ledger.items():
            if token_id not in token_metadata_dict:
                raise Exception("Ledger contains a token_id with no metadata")
            supply[token_id] += amount
        return (ledger, supply, token_metadata_dict)

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.ledger.get((owner, token_id), 0)

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.supply.get(token_id, sp.nat(0))

    def transfer_tx_(self, from_, tx):
        from_ = (from_, tx.token_id)
        self.data.ledger[from_] = sp.as_nat(
            self.data.ledger.get(from_, 0) - tx.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        to_ = (tx.to_, tx.token_id)
        self.data.ledger[to_] = self.data.ledger.get(to_, 0) + tx.amount

Ancestors

Class variables

var ledger_type

Methods

def initial_mint(self, token_metadata=[], ledger={})

Perform a mint before the origination.

Returns ledger, supply and token_metadata.

Expand source code
def initial_mint(self, token_metadata=[], ledger={}):
    """Perform a mint before the origination.

    Returns `ledger`, `supply` and `token_metadata`.
    """
    token_metadata_dict = {}
    supply = {}
    for token_id, metadata in enumerate(token_metadata):
        metadata = sp.record(token_id=token_id, token_info=metadata)
        token_metadata_dict[token_id] = metadata
        # Token that are in token_metadata and not in ledger exist with supply = 0
        supply[token_id] = 0
    for (address, token_id), amount in ledger.items():
        if token_id not in token_metadata_dict:
            raise Exception("Ledger contains a token_id with no metadata")
        supply[token_id] += amount
    return (ledger, supply, token_metadata_dict)
def balance_(self, owner, token_id)
Expand source code
def balance_(self, owner, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return self.data.ledger.get((owner, token_id), 0)
def supply_(self, token_id)
Expand source code
def supply_(self, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return self.data.supply.get(token_id, sp.nat(0))
def transfer_tx_(self, from_, tx)
Expand source code
def transfer_tx_(self, from_, tx):
    from_ = (from_, tx.token_id)
    self.data.ledger[from_] = sp.as_nat(
        self.data.ledger.get(from_, 0) - tx.amount,
        message="FA2_INSUFFICIENT_BALANCE",
    )
    # Do the transfer
    to_ = (tx.to_, tx.token_id)
    self.data.ledger[to_] = self.data.ledger.get(to_, 0) + tx.amount

Inherited members

class Fa2SingleAsset (metadata, token_metadata=None, ledger={}, policy=None, metadata_base=None)

Base class for a FA2 contract with single (fungible) asset.

Respects the FA2 standard.

Expand source code
class Fa2SingleAsset(Common):
    """Base class for a FA2 contract with single (fungible) asset.

    Respects the FA2 standard.
    """

    ledger_type = "SingleAsset"

    def __init__(
        self, metadata, token_metadata=None, ledger={}, policy=None, metadata_base=None
    ):
        if token_metadata is None:
            if len(ledger) > 0:
                raise Exception("Ledger not empty while metadata is None")
            token_metadata = {}
        else:
            token_metadata = {0: sp.record(token_id=0, token_info=token_metadata)}
        self.init(
            ledger=sp.big_map(ledger, tkey=sp.TAddress, tvalue=sp.TNat),
            metadata=sp.set_type_expr(metadata, sp.TBigMap(sp.TString, sp.TBytes)),
            last_token_id=sp.nat(len(token_metadata)),
            supply=sum(ledger.values()),
        )
        Common.__init__(
            self,
            policy=policy,
            metadata_base=metadata_base,
            token_metadata=token_metadata,
        )

    def balance_(self, owner, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.ledger.get(owner, 0)

    def transfer_tx_(self, from_, tx):
        self.data.ledger[from_] = sp.as_nat(
            self.data.ledger.get(from_, 0) - tx.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )
        # Do the transfer
        self.data.ledger[tx.to_] = self.data.ledger.get(tx.to_, 0) + tx.amount

    def supply_(self, token_id):
        sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
        return self.data.supply

Ancestors

Class variables

var ledger_type

Methods

def balance_(self, owner, token_id)
Expand source code
def balance_(self, owner, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return self.data.ledger.get(owner, 0)
def transfer_tx_(self, from_, tx)
Expand source code
def transfer_tx_(self, from_, tx):
    self.data.ledger[from_] = sp.as_nat(
        self.data.ledger.get(from_, 0) - tx.amount,
        message="FA2_INSUFFICIENT_BALANCE",
    )
    # Do the transfer
    self.data.ledger[tx.to_] = self.data.ledger.get(tx.to_, 0) + tx.amount
def supply_(self, token_id)
Expand source code
def supply_(self, token_id):
    sp.verify(self.is_defined(token_id), "FA2_TOKEN_UNDEFINED")
    return self.data.supply

Inherited members

class Admin (administrator)

(Mixin) Provide the basics for having an administrator in the contract.

Adds an administrator attribute in the storage record. Provides a set_administrator entrypoint. Provides a is_administrator meta- programming function.

Expand source code
class Admin:
    """(Mixin) Provide the basics for having an administrator in the contract.

    Adds an `administrator` attribute in the storage record. Provides a
    `set_administrator` entrypoint. Provides a `is_administrator` meta-
    programming function.
    """

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

    def is_administrator(self, sender):
        return sender == self.data.administrator

    @sp.entrypoint
    def set_administrator(self, params):
        """(Admin only) Set the contract administrator."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        self.data.administrator = params

Methods

def is_administrator(self, sender)
Expand source code
def is_administrator(self, sender):
    return sender == self.data.administrator
def set_administrator(self, params)

Entrypoint. (Admin only) Set the contract administrator.

Expand source code
@sp.entrypoint
def set_administrator(self, params):
    """(Admin only) Set the contract administrator."""
    sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
    self.data.administrator = params
class ChangeMetadata

(Mixin) Provide an entrypoint to change contract metadata.

Requires the Admin mixin.

Expand source code
class ChangeMetadata:
    """(Mixin) Provide an entrypoint to change contract metadata.

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def set_metadata(self, metadata):
        """(Admin only) Set the contract metadata."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        self.data.metadata = metadata

Methods

def set_metadata(self, metadata)

Entrypoint. (Admin only) Set the contract metadata.

Expand source code
@sp.entrypoint
def set_metadata(self, metadata):
    """(Admin only) Set the contract metadata."""
    sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
    self.data.metadata = metadata
class WithdrawMutez

(Mixin) Provide an entrypoint to withdraw mutez that are in the contract's balance.

Requires the Admin mixin.

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

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def withdraw_mutez(self, destination, amount):
        """(Admin only) Transfer `amount` mutez to `destination`."""
        sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
        sp.send(destination, amount)

Methods

def withdraw_mutez(self, destination, amount)

Entrypoint. (Admin only) Transfer amount mutez to destination.

Expand source code
@sp.entrypoint
def withdraw_mutez(self, destination, amount):
    """(Admin only) Transfer `amount` mutez to `destination`."""
    sp.verify(self.is_administrator(sp.sender), message="FA2_NOT_ADMIN")
    sp.send(destination, amount)
class OffchainviewTokenMetadata

(Mixin) If present indexers use it to retrieve the token's metadata.

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

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

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

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

Methods

def token_metadata(self, token_id)

Offchain view. Returns the token-metadata URI for the given token.

Expand source code
@sp.offchain_view()
def token_metadata(self, token_id):
    """Returns the token-metadata URI for the given token."""
    sp.result(self.data.token_metadata[token_id])
class OnchainviewBalanceOf

(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.

Expand source code
class OnchainviewBalanceOf:
    """(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.onchain_view()
    def get_balance_of(self, requests):
        """Onchain view equivalent to the `balance_of` entrypoint."""
        sp.set_type(requests, sp.TList(t_balance_of_request))
        sp.result(
            sp.set_type_expr(
                self.balance_of_batch(requests), sp.TList(t_balance_of_response)
            )
        )

Methods

def get_balance_of(self, requests)

Onchain view. Onchain view equivalent to the balance_of entrypoint.

Expand source code
@sp.onchain_view()
def get_balance_of(self, requests):
    """Onchain view equivalent to the `balance_of` entrypoint."""
    sp.set_type(requests, sp.TList(t_balance_of_request))
    sp.result(
        sp.set_type_expr(
            self.balance_of_batch(requests), sp.TList(t_balance_of_response)
        )
    )
class MintNft

(Mixin) Non-standard mint entrypoint for FA2Nft with incrementing id.

Requires the Admin mixin.

Expand source code
class MintNft:
    """(Mixin) Non-standard `mint` entrypoint for FA2Nft with incrementing id.

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint new or existing tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    to_=sp.TAddress,
                    metadata=sp.TMap(sp.TString, sp.TBytes),
                ).layout(("to_", "metadata"))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            token_id = sp.compute(self.data.last_token_id)
            metadata = sp.record(token_id=token_id, token_info=action.metadata)
            self.data.token_metadata[token_id] = metadata
            self.data.ledger[token_id] = action.to_
            self.data.last_token_id += 1

Methods

def mint(self, batch)

Entrypoint. Admin can mint new or existing tokens.

Expand source code
@sp.entrypoint
def mint(self, batch):
    """Admin can mint new or existing tokens."""
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(
                to_=sp.TAddress,
                metadata=sp.TMap(sp.TString, sp.TBytes),
            ).layout(("to_", "metadata"))
        ),
    )
    sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
    with sp.for_("action", batch) as action:
        token_id = sp.compute(self.data.last_token_id)
        metadata = sp.record(token_id=token_id, token_info=action.metadata)
        self.data.token_metadata[token_id] = metadata
        self.data.ledger[token_id] = action.to_
        self.data.last_token_id += 1
class MintFungible

(Mixin) Non-standard mint entrypoint for FA2Fungible with incrementing id.

Requires the Admin mixin.

Expand source code
class MintFungible:
    """(Mixin) Non-standard `mint` entrypoint for FA2Fungible with incrementing
    id.

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    to_=sp.TAddress,
                    token=sp.TVariant(
                        new=sp.TMap(sp.TString, sp.TBytes), existing=sp.TNat
                    ),
                    amount=sp.TNat,
                ).layout(("to_", ("token", "amount")))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            with action.token.match_cases() as arg:
                with arg.match("new") as metadata:
                    token_id = sp.compute(self.data.last_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.last_token_id += 1
                with arg.match("existing") as token_id:
                    sp.verify(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_, 0) + action.amount
                    )

Methods

def mint(self, batch)

Entrypoint. Admin can mint tokens.

Expand source code
@sp.entrypoint
def mint(self, batch):
    """Admin can mint tokens."""
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(
                to_=sp.TAddress,
                token=sp.TVariant(
                    new=sp.TMap(sp.TString, sp.TBytes), existing=sp.TNat
                ),
                amount=sp.TNat,
            ).layout(("to_", ("token", "amount")))
        ),
    )
    sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
    with sp.for_("action", batch) as action:
        with action.token.match_cases() as arg:
            with arg.match("new") as metadata:
                token_id = sp.compute(self.data.last_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.last_token_id += 1
            with arg.match("existing") as token_id:
                sp.verify(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_, 0) + action.amount
                )
class MintSingleAsset

(Mixin) Non-standard mint entrypoint for FA2SingleAsset.

Requires the Admin mixin.

Expand source code
class MintSingleAsset:
    """(Mixin) Non-standard `mint` entrypoint for FA2SingleAsset.

    Requires the `Admin` mixin.
    """

    @sp.entrypoint
    def mint(self, batch):
        """Admin can mint tokens."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(to_=sp.TAddress, amount=sp.TNat).layout(("to_", "amount"))
            ),
        )
        sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(0), "FA2_TOKEN_UNDEFINED")
            self.data.supply += action.amount
            self.data.ledger[action.to_] = (
                self.data.ledger.get(action.to_, 0) + action.amount
            )

Methods

def mint(self, batch)

Entrypoint. Admin can mint tokens.

Expand source code
@sp.entrypoint
def mint(self, batch):
    """Admin can mint tokens."""
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(to_=sp.TAddress, amount=sp.TNat).layout(("to_", "amount"))
        ),
    )
    sp.verify(self.is_administrator(sp.sender), "FA2_NOT_ADMIN")
    with sp.for_("action", batch) as action:
        sp.verify(self.is_defined(0), "FA2_TOKEN_UNDEFINED")
        self.data.supply += action.amount
        self.data.ledger[action.to_] = (
            self.data.ledger.get(action.to_, 0) + action.amount
        )
class BurnNft

(Mixin) Non-standard burn entrypoint for FA2Nft that uses the transfer policy permission.

Expand source code
class BurnNft:
    """(Mixin) Non-standard `burn` entrypoint for FA2Nft that uses the transfer
    policy permission."""

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

        Burning an nft destroys its metadata.
        """
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            with sp.if_(action.amount > 0):
                sp.verify(
                    (action.amount == sp.nat(1))
                    & (self.data.ledger[action.token_id] == action.from_),
                    message="FA2_INSUFFICIENT_BALANCE",
                )
                # Burn the token
                del self.data.ledger[action.token_id]
                del self.data.token_metadata[action.token_id]

Methods

def burn(self, batch)

Entrypoint. Users can burn tokens if they have the transfer policy permission.

Burning an nft destroys its metadata.

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

    Burning an nft destroys its metadata.
    """
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(
                from_=sp.TAddress,
                token_id=sp.TNat,
                amount=sp.TNat,
            ).layout(("from_", ("token_id", "amount")))
        ),
    )
    sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
    with sp.for_("action", batch) as action:
        sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
        self.policy.check_tx_transfer_permissions(
            self, action.from_, action.from_, action.token_id
        )
        with sp.if_(action.amount > 0):
            sp.verify(
                (action.amount == sp.nat(1))
                & (self.data.ledger[action.token_id] == action.from_),
                message="FA2_INSUFFICIENT_BALANCE",
            )
            # Burn the token
            del self.data.ledger[action.token_id]
            del self.data.token_metadata[action.token_id]
class BurnFungible

(Mixin) Non-standard burn entrypoint for FA2Fungible that uses the transfer policy permission.

Expand source code
class BurnFungible:
    """(Mixin) Non-standard `burn` entrypoint for FA2Fungible that uses the
    transfer policy permission."""

    @sp.entrypoint
    def burn(self, batch):
        """Users can burn tokens if they have the transfer policy
        permission."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            from_ = (action.from_, action.token_id)
            # Burn the tokens
            self.data.ledger[from_] = sp.as_nat(
                self.data.ledger.get(from_, 0) - action.amount,
                message="FA2_INSUFFICIENT_BALANCE",
            )

            supply = sp.compute(
                sp.is_nat(self.data.supply.get(action.token_id, 0) - action.amount)
            )
            with supply.match_cases() as arg:
                with arg.match("Some") as nat_supply:
                    self.data.supply[action.token_id] = nat_supply
                with arg.match("None"):
                    self.data.supply[action.token_id] = 0

Methods

def burn(self, batch)

Entrypoint. Users can burn tokens if they have the transfer policy permission.

Expand source code
@sp.entrypoint
def burn(self, batch):
    """Users can burn tokens if they have the transfer policy
    permission."""
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(
                from_=sp.TAddress,
                token_id=sp.TNat,
                amount=sp.TNat,
            ).layout(("from_", ("token_id", "amount")))
        ),
    )
    sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
    with sp.for_("action", batch) as action:
        sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
        self.policy.check_tx_transfer_permissions(
            self, action.from_, action.from_, action.token_id
        )
        from_ = (action.from_, action.token_id)
        # Burn the tokens
        self.data.ledger[from_] = sp.as_nat(
            self.data.ledger.get(from_, 0) - action.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )

        supply = sp.compute(
            sp.is_nat(self.data.supply.get(action.token_id, 0) - action.amount)
        )
        with supply.match_cases() as arg:
            with arg.match("Some") as nat_supply:
                self.data.supply[action.token_id] = nat_supply
            with arg.match("None"):
                self.data.supply[action.token_id] = 0
class BurnSingleAsset

(Mixin) Non-standard burn entrypoint for FA2SingleAsset that uses the transfer policy permission.

Expand source code
class BurnSingleAsset:
    """(Mixin) Non-standard `burn` entrypoint for FA2SingleAsset that uses the
    transfer policy permission."""

    @sp.entrypoint
    def burn(self, batch):
        """Users can burn tokens if they have the transfer policy
        permission."""
        sp.set_type(
            batch,
            sp.TList(
                sp.TRecord(
                    from_=sp.TAddress,
                    token_id=sp.TNat,
                    amount=sp.TNat,
                ).layout(("from_", ("token_id", "amount")))
            ),
        )
        sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
        with sp.for_("action", batch) as action:
            sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
            self.policy.check_tx_transfer_permissions(
                self, action.from_, action.from_, action.token_id
            )
            # Burn the tokens
            self.data.ledger[action.from_] = sp.as_nat(
                self.data.ledger.get(action.from_, 0) - action.amount,
                message="FA2_INSUFFICIENT_BALANCE",
            )

            supply = sp.compute(sp.is_nat(self.data.supply - action.amount))
            with supply.match_cases() as arg:
                with arg.match("Some") as nat_supply:
                    self.data.supply = nat_supply
                with arg.match("None"):
                    self.data.supply = 0

Methods

def burn(self, batch)

Entrypoint. Users can burn tokens if they have the transfer policy permission.

Expand source code
@sp.entrypoint
def burn(self, batch):
    """Users can burn tokens if they have the transfer policy
    permission."""
    sp.set_type(
        batch,
        sp.TList(
            sp.TRecord(
                from_=sp.TAddress,
                token_id=sp.TNat,
                amount=sp.TNat,
            ).layout(("from_", ("token_id", "amount")))
        ),
    )
    sp.verify(self.policy.supports_transfer, "FA2_TX_DENIED")
    with sp.for_("action", batch) as action:
        sp.verify(self.is_defined(action.token_id), "FA2_TOKEN_UNDEFINED")
        self.policy.check_tx_transfer_permissions(
            self, action.from_, action.from_, action.token_id
        )
        # Burn the tokens
        self.data.ledger[action.from_] = sp.as_nat(
            self.data.ledger.get(action.from_, 0) - action.amount,
            message="FA2_INSUFFICIENT_BALANCE",
        )

        supply = sp.compute(sp.is_nat(self.data.supply - action.amount))
        with supply.match_cases() as arg:
            with arg.match("Some") as nat_supply:
                self.data.supply = nat_supply
            with arg.match("None"):
                self.data.supply = 0
class TestReceiverBalanceOf

Helper used to test the balance_of entrypoint.

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

Expand source code
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.init(last_known_balances=sp.big_map())

    @sp.entrypoint
    def receive_balances(self, params):
        sp.set_type(params, sp.TList(t_balance_of_response))
        with sp.for_("resp", params) as resp:
            owner = (resp.request.owner, resp.request.token_id)
            with sp.if_(self.data.last_known_balances.contains(sp.sender)):
                self.data.last_known_balances[sp.sender][owner] = resp.balance
            with sp.else_():
                self.data.last_known_balances[sp.sender] = sp.map({owner: resp.balance})

Ancestors

  • smartpy.Contract

Methods

def receive_balances(self, params)

Entrypoint.

Expand source code
@sp.entrypoint
def receive_balances(self, params):
    sp.set_type(params, sp.TList(t_balance_of_response))
    with sp.for_("resp", params) as resp:
        owner = (resp.request.owner, resp.request.token_id)
        with sp.if_(self.data.last_known_balances.contains(sp.sender)):
            self.data.last_known_balances[sp.sender][owner] = resp.balance
        with sp.else_():
            self.data.last_known_balances[sp.sender] = sp.map({owner: resp.balance})