templates.fa2_nft_minimal

  1import smartpy as sp
  2
  3# FA2 standard: https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md
  4# Documentation: https://smartpy.io/guides/FA2-lib/overview
  5
  6
  7@sp.module
  8def main():
  9    balance_of_args: type = sp.record(
 10        requests=sp.list[sp.record(owner=sp.address, token_id=sp.nat)],
 11        callback=sp.contract[
 12            sp.list[
 13                sp.record(
 14                    request=sp.record(owner=sp.address, token_id=sp.nat), balance=sp.nat
 15                ).layout(("request", "balance"))
 16            ]
 17        ],
 18    ).layout(("requests", "callback"))
 19
 20    class Fa2NftMinimal(sp.Contract):
 21        """Minimal FA2 contract for NFTs.
 22
 23        This is a minimal self contained implementation example showing how to
 24        implement an NFT contract following the FA2 standard in SmartPy. It is for
 25        illustrative purposes only. For a more flexible toolbox aimed at real world
 26        applications please refer to FA2_lib."
 27        """
 28
 29        def __init__(self, administrator, metadata):
 30            self.data.administrator = administrator
 31            self.data.ledger = sp.cast(sp.big_map(), sp.big_map[sp.nat, sp.address])
 32            self.data.metadata = metadata
 33            self.data.next_token_id = sp.nat(0)
 34            self.data.operators = sp.cast(
 35                sp.big_map(),
 36                sp.big_map[
 37                    sp.record(
 38                        owner=sp.address,
 39                        operator=sp.address,
 40                        token_id=sp.nat,
 41                    ).layout(("owner", ("operator", "token_id"))),
 42                    sp.unit,
 43                ],
 44            )
 45            self.data.token_metadata = sp.cast(
 46                sp.big_map(),
 47                sp.big_map[
 48                    sp.nat,
 49                    sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]),
 50                ],
 51            )
 52
 53            # TODO: pass metadata_base as an argument
 54            # metadata_base["views"] = [
 55            #     self.all_tokens,
 56            #     self.get_balance,
 57            #     self.is_operator,
 58            #     self.total_supply,
 59            # ]
 60            # self.init_metadata("metadata_base", metadata_base)
 61
 62        @sp.entrypoint
 63        def transfer(self, batch):
 64            """Accept a list of transfer operations.
 65
 66            Each transfer operation specifies a source: `from_` and a list
 67            of transactions. Each transaction specifies the destination: `to_`,
 68            the `token_id` and the `amount` to be transferred.
 69
 70            Args:
 71                batch: List of transfer operations.
 72            Raises:
 73                `FA2_TOKEN_UNDEFINED`, `FA2_NOT_OPERATOR`, `FA2_INSUFFICIENT_BALANCE`
 74            """
 75            for transfer in batch:
 76                for tx in transfer.txs:
 77                    sp.cast(
 78                        tx,
 79                        sp.record(
 80                            to_=sp.address, token_id=sp.nat, amount=sp.nat
 81                        ).layout(("to_", ("token_id", "amount"))),
 82                    )
 83                    assert tx.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
 84                    assert transfer.from_ == sp.sender or self.data.operators.contains(
 85                        sp.record(
 86                            owner=transfer.from_,
 87                            operator=sp.sender,
 88                            token_id=tx.token_id,
 89                        )
 90                    ), "FA2_NOT_OPERATOR"
 91                    if tx.amount > 0:
 92                        assert (
 93                            tx.amount == 1
 94                            and self.data.ledger[tx.token_id] == transfer.from_
 95                        ), "FA2_INSUFFICIENT_BALANCE"
 96                        self.data.ledger[tx.token_id] = tx.to_
 97
 98        @sp.entrypoint
 99        def update_operators(self, actions):
100            """Accept a list of variants to add or remove operators.
101
102            Operators can perform transfer on behalf of the owner.
103            Owner is a Tezos address which can hold tokens.
104
105            Only the owner can change its operators.
106
107            Args:
108                actions: List of operator update actions.
109            Raises:
110                `FA2_NOT_OWNER`
111            """
112            for action in actions:
113                with sp.match(action):
114                    with sp.case.add_operator as operator:
115                        assert operator.owner == sp.sender, "FA2_NOT_OWNER"
116                        self.data.operators[operator] = ()
117                    with sp.case.remove_operator as operator:
118                        assert operator.owner == sp.sender, "FA2_NOT_OWNER"
119                        del self.data.operators[operator]
120
121        @sp.entrypoint
122        def balance_of(self, param):
123            """Send the balance of multiple account / token pairs to a callback
124            address.
125
126            transfer 0 mutez to `callback` with corresponding response.
127
128            Args:
129                callback (contract): Where to callback the answer.
130                requests: List of requested balances.
131            Raises:
132                `FA2_TOKEN_UNDEFINED`, `FA2_CALLBACK_NOT_FOUND`
133            """
134            sp.cast(param, balance_of_args)
135            balances = []
136            for req in param.requests:
137                assert req.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
138                balances.push(
139                    sp.record(
140                        request=sp.record(owner=req.owner, token_id=req.token_id),
141                        balance=(
142                            1 if self.data.ledger[req.token_id] == req.owner else 0
143                        ),
144                    )
145                )
146
147            sp.transfer(reversed(balances), sp.mutez(0), param.callback)
148
149        @sp.entrypoint
150        def mint(self, to_, metadata):
151            """(Admin only) Create a new token with an incremented id and assign
152            it. to `to_`.
153
154            Args:
155                to_ (address): Receiver of the tokens.
156                metadata (map of string bytes): Metadata of the token.
157            Raises:
158                `FA2_NOT_ADMIN`
159            """
160            assert sp.sender == self.data.administrator, "FA2_NOT_ADMIN"
161            token_id = self.data.next_token_id
162            self.data.token_metadata[token_id] = sp.record(
163                token_id=token_id, token_info=metadata
164            )
165            self.data.ledger[token_id] = to_
166            self.data.next_token_id += 1
167
168        @sp.offchain_view
169        def all_tokens(self):
170            """Return the list of all the `token_id` known to the contract."""
171            return range(0, self.data.next_token_id)
172
173        @sp.offchain_view
174        def get_balance(self, params):
175            """Return the balance of an address for the specified `token_id`."""
176            sp.cast(
177                params,
178                sp.record(owner=sp.address, token_id=sp.nat).layout(
179                    ("owner", "token_id")
180                ),
181            )
182            assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
183            return (
184                sp.nat(1)
185                if self.data.ledger[params.token_id] == params.owner
186                else sp.nat(0)
187            )
188
189        @sp.offchain_view
190        def total_supply(self, params):
191            """Return the total number of tokens for the given `token_id` if known
192            or fail if not."""
193            assert params.token_id < self.data.next_token_id, "FA2_TOKEN_UNDEFINED"
194            return 1
195
196        @sp.offchain_view
197        def is_operator(self, params):
198            """Return whether `operator` is allowed to transfer `token_id` tokens
199            owned by `owner`."""
200            return self.data.operators.contains(params)
201
202    class Fa2NftMinimalTest(Fa2NftMinimal):
203        def __init__(
204            self, administrator, metadata, ledger, token_metadata, next_token_id
205        ):
206            Fa2NftMinimal.__init__(self, administrator, metadata)
207
208            self.data.next_token_id = next_token_id
209            self.data.ledger = ledger
210            self.data.token_metadata = token_metadata
211
212
213# metadata_base = {
214#     "name": "FA2 NFT minimal",
215#     "version": "1.0.0",
216#     "description": "This is a minimal implementation of FA2 (TZIP-012) using SmartPy.",
217#     "interfaces": ["TZIP-012", "TZIP-016"],
218#     "authors": ["SmartPy <https://smartpy.io/#contact>"],
219#     "homepage": "https://smartpy.io/ide?template=fa2_nft_minimal.py",
220#     "source": {
221#         "tools": ["SmartPy"],
222#         "location": "https://gitlab.com/SmartPy/smartpy/-/raw/master/python/templates/fa2_nft_minimal.py",
223#     },
224#     "permissions": {
225#         "operator": "owner-or-operator-transfer",
226#         "receiver": "owner-no-hook",
227#         "sender": "owner-no-hook",
228#     },
229# }
230
231if "main" in __name__:
232
233    def make_metadata(symbol, name, decimals):
234        """Helper function to build metadata JSON bytes values."""
235        return sp.map(
236            l={
237                "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals),
238                "name": sp.scenario_utils.bytes_of_string(name),
239                "symbol": sp.scenario_utils.bytes_of_string(symbol),
240            }
241        )
242
243    admin = sp.test_account("Administrator")
244    alice = sp.test_account("Alice")
245    tok0_md = make_metadata(name="Token Zero", decimals=1, symbol="Tok0")
246    tok1_md = make_metadata(name="Token One", decimals=1, symbol="Tok1")
247    tok2_md = make_metadata(name="Token Two", decimals=1, symbol="Tok2")
248
249    @sp.add_test()
250    def test():
251        scenario = sp.test_scenario("Minimal test", main)
252        c1 = main.Fa2NftMinimal(
253            admin.address, sp.scenario_utils.metadata_of_url("https://example.com")
254        )
255        scenario += c1
256
257    from smartpy.templates import fa2_lib_testing as testing
258
259    kwargs = {
260        "class_": main.Fa2NftMinimalTest,
261        "kwargs": {
262            "administrator": admin.address,
263            "metadata": sp.scenario_utils.metadata_of_url("https://example.com"),
264            "ledger": sp.big_map(
265                {0: alice.address, 1: alice.address, 2: alice.address}
266            ),
267            "token_metadata": sp.big_map(
268                {
269                    0: sp.record(token_id=0, token_info=tok0_md),
270                    1: sp.record(token_id=1, token_info=tok1_md),
271                    2: sp.record(token_id=2, token_info=tok2_md),
272                }
273            ),
274            "next_token_id": 3,
275        },
276        "ledger_type": "NFT",
277        "test_name": "",
278        "modules": main,
279    }
280
281    testing.test_core_interfaces(**kwargs)
282    testing.test_transfer(**kwargs)
283    testing.test_balance_of(**kwargs)
284    testing.test_owner_or_operator_transfer(**kwargs)