Module fa2_fungible_minimal
Expand source code
import smartpy as sp
# FA2 standard: https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md
# Documentation: https://smartpy.io/guides/FA2-lib/overview
@sp.module
def main():
balance_of_args: type = sp.record(
requests=sp.list[sp.record(owner=sp.address, token_id=sp.nat)],
callback=sp.contract[
sp.list[
sp.record(
request=sp.record(owner=sp.address, token_id=sp.nat), balance=sp.nat
).layout(("request", "balance"))
]
],
).layout(("requests", "callback"))
class Fa2FungibleMinimal(sp.Contract):
"""Minimal FA2 contract for fungible tokens.
This is a minimal self contained implementation example showing how to
implement an NFT following the FA2 standard in SmartPy. It is for
illustrative purposes only. For a more flexible toolbox aimed at real world
applications please refer to FA2_lib.
"""
def __init__(self, administrator, metadata):
self.data.administrator = administrator
self.data.ledger = sp.cast(
sp.big_map(), sp.big_map[sp.pair[sp.address, sp.nat], sp.nat]
)
self.data.metadata = metadata
self.data.next_token_id = sp.nat(0)
self.data.operators = sp.cast(
sp.big_map(),
sp.big_map[
sp.record(
owner=sp.address,
operator=sp.address,
token_id=sp.nat,
).layout(("owner", ("operator", "token_id"))),
sp.unit,
],
)
self.data.supply = sp.cast(sp.big_map(), sp.big_map[sp.nat, sp.nat])
self.data.token_metadata = sp.cast(
sp.big_map(),
sp.big_map[
sp.nat,
sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
],
)
# TODO: pass metadata_base as an argument
# metadata_base["views"] = [
# self.all_tokens,
# self.get_balance,
# self.is_operator,
# self.total_supply,
# ]
# self.init_metadata("metadata_base", metadata_base)
@sp.entrypoint
def transfer(self, batch):
"""Accept a list of transfer operations.
Each transfer operation specifies a source: `from_` and a list
of transactions. Each transaction specifies the destination: `to_`,
the `token_id` and the `amount` to be transferred.
Args:
batch: List of transfer operations.
Raises:
`FA2_TOKEN_UNDEFINED`, `FA2_NOT_OPERATOR`, `FA2_INSUFFICIENT_BALANCE`
"""
for transfer in batch:
for tx in transfer.txs:
sp.cast(
tx,
sp.record(
to_=sp.address, token_id=sp.nat, amount=sp.nat
).layout(("to_", ("token_id", "amount"))),
)
assert tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
from_ = (transfer.from_, tx.token_id)
to_ = (tx.to_, tx.token_id)
assert transfer.from_ == sp.sender or self.data.operators.contains(
sp.record(
owner=transfer.from_,
operator=sp.sender,
token_id=tx.token_id,
)
), "FA2_NOT_OPERATOR"
self.data.ledger[from_] = sp.as_nat(
self.data.ledger.get(from_, default=0) - tx.amount,
error="FA2_INSUFFICIENT_BALANCE",
)
self.data.ledger[to_] = (
self.data.ledger.get(to_, default=0) + tx.amount
)
@sp.entrypoint
def update_operators(self, actions):
"""Accept a list of variants to add or remove operators.
Operators can perform transfer on behalf of the owner.
Owner is a Tezos address which can hold tokens.
Only the owner can change its operators.
Args:
actions: List of operator update actions.
Raises:
`FA2_NOT_OWNER`
"""
for action in actions:
with sp.match(action):
with sp.case.add_operator as operator:
assert operator.owner == sp.sender, "FA2_NOT_OWNER"
self.data.operators[operator] = ()
with sp.case.remove_operator as operator:
assert operator.owner == sp.sender, "FA2_NOT_OWNER"
del self.data.operators[operator]
@sp.entrypoint
def balance_of(self, param):
"""Send the balance of multiple account / token pairs to a
callback address.
transfer 0 mutez to `callback` with corresponding response.
Args:
callback (contract): Where we callback the answer.
requests: List of requested balances.
Raises:
`FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND`
"""
sp.cast(param, balance_of_args)
balances = []
for req in param.requests:
assert req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
balances.push(
sp.record(
request=sp.record(owner=req.owner, token_id=req.token_id),
balance=self.data.ledger.get(
(req.owner, req.token_id), default=0
),
)
)
sp.transfer(reversed(balances), sp.mutez(0), param.callback)
@sp.entrypoint
def mint(self, to_, amount, token):
"""(Admin only) Create new tokens from scratch and assign
them to `to_`.
If `token` is "existing": increase the supply of the `token_id`.
If `token` is "new": create a new token and assign the `metadata`.
Args:
to_ (address): Receiver of the tokens.
amount (nat): Amount of token to be minted.
token (variant): "_new_": id of the token, "_existing_": metadata of the token.
Raises:
`FA2_NOT_ADMIN`, `FA2_TOKEN_UNDEFINED`
"""
assert sp.sender == self.data.administrator, "FA2_NOT_ADMIN"
with sp.match(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] = amount
self.data.ledger[(to_, token_id)] = amount
self.data.next_token_id += 1
with sp.case.existing as token_id:
assert token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
self.data.supply[token_id] += amount
self.data.ledger[(to_, token_id)] = (
self.data.ledger.get((to_, token_id), default=0) + amount
)
@sp.offchain_view
def all_tokens(self):
"""(Offchain view) Return the list of all the `token_id` known to the contract."""
return sp.range(0, self.data.next_token_id)
@sp.offchain_view
def get_balance(self, params):
"""(Offchain view) Return the balance of an address for the specified `token_id`."""
sp.cast(
params,
sp.record(owner=sp.address, token_id=sp.nat).layout(
("owner", "token_id")
),
)
assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
return self.data.ledger.get((params.owner, params.token_id), default=0)
@sp.offchain_view
def total_supply(self, params):
"""(Offchain view) Return the total number of tokens for the given `token_id` if known or
fail if not."""
assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
return self.data.supply.get(params.token_id, default=0)
@sp.offchain_view
def is_operator(self, params):
"""(Offchain view) Return whether `operator` is allowed to transfer `token_id` tokens
owned by `owner`."""
return self.data.operators.contains(params)
class Fa2FungibleMinimalTest(Fa2FungibleMinimal):
def __init__(
self, administrator, metadata, ledger, token_metadata, next_token_id
):
Fa2FungibleMinimal.__init__(self, administrator, metadata)
self.data.next_token_id = next_token_id
self.data.ledger = ledger
self.data.token_metadata = token_metadata
# metadata_base = {
# "name": "FA2 fungible minimal",
# "version": "1.0.0",
# "description": "This is a minimal implementation of FA2 (TZIP-012) using SmartPy.",
# "interfaces": ["TZIP-012", "TZIP-016"],
# "authors": ["SmartPy <https://smartpy.io/#contact>"],
# "homepage": "https://smartpy.io/ide?template=fa2_fungible_minimal.py",
# "source": {
# "tools": ["SmartPy"],
# "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/fa2_fungible_minimal.py",
# },
# "permissions": {
# "operator": "owner-or-operator-transfer",
# "receiver": "owner-no-hook",
# "sender": "owner-no-hook",
# },
# }
if "templates" not in __name__:
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),
}
)
admin = sp.test_account("Administrator")
alice = sp.test_account("Alice")
tok0_md = make_metadata(name="Token Zero", decimals=1, symbol="Tok0")
tok1_md = make_metadata(name="Token One", decimals=1, symbol="Tok1")
tok2_md = make_metadata(name="Token Two", decimals=1, symbol="Tok2")
@sp.add_test(name="Test")
def test():
scenario = sp.test_scenario(main)
c1 = main.Fa2FungibleMinimal(
admin.address, sp.utils.metadata_of_url("https//example.com")
)
scenario += c1
from templates import fa2_lib_testing as testing
c1 = main.Fa2FungibleMinimalTest(
administrator=admin.address,
metadata=sp.utils.metadata_of_url("https://example.com"),
ledger=sp.big_map(
{
(alice.address, 0): 42,
(alice.address, 1): 42,
(alice.address, 2): 42,
}
),
token_metadata=sp.big_map(
{
0: sp.record(token_id=0, token_info=tok0_md),
1: sp.record(token_id=1, token_info=tok1_md),
2: sp.record(token_id=2, token_info=tok2_md),
}
),
next_token_id=3,
)
kwargs = {"modules": main, "ledger_type": "Fungible"}
testing.test_core_interfaces(c1, **kwargs)
testing.test_transfer(c1, **kwargs)
testing.test_owner_or_operator_transfer(c1, **kwargs)
testing.test_balance_of(c1, **kwargs)
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), } )