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://legacy.smartpy.io/docs/guides/FA/FA2
t_balance_of_params = 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 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_base, metadata_url):
self.init(
administrator=administrator,
ledger=sp.big_map(tkey=sp.TPair(sp.TAddress, sp.TNat), tvalue=sp.TNat),
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,
),
supply=sp.big_map(tkey=sp.TNat, tvalue=sp.TNat),
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")
from_ = (transfer.from_, tx.token_id)
to_ = (tx.to_, tx.token_id)
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",
)
self.data.ledger[from_] = sp.as_nat(
self.data.ledger.get(from_, 0) - tx.amount,
"FA2_INSUFFICIENT_BALANCE",
)
self.data.ledger[to_] = self.data.ledger.get(to_, 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`
"""
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 we 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=self.data.ledger.get((req.owner, req.token_id), 0),
)
)
sp.set_type(args, t_balance_of_params)
sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.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`
"""
sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN")
with token.match_cases() as arg:
with arg.match("new") as metadata:
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.supply[token_id] = amount
self.data.ledger[(to_, token_id)] = amount
self.data.next_token_id += 1
with arg.match("existing") as token_id:
sp.verify(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), 0) + amount
)
@sp.offchain_view(pure=True)
def all_tokens(self):
"""(Offchain view) 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):
"""(Offchain view) 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(self.data.ledger.get((params.owner, params.token_id), 0))
@sp.offchain_view(pure=True)
def total_supply(self, params):
"""(Offchain view) 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(self.data.supply.get(params.token_id, 0))
@sp.offchain_view(pure=True)
def is_operator(self, params):
"""(Offchain view) Return whether `operator` is allowed to transfer `token_id` tokens
owned by `owner`."""
sp.result(self.data.operators.contains(params))
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://legacy.smartpy.io/#contact>"],
"homepage": "https://legacy.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__:
fa2_admin = sp.test_account("fa2_admin")
@sp.add_test(name="Test")
def test():
scenario = sp.test_scenario()
c1 = Fa2FungibleMinimal(fa2_admin.address, metadata_base, "https//example.com")
scenario += c1
sp.add_compilation_target(
"Fa2FungibleMinimal",
Fa2FungibleMinimal(fa2_admin.address, metadata_base, "https//example.com"),
)
Classes
class Fa2FungibleMinimal (administrator, metadata_base, metadata_url)
-
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.
Expand source code
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_base, metadata_url): self.init( administrator=administrator, ledger=sp.big_map(tkey=sp.TPair(sp.TAddress, sp.TNat), tvalue=sp.TNat), 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, ), supply=sp.big_map(tkey=sp.TNat, tvalue=sp.TNat), 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") from_ = (transfer.from_, tx.token_id) to_ = (tx.to_, tx.token_id) 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", ) self.data.ledger[from_] = sp.as_nat( self.data.ledger.get(from_, 0) - tx.amount, "FA2_INSUFFICIENT_BALANCE", ) self.data.ledger[to_] = self.data.ledger.get(to_, 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` """ 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 we 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=self.data.ledger.get((req.owner, req.token_id), 0), ) ) sp.set_type(args, t_balance_of_params) sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.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` """ sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN") with token.match_cases() as arg: with arg.match("new") as metadata: 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.supply[token_id] = amount self.data.ledger[(to_, token_id)] = amount self.data.next_token_id += 1 with arg.match("existing") as token_id: sp.verify(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), 0) + amount ) @sp.offchain_view(pure=True) def all_tokens(self): """(Offchain view) 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): """(Offchain view) 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(self.data.ledger.get((params.owner, params.token_id), 0)) @sp.offchain_view(pure=True) def total_supply(self, params): """(Offchain view) 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(self.data.supply.get(params.token_id, 0)) @sp.offchain_view(pure=True) def is_operator(self, params): """(Offchain view) 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") from_ = (transfer.from_, tx.token_id) to_ = (tx.to_, tx.token_id) 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", ) self.data.ledger[from_] = sp.as_nat( self.data.ledger.get(from_, 0) - tx.amount, "FA2_INSUFFICIENT_BALANCE", ) self.data.ledger[to_] = self.data.ledger.get(to_, 0) + tx.amount
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 we 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 we 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=self.data.ledger.get((req.owner, req.token_id), 0), ) ) sp.set_type(args, t_balance_of_params) sp.transfer(args.requests.map(f_process_request), sp.mutez(0), args.callback)
def mint(self, to_, amount, token)
-
Entrypoint. (Admin only) Create new tokens from scratch and assign them to
to_
.If
token
is "existing": increase the supply of thetoken_id
. Iftoken
is "new": create a new token and assign themetadata
.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
Expand source code
@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` """ sp.verify(sp.sender == self.data.administrator, "FA2_NOT_ADMIN") with token.match_cases() as arg: with arg.match("new") as metadata: 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.supply[token_id] = amount self.data.ledger[(to_, token_id)] = amount self.data.next_token_id += 1 with arg.match("existing") as token_id: sp.verify(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), 0) + amount )
def all_tokens(self)
-
Offchain view. (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): """(Offchain view) 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. (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): """(Offchain view) 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(self.data.ledger.get((params.owner, params.token_id), 0))
def total_supply(self, params)
-
Offchain view. (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): """(Offchain view) 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(self.data.supply.get(params.token_id, 0))
def is_operator(self, params)
-
Offchain view. (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): """(Offchain view) Return whether `operator` is allowed to transfer `token_id` tokens owned by `owner`.""" sp.result(self.data.operators.contains(params))