Module fa2_nft_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://legacy.smartpy.io/docs/guides/FA/FA2
t_balance_of_args = sp.TRecord(
requests=sp.TList(sp.TRecord(owner=sp.TAddress, token_id=sp.TNat)),
callback=sp.TContract(
sp.TList(
sp.TRecord(
request=sp.TRecord(owner=sp.TAddress, token_id=sp.TNat), balance=sp.TNat
).layout(("request", "balance"))
)
),
).layout(("requests", "callback"))
class Fa2NftMinimal(sp.Contract):
"""Minimal FA2 contract for NFTs.
This is a minimal self contained implementation example showing how to
implement an NFT contract 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_base, metadata_url):
self.init(
administrator=administrator,
ledger=sp.big_map(tkey=sp.TNat, tvalue=sp.TAddress),
metadata=sp.utils.metadata_of_url(metadata_url),
next_token_id=sp.nat(0),
operators=sp.big_map(
tkey=sp.TRecord(
owner=sp.TAddress,
operator=sp.TAddress,
token_id=sp.TNat,
).layout(("owner", ("operator", "token_id"))),
tvalue=sp.TUnit,
),
token_metadata=sp.big_map(
tkey=sp.TNat,
tvalue=sp.TRecord(
token_id=sp.TNat,
token_info=sp.TMap(sp.TString, sp.TBytes),
),
),
)
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`
"""
with sp.for_("transfer", batch) as transfer:
with sp.for_("tx", transfer.txs) as tx:
sp.set_type(
tx,
sp.TRecord(
to_=sp.TAddress, token_id=sp.TNat, amount=sp.TNat
).layout(("to_", ("token_id", "amount"))),
)
sp.verify(tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED")
sp.verify(
(transfer.from_ == sp.sender)
| self.data.operators.contains(
sp.record(
owner=transfer.from_,
operator=sp.sender,
token_id=tx.token_id,
)
),
"FA2_NOT_OPERATOR",
)
with sp.if_(tx.amount > 0):
sp.verify(
(tx.amount == 1)
& (self.data.ledger[tx.token_id] == transfer.from_),
"FA2_INSUFFICIENT_BALANCE",
)
self.data.ledger[tx.token_id] = tx.to_
@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`
"""
with sp.for_("update", actions) as action:
with action.match_cases() as arg:
with arg.match("add_operator") as operator:
sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER")
self.data.operators[operator] = sp.unit
with arg.match("remove_operator") as operator:
sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER")
del self.data.operators[operator]
@sp.entrypoint
def balance_of(self, args):
"""Send the balance of multiple account / token pairs to a callback
address.
transfer 0 mutez to `callback` with corresponding response.
Args:
callback (contract): Where to callback the answer.
requests: List of requested balances.
Raises:
`FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND`
"""
def f_process_request(req):
sp.verify(req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED")
sp.result(
sp.record(
request=sp.record(owner=req.owner, token_id=req.token_id),
balance=sp.eif(
self.data.ledger[req.token_id] == req.owner, sp.nat(1), 0
),
)
)
sp.set_type(args, t_balance_of_args)
sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.callback)
@sp.entrypoint
def mint(self, to_, metadata):
"""(Admin only) Create a new token with an incremented id and assign
it. to `to_`.
Args:
to_ (address): Receiver of the tokens.
metadata (map of string bytes): Metadata of the token.
Raises:
`FA2_NOT_ADMIN`
"""
sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN")
token_id = sp.compute(self.data.next_token_id)
self.data.token_metadata[token_id] = sp.record(
token_id=token_id, token_info=metadata
)
self.data.ledger[token_id] = to_
self.data.next_token_id += 1
@sp.offchain_view(pure=True)
def all_tokens(self):
"""Return the list of all the `token_id` known to the contract."""
sp.result(sp.range(0, self.data.next_token_id))
@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.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED")
sp.result(sp.eif(self.data.ledger[params.token_id] == params.owner, 1, 0))
@sp.offchain_view(pure=True)
def total_supply(self, params):
"""Return the total number of tokens for the given `token_id` if known
or fail if not."""
sp.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED")
sp.result(1)
@sp.offchain_view(pure=True)
def is_operator(self, params):
"""Return whether `operator` is allowed to transfer `token_id` tokens
owned by `owner`."""
sp.result(self.data.operators.contains(params))
metadata_base = {
"name": "FA2 NFT 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://legacy.smartpy.io/#contact>"],
"homepage": "https://legacy.smartpy.io/ide?template=fa2_nft_minimal.py",
"source": {
"tools": ["SmartPy"],
"location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/fa2_nft_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")
bob = sp.test_account("Bob")
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()
c1 = Fa2NftMinimal(admin.address, metadata_base, "https://example.com")
scenario += c1
sp.add_compilation_target(
"Fa2NftMinimal",
Fa2NftMinimal(admin.address, metadata_base, "https://example.com"),
)
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 Fa2NftMinimal (administrator, metadata_base, metadata_url)
-
Minimal FA2 contract for NFTs.
This is a minimal self contained implementation example showing how to implement an NFT contract 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."
Expand source code
class Fa2NftMinimal(sp.Contract): """Minimal FA2 contract for NFTs. This is a minimal self contained implementation example showing how to implement an NFT contract 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_base, metadata_url): self.init( administrator=administrator, ledger=sp.big_map(tkey=sp.TNat, tvalue=sp.TAddress), metadata=sp.utils.metadata_of_url(metadata_url), next_token_id=sp.nat(0), operators=sp.big_map( tkey=sp.TRecord( owner=sp.TAddress, operator=sp.TAddress, token_id=sp.TNat, ).layout(("owner", ("operator", "token_id"))), tvalue=sp.TUnit, ), token_metadata=sp.big_map( tkey=sp.TNat, tvalue=sp.TRecord( token_id=sp.TNat, token_info=sp.TMap(sp.TString, sp.TBytes), ), ), ) 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` """ with sp.for_("transfer", batch) as transfer: with sp.for_("tx", transfer.txs) as tx: sp.set_type( tx, sp.TRecord( to_=sp.TAddress, token_id=sp.TNat, amount=sp.TNat ).layout(("to_", ("token_id", "amount"))), ) sp.verify(tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.verify( (transfer.from_ == sp.sender) | self.data.operators.contains( sp.record( owner=transfer.from_, operator=sp.sender, token_id=tx.token_id, ) ), "FA2_NOT_OPERATOR", ) with sp.if_(tx.amount > 0): sp.verify( (tx.amount == 1) & (self.data.ledger[tx.token_id] == transfer.from_), "FA2_INSUFFICIENT_BALANCE", ) self.data.ledger[tx.token_id] = tx.to_ @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` """ with sp.for_("update", actions) as action: with action.match_cases() as arg: with arg.match("add_operator") as operator: sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER") self.data.operators[operator] = sp.unit with arg.match("remove_operator") as operator: sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER") del self.data.operators[operator] @sp.entrypoint def balance_of(self, args): """Send the balance of multiple account / token pairs to a callback address. transfer 0 mutez to `callback` with corresponding response. Args: callback (contract): Where to callback the answer. requests: List of requested balances. Raises: `FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND` """ def f_process_request(req): sp.verify(req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result( sp.record( request=sp.record(owner=req.owner, token_id=req.token_id), balance=sp.eif( self.data.ledger[req.token_id] == req.owner, sp.nat(1), 0 ), ) ) sp.set_type(args, t_balance_of_args) sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.callback) @sp.entrypoint def mint(self, to_, metadata): """(Admin only) Create a new token with an incremented id and assign it. to `to_`. Args: to_ (address): Receiver of the tokens. metadata (map of string bytes): Metadata of the token. Raises: `FA2_NOT_ADMIN` """ sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN") token_id = sp.compute(self.data.next_token_id) self.data.token_metadata[token_id] = sp.record( token_id=token_id, token_info=metadata ) self.data.ledger[token_id] = to_ self.data.next_token_id += 1 @sp.offchain_view(pure=True) def all_tokens(self): """Return the list of all the `token_id` known to the contract.""" sp.result(sp.range(0, self.data.next_token_id)) @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.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result(sp.eif(self.data.ledger[params.token_id] == params.owner, 1, 0)) @sp.offchain_view(pure=True) def total_supply(self, params): """Return the total number of tokens for the given `token_id` if known or fail if not.""" sp.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result(1) @sp.offchain_view(pure=True) def is_operator(self, params): """Return whether `operator` is allowed to transfer `token_id` tokens owned by `owner`.""" sp.result(self.data.operators.contains(params))
Ancestors
- smartpy.Contract
Methods
def transfer(self, batch)
-
Entrypoint. Accept a list of transfer operations.
Each transfer operation specifies a source:
from_
and a list of transactions. Each transaction specifies the destination:to_
, thetoken_id
and theamount
to be transferred.Args
batch
- List of transfer operations.
Raises
FA2_TOKEN_UNDEFINED
,FA2_NOT_OPERATOR
,FA2_INSUFFICIENT_BALANCE
Expand source code
@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` """ with sp.for_("transfer", batch) as transfer: with sp.for_("tx", transfer.txs) as tx: sp.set_type( tx, sp.TRecord( to_=sp.TAddress, token_id=sp.TNat, amount=sp.TNat ).layout(("to_", ("token_id", "amount"))), ) sp.verify(tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.verify( (transfer.from_ == sp.sender) | self.data.operators.contains( sp.record( owner=transfer.from_, operator=sp.sender, token_id=tx.token_id, ) ), "FA2_NOT_OPERATOR", ) with sp.if_(tx.amount > 0): sp.verify( (tx.amount == 1) & (self.data.ledger[tx.token_id] == transfer.from_), "FA2_INSUFFICIENT_BALANCE", ) self.data.ledger[tx.token_id] = tx.to_
def update_operators(self, actions)
-
Entrypoint. 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
Expand source code
@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` """ with sp.for_("update", actions) as action: with action.match_cases() as arg: with arg.match("add_operator") as operator: sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER") self.data.operators[operator] = sp.unit with arg.match("remove_operator") as operator: sp.verify(operator.owner == sp.sender, "FA2_NOT_OWNER") del self.data.operators[operator]
def balance_of(self, args)
-
Entrypoint. Send the balance of multiple account / token pairs to a callback address.
transfer 0 mutez to
callback
with corresponding response.Args
callback
:contract
- Where to callback the answer.
requests
- List of requested balances.
Raises
FA2_TOKEN_UNDEFINED
,FA2_CALLBACK_NOT_FOUND
Expand source code
@sp.entrypoint def balance_of(self, args): """Send the balance of multiple account / token pairs to a callback address. transfer 0 mutez to `callback` with corresponding response. Args: callback (contract): Where to callback the answer. requests: List of requested balances. Raises: `FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND` """ def f_process_request(req): sp.verify(req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result( sp.record( request=sp.record(owner=req.owner, token_id=req.token_id), balance=sp.eif( self.data.ledger[req.token_id] == req.owner, sp.nat(1), 0 ), ) ) sp.set_type(args, t_balance_of_args) sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.callback)
def mint(self, to_, metadata)
-
Entrypoint. (Admin only) Create a new token with an incremented id and assign it. to
to_
.Args
to_
:address
- Receiver of the tokens.
metadata
:map
ofstring bytes
- Metadata of the token.
Raises
FA2_NOT_ADMIN
Expand source code
@sp.entrypoint def mint(self, to_, metadata): """(Admin only) Create a new token with an incremented id and assign it. to `to_`. Args: to_ (address): Receiver of the tokens. metadata (map of string bytes): Metadata of the token. Raises: `FA2_NOT_ADMIN` """ sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN") token_id = sp.compute(self.data.next_token_id) self.data.token_metadata[token_id] = sp.record( token_id=token_id, token_info=metadata ) self.data.ledger[token_id] = to_ self.data.next_token_id += 1
def all_tokens(self)
-
Offchain view. Return the list of all the
token_id
known to the contract.Expand source code
@sp.offchain_view(pure=True) def all_tokens(self): """Return the list of all the `token_id` known to the contract.""" sp.result(sp.range(0, self.data.next_token_id))
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.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result(sp.eif(self.data.ledger[params.token_id] == params.owner, 1, 0))
def total_supply(self, params)
-
Offchain view. Return the total number of tokens for the given
token_id
if known or fail if not.Expand source code
@sp.offchain_view(pure=True) def total_supply(self, params): """Return the total number of tokens for the given `token_id` if known or fail if not.""" sp.verify(params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED") sp.result(1)
def is_operator(self, params)
-
Offchain view. Return whether
operator
is allowed to transfertoken_id
tokens owned byowner
.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.result(self.data.operators.contains(params))