templates.fa2_lib
FA2 library with the new SmartPy syntax.
Warning: Work in progress. Currently the metadata cannot be generated. It means that you cannot originate your contract with the metadata (offchain-views, contract's name and mandatory metadata keys)
1""" 2FA2 library with the new SmartPy syntax. 3 4Warning: 5 Work in progress. 6 Currently the metadata cannot be generated. 7 It means that you cannot originate your contract with the metadata 8 (offchain-views, contract's name and mandatory metadata keys) 9""" 10 11import smartpy as sp 12 13 14@sp.module 15def t(): 16 operator_permission: type = sp.record( 17 owner=sp.address, operator=sp.address, token_id=sp.nat 18 ).layout(("owner", ("operator", "token_id"))) 19 20 update_operators_params: type = list[ 21 sp.variant( 22 add_operator=operator_permission, remove_operator=operator_permission 23 ) 24 ] 25 26 tx: type = sp.record( 27 to_=sp.address, 28 token_id=sp.nat, 29 amount=sp.nat, 30 ).layout(("to_", ("token_id", "amount"))) 31 32 transfer_batch: type = sp.record( 33 from_=sp.address, 34 txs=list[tx], 35 ).layout(("from_", "txs")) 36 37 transfer_params: type = list[transfer_batch] 38 39 balance_of_request: type = sp.record(owner=sp.address, token_id=sp.nat).layout( 40 ("owner", "token_id") 41 ) 42 43 balance_of_response: type = sp.record( 44 request=balance_of_request, balance=sp.nat 45 ).layout(("request", "balance")) 46 47 balance_of_params: type = sp.record( 48 callback=sp.contract[list[balance_of_response]], 49 requests=list[balance_of_request], 50 ).layout(("requests", "callback")) 51 52 balance_params: type = sp.pair[ 53 sp.lambda_(sp.nat, sp.bool, with_storage="read-only"), balance_of_request 54 ] 55 56 token_metadata: type = sp.big_map[ 57 sp.nat, 58 sp.record(token_id=sp.nat, token_info=sp.map[sp.string, sp.bytes]).layout( 59 ("token_id", "token_info") 60 ), 61 ] 62 63 metadata: type = sp.big_map[sp.string, sp.bytes] 64 65 ledger_nft: type = sp.big_map[sp.nat, sp.address] 66 67 ledger_fungible: type = sp.big_map[sp.pair[sp.address, sp.nat], sp.nat] 68 69 ledger_single_asset: type = sp.big_map[sp.address, sp.nat] 70 71 supply_fungible: type = sp.big_map[sp.nat, sp.nat] 72 73 tx_transfer_permission: type = sp.record( 74 from_=sp.address, to_=sp.address, token_id=sp.nat 75 ) 76 77 78@sp.module 79def main(): 80 ############ 81 # Policies # 82 ############ 83 84 class NoTransfer(sp.Contract): 85 """No transfer allowed.""" 86 87 def __init__(self): 88 self.private.policy = sp.record( 89 name="no-transfer", 90 supports_transfer=False, 91 supports_operator=False, 92 ) 93 94 @sp.private() 95 def check_operator_update_permissions_(self, operator_permission): 96 sp.cast(operator_permission, t.operator_permission) 97 raise "FA2_OPERATORS_UNSUPPORTED" 98 return () 99 100 @sp.private() 101 def check_tx_transfer_permissions_(self, params): 102 sp.cast(params, t.tx_transfer_permission) 103 raise "FA2_TX_DENIED" 104 return () 105 106 @sp.private() 107 def is_operator_(self, operator_permission): 108 sp.cast(operator_permission, t.operator_permission) 109 return False 110 111 class OwnerTransfer(sp.Contract): 112 """Only owner are allowed to transfer, no operators.""" 113 114 def __init__(self): 115 self.private.policy = sp.record( 116 name="owner-transfer", 117 supports_transfer=True, 118 supports_operator=False, 119 ) 120 121 @sp.private() 122 def check_operator_update_permissions_(self, operator_permission): 123 sp.cast(operator_permission, t.operator_permission) 124 raise "FA2_OPERATORS_UNSUPPORTED" 125 return () 126 127 @sp.private() 128 def check_tx_transfer_permissions_(self, params): 129 sp.cast(params, t.tx_transfer_permission) 130 assert sp.sender == params.from_, "FA2_NOT_OWNER" 131 132 @sp.private() 133 def is_operator_(self, operator_permission): 134 sp.cast(operator_permission, t.operator_permission) 135 return False 136 137 class OwnerOrOperatorTransfer(sp.Contract): 138 """Owner or operator are allowed to transfer.""" 139 140 def __init__(self): 141 self.private.policy = sp.record( 142 name="owner-or-operator-transfer", 143 supports_transfer=True, 144 supports_operator=True, 145 ) 146 self.data.operators = sp.cast( 147 sp.big_map(), sp.big_map[t.operator_permission, sp.unit] 148 ) 149 150 @sp.private() 151 def check_operator_update_permissions_(self, operator_permission): 152 sp.cast(operator_permission, t.operator_permission) 153 assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER" 154 155 @sp.private(with_storage="read-only") 156 def check_tx_transfer_permissions_(self, params): 157 sp.cast(params, t.tx_transfer_permission) 158 sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit]) 159 assert (sp.sender == params.from_) or self.data.operators.contains( 160 sp.record( 161 owner=params.from_, operator=sp.sender, token_id=params.token_id 162 ) 163 ), "FA2_NOT_OPERATOR" 164 165 @sp.private(with_storage="read-only") 166 def is_operator_(self, operator_permission): 167 sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit]) 168 return self.data.operators.contains(operator_permission) 169 170 ########## 171 # Common # 172 ########## 173 174 class CommonInterface(OwnerOrOperatorTransfer): 175 def __init__(self): 176 OwnerOrOperatorTransfer.__init__(self) 177 self.data.token_metadata = sp.cast(sp.big_map(), t.token_metadata) 178 self.data.metadata = sp.cast(sp.big_map(), t.metadata) 179 self.data.next_token_id = 0 180 181 @sp.private() 182 def balance_(self, params): 183 """Return the balance of an account. 184 Must be redefined in child""" 185 sp.cast(params, t.balance_params) 186 raise "NotImplemented" 187 return 0 188 189 @sp.private() 190 def is_defined_(self, token_id): 191 """Return True if the token is defined, else otherwise. 192 Must be redefined in child""" 193 sp.cast(token_id, sp.nat) 194 raise "NotImplemented" 195 return False 196 197 @sp.private() 198 def transfer_tx_(self, params): 199 """Perform the transfer action. 200 Must be redefined in child""" 201 sp.cast(params, sp.record(from_=sp.address, tx=t.tx)) 202 raise "NotImplemented" 203 return () 204 205 @sp.private() 206 def supply_(self, params): 207 """Return the supply of a token. 208 Must be redefined in child""" 209 (is_defined, token_id) = params 210 sp.cast(token_id, sp.nat) 211 raise "NotImplemented" 212 return 0 213 214 class Common(CommonInterface): 215 """Common logic between Nft, Fungible and SingleAsset.""" 216 217 def __init__(self, metadata): 218 CommonInterface.__init__(self) 219 self.data.metadata = sp.cast(metadata, t.metadata) 220 221 @sp.private(with_storage="read-only") 222 def is_defined_(self, token_id): 223 """Return True if the token is defined, else otherwise.""" 224 return self.data.token_metadata.contains(token_id) 225 226 # Entrypoints 227 228 @sp.entrypoint 229 def update_operators(self, batch): 230 """Accept a list of variants to add or remove operators who can perform 231 transfers on behalf of the owner.""" 232 sp.cast(batch, t.update_operators_params) 233 if self.private.policy.supports_operator: 234 for action in batch: 235 with sp.match(action): 236 with sp.case.add_operator as operator: 237 _ = self.check_operator_update_permissions_(operator) 238 self.data.operators[operator] = () 239 with sp.case.remove_operator as operator: 240 _ = self.check_operator_update_permissions_(operator) 241 del self.data.operators[operator] 242 else: 243 raise "FA2_OPERATORS_UNSUPPORTED" 244 245 @sp.entrypoint 246 def balance_of(self, params): 247 """Send the balance of multiple account / token pairs to a callback 248 address. 249 250 `balance_` and `is_defined_` must be defined in the child class. 251 """ 252 sp.cast(params, t.balance_of_params) 253 254 @sp.effects(with_storage="read-write") 255 def f_process_request(param): 256 (req, is_defined, balance) = param 257 sp.cast(req, t.balance_of_request) 258 return sp.cast( 259 sp.record( 260 request=req, 261 balance=balance((is_defined, req)), 262 ), 263 t.balance_of_response, 264 ) 265 266 answers = [ 267 f_process_request((x, self.is_defined_, self.balance_)) 268 for x in params.requests 269 ] 270 sp.transfer(answers, sp.mutez(0), params.callback) 271 272 @sp.entrypoint 273 def transfer(self, batch): 274 """Accept a list of transfer operations between a source and multiple 275 destinations. 276 277 `transfer_tx_` and `is_defined_` must be defined in the child class. 278 """ 279 sp.cast(batch, t.transfer_params) 280 if self.private.policy.supports_transfer: 281 for transfer in batch: 282 for tx in transfer.txs: 283 # The ordering of assert is important: 284 # 1) token_undefined, 2) transfer permission 3) balance 285 assert self.is_defined_(tx.token_id), "FA2_TOKEN_UNDEFINED" 286 _ = self.check_tx_transfer_permissions_( 287 sp.record( 288 from_=transfer.from_, to_=tx.to_, token_id=tx.token_id 289 ) 290 ) 291 if tx.amount > 0: 292 tx_params = sp.record(from_=transfer.from_, tx=tx) 293 self.transfer_tx_(tx_params) 294 else: 295 raise "FA2_TX_DENIED" 296 297 # Offchain views 298 299 @sp.offchain_view() 300 def all_tokens(self): 301 """Return the list of all the token IDs known to the contract.""" 302 return range(0, self.data.next_token_id) 303 304 @sp.offchain_view() 305 def is_operator(self, operator_permission): 306 """Return whether `operator` is allowed to transfer `token_id` tokens 307 owned by `owner`.""" 308 sp.cast(operator_permission, t.operator_permission) 309 return self.is_operator_(operator_permission) 310 311 @sp.offchain_view 312 def get_balance(self, params): 313 """Return the balance of an address for the specified `token_id`.""" 314 sp.cast(params, t.balance_of_request) 315 return self.balance_((self.is_defined_, params)) 316 317 @sp.offchain_view() 318 def total_supply(self, params): 319 """Return the total number of tokens for the given `token_id`.""" 320 supply = self.supply_((self.is_defined_, params.token_id)) 321 return sp.cast(supply, sp.nat) 322 323 ################ 324 # Base classes # 325 ################ 326 327 class NftInterface(sp.Contract): 328 def __init__(self): 329 self.data.ledger = sp.cast(sp.big_map(), t.ledger_nft) 330 self.private.ledger_type = "NFT" 331 332 class Nft(NftInterface, Common): 333 def __init__(self, metadata, ledger, token_metadata): 334 Common.__init__(self, metadata) 335 NftInterface.__init__(self) 336 337 for token_info in token_metadata: 338 self.data.token_metadata[self.data.next_token_id] = sp.record( 339 token_id=self.data.next_token_id, token_info=token_info 340 ) 341 self.data.next_token_id += 1 342 343 for token in ledger.items(): 344 assert self.data.token_metadata.contains( 345 token.key 346 ), "The `ledger` parameter contains a key no contained in `token_metadata`." 347 self.data.ledger[token.key] = token.value 348 349 @sp.private(with_storage="read-only") 350 def balance_(self, params): 351 (is_defined, balance_params) = params 352 assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED" 353 return ( 354 1 355 if self.data.ledger[balance_params.token_id] == balance_params.owner 356 else 0 357 ) 358 359 @sp.private(with_storage="read-write") 360 def transfer_tx_(self, params): 361 assert ( 362 params.tx.amount == 1 363 and self.data.ledger[params.tx.token_id] == params.from_ 364 ), "FA2_INSUFFICIENT_BALANCE" 365 366 # Makes the transfer 367 self.data.ledger[params.tx.token_id] = params.tx.to_ 368 369 @sp.private(with_storage="read-only") 370 def supply_(self, params): 371 (is_defined, token_id) = params 372 assert is_defined(token_id), "FA2_TOKEN_UNDEFINED" 373 return sp.cast(1, sp.nat) 374 375 class FungibleInterface(sp.Contract): 376 def __init__(self): 377 self.private.ledger_type = "Fungible" 378 self.data.ledger = sp.cast(sp.big_map(), t.ledger_fungible) 379 self.data.supply = sp.cast(sp.big_map(), t.supply_fungible) 380 381 class Fungible(FungibleInterface, Common): 382 def __init__(self, metadata, ledger, token_metadata): 383 Common.__init__(self, metadata) 384 FungibleInterface.__init__(self) 385 386 for token_info in token_metadata: 387 token_id = self.data.next_token_id 388 self.data.token_metadata[token_id] = sp.record( 389 token_id=token_id, token_info=token_info 390 ) 391 self.data.supply[token_id] = 0 392 self.data.next_token_id += 1 393 394 for token in ledger.items(): 395 token_id = sp.snd(token.key) 396 assert self.data.token_metadata.contains( 397 token_id 398 ), "The `ledger` parameter contains a key no contained in `token_metadata`." 399 self.data.supply[token_id] += token.value 400 self.data.ledger[token.key] = token.value 401 402 @sp.private(with_storage="read-only") 403 def balance_(self, params): 404 (is_defined, balance_params) = params 405 assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED" 406 return self.data.ledger.get( 407 (balance_params.owner, balance_params.token_id), default=0 408 ) 409 410 @sp.private(with_storage="read-write") 411 def transfer_tx_(self, params): 412 # Makes the transfer 413 from_ = (params.from_, params.tx.token_id) 414 self.data.ledger[from_] = sp.as_nat( 415 self.data.ledger.get(from_, default=0) - params.tx.amount, 416 error="FA2_INSUFFICIENT_BALANCE", 417 ) 418 to_ = (params.tx.to_, params.tx.token_id) 419 self.data.ledger[to_] = ( 420 self.data.ledger.get(to_, default=0) + params.tx.amount 421 ) 422 423 @sp.private(with_storage="read-only") 424 def supply_(self, params): 425 (is_defined, token_id) = params 426 assert is_defined(token_id), "FA2_TOKEN_UNDEFINED" 427 return self.data.supply[token_id] 428 429 class SingleAssetInterface(sp.Contract): 430 def __init__(self): 431 self.private.ledger_type = "SingleAsset" 432 self.data.ledger = sp.cast(sp.big_map(), t.ledger_single_asset) 433 self.data.supply = sp.cast(0, sp.nat) 434 435 class SingleAsset(SingleAssetInterface, Common): 436 def __init__(self, metadata, ledger, token_metadata): 437 Common.__init__(self, metadata) 438 SingleAssetInterface.__init__(self) 439 440 self.data.token_metadata[0] = sp.record( 441 token_id=0, token_info=token_metadata 442 ) 443 for token in ledger.items(): 444 self.data.supply += token.value 445 self.data.ledger[token.key] = token.value 446 447 @sp.private(with_storage="read-only") 448 def balance_(self, params): 449 (is_defined, balance_params) = params 450 assert is_defined(balance_params.token_id), "FA2_TOKEN_UNDEFINED" 451 return self.data.ledger.get(balance_params.owner, default=0) 452 453 @sp.private(with_storage="read-write") 454 def transfer_tx_(self, params): 455 # Makes the transfer 456 self.data.ledger[params.from_] = sp.as_nat( 457 self.data.ledger.get(params.from_, default=0) - params.tx.amount, 458 error="FA2_INSUFFICIENT_BALANCE", 459 ) 460 self.data.ledger[params.tx.to_] = ( 461 self.data.ledger.get(params.tx.to_, default=0) + params.tx.amount 462 ) 463 464 @sp.private(with_storage="read-only") 465 def supply_(self, params): 466 (is_defined, token_id) = params 467 assert is_defined(token_id), "FA2_TOKEN_UNDEFINED" 468 return self.data.supply 469 470 ########## 471 # Mixins # 472 ########## 473 474 class AdminInterface(sp.Contract): 475 def __init__(self): 476 self.data.administrator = sp.address("") 477 478 @sp.private() 479 def is_administrator_(self): 480 raise "NotImplemented" 481 return False 482 483 class Admin(sp.Contract): 484 """(Mixin) Provide the basics for having an administrator in the contract. 485 486 Adds an `administrator` attribute in the storage. Provides a 487 `set_administrator` entrypoint. 488 """ 489 490 def __init__(self, administrator): 491 self.data.administrator = administrator 492 493 @sp.private(with_storage="read-only") 494 def is_administrator_(self): 495 return sp.sender == self.data.administrator 496 497 @sp.entrypoint 498 def set_administrator(self, administrator): 499 """(Admin only) Set the contract administrator.""" 500 assert self.is_administrator_(), "FA2_NOT_ADMIN" 501 self.data.administrator = administrator 502 503 class ChangeMetadata(AdminInterface): 504 """(Mixin) Provide an entrypoint to change contract metadata. 505 506 Requires the `Admin` mixin. 507 """ 508 509 def __init__(self): 510 AdminInterface.__init__(self) 511 self.data.metadata = sp.cast(sp.big_map(), sp.big_map[sp.string, sp.bytes]) 512 513 @sp.entrypoint 514 def set_metadata(self, metadata): 515 """(Admin only) Set the contract metadata.""" 516 assert self.is_administrator_(), "FA2_NOT_ADMIN" 517 self.data.metadata = metadata 518 519 class WithdrawMutez(AdminInterface): 520 """(Mixin) Provide an entrypoint to withdraw mutez that are in the 521 contract's balance. 522 523 Requires the `Admin` mixin. 524 """ 525 526 def __init__(self): 527 AdminInterface.__init__(self) 528 529 @sp.entrypoint 530 def withdraw_mutez(self, destination, amount): 531 """(Admin only) Transfer `amount` mutez to `destination`.""" 532 assert self.is_administrator_(), "FA2_NOT_ADMIN" 533 sp.send(destination, amount) 534 535 class OffchainviewTokenMetadata(CommonInterface): 536 """(Mixin) If present indexers use it to retrieve the token's metadata. 537 538 Warning: If someone can change the contract's metadata they can change how 539 indexers see every token metadata. 540 """ 541 542 def __init__(self): 543 CommonInterface.__init__(self) 544 545 @sp.offchain_view() 546 def token_metadata(self, token_id): 547 """Returns the token-metadata URI for the given token.""" 548 assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED" 549 return self.data.token_metadata[token_id] 550 551 class OnchainviewBalanceOf(sp.Contract): 552 """(Mixin) Non-standard onchain view equivalent to `balance_of`. 553 554 Before onchain views were introduced in Michelson, the standard way 555 of getting value from a contract was through a callback. Now that 556 views are here we can create a view for the old style one. 557 """ 558 559 @sp.private(with_storage="read-write") 560 def balance_(self, params): 561 raise "NotImplemented" 562 return 0 563 564 @sp.private(with_storage="read-write") 565 def is_defined_(self, params): 566 sp.cast(params, sp.nat) 567 raise "NotImplemented" 568 return False 569 570 @sp.onchain_view() 571 def get_balance_of(self, requests): 572 """Onchain view equivalent to the `balance_of` entrypoint.""" 573 sp.cast(requests, sp.list[t.balance_of_request]) 574 575 @sp.effects(with_storage="read-write") 576 def f_process_request_(param): 577 (req, is_defined, balance) = param 578 return sp.cast( 579 sp.record( 580 request=req, 581 balance=balance((is_defined, req)), 582 ), 583 t.balance_of_response, 584 ) 585 586 return [ 587 f_process_request_((x, self.is_defined_, self.balance_)) 588 for x in requests 589 ] 590 591 class MintNft(AdminInterface, NftInterface, CommonInterface): 592 """(Mixin) Non-standard `mint` entrypoint for FA2Nft with incrementing id. 593 594 Requires the `Admin` mixin. 595 """ 596 597 def __init__(self): 598 CommonInterface.__init__(self) 599 NftInterface.__init__(self) 600 AdminInterface.__init__(self) 601 602 @sp.entrypoint 603 def mint(self, batch): 604 """Admin can mint new or existing tokens.""" 605 sp.cast( 606 batch, 607 sp.list[ 608 sp.record( 609 to_=sp.address, 610 metadata=sp.map[sp.string, sp.bytes], 611 ).layout(("to_", "metadata")) 612 ], 613 ) 614 assert self.is_administrator_(), "FA2_NOT_ADMIN" 615 for action in batch: 616 token_id = self.data.next_token_id 617 self.data.token_metadata[token_id] = sp.record( 618 token_id=token_id, token_info=action.metadata 619 ) 620 self.data.ledger[token_id] = action.to_ 621 self.data.next_token_id += 1 622 623 class MintFungible(AdminInterface, FungibleInterface, CommonInterface): 624 """(Mixin) Non-standard `mint` entrypoint for FA2Fungible with incrementing 625 id. 626 627 Requires the `Admin` mixin. 628 """ 629 630 def __init__(self): 631 CommonInterface.__init__(self) 632 FungibleInterface.__init__(self) 633 AdminInterface.__init__(self) 634 635 @sp.entrypoint 636 def mint(self, batch): 637 """Admin can mint tokens.""" 638 sp.cast( 639 batch, 640 sp.list[ 641 sp.record( 642 to_=sp.address, 643 token=sp.variant( 644 new=sp.map[sp.string, sp.bytes], existing=sp.nat 645 ), 646 amount=sp.nat, 647 ).layout(("to_", ("token", "amount"))) 648 ], 649 ) 650 assert self.is_administrator_(), "FA2_NOT_ADMIN" 651 for action in batch: 652 with sp.match(action.token): 653 with sp.case.new as metadata: 654 token_id = self.data.next_token_id 655 self.data.token_metadata[token_id] = sp.record( 656 token_id=token_id, token_info=metadata 657 ) 658 self.data.supply[token_id] = action.amount 659 self.data.ledger[(action.to_, token_id)] = action.amount 660 self.data.next_token_id += 1 661 with sp.case.existing as token_id: 662 assert self.is_defined_(token_id), "FA2_TOKEN_UNDEFINED" 663 self.data.supply[token_id] += action.amount 664 from_ = (action.to_, token_id) 665 self.data.ledger[from_] = ( 666 self.data.ledger.get(from_, default=0) + action.amount 667 ) 668 669 class MintSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface): 670 """(Mixin) Non-standard `mint` entrypoint for FA2SingleAsset. 671 672 Requires the `Admin` mixin. 673 """ 674 675 def __init__(self): 676 CommonInterface.__init__(self) 677 SingleAssetInterface.__init__(self) 678 AdminInterface.__init__(self) 679 680 @sp.entrypoint 681 def mint(self, batch): 682 """Admin can mint tokens.""" 683 sp.cast( 684 batch, 685 sp.list[ 686 sp.record(to_=sp.address, amount=sp.nat).layout(("to_", "amount")) 687 ], 688 ) 689 assert self.is_administrator_(), "FA2_NOT_ADMIN" 690 for action in batch: 691 assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED" 692 self.data.supply += action.amount 693 self.data.ledger[action.to_] = ( 694 self.data.ledger.get(action.to_, default=0) + action.amount 695 ) 696 697 class BurnNft(AdminInterface, NftInterface, CommonInterface): 698 """(Mixin) Non-standard `burn` entrypoint for FA2Nft that uses the transfer 699 policy permission.""" 700 701 def __init__(self): 702 CommonInterface.__init__(self) 703 NftInterface.__init__(self) 704 AdminInterface.__init__(self) 705 706 @sp.entrypoint 707 def burn(self, batch): 708 """Users can burn tokens if they have the transfer policy permission. 709 710 Burning an nft destroys its metadata. 711 """ 712 sp.cast( 713 batch, 714 sp.list[ 715 sp.record( 716 from_=sp.address, 717 token_id=sp.nat, 718 amount=sp.nat, 719 ).layout(("from_", ("token_id", "amount"))) 720 ], 721 ) 722 assert self.private.policy.supports_transfer, "FA2_TX_DENIED" 723 for action in batch: 724 assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED" 725 self.check_tx_transfer_permissions_( 726 sp.record( 727 from_=action.from_, to_=action.from_, token_id=action.token_id 728 ) 729 ) 730 if action.amount > 0: 731 assert (action.amount == 1) and ( 732 self.data.ledger[action.token_id] == action.from_ 733 ), "FA2_INSUFFICIENT_BALANCE" 734 # Burn the token 735 del self.data.ledger[action.token_id] 736 del self.data.token_metadata[action.token_id] 737 738 class BurnFungible(AdminInterface, FungibleInterface, CommonInterface): 739 """(Mixin) Non-standard `burn` entrypoint for FA2Fungible that uses the 740 transfer policy permission.""" 741 742 def __init__(self): 743 CommonInterface.__init__(self) 744 FungibleInterface.__init__(self) 745 AdminInterface.__init__(self) 746 747 @sp.entrypoint 748 def burn(self, batch): 749 """Users can burn tokens if they have the transfer policy 750 permission.""" 751 sp.cast( 752 batch, 753 sp.list[ 754 sp.record( 755 from_=sp.address, 756 token_id=sp.nat, 757 amount=sp.nat, 758 ).layout(("from_", ("token_id", "amount"))) 759 ], 760 ) 761 assert self.private.policy.supports_transfer, "FA2_TX_DENIED" 762 for action in batch: 763 assert self.is_defined_(action.token_id), "FA2_TOKEN_UNDEFINED" 764 self.check_tx_transfer_permissions_( 765 sp.record( 766 from_=action.from_, to_=action.from_, token_id=action.token_id 767 ) 768 ) 769 from_ = (action.from_, action.token_id) 770 # Burn the tokens 771 self.data.ledger[from_] = sp.as_nat( 772 self.data.ledger.get(from_, default=0) - action.amount, 773 error="FA2_INSUFFICIENT_BALANCE", 774 ) 775 776 is_supply = sp.is_nat( 777 self.data.supply.get(action.token_id, default=0) - action.amount 778 ) 779 with sp.match(is_supply): 780 with sp.case.Some as supply: 781 self.data.supply[action.token_id] = supply 782 with None: 783 self.data.supply[action.token_id] = 0 784 785 class BurnSingleAsset(AdminInterface, SingleAssetInterface, CommonInterface): 786 """(Mixin) Non-standard `burn` entrypoint for FA2SingleAsset that uses the 787 transfer policy permission.""" 788 789 def __init__(self): 790 CommonInterface.__init__(self) 791 SingleAssetInterface.__init__(self) 792 AdminInterface.__init__(self) 793 794 @sp.entrypoint 795 def burn(self, batch): 796 """Users can burn tokens if they have the transfer policy 797 permission.""" 798 sp.cast( 799 batch, 800 sp.list[ 801 sp.record( 802 from_=sp.address, 803 token_id=sp.nat, 804 amount=sp.nat, 805 ).layout(("from_", ("token_id", "amount"))) 806 ], 807 ) 808 assert self.private.policy.supports_transfer, "FA2_TX_DENIED" 809 for action in batch: 810 assert self.is_defined_(0), "FA2_TOKEN_UNDEFINED" 811 self.check_tx_transfer_permissions_( 812 sp.record( 813 from_=action.from_, to_=action.from_, token_id=action.token_id 814 ) 815 ) 816 # Burn the tokens 817 self.data.ledger[action.from_] = sp.as_nat( 818 self.data.ledger.get(action.from_, default=0) - action.amount, 819 error="FA2_INSUFFICIENT_BALANCE", 820 ) 821 822 is_supply = sp.is_nat(self.data.supply - action.amount) 823 with sp.match(is_supply): 824 with sp.case.Some as supply: 825 self.data.supply = supply 826 with None: 827 self.data.supply = 0 828 829 ######################### 830 # Non standard policies # 831 ######################### 832 833 class PauseOwnerOrOperatorTransfer(AdminInterface): 834 """Owner or operator can transfers. Transfers can be paused by the admin. 835 836 Adds a `set_pause` operator. 837 838 Requires the `Admin` mixin.""" 839 840 def __init__(self): 841 AdminInterface.__init__(self) 842 self.private.policy = sp.record( 843 name="pauseable-owner-or-operator-transfer", 844 supports_transfer=True, 845 supports_operator=True, 846 ) 847 self.data.paused = False 848 self.data.operators = sp.cast( 849 sp.big_map(), sp.big_map[t.operator_permission, sp.unit] 850 ) 851 852 @sp.private(with_storage="read-only") 853 def check_operator_update_permissions_(self, operator_permission): 854 sp.cast(operator_permission, t.operator_permission) 855 assert not self.data.paused, ("FA2_OPERATORS_UNSUPPORTED", "FA2_PAUSED") 856 assert operator_permission.owner == sp.sender, "FA2_NOT_OWNER" 857 858 @sp.private(with_storage="read-only") 859 def check_tx_transfer_permissions_(self, params): 860 sp.cast( 861 params, 862 sp.record( 863 from_=sp.address, 864 to_=sp.address, 865 token_id=sp.nat, 866 ), 867 ) 868 sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit]) 869 assert not self.data.paused, ("FA2_TX_DENIED", "FA2_PAUSED") 870 assert (sp.sender == params.from_) or self.data.operators.contains( 871 sp.record( 872 owner=params.from_, operator=sp.sender, token_id=params.token_id 873 ) 874 ), "FA2_NOT_OPERATOR" 875 876 @sp.private(with_storage="read-only") 877 def is_operator(self, operator_permission): 878 sp.cast(self.data.operators, sp.big_map[t.operator_permission, sp.unit]) 879 return self.data.operators.contains(operator_permission) 880 881 @sp.entrypoint 882 def set_pause(self, params): 883 assert self.is_administrator_(), "FA2_NOT_ADMIN" 884 self.data.paused = params 885 886 887########### 888# Helpers # 889########### 890 891 892@sp.module 893def Helpers(): 894 class TestReceiverBalanceOf(sp.Contract): 895 """Helper used to test the `balance_of` entrypoint. 896 897 Don't use it on-chain as it can be gas locked. 898 """ 899 900 def __init__(self): 901 self.last_known_balances = sp.big_map() 902 903 @sp.entrypoint 904 def receive_balances(self, params): 905 sp.cast(params, list[t.balance_of_response]) 906 for resp in params: 907 owner = (resp.request.owner, resp.request.token_id) 908 if self.data.last_known_balances.contains(sp.sender): 909 self.data.last_known_balances[sp.sender][owner] = resp.balance 910 else: 911 self.data.last_known_balances[sp.sender] = {owner: resp.balance} 912 913 914def make_metadata(symbol, name, decimals): 915 """Helper function to build metadata JSON bytes values.""" 916 return sp.map( 917 l={ 918 "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals), 919 "name": sp.scenario_utils.bytes_of_string(name), 920 "symbol": sp.scenario_utils.bytes_of_string(symbol), 921 } 922 )
def
make_metadata(symbol, name, decimals):
915def make_metadata(symbol, name, decimals): 916 """Helper function to build metadata JSON bytes values.""" 917 return sp.map( 918 l={ 919 "decimals": sp.scenario_utils.bytes_of_string("%d" % decimals), 920 "name": sp.scenario_utils.bytes_of_string(name), 921 "symbol": sp.scenario_utils.bytes_of_string(symbol), 922 } 923 )
Helper function to build metadata JSON bytes values.