├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── deploy ├── index.js ├── index.ts ├── package-lock.json ├── package.json └── tsconfig.json ├── examples ├── marketplace.jsligo ├── mintable.mligo └── myTzip12NFTImplementation.jsligo ├── lib ├── fa2.1 │ ├── MultiAsset.parameterList.jsligo │ ├── MultiAsset.storageList.jsligo │ ├── NFT.parameterList.jsligo │ ├── NFT.storageList.jsligo │ ├── data │ │ ├── admin.jsligo │ │ ├── amount.jsligo │ │ ├── approvals.jsligo │ │ ├── errors.jsligo │ │ ├── ledger.jsligo │ │ ├── metadata.jsligo │ │ ├── operators.jsligo │ │ ├── proxy.jsligo │ │ ├── storage.jsligo │ │ ├── token.jsligo │ │ └── tokenMetadata.jsligo │ ├── entrypoints │ │ ├── approve.jsligo │ │ ├── balance_of.jsligo │ │ ├── burn.jsligo │ │ ├── export_ticket.jsligo │ │ ├── import_ticket.jsligo │ │ ├── lambda_export.jsligo │ │ ├── mint.jsligo │ │ ├── transfer.jsligo │ │ └── update.jsligo │ ├── fa2.1-NFT.jsligo │ ├── fa2.1-generic.jsligo │ ├── fa2.1-multi-asset.jsligo │ ├── fa2.1-single-asset.jsligo │ ├── proxy.jsligo │ ├── tzip12.datatypes.jsligo │ ├── tzip26.datatypes.jsligo │ ├── tzip26.interfaces.jsligo │ └── views.jsligo ├── fa2 │ ├── asset │ │ ├── extendable_multi_asset.impl.jsligo │ │ ├── extendable_multi_asset.impl.mligo │ │ ├── extendable_single_asset.impl.jsligo │ │ ├── extendable_single_asset.impl.mligo │ │ ├── multi_asset.impl.jsligo │ │ ├── multi_asset.impl.mligo │ │ ├── single_asset.impl.jsligo │ │ └── single_asset.impl.mligo │ ├── common │ │ ├── assertions.jsligo │ │ ├── errors.mligo │ │ ├── tzip12.datatypes.jsligo │ │ ├── tzip12.interfaces.jsligo │ │ └── tzip16.datatypes.jsligo │ └── nft │ │ ├── extendable_nft.impl.jsligo │ │ ├── extendable_nft.impl.mligo │ │ ├── nft.impl.jsligo │ │ └── nft.impl.mligo ├── main.jsligo └── main.mligo ├── ligo.json └── test ├── fa2 ├── balance_of_callback_contract.mligo ├── multi_asset.test.mligo ├── multi_asset_jsligo.test.mligo ├── nft │ ├── e2e_mutation.test.mligo │ ├── nft.test.mligo │ ├── nft_jsligo.test.mligo │ ├── views.test.mligo │ └── views_test_contract.mligo ├── single_asset.test.mligo └── single_asset_jsligo.test.mligo └── helpers ├── list.mligo ├── nft_helpers.jsligo └── nft_helpers.mligo /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | **/node_modules 3 | **/deploy.js 4 | .ligo/ 5 | /compiled 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-2023 the LigoLang team. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | ligo_compiler?=docker run --rm -v "$(PWD)":"$(PWD)" -w "$(PWD)" ligolang/ligo:1.6.0 4 | # ^ Override this variable when you run make command by make ligo_compiler= 5 | # ^ Otherwise use default one (you'll need docker) 6 | PROTOCOL_OPT?= 7 | 8 | project_root=--project-root . 9 | # ^ required when using packages 10 | 11 | help: 12 | @grep -E '^[ a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ 13 | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' 14 | 15 | compile = $(ligo_compiler) compile contract $(project_root) ./lib/$(1) -o ./compiled/$(2) $(3) $(PROTOCOL_OPT) 16 | # ^ compile contract to michelson or micheline 17 | 18 | test = $(ligo_compiler) run test $(project_root) ./test/$(1) $(PROTOCOL_OPT) 19 | # ^ run given test file 20 | 21 | compile: ## compile contracts 22 | @if [ ! -d ./compiled ]; then mkdir -p ./compiled/fa2/nft && mkdir -p ./compiled/fa2/asset ; fi 23 | @echo "Compiling contracts..." 24 | @echo "fa2.1/fa2.1-NFT.jsligo" 25 | @$(call compile,fa2.1/fa2.1-NFT.jsligo,fa2/asset/fa2.1-single-asset.jsligo.json) 26 | @$(call compile,fa2.1/fa2.1-NFT.jsligo,fa2/asset/fa2.1-single-asset.jsligo.json,--michelson-format json) 27 | @echo "fa2.1/fa2.1-single-asset.jsligo" 28 | @$(call compile,fa2.1/fa2.1-single-asset.jsligo,fa2/asset/fa2.1-single-asset.jsligo.json) 29 | @$(call compile,fa2.1/fa2.1-single-asset.jsligo,fa2/asset/fa2.1-single-asset.jsligo.json,--michelson-format json) 30 | @echo "fa2.1/fa2.1-multi-asset.jsligo" 31 | @$(call compile,fa2.1/fa2.1-multi-asset.jsligo,fa2/asset/fa2.1-multi-asset.jsligo.tz) 32 | @$(call compile,fa2.1/fa2.1-multi-asset.jsligo,fa2/asset/fa2.1-multi-asset.jsligo.json,--michelson-format json) 33 | 34 | @echo "fa2/nft/nft.impl.mligo" 35 | @$(call compile,fa2/nft/nft.impl.mligo,fa2/nft/nft.impl.mligo.tz) 36 | @$(call compile,fa2/nft/nft.impl.mligo,fa2/nft/nft.impl.mligo.json,--michelson-format json) 37 | @echo "fa2/asset/single_asset.impl.mligo" 38 | @$(call compile,fa2/asset/single_asset.impl.mligo,fa2/asset/single_asset.impl.mligo.tz) 39 | @$(call compile,fa2/asset/single_asset.impl.mligo,fa2/asset/single_asset.impl.mligo.json,--michelson-format json) 40 | @echo "fa2/asset/multi_asset.impl.mligo" 41 | @$(call compile,fa2/asset/multi_asset.impl.mligo,fa2/asset/multi_asset.impl.mligo.tz) 42 | @$(call compile,fa2/asset/multi_asset.impl.mligo,fa2/asset/multi_asset.impl.mligo.json,--michelson-format json) 43 | @echo "Compiled contracts!" 44 | clean: ## clean up 45 | @rm -rf compiled 46 | 47 | deploy: deploy_deps deploy.js 48 | 49 | deploy.js: 50 | @echo "Running deploy script\n" 51 | @cd deploy && npm i && npm start 52 | 53 | deploy_deps: 54 | @echo "Installing deploy script dependencies" 55 | @cd deploy && npm install 56 | @echo "" 57 | 58 | install: ## install dependencies 59 | @$(ligo_compiler) install 60 | 61 | .PHONY: test 62 | test: ## run tests (SUITE=permit make test) 63 | ifndef SUITE 64 | @$(call test,fa2/single_asset.test.mligo) 65 | @$(call test,fa2/single_asset_jsligo.test.mligo) 66 | @$(call test,fa2/multi_asset.test.mligo) 67 | @$(call test,fa2/nft/nft.test.mligo) 68 | @$(call test,fa2/multi_asset_jsligo.test.mligo) 69 | @$(call test,fa2/nft/nft_jsligo.test.mligo) 70 | @$(call test,fa2/nft/views.test.mligo) 71 | @$(call test,fa2/nft/e2e_mutation.test.mligo) 72 | else 73 | @$(call test,$(SUITE).test.mligo) 74 | endif 75 | 76 | lint: ## lint code 77 | @npx eslint ./scripts --ext .ts 78 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # FA2 tokens 2 | 3 | `ligo/fa` library provides : 4 | 5 | - the [interface](./lib/fa2/common/tzip12.interfaces.jsligo) and [types](./lib/fa2/common/tzip12.datatypes.jsligo) defined by [FA2 (TZIP-12)](https://tzip.tezosagora.org/proposal/tzip-12/) 6 | - a [LIGO](https://ligolang.org/) implementation for : 7 | - unique [NFTs](./lib/fa2/nft/nft.impl.jsligo): This contract implements the FA2 interface for 8 | NFT(non-fungible-token) where a token can belong to only one address at a time 9 | (1:1) 10 | - [Single Assets](./lib/fa2/asset/single_asset.impl.mligo): This is an implementation of 11 | Single Asset Token where a different amount of single token can belong to multiple 12 | addresses at a time (1:n) 13 | - [Multiple Assets](./lib/fa2/asset/multi_asset.impl.mligo): This is an implementation of 14 | Multi Asset Token where there are several token ids (available in different amounts) 15 | and they can belong to multiple addresses (m:n) 16 | 17 | - the [interface](./lib/fa2.1/common/tzip26.interfaces.jsligo) and [types](./lib/fa2.1/common/tzip26.datatypes.jsligo) defined by [FA2.1 (TZIP-26)](https://tzip.tezosagora.org/proposal/tzip-26/) 18 | - a [LIGO](https://ligolang.org/) implementation for : 19 | - unique [NFTs](./lib/fa2.1/fa2.1-NFT.jsligo): This contract implements the FA2.1 interface for 20 | NFT(non-fungible-token) where a token can belong to only one address at a time 21 | (1:1) 22 | - [Single Assets](./lib/fa2.1/fa2.1-single-asset.jsligo): This is an implementation of 23 | Single Asset Token where a different amount of single token can belong to multiple 24 | addresses at a time (1:n) 25 | - [Multiple Assets](./lib/fa2.1/fa2.1-multi-asset.jsligo): This is an implementation of 26 | Multi Asset Token where there are several token ids (available in different amounts) 27 | and they can belong to multiple addresses (m:n) 28 | 29 | ## Use the implementation directly 30 | 31 | The library provides you 3 template implementations ready to deploy 32 | 33 | 1. To install this package, run `ligo install @ligo/fa`. It will download the files 34 | 1. Deploy the NFT contract with Taquito the Ghostnet with `alice` wallet 35 | 36 | ```bash 37 | make compile 38 | make deploy 39 | ``` 40 | 41 | ## Extend an implementation 42 | 43 | If you need additional features in your contract, you can use the extendable version. An example is 44 | available in the file `examples/mintable.mligo`. Using the extension mechanism, it adds an admin 45 | address to the storage, as well as a `mint` entrypoint to mint owner-less NFTs. Only the admin can 46 | call this entrypoint. 47 | 48 | Install the library and create a new file 49 | 50 | ```bash 51 | ligo install @ligo/fa 52 | touch mintable.mligo 53 | ``` 54 | 55 | To extend the storage, define the type of the extension and refer to the original storage type as 56 | such: 57 | 58 | ```ocaml 59 | #import "@ligo/fa/lib/main.mligo" "FA2" 60 | 61 | module NFT = FA2.NFTExtendable 62 | 63 | type extension = { 64 | admin: address 65 | } 66 | 67 | type storage = extension NFT.storage 68 | type ret = operation list * storage 69 | ``` 70 | 71 | Importing the library allows you to refer to the TZIP12 operations signatures and make it easier to 72 | redefine all the entrypoints and views that are required: 73 | 74 | ```ocaml 75 | (* Standard FA2 interface, copied from the source *) 76 | 77 | [@entry] 78 | let transfer (t: NFT.TZIP12.transfer) (s: storage) : ret = 79 | NFT.transfer t s 80 | 81 | [@entry] 82 | let balance_of (b: NFT.TZIP12.balance_of) (s: storage) : ret = 83 | NFT.balance_of b s 84 | 85 | (* Etc. *) 86 | ``` 87 | 88 | To make it easier to define new entrypoints, some functions are available in the library, and you 89 | can also use the `storage` fields directly: 90 | 91 | ```ocaml 92 | (* Extension *) 93 | 94 | type mint = { 95 | owner : address; 96 | token_id : nat; 97 | } 98 | 99 | [@entry] 100 | let mint (mint : mint) (s : storage): ret = 101 | let sender = Tezos.get_sender () in 102 | let () = assert (sender = s.extension.admin) in 103 | let () = NFT.Assertions.assert_token_exist s.token_metadata mint.token_id in 104 | (* Check that nobody owns the token already *) 105 | let () = assert (Option.is_none (Big_map.find_opt mint.token_id s.ledger)) in 106 | let s = NFT.set_balance s mint.owner mint.token_id in 107 | [], s 108 | ``` 109 | 110 | Note that this version requires the minted NFTs to be already defined in the `token_metadata` big 111 | map. However, you can also change the `mint` entrypoint to create new tokens dynamically. 112 | 113 | ## Implement the interface differently 114 | 115 | If you are not happy with the default NFT implementation, you can define your own 116 | 117 | Create a new file 118 | 119 | ```bash 120 | touch myTzip12NFTImplementation.jsligo 121 | ``` 122 | 123 | Import some code and define implementation of missing types `ledger` and `operators` 124 | 125 | ```ligolang 126 | #import "@ligo/fa/lib/fa2/common/errors.mligo" "Errors" 127 | 128 | #import "@ligo/fa/lib/fa2/common/assertions.jsligo" "Assertions" 129 | 130 | #import "@ligo/fa/lib/fa2/common/tzip12.datatypes.jsligo" "TZIP12" 131 | 132 | #import "@ligo/fa/lib/fa2/common/tzip12.interfaces.jsligo" "TZIP12Interface" 133 | 134 | #import "@ligo/fa/lib/fa2/common/tzip16.datatypes.jsligo" "TZIP16" 135 | 136 | export namespace NFT implements TZIP12Interface.FA2{ 137 | export type ledger = big_map; 138 | type operator = address; 139 | export type operators = big_map<[address, operator], set>; 140 | export type storage = { 141 | ledger: ledger, 142 | operators: operators, 143 | token_metadata: TZIP12.tokenMetadata, 144 | metadata: TZIP16.metadata 145 | }; 146 | type ret = [list, storage]; 147 | 148 | } 149 | ``` 150 | 151 | Copy the missing entrypoints from the TZIP12 interface and give your own implementation 152 | 153 | ```ligolang 154 | @entry 155 | const transfer = (p: TZIP12.transfer, s: storage): ret => { 156 | failwith("TODO"); 157 | }; 158 | @entry 159 | const balance_of = (p: TZIP12.balance_of, s: storage): ret => { 160 | failwith("TODO"); 161 | }; 162 | @entry 163 | const update_operators = (p: TZIP12.update_operators, s: storage): ret => { 164 | failwith("TODO"); 165 | }; 166 | ``` 167 | 168 | Compile it (do not forget to add the parameter -m NFT as you have to define a namespace to be able to implement an interface) 169 | 170 | ```bash 171 | ligo compile contract myTzip12NFTImplementation.jsligo -m NFT 172 | ``` 173 | -------------------------------------------------------------------------------- /deploy/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | Object.defineProperty(exports, "__esModule", { value: true }); 15 | const signer_1 = require("@taquito/signer"); 16 | const taquito_1 = require("@taquito/taquito"); 17 | const utils_1 = require("@taquito/utils"); 18 | const nft_impl_mligo_json_1 = __importDefault(require("../compiled/fa2/nft/nft.impl.mligo.json")); 19 | const RPC_ENDPOINT = "https://ghostnet.tezos.marigold.dev"; 20 | function main() { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | const Tezos = new taquito_1.TezosToolkit(RPC_ENDPOINT); 23 | //set alice key 24 | Tezos.setProvider({ 25 | signer: yield signer_1.InMemorySigner.fromSecretKey("edskRpm2mUhvoUjHjXgMoDRxMKhtKfww1ixmWiHCWhHuMEEbGzdnz8Ks4vgarKDtxok7HmrEo1JzkXkdkvyw7Rtw6BNtSd7MJ7"), 26 | }); 27 | const ledger = new taquito_1.MichelsonMap(); 28 | ledger.set(0, "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb"); 29 | const token_metadata = new taquito_1.MichelsonMap(); 30 | const token_info = new taquito_1.MichelsonMap(); 31 | token_info.set("name", (0, utils_1.char2Bytes)("My super token")); 32 | token_info.set("description", (0, utils_1.char2Bytes)("Lorem ipsum ...")); 33 | token_info.set("symbol", (0, utils_1.char2Bytes)("XXX")); 34 | token_info.set("decimals", (0, utils_1.char2Bytes)("0")); 35 | token_metadata.set(0, { token_id: 0, token_info }); 36 | const metadata = new taquito_1.MichelsonMap(); 37 | metadata.set("", (0, utils_1.char2Bytes)("tezos-storage:data")); 38 | metadata.set("data", (0, utils_1.char2Bytes)(`{ 39 | "name":"FA2", 40 | "description":"Example FA2 implementation", 41 | "version":"0.1.0", 42 | "license":{"name":"MIT"}, 43 | "authors":["Benjamin Fuentes"], 44 | "homepage":"", 45 | "source":{"tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, 46 | "interfaces":["TZIP-012"], 47 | "errors":[], 48 | "views":[] 49 | 50 | }`)); 51 | const operators = new taquito_1.MichelsonMap(); 52 | const initialStorage = { 53 | ledger, 54 | metadata, 55 | token_metadata, 56 | operators, 57 | }; 58 | try { 59 | const originated = yield Tezos.contract.originate({ 60 | code: nft_impl_mligo_json_1.default, 61 | storage: initialStorage, 62 | }); 63 | console.log(`Waiting for nftContract ${originated.contractAddress} to be confirmed...`); 64 | yield originated.confirmation(2); 65 | console.log("confirmed contract: ", originated.contractAddress); 66 | } 67 | catch (error) { 68 | console.log(error); 69 | } 70 | }); 71 | } 72 | main(); 73 | -------------------------------------------------------------------------------- /deploy/index.ts: -------------------------------------------------------------------------------- 1 | import { InMemorySigner } from "@taquito/signer"; 2 | import { MichelsonMap, TezosToolkit } from "@taquito/taquito"; 3 | import { char2Bytes } from "@taquito/utils"; 4 | 5 | import nftContract from "../compiled/fa2/nft/nft.impl.mligo.json"; 6 | 7 | const RPC_ENDPOINT = "https://ghostnet.tezos.marigold.dev"; 8 | 9 | async function main() { 10 | const Tezos = new TezosToolkit(RPC_ENDPOINT); 11 | 12 | //set alice key 13 | Tezos.setProvider({ 14 | signer: await InMemorySigner.fromSecretKey( 15 | "edskRpm2mUhvoUjHjXgMoDRxMKhtKfww1ixmWiHCWhHuMEEbGzdnz8Ks4vgarKDtxok7HmrEo1JzkXkdkvyw7Rtw6BNtSd7MJ7" 16 | ), 17 | }); 18 | 19 | const ledger = new MichelsonMap(); 20 | ledger.set(0, "tz1VSUr8wwNhLAzempoch5d6hLRiTh8Cjcjb"); 21 | 22 | const token_metadata = new MichelsonMap(); 23 | const token_info = new MichelsonMap(); 24 | token_info.set("name", char2Bytes("My super token")); 25 | token_info.set("description", char2Bytes("Lorem ipsum ...")); 26 | token_info.set("symbol", char2Bytes("XXX")); 27 | token_info.set("decimals", char2Bytes("0")); 28 | 29 | token_metadata.set(0, { token_id: 0, token_info }); 30 | 31 | const metadata = new MichelsonMap(); 32 | metadata.set("", char2Bytes("tezos-storage:data")); 33 | metadata.set( 34 | "data", 35 | char2Bytes(`{ 36 | "name":"FA2", 37 | "description":"Example FA2 implementation", 38 | "version":"0.1.0", 39 | "license":{"name":"MIT"}, 40 | "authors":["Benjamin Fuentes"], 41 | "homepage":"", 42 | "source":{"tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, 43 | "interfaces":["TZIP-012"], 44 | "errors":[], 45 | "views":[] 46 | 47 | }`) 48 | ); 49 | 50 | const operators = new MichelsonMap(); 51 | 52 | const initialStorage = { 53 | ledger, 54 | metadata, 55 | token_metadata, 56 | operators, 57 | }; 58 | 59 | try { 60 | const originated = await Tezos.contract.originate({ 61 | code: nftContract, 62 | storage: initialStorage, 63 | }); 64 | console.log( 65 | `Waiting for nftContract ${originated.contractAddress} to be confirmed...` 66 | ); 67 | await originated.confirmation(2); 68 | console.log("confirmed contract: ", originated.contractAddress); 69 | } catch (error: any) { 70 | console.log(error); 71 | } 72 | } 73 | 74 | main(); 75 | -------------------------------------------------------------------------------- /deploy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deploy-nft", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "start": "tsc && node index.js" 7 | }, 8 | "dependencies": { 9 | "@taquito/signer": "^17.3.1", 10 | "@taquito/taquito": "^17.3.1", 11 | "@taquito/utils": "^17.3.1" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^5.2.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/marketplace.jsligo: -------------------------------------------------------------------------------- 1 | #import "../lib/main.mligo" "FA2" 2 | 3 | import Contract = FA2.NFT 4 | 5 | // FIXME These files make no sense and should probably be removed 6 | // but here we're just showcasing how to import the library 7 | 8 | export type storage = { 9 | administrators: set
, 10 | ledger: Contract.ledger, 11 | metadata: Contract.TZIP16.metadata, 12 | token_metadata: Contract.TZIP12.tokenMetadata, 13 | operators: Contract.operators, 14 | }; 15 | 16 | type ret = [list, storage]; 17 | 18 | @entry 19 | const transfer = (p: Contract.TZIP12.transfer, s: storage): ret => { 20 | const ret2: [list, Contract.storage] = 21 | Contract.transfer( 22 | p, 23 | { 24 | ledger: s.ledger, 25 | metadata: s.metadata, 26 | token_metadata: s.token_metadata, 27 | operators: s.operators, 28 | } 29 | ); 30 | return [ 31 | ret2[0], 32 | { 33 | ...s, 34 | ledger: ret2[1].ledger, 35 | metadata: ret2[1].metadata, 36 | token_metadata: ret2[1].token_metadata, 37 | operators: ret2[1].operators, 38 | } 39 | ] 40 | }; 41 | 42 | @entry 43 | const balance_of = (p: Contract.TZIP12.balance_of, s: storage): ret => { 44 | const ret2: [list, Contract.storage] = 45 | Contract.balance_of( 46 | p, 47 | { 48 | ledger: s.ledger, 49 | metadata: s.metadata, 50 | token_metadata: s.token_metadata, 51 | operators: s.operators, 52 | } 53 | ); 54 | return [ 55 | ret2[0], 56 | { 57 | ...s, 58 | ledger: ret2[1].ledger, 59 | metadata: ret2[1].metadata, 60 | token_metadata: ret2[1].token_metadata, 61 | operators: ret2[1].operators, 62 | } 63 | ] 64 | }; 65 | 66 | @entry 67 | const update_operators = (p: Contract.TZIP12.update_operators, s: storage): ret => { 68 | const ret2: [list, Contract.storage] = 69 | Contract.update_operators( 70 | p, 71 | { 72 | ledger: s.ledger, 73 | metadata: s.metadata, 74 | token_metadata: s.token_metadata, 75 | operators: s.operators, 76 | } 77 | ); 78 | return [ 79 | ret2[0], 80 | { 81 | ...s, 82 | ledger: ret2[1].ledger, 83 | metadata: ret2[1].metadata, 84 | token_metadata: ret2[1].token_metadata, 85 | operators: ret2[1].operators, 86 | } 87 | ] 88 | }; 89 | -------------------------------------------------------------------------------- /examples/mintable.mligo: -------------------------------------------------------------------------------- 1 | #import "../lib/main.mligo" "FA2" 2 | 3 | module NFT = FA2.NFTExtendable 4 | 5 | type extension = { 6 | admin: address 7 | } 8 | 9 | type storage = extension NFT.storage 10 | type ret = operation list * storage 11 | 12 | (* Extension *) 13 | 14 | type mint = { 15 | owner : address; 16 | token_id : nat; 17 | } 18 | 19 | [@entry] 20 | let mint (mint : mint) (s : storage): ret = 21 | let sender = Tezos.get_sender () in 22 | let () = assert (sender = s.extension.admin) in 23 | let () = NFT.Assertions.assert_token_exist s.token_metadata mint.token_id in 24 | (* Check that nobody owns the token already *) 25 | let () = assert (Option.is_none (Big_map.find_opt mint.token_id s.ledger)) in 26 | let s = NFT.set_balance s mint.owner mint.token_id in 27 | [], s 28 | 29 | (* Standard FA2 interface, copied from the source *) 30 | 31 | [@entry] 32 | let transfer (t: NFT.TZIP12.transfer) (s: storage) : ret = 33 | NFT.transfer t s 34 | 35 | [@entry] 36 | let balance_of (b: NFT.TZIP12.balance_of) (s: storage) : ret = 37 | NFT.balance_of b s 38 | 39 | [@entry] 40 | let update_operators (u: NFT.TZIP12.update_operators) (s: storage) : ret = 41 | NFT.update_operators u s 42 | 43 | [@view] 44 | let get_balance (p : (address * nat)) (s : storage) : nat = 45 | NFT.get_balance p s 46 | 47 | [@view] 48 | let total_supply (token_id : nat) (s : storage) : nat = 49 | NFT.total_supply token_id s 50 | 51 | [@view] 52 | let all_tokens (_ : unit) (s : storage) : nat set = 53 | NFT.all_tokens () s 54 | 55 | [@view] 56 | let is_operator (op : NFT.TZIP12.operator) (s : storage) : bool = 57 | NFT.is_operator op s 58 | 59 | [@view] 60 | let token_metadata (p : nat) (s : storage) : NFT.TZIP12.tokenMetadataData = 61 | NFT.token_metadata p s 62 | 63 | -------------------------------------------------------------------------------- /examples/myTzip12NFTImplementation.jsligo: -------------------------------------------------------------------------------- 1 | #import "../lib/fa2/common/errors.mligo" "Errors" 2 | 3 | #import "../lib/fa2/common/assertions.jsligo" "Assertions" 4 | 5 | #import "../lib/fa2/common/tzip12.datatypes.jsligo" "TZIP12" 6 | 7 | #import "../lib/fa2/common/tzip12.interfaces.jsligo" "TZIP12Interface" 8 | 9 | #import "../lib/fa2/common/tzip16.datatypes.jsligo" "TZIP16" 10 | 11 | export namespace NFT implements TZIP12Interface.FA2{ 12 | export type ledger = big_map; 13 | type operator = address; 14 | export type operators = big_map<[address, operator], set>; 15 | export type storage = { 16 | extension: unit, 17 | ledger: ledger, 18 | operators: operators, 19 | token_metadata: TZIP12.tokenMetadata, 20 | metadata: TZIP16.metadata, 21 | }; 22 | type ret = [list, storage]; 23 | @entry 24 | const transfer = (p: TZIP12.transfer, s: storage): ret => { 25 | failwith("TODO"); 26 | }; 27 | @entry 28 | const balance_of = (p: TZIP12.balance_of, s: storage): ret => { 29 | failwith("TODO"); 30 | }; 31 | @entry 32 | const update_operators = (p: TZIP12.update_operators, s: storage): ret => { 33 | failwith("TODO"); 34 | }; 35 | 36 | @view 37 | const get_balance = (p: [address, nat], s: storage): nat => { 38 | failwith("TODO"); 39 | } 40 | 41 | @view 42 | const total_supply = (token_id: nat, s: storage): nat => { 43 | failwith("TODO"); 44 | } 45 | 46 | @view 47 | const all_tokens = (_: unit, s: storage): set => { 48 | failwith("TODO"); 49 | } 50 | 51 | @view 52 | const is_operator = (op: TZIP12.operator, s: storage): bool => { 53 | failwith("TODO"); 54 | } 55 | 56 | @view 57 | const token_metadata = (p: nat, s: storage): TZIP12.tokenMetadataData => { 58 | failwith("TODO"); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/fa2.1/MultiAsset.parameterList.jsligo: -------------------------------------------------------------------------------- 1 | #import "fa2.1-multi-asset.jsligo" "Contract" 2 | 3 | // When this file was created, the smart contract was defined with an entrypoint using `@entry` that was within a namespace. As such, the examples below are written with that assumption in mind. 4 | 5 | // IMPORTANT: We suggest always explicitly typing your parameter values: 6 | // E.g.: `const parameter: int = 10` or `const parameter: Contract.parameter = 10` -------------------------------------------------------------------------------- /lib/fa2.1/MultiAsset.storageList.jsligo: -------------------------------------------------------------------------------- 1 | #import "fa2.1-multi-asset.jsligo" "Contract" 2 | 3 | // When this file was created, the smart contract was defined with an entrypoint using `@entry` that was within a namespace. As such, the examples below are written with that assumption in mind. 4 | 5 | // IMPORTANT: We suggest always explicitly typing your storage values: 6 | // E.g.: `const storage: int = 10` or `const storage: Contract.storage = 10` -------------------------------------------------------------------------------- /lib/fa2.1/NFT.parameterList.jsligo: -------------------------------------------------------------------------------- 1 | #import "fa2.1-NFT.jsligo" "Contract" 2 | 3 | // When this file was created, the smart contract was defined with an entrypoint using `@entry` that was within a namespace. As such, the examples below are written with that assumption in mind. 4 | 5 | // IMPORTANT: We suggest always explicitly typing your parameter values: 6 | // E.g.: `const parameter: int = 10` or `const parameter: Contract.parameter = 10` -------------------------------------------------------------------------------- /lib/fa2.1/NFT.storageList.jsligo: -------------------------------------------------------------------------------- 1 | #import "fa2.1-NFT.jsligo" "Contract" 2 | 3 | // When this file was created, the smart contract was defined with an entrypoint using `@entry` that was within a namespace. As such, the examples below are written with that assumption in mind. 4 | 5 | // IMPORTANT: We suggest always explicitly typing your storage values: 6 | // E.g.: `const storage: int = 10` or `const storage: Contract.storage = 10` -------------------------------------------------------------------------------- /lib/fa2.1/data/admin.jsligo: -------------------------------------------------------------------------------- 1 | // #import "storage.jsligo" "Storage" 2 | 3 | export type T = address; 4 | 5 | // export const isCallerAdmin = (s: Storage.T): bool => { 6 | // return caller == admin; 7 | // }; 8 | -------------------------------------------------------------------------------- /lib/fa2.1/data/amount.jsligo: -------------------------------------------------------------------------------- 1 | export type T = nat; 2 | -------------------------------------------------------------------------------- /lib/fa2.1/data/approvals.jsligo: -------------------------------------------------------------------------------- 1 | #import "errors.jsligo" "Errors" 2 | #import "token.jsligo" "Token" 3 | #import "amount.jsligo" "Amount" 4 | 5 | export type owner = address; 6 | export type spender = address; 7 | 8 | export type T = big_map<[owner, spender, Token.T], Amount.T>; 9 | 10 | export const get_amount = ( 11 | approvals: T, 12 | owner: owner, 13 | spender: spender, 14 | token_id: Token.T 15 | ): Amount.T => { 16 | return match(Big_map.find_opt([owner, spender, token_id], approvals)) { 17 | when(Some(amount)): do { 18 | return amount 19 | }; 20 | when(None): do { 21 | return (0 as nat) 22 | }; 23 | }; 24 | }; 25 | 26 | export const set_amount = ( 27 | approvals: T, 28 | owner: owner, 29 | spender: spender, 30 | token_id: Token.T, 31 | amount_: Amount.T 32 | ): T => { 33 | return Big_map.update([owner, spender, token_id], Some(amount_), approvals); 34 | }; 35 | 36 | export const decrease_approved_amount = ( 37 | approvals: T, 38 | from_: owner, 39 | spender: spender, 40 | token_id: Token.T, 41 | amount_: Amount.T 42 | ): T => { 43 | let balance_ = get_amount(approvals, from_, spender, token_id); 44 | Assert.Error.assert((balance_ >= amount_), Errors.ins_balance); 45 | balance_ = abs(balance_ - amount_); 46 | 47 | return set_amount(approvals, from_, spender, token_id, balance_); 48 | }; 49 | 50 | const increase_approved_amount = ( 51 | approvals: T, 52 | to_: owner, 53 | spender: spender, 54 | token_id: Token.T, 55 | amount_: Amount.T 56 | ): T => { 57 | let balance_ = get_amount(approvals, to_, spender, token_id); 58 | balance_ = balance_ + amount_; 59 | 60 | return set_amount(approvals, to_, spender, token_id, balance_); 61 | }; 62 | -------------------------------------------------------------------------------- /lib/fa2.1/data/errors.jsligo: -------------------------------------------------------------------------------- 1 | type t = string; 2 | 3 | export const undefined_token = "FA2_TOKEN_UNDEFINED"; 4 | export const ins_balance = "FA2_INSUFFICIENT_BALANCE"; 5 | export const no_transfer = "FA2_TX_DENIED"; 6 | export const not_owner = "FA2_NOT_OWNER"; 7 | export const not_operator = "FA2_NOT_OPERATOR"; 8 | export const not_supported = "FA2_OPERATORS_UNSUPPORTED"; 9 | export const rec_hook_fail = "FA2_RECEIVER_HOOK_FAILED"; 10 | export const send_hook_fail = "FA2_SENDER_HOOK_FAILED"; 11 | export const rec_hook_undef = "FA2_RECEIVER_HOOK_UNDEFINED"; 12 | export const send_hook_under = "FA2_SENDER_HOOK_UNDEFINED"; 13 | 14 | export const invalid_ticket = "FA2.1_INVALID_TICKET"; 15 | export const invalid_destination = "FA2.1_INVALID_DESTINATION"; 16 | export const unsafe_approval = "FA2.1_UNSAFE_APPROVAL_CHANGE"; 17 | export const insufficient_allowance = "FA2.1_INSUFFICIENT_ALLOWANCE"; 18 | 19 | export const cannot_create_ticket = "Ticket cannot be created (amount is problably 0)"; 20 | export const storage_has_no_operators = "The storage does not support operators management"; 21 | export const only_sender_manage_operators = "The sender can only manage operators for their own token"; 22 | export const amount_net_expected = "NFT transaction amount should be 1n"; 23 | 24 | export const invalid_proxy = "FA2.1_INVALID_PROXY"; 25 | 26 | export const not_admin = "SENDER_NOT_ADMIN"; 27 | -------------------------------------------------------------------------------- /lib/fa2.1/data/ledger.jsligo: -------------------------------------------------------------------------------- 1 | #import "errors.jsligo" "Errors" 2 | #import "token.jsligo" "Token" 3 | #import "amount.jsligo" "Amount" 4 | 5 | type owner = address; 6 | 7 | export type ledger_module = { 8 | data : L; 9 | empty : L; 10 | increase : (p : [L, owner, Token.T, Amount.T]) => L; 11 | decrease : (p : [L, owner, Token.T, Amount.T]) => L; 12 | balance_of : (p: [L, owner, Token.T]) => Amount.T; 13 | supply: (p: [L, Token.T]) => option; 14 | }; 15 | 16 | //@inline 17 | export const decrease_token_amount_for_user = ( 18 | ledger_module: ledger_module, 19 | from_: owner, 20 | token_id: Token.T, 21 | amount_: nat 22 | ): ledger_module => { 23 | const data = ledger_module.decrease([ledger_module.data, from_, token_id, amount_]); 24 | 25 | return { ...ledger_module, data }; 26 | }; 27 | 28 | //@inline 29 | export const increase_token_amount_for_user = ( 30 | ledger_module: ledger_module, 31 | to_: owner, 32 | token_id: Token.T, 33 | amount_: nat 34 | ): ledger_module => { 35 | const data = ledger_module.increase([ledger_module.data, to_, token_id, amount_]); 36 | 37 | return { ...ledger_module, data }; 38 | }; 39 | 40 | //@inline 41 | export const get_supply = ( 42 | ledger_module: ledger_module, 43 | token_id: Token.T 44 | ): option => 45 | ledger_module.supply([ledger_module.data, token_id]); 46 | 47 | //@inline 48 | export const get_for_user = ( 49 | ledger_module: ledger_module, 50 | owner: owner, 51 | token_id: Token.T 52 | ): Amount.T => { 53 | return ledger_module.balance_of([ledger_module.data, owner, token_id]); 54 | }; 55 | 56 | 57 | /* Possible types as defined in the TZIP-12 */ 58 | 59 | export namespace Common_Asset { 60 | export const balance_of = (value: option): Amount.T => { 61 | match(value) { 62 | when(Some(v)): do { return v }; 63 | when(None()): do { return (0 as nat) }; 64 | }; 65 | }; 66 | 67 | export const add_to_val = (amount_: Amount.T, old_value: option): Amount.T => { 68 | const value = balance_of(old_value); 69 | 70 | return value + amount_; 71 | }; 72 | 73 | export const sub_to_val = (amount_: Amount.T, old_value: option): Amount.T => { 74 | const value = balance_of(old_value); 75 | Assert.Error.assert(value >= amount_, Errors.ins_balance); 76 | 77 | return abs(value - amount_); 78 | }; 79 | }; 80 | 81 | export namespace Single_asset { 82 | export type L = { 83 | values: big_map; 84 | supply: Amount.T; 85 | }; 86 | 87 | export const empty: L = { 88 | values: Big_map.empty; 89 | supply: (0 as nat); 90 | }; 91 | 92 | export const get_for_user = (ledger: L, owner: owner): option => 93 | Big_map.find_opt(owner, ledger.values); 94 | 95 | export const set_for_user = (ledger: L, owner: owner, value: Amount.T): L => { 96 | const values = Big_map.update(owner, Some(value), ledger.values); 97 | 98 | return { ...ledger, values }; 99 | }; 100 | 101 | export const get_supply = (ledger: L): Amount.T => { 102 | return ledger.supply; 103 | }; 104 | 105 | export const set_supply = (ledger: L, value: Amount.T): L => { 106 | const supply = value; 107 | 108 | return { ...ledger, supply }; 109 | }; 110 | 111 | export const decrease = ( 112 | [ledger, from_, _token_id, amount_]: [L, owner, Token.T, Amount.T] 113 | ) : L => { 114 | return set_supply( 115 | set_for_user( 116 | ledger, 117 | from_, 118 | Common_Asset.sub_to_val(amount_, get_for_user(ledger, from_)) 119 | ), 120 | Common_Asset.sub_to_val(amount_, Some(get_supply(ledger))) 121 | ); 122 | }; 123 | 124 | export const increase = ( 125 | [ledger, to_, _token_id, amount_] : [L, owner, Token.T, Amount.T] 126 | ): L => { 127 | return set_supply( 128 | set_for_user( 129 | ledger, 130 | to_, 131 | Common_Asset.add_to_val(amount_, get_for_user(ledger, to_)) 132 | ), 133 | Common_Asset.add_to_val(amount_, Some(get_supply(ledger))) 134 | ); 135 | }; 136 | 137 | export const balance_of = ( 138 | [ledger, owner, _token_id]: [L, owner, Token.T] 139 | ) : Amount.T => { 140 | return Common_Asset.balance_of(get_for_user(ledger, owner)); 141 | }; 142 | 143 | export const supply = ( 144 | [ledger, token_id]: [L, Token.T] 145 | ): option => { 146 | if (token_id == 0n) { 147 | return Some(get_supply(ledger)); 148 | } else { 149 | return None(); 150 | } 151 | }; 152 | 153 | export const ledger_module = (data: L): ledger_module => { 154 | return { 155 | data, 156 | empty, 157 | increase, 158 | decrease, 159 | balance_of, 160 | supply, 161 | }; 162 | }; 163 | }; 164 | 165 | export namespace Multi_asset { 166 | export type L = { 167 | ledger: big_map<[address, Token.T], Amount.T>; 168 | supply: big_map; 169 | }; 170 | 171 | export const empty: L = { 172 | ledger: Big_map.empty; 173 | supply: Big_map.empty; 174 | }; 175 | 176 | export const make_key = (a: address, t: Token.T): [address, Token.T] => { 177 | return [a, t]; 178 | }; 179 | 180 | export const get_for_user = (ledger: L, owner: owner, token_id: Token.T): option => { 181 | const key = make_key(owner, token_id); 182 | 183 | return Big_map.find_opt(key, ledger.ledger); 184 | }; 185 | 186 | export const set_for_user = (ledger: L, owner: owner, token_id: Token.T, value: Amount.T): L => { 187 | const key = make_key(owner, token_id); 188 | const assets = Big_map.update(key, Some(value), ledger.ledger); 189 | 190 | return { ...ledger, ledger: assets }; 191 | }; 192 | 193 | export const supply = ( 194 | [ledger, token_id]: [L, Token.T] 195 | ): option => 196 | Big_map.find_opt(token_id, ledger.supply); 197 | 198 | export const get_supply = (ledger: L, token_id: Token.T): Amount.T => 199 | Option.value(0n, supply([ledger, token_id])) 200 | 201 | export const set_supply = (ledger: L, token_id: Token.T, value: Amount.T): L => { 202 | const supply = Big_map.update(token_id, Some(value), ledger.supply); 203 | 204 | return { ...ledger, supply }; 205 | }; 206 | 207 | export const decrease = ( 208 | [ledger, from_, token_id, amount_] : [L, owner, Token.T, Amount.T] 209 | ): L => { 210 | // const balance_option = get_for_user(ledger, from_, token_id); 211 | // const balance_ = Common_Asset.sub_to_val(amount_, get_for_user(ledger, from_, token_id)); 212 | // let newLedger = set_for_user(ledger, from_, token_id, balance_); 213 | 214 | // return ledger; 215 | return set_supply( 216 | set_for_user( 217 | ledger, 218 | from_, 219 | token_id, 220 | Common_Asset.sub_to_val(amount_, get_for_user(ledger, from_, token_id)) 221 | ), 222 | token_id, 223 | Common_Asset.sub_to_val(amount_, Some(get_supply(ledger, token_id))) 224 | ); 225 | }; 226 | 227 | export const increase = ( 228 | [ledger, to_, token_id, amount_] : [L, owner, Token.T, Amount.T] 229 | ): L => { 230 | return set_supply( 231 | set_for_user( 232 | ledger, 233 | to_, 234 | token_id, 235 | Common_Asset.add_to_val(amount_, get_for_user(ledger, to_, token_id)) 236 | ), 237 | token_id, 238 | Common_Asset.add_to_val(amount_, Some(get_supply(ledger, token_id))) 239 | ); 240 | }; 241 | 242 | export const balance_of = ( 243 | [ledger, owner, token_id]: [L, owner, Token.T] 244 | ): Amount.T => { 245 | const value = get_for_user(ledger, owner, token_id); 246 | 247 | return Common_Asset.balance_of(value); 248 | }; 249 | 250 | export const ledger_module = (data: L): ledger_module => { 251 | return { 252 | data, 253 | empty, 254 | increase, 255 | decrease, 256 | balance_of, 257 | supply, 258 | }; 259 | }; 260 | } 261 | 262 | export namespace NFT { 263 | export type L = big_map; 264 | 265 | export const empty: L = Big_map.empty; 266 | 267 | export const _balance_of = (address: address, value: option): Amount.T => { 268 | return match(value) { 269 | when(Some(own)): do { return (own == address ? (1 as nat) : (0 as nat)) }; 270 | when(None()): do { return (0 as nat) }; 271 | }; 272 | }; 273 | 274 | export const add_to_val = (address: address, amount: Amount.T): option => { 275 | return amount == (1 as nat) ? Some(address) : failwith(Errors.amount_net_expected); 276 | }; 277 | 278 | export const sub_to_val = (amount: Amount.T): option => { 279 | return amount == (1 as nat) ? (None() as option) : failwith(Errors.amount_net_expected); 280 | }; 281 | 282 | export const get_for_user = (ledger: L, token_id: Token.T): option => { 283 | return Big_map.find_opt(token_id, ledger); 284 | }; 285 | 286 | export const set_for_user = (ledger: L, owner: owner, token_id: Token.T): L => { 287 | return Big_map.update(token_id, Some(owner), ledger); 288 | }; 289 | 290 | export const decrease = ( 291 | [ledger, from_, token_id, amount_] : [L, owner, Token.T, Amount.T] 292 | ): L => { 293 | get_for_user(ledger, token_id); 294 | sub_to_val(amount_); 295 | 296 | return set_for_user(ledger, from_, token_id); 297 | }; 298 | 299 | export const increase = ( 300 | [ledger, to_, token_id, amount_] : [L, owner, Token.T, Amount.T] 301 | ): L => { 302 | get_for_user(ledger, token_id); 303 | add_to_val(to_, amount_); 304 | 305 | return set_for_user(ledger, to_, token_id); 306 | }; 307 | 308 | export const balance_of = ( 309 | [ledger, owner, token_id]: [L, owner, Token.T] 310 | ): Amount.T => { 311 | const b = _balance_of(owner, get_for_user(ledger, token_id)); 312 | return (b as Amount.T); 313 | }; 314 | 315 | export const supply = ( 316 | [ledger, token_id]: [L, Token.T] 317 | ): option => { 318 | return match(get_for_user(ledger, token_id)) { 319 | when(Some(_)): Some(1n); 320 | when(None()): None(); 321 | }; 322 | }; 323 | 324 | export const ledger_module = (data: L): ledger_module => { 325 | return { 326 | data, 327 | empty, 328 | increase, 329 | decrease, 330 | balance_of, 331 | supply, 332 | }; 333 | }; 334 | } 335 | -------------------------------------------------------------------------------- /lib/fa2.1/data/metadata.jsligo: -------------------------------------------------------------------------------- 1 | /* 2 | { 3 | "name":"", (* Contract name, recommended *) 4 | "description":"", (* details information *) 5 | "version":"", (* semver version number *) 6 | "license":{"name":"", "details":""}, (* details is optional *) 7 | "authors":[], (* List of authors with format "Name<'contact'>" *) 8 | "homepage":"", (* URL for human-consumption *) 9 | "source":{"tools":[], "location":""}, (* High-level language source *) 10 | "interfaces":[], (* TZIP-XYZ others than TZIP-016 *) 11 | "errors":[], (* List of errors translation object : {"error":,"expension":, "languages":[]} *) 12 | "views":[] (* List of off-chain-view objects *) 13 | } */ 14 | 15 | export type t = big_map; 16 | 17 | // // @bytes 18 | // let metadata = [{ 19 | // "name":"FA2.1", 20 | // "description":"FA2.1 implementation", 21 | // "version":"1.0.0", 22 | // "license":{"name":"MIT"}, 23 | // "authors":[ 24 | // "Melwyn Saldanha ", 25 | // "Didier Plaindoux " 26 | // ], 27 | // "homepage":"", 28 | // "source":{"tools":["Ligo"], "location": "https://github.com/ligolang/generic-fa2.1"}, 29 | // "interfaces":["TZIP-012"], 30 | // "errors":[], 31 | // "views":[] 32 | // }]; 33 | 34 | // let init () : t = Big_map.literal [ 35 | // ("", [%bytes {|tezos-storage:data|}]); 36 | // ("data", metadata); 37 | // ] 38 | -------------------------------------------------------------------------------- /lib/fa2.1/data/operators.jsligo: -------------------------------------------------------------------------------- 1 | #import "errors.jsligo" "Errors" 2 | #import "token.jsligo" "Token" 3 | 4 | type owner = address; 5 | type operator = address; 6 | 7 | export type t = big_map<[owner, Token.T], set>; 8 | 9 | export const assert_authorisation = ( 10 | operators : t, 11 | from_ : address, 12 | token_id : Token.T 13 | ): unit => { 14 | const sender_ = Tezos.get_sender(); 15 | if (sender_ == from_) { 16 | return; 17 | } else { 18 | const authorized = match(Big_map.find_opt([from_, token_id], operators)) { 19 | when(Some(v)): do { return v }; 20 | when(None): do { return Set.empty }; 21 | }; 22 | 23 | if (Set.mem(sender_, authorized)) { 24 | return; 25 | } else { 26 | failwith(Errors.not_operator); 27 | } 28 | }; 29 | }; 30 | 31 | const assert_update_permission = (owner: owner): unit => { 32 | Assert.Error.assert(owner == Tezos.get_sender(), Errors.only_sender_manage_operators); 33 | }; 34 | 35 | export const add_operator = ( 36 | operators: t, 37 | owner: owner, 38 | operator: operator, 39 | token_id: Token.T 40 | ): t => { 41 | if (owner == operator) { 42 | return operators; 43 | } else { 44 | assert_update_permission(owner); 45 | let auth = match(Big_map.find_opt([owner, token_id], operators)) { 46 | when(Some(v)): do { return v }; 47 | when(None): do { return Set.empty }; 48 | }; 49 | auth = Set.add(operator, auth); 50 | return Big_map.update([owner, token_id], Some(auth), operators); 51 | }; 52 | }; 53 | 54 | export const remove_operator = ( 55 | operators: t, 56 | owner: owner, 57 | operator: operator, 58 | token_id: Token.T 59 | ): t => { 60 | if (owner == operator) { 61 | return operators; 62 | } else { 63 | assert_update_permission(owner); 64 | let auth = match(Big_map.find_opt([owner, token_id], operators)) { 65 | when(Some(v)): do { return v }; 66 | when(None): do { return Set.empty }; 67 | }; 68 | auth = Set.remove(operator, auth); 69 | if (Set.size(auth) == (0 as nat)) { 70 | return Big_map.remove([owner, token_id], operators); 71 | } else { 72 | return Big_map.update([owner, token_id], Some(auth), operators); 73 | }; 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /lib/fa2.1/data/proxy.jsligo: -------------------------------------------------------------------------------- 1 | export type T = address; 2 | -------------------------------------------------------------------------------- /lib/fa2.1/data/storage.jsligo: -------------------------------------------------------------------------------- 1 | #import "errors.jsligo" "Errors" 2 | #import "metadata.jsligo" "Metadata" 3 | #import "token.jsligo" "Token" 4 | #import "operators.jsligo" "Operators" 5 | #import "approvals.jsligo" "Approvals" 6 | #import "tokenMetadata.jsligo" "TokenMetadata" 7 | #import "ledger.jsligo" "Ledger" 8 | #import "proxy.jsligo" "Proxy" 9 | 10 | export type T = { 11 | metadata: Metadata.t; 12 | assets: L; 13 | token_metadata: TokenMetadata.T; 14 | operators: option; 15 | approvals: Approvals.T; 16 | proxy: Proxy.T; 17 | extension: A; 18 | }; 19 | 20 | export const token_exist = (s: T, token_id): bool => { 21 | return Big_map.mem(token_id, s.token_metadata); 22 | }; 23 | 24 | export const assert_token_exist = (s: T, token_id): unit => { 25 | if (!token_exist(s, token_id)) { 26 | failwith(Errors.undefined_token); 27 | }; 28 | }; 29 | 30 | const get_ledger = (s: T): L => { 31 | return s.assets; 32 | }; 33 | 34 | export const set_ledger = (s: T, ledger: L): T => { 35 | return { ...s, assets: ledger }; 36 | }; 37 | 38 | export const get_operators = (s: T): option => { 39 | return s.operators; 40 | }; 41 | 42 | export const set_operators = (s: T, operators: Operators.t): T => { 43 | return { ...s, operators: Some(operators) }; 44 | }; 45 | 46 | export const get_approvals = (s: T): Approvals.T => { 47 | return s.approvals; 48 | }; 49 | 50 | export const set_approvals = (s: T, approvals: Approvals.T): T => { 51 | return { ...s, approvals }; 52 | }; 53 | 54 | export const get_token_metadata = (s: T): TokenMetadata.T => { 55 | return s.token_metadata; 56 | }; 57 | 58 | export const set_token_metadata = (s: T, token_metadata: TokenMetadata.T): T => { 59 | return { ...s, token_metadata }; 60 | }; 61 | -------------------------------------------------------------------------------- /lib/fa2.1/data/token.jsligo: -------------------------------------------------------------------------------- 1 | export type T = nat; 2 | -------------------------------------------------------------------------------- /lib/fa2.1/data/tokenMetadata.jsligo: -------------------------------------------------------------------------------- 1 | #import "token.jsligo" "Token" 2 | 3 | type data = { 4 | token_id : Token.T; 5 | token_info: map; 6 | }; 7 | 8 | export type T = big_map; 9 | 10 | export const get_token_info = (tokenMetadata: T, tokenId: Token.T): map => { 11 | return match(Big_map.find_opt(tokenId, tokenMetadata)) { 12 | when(Some(token)): do { return token.token_info }; 13 | when(None()): do { return Map.empty }; 14 | }; 15 | }; 16 | 17 | export const set_token_info = (tokenMetadata: T, tokenId: Token.T, tokenInfo: map): T => { 18 | return Big_map.update(tokenId, Some({ 19 | token_id: tokenId, 20 | token_info: tokenInfo 21 | }), tokenMetadata); 22 | }; 23 | 24 | // (** 25 | // This should be initialized at origination, conforming to either: 26 | // TZIP-12 : https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#token-metadata 27 | // or 28 | // TZIP-16 : https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#contract-metadata-tzip-016 29 | // *) 30 | 31 | // let data1 = Map.literal [ 32 | // ("name", [%bytes {| "FA2 multi asset 1" |}]); 33 | // ("symbol", [%bytes {| "FMA1" |}]); 34 | // ("decimals", [%bytes {| "3" |}]); 35 | // ] 36 | 37 | // let data3 = Map.literal [ 38 | // ("name", [%bytes {| "FA2 multi asset 3" |}]); 39 | // ("symbol", [%bytes {| "FMA3" |}]); 40 | // ("decimals", [%bytes {| "3" |}]); 41 | // ] 42 | 43 | 44 | // let init () : t = Big_map.literal [ 45 | // (1n, {token_id=1n;token_info=data1}); 46 | // (3n, {token_id=3n;token_info=data3}); 47 | // ] -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/approve.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/errors.jsligo" "Errors" 2 | #import "../data/token.jsligo" "Token" 3 | #import "../data/approvals.jsligo" "Approvals" 4 | #import "../data/storage.jsligo" "Storage" 5 | 6 | type Storage = Storage.T; 7 | 8 | type approve = 9 | // @layout comb 10 | { 11 | owner : address; 12 | spender : address; 13 | token_id : Token.T; 14 | old_value : nat; 15 | new_value : nat; 16 | }; 17 | 18 | type approvals = list; 19 | export type T = approvals; 20 | 21 | 22 | type allowance_update_type = 23 | @layout("comb") 24 | { 25 | owner : address; 26 | spender : address; 27 | token_id : Token.T; 28 | new_allowance : nat; 29 | diff : int; 30 | }; 31 | 32 | type event_map_value = [nat, int]; 33 | type event_map_key = [address, address, Token.T]; 34 | 35 | const _approve = (approve: approve, approvals: Approvals.T): Approvals.T => { 36 | const { owner, spender, token_id, old_value, new_value } = approve; 37 | const amount = Approvals.get_amount(approvals, owner, spender, token_id); 38 | Assert.Error.assert(amount == old_value, Errors.unsafe_approval); 39 | Assert.Error.assert(owner == Tezos.get_sender(), Errors.not_owner); 40 | return Approvals.set_amount(approvals, owner, spender, token_id, new_value); 41 | }; 42 | 43 | export const approve = ( 44 | to_approve: approvals, 45 | storage: Storage 46 | ): [list, Storage] => { 47 | let event_approvals: map = Map.empty; 48 | let newApprovals: Approvals.T = Storage.get_approvals(storage); 49 | let finalOperations = list([]); 50 | 51 | for (const a of to_approve) { 52 | newApprovals = _approve(a, newApprovals); 53 | 54 | const key: event_map_key = [a.owner, a.spender, a.token_id]; 55 | const oldData: option = Map.find_opt(key, event_approvals); 56 | 57 | let diff: int = 0; 58 | match(oldData) { 59 | when(Some(data)): do { diff = data[1] + (a.new_value - a.old_value)}; 60 | when(None): do { diff = a.new_value - a.old_value }; 61 | }; 62 | event_approvals = Map.add(key, [a.new_value, diff], event_approvals); 63 | } 64 | let finalEventApprovals: list = Map.fold( 65 | ([acc, [key, value]]: [list, [event_map_key, event_map_value]]) => { 66 | if (value[1] != 0) { 67 | const event_data: allowance_update_type = { 68 | owner: key[0], 69 | spender: key[1], 70 | token_id: key[2], 71 | new_allowance: value[0], 72 | diff: value[1] 73 | }; 74 | const event_balance = Tezos.emit("%allowance_update", event_data); 75 | return list([event_balance, ...acc]); 76 | } 77 | return acc; 78 | }, 79 | event_approvals, 80 | list([]) 81 | ) 82 | 83 | /** Reverse event list **/ 84 | 85 | for (const op of finalEventApprovals) { 86 | finalOperations = list([op, ...finalOperations]); 87 | } 88 | 89 | const newStorage = Storage.set_approvals(storage, newApprovals); 90 | 91 | return [finalOperations, newStorage]; 92 | }; -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/balance_of.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/token.jsligo" "Token" 2 | #import "../data/amount.jsligo" "Amount" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | 6 | type Storage = Storage.T; 7 | 8 | type request = { 9 | owner : address; 10 | token_id : Token.T; 11 | }; 12 | 13 | type callback = 14 | // @layout comb 15 | { 16 | request : request; 17 | balance : Amount.T; 18 | }; 19 | 20 | type balance_of = 21 | // @layout comb 22 | { 23 | requests : list; 24 | callback : contract>; 25 | }; 26 | export type T = balance_of; 27 | 28 | type LedgerModule = Ledger.ledger_module; 29 | 30 | const get_balance_info = ( 31 | storage : Storage, 32 | ledger_module : LedgerModule, 33 | request : request 34 | ) : callback => { 35 | const { owner, token_id } = request; 36 | Storage.assert_token_exist(storage, token_id); 37 | 38 | return { request, balance : ledger_module.balance_of([ledger_module.data, owner, token_id]) }; 39 | }; 40 | 41 | export const balance_of = ( 42 | balance: balance_of, 43 | storage: Storage, 44 | ledgerModule: LedgerModule 45 | ) : [list, Storage] => { 46 | const { requests, callback } = balance; 47 | 48 | return [list([ 49 | Tezos.Next.Operation.transaction( 50 | List.map( 51 | (request) => get_balance_info(storage, ledgerModule, request), 52 | requests 53 | ), 54 | (0 as tez), 55 | callback 56 | ) 57 | ]), storage]; 58 | }; 59 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/burn.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/amount.jsligo" "Amount" 2 | #import "../data/errors.jsligo" "Errors" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/token.jsligo" "Token" 6 | #import "../data/tokenMetadata.jsligo" "TokenMetadata" 7 | #import "./transfer.jsligo" "Transfer" 8 | #import "../data/approvals.jsligo" "Approvals" 9 | 10 | type Storage = Storage.T; 11 | type LedgerModule = Ledger.ledger_module; 12 | 13 | type BurnParam = { 14 | from_ : address; 15 | token_id : Token.T; 16 | amount : Amount.T; 17 | }; 18 | 19 | type total_supply_update_type = 20 | @layout("comb") 21 | { 22 | token_id: Token.T, 23 | new_total_supply: nat, 24 | diff: int 25 | }; 26 | 27 | type balance_event_data = 28 | @layout("comb") 29 | { 30 | owner: address, 31 | token_id: nat, 32 | new_balance: nat, 33 | diff: int 34 | }; 35 | 36 | type allowance_update_type = 37 | @layout("comb") 38 | { 39 | owner : address; 40 | spender : address; 41 | token_id : Token.T; 42 | new_allowance : nat; 43 | diff : int; 44 | }; 45 | 46 | export type T = BurnParam; 47 | 48 | 49 | type Burn = BurnParam; 50 | export type T = BurnParam; 51 | 52 | export const burn = ( 53 | burnParam: BurnParam, 54 | storage: Storage, 55 | ledger: LedgerModule 56 | ): [list, Storage] => { 57 | let operations = list([]); 58 | const { from_, token_id, amount } = burnParam; 59 | 60 | Storage.assert_token_exist(storage, token_id); 61 | 62 | const approvals = Storage.get_approvals(storage); 63 | 64 | const oldApprovals = Approvals.get_amount(approvals, from_, Tezos.get_sender(), token_id); 65 | 66 | const authorizedApprovals = Transfer.authorize_transfer(storage, approvals, from_, token_id, amount); 67 | 68 | const newApprovals = Approvals.get_amount(authorizedApprovals, from_, Tezos.get_sender(), token_id); 69 | 70 | if (oldApprovals != newApprovals) { 71 | const allowance_update: allowance_update_type = { 72 | owner: from_, 73 | spender: Tezos.get_sender(), 74 | token_id: token_id, 75 | new_allowance: newApprovals, 76 | diff: newApprovals - oldApprovals; 77 | }; 78 | const allowance_event_op = Tezos.emit("%allowance_update", allowance_update); 79 | operations = list([allowance_event_op, ...operations]); 80 | } 81 | 82 | const newLedger = Ledger.decrease_token_amount_for_user(ledger, from_, token_id, amount); 83 | 84 | let newStorage = Storage.set_approvals(storage, authorizedApprovals); 85 | newStorage = Storage.set_ledger(newStorage, newLedger.data); 86 | 87 | let oldSupply = Ledger.get_supply(ledger, token_id); 88 | const balance = Ledger.get_for_user(ledger, from_, token_id); 89 | const supply_update: total_supply_update_type = 90 | { 91 | token_id: token_id, 92 | new_total_supply: abs(oldSupply-amount), 93 | diff: -amount, 94 | }; 95 | const balance_update: balance_event_data = 96 | { 97 | owner: from_, 98 | token_id: token_id, 99 | new_balance: abs(balance - amount), 100 | diff: -amount, 101 | }; 102 | const event_balance = Tezos.emit("%balance_update", balance_update); 103 | const event_supply = Tezos.emit("%total_supply_update", supply_update); 104 | 105 | operations = list([event_supply, ...operations]); 106 | operations = list([event_balance, ...operations]); 107 | 108 | return [operations, newStorage]; 109 | }; 110 | 111 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/export_ticket.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/errors.jsligo" "Errors" 2 | #import "../data/token.jsligo" "Token" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/approvals.jsligo" "Approvals" 6 | #import "./transfer.jsligo" "Transfer" 7 | 8 | type Storage = Storage.T; 9 | 10 | type ExportedTicket = ticket<[Token.T, option]>; 11 | 12 | type TicketToExport = 13 | // @layout comb 14 | { 15 | from_ : address; 16 | token_id : Token.T; 17 | amount : nat; 18 | }; 19 | 20 | type ExportTicketRequest = { 21 | to_ : address; 22 | ticketsToExport : list; 23 | }; 24 | 25 | type ExportTicketsTo = { 26 | to_ : address; 27 | ticketsToExport : list; 28 | }; 29 | 30 | type ExportTicketOperation = { 31 | destination : option
; 32 | requests : list; 33 | }; 34 | 35 | type balance_event_data = 36 | @layout("comb") 37 | { 38 | owner: address, 39 | token_id: nat, 40 | new_balance: nat, 41 | diff: int 42 | }; 43 | 44 | type allowance_update_type = 45 | @layout("comb") 46 | { 47 | owner: address, 48 | spender: address, 49 | token_id: nat, 50 | new_allowance: nat, 51 | diff: int 52 | }; 53 | 54 | type ExportTicket = ExportTicketOperation; 55 | export type T = ExportTicket; 56 | 57 | type LedgerModule = Ledger.ledger_module; 58 | 59 | const create_ticket = ( 60 | storage: Storage, 61 | ticketToExport : TicketToExport, 62 | ledger: LedgerModule 63 | ) : [list, ExportedTicket, LedgerModule, Storage] => { 64 | const { from_, token_id, amount } = ticketToExport; 65 | let operations : list = list([]); 66 | 67 | /** Allowance & Operator checks **/ 68 | 69 | Storage.assert_token_exist(storage, token_id); 70 | const approvals = Storage.get_approvals(storage); 71 | 72 | const oldApprovals = Approvals.get_amount(approvals, from_, Tezos.get_sender(), token_id); 73 | const authorizedApprovals = Transfer.authorize_transfer(storage, approvals, from_, token_id, amount); 74 | const newApprovals = Approvals.get_amount(authorizedApprovals, from_, Tezos.get_sender(), token_id); 75 | 76 | if (oldApprovals != newApprovals) { 77 | const allowance_update: allowance_update_type = { 78 | owner: from_, 79 | spender: Tezos.get_sender(), 80 | token_id: token_id, 81 | new_allowance: newApprovals, 82 | diff: newApprovals - oldApprovals; 83 | }; 84 | const allowance_event_op = Tezos.emit("%allowance_update", allowance_update); 85 | operations = list([allowance_event_op, ...operations]); 86 | } 87 | const balance = Ledger.get_for_user(ledger, from_, token_id); 88 | const balance_update: balance_event_data = 89 | { 90 | owner: from_, 91 | token_id: token_id, 92 | new_balance: abs(balance - amount), 93 | diff: -amount, 94 | }; 95 | const event_balance = Tezos.emit("%balance_update", balance_update); 96 | operations = list([event_balance, ...operations]); 97 | const newLedger = Ledger.decrease_token_amount_for_user(ledger, from_, token_id, amount); 98 | let newStorage = Storage.set_approvals(storage, authorizedApprovals); 99 | newStorage = Storage.set_ledger(newStorage, newLedger.data); 100 | const ticket = Option.value_with_error( 101 | Errors.cannot_create_ticket, 102 | Tezos.Next.Ticket.create([token_id, None()], amount) 103 | ); 104 | return [operations, ticket, newLedger, newStorage]; 105 | }; 106 | 107 | export const export_tickets = ( 108 | op : ExportTicket, 109 | storage : Storage, 110 | ledger : LedgerModule 111 | ) : [list, Storage] => { 112 | let operations : list = list([]); 113 | let finalOperations = list([]); 114 | let newLedger = ledger; 115 | let finalStorage = storage; 116 | 117 | if (Option.is_none(op.destination)) { 118 | for (const request of op.requests) { 119 | for (const tk of request.ticketsToExport) { 120 | const [newOps, ticket, updatedLedger, newStorage] = create_ticket(storage, tk, newLedger); 121 | newLedger = updatedLedger; 122 | operations = list([...newOps, ...operations]); 123 | const ticketReceiver: contract = Tezos.get_contract_with_error(request.to_, Errors.invalid_destination); 124 | operations = list([Tezos.Next.Operation.transaction(ticket, 0tez, ticketReceiver), ...operations]); 125 | finalStorage = newStorage; 126 | } 127 | } 128 | } 129 | if (Option.is_some(op.destination)) { 130 | for (const request of op.requests) { 131 | for (const tk of request.ticketsToExport) { 132 | const [newOps, ticket, updatedLedger, newStorage] = create_ticket(storage, tk, newLedger); 133 | newLedger = updatedLedger; 134 | operations = list([...newOps, ...operations]); 135 | let ticketOps : list = list([ticket]); 136 | let finalTicketOps : list = list([{to_: request.to_, ticketsToExport: ticketOps}]); 137 | const ticketReceiver: contract> = Tezos.get_contract_with_error(Option.value_with_error("option is None", op.destination), Errors.invalid_destination); 138 | operations = list([Tezos.Next.Operation.transaction(finalTicketOps, 0tez, ticketReceiver), ...operations]); 139 | finalStorage = newStorage; 140 | } 141 | } 142 | } 143 | 144 | /** Reverse operations list **/ 145 | 146 | for (const op of operations) { 147 | finalOperations = list([op, ...finalOperations]); 148 | } 149 | 150 | return [finalOperations, Storage.set_ledger(finalStorage, newLedger.data)]; 151 | }; 152 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/import_ticket.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/errors.jsligo" "Errors" 2 | #import "../data/token.jsligo" "Token" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/amount.jsligo" "Amount" 6 | 7 | type Storage = Storage.T; 8 | 9 | type imported_ticket = ticket<[Token.T, option]>; 10 | 11 | type tickets_to_import_to = { 12 | to_ : address; 13 | tickets_to_import : list; 14 | }; 15 | 16 | type balance_event_data = 17 | @layout("comb") 18 | { 19 | owner: address, 20 | token_id: nat, 21 | new_balance: nat, 22 | diff: int 23 | }; 24 | 25 | export type transaction = 26 | @layout("comb") 27 | { 28 | to_ : option
; 29 | token_id : Token.T; 30 | amount : Amount.T; 31 | }; 32 | 33 | export type transfer = 34 | @layout("comb") 35 | { 36 | from_ : option
; 37 | txs : list; 38 | }; 39 | 40 | type import_ticket = list; 41 | export type T = import_ticket; 42 | 43 | type LedgerModule = Ledger.ledger_module; 44 | 45 | const make_transaction = (to_: option
, token_id: Token.T, amount: Amount.T): transaction => { 46 | return { to_; token_id; amount }; 47 | } 48 | 49 | const make_transfer = (from_: option
, txs: list): transfer => { 50 | return { from_; txs }; 51 | } 52 | 53 | // @inline 54 | const assert_ticketer_is_self_address = (ticketer : address) : unit => 55 | Assert.Error.assert (Tezos.get_self_address() == ticketer, Errors.invalid_ticket); 56 | 57 | const import_ticket_to = ( 58 | to_ : address, 59 | imported_ticket : imported_ticket, 60 | [transactions, ledger, operations] : [list, LedgerModule, list] 61 | ) : [list, LedgerModule, list] => { 62 | let operationsEvent = operations; 63 | const [[ticketer, [[token_id, _data], amount]], _] = Tezos.Next.Ticket.read (imported_ticket); 64 | assert_ticketer_is_self_address(ticketer); 65 | const newLedger = Ledger.increase_token_amount_for_user(ledger, to_, token_id, amount); 66 | const transaction = make_transaction(Some(to_), token_id, amount); 67 | const balance = Ledger.get_for_user(ledger, to_, token_id); 68 | const balance_update: balance_event_data = 69 | { 70 | owner: to_, 71 | token_id: token_id, 72 | new_balance: balance + amount, 73 | diff: int(amount), 74 | }; 75 | const event_balance = Tezos.emit("%balance_update", balance_update); 76 | operationsEvent = list([event_balance, ...operationsEvent]); 77 | return [list([transaction, ...transactions]), newLedger, operationsEvent]; 78 | }; 79 | 80 | export const import_tickets_to = ( 81 | tickets_to_import_to : tickets_to_import_to, 82 | [transfers, ledger, operations] : [list, LedgerModule, list] 83 | ): [list, LedgerModule, list] => { 84 | const { to_, tickets_to_import } = tickets_to_import_to; 85 | const [transaction, newLedger, events] = List.fold_left( ([r, t]) => import_ticket_to(to_, t, r), [list([]), ledger, operations], tickets_to_import ); 86 | const transfer = make_transfer(None(), transaction); 87 | 88 | return [list([transfer, ...transfers]), newLedger, events]; 89 | }; 90 | 91 | export const import_tickets = ( 92 | importTicket : import_ticket, 93 | storage : Storage, 94 | ledger: LedgerModule 95 | ) : [list, Storage] => { 96 | let operations = list([]); 97 | let finalOperations = list([]); 98 | 99 | const [_transactions, newLedger, events] = List.fold_left( 100 | ([r, t]) => import_tickets_to(t, r), 101 | [list([]), ledger, operations], 102 | importTicket 103 | ); 104 | 105 | /** Reverse operations list **/ 106 | 107 | for (const op of events) { 108 | finalOperations = list([op, ...finalOperations]); 109 | } 110 | 111 | return [finalOperations, Storage.set_ledger(storage, newLedger.data)]; 112 | }; 113 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/lambda_export.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/errors.jsligo" "Errors" 2 | #import "../data/token.jsligo" "Token" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/approvals.jsligo" "Approvals" 6 | #import "./transfer.jsligo" "Transfer" 7 | 8 | type Storage = Storage.T; 9 | 10 | type ExportedTicket = ticket<[Token.T, option]>; 11 | 12 | type TicketToExport = 13 | // @layout comb 14 | { 15 | from_ : address; 16 | token_id : Token.T; 17 | amount : nat; 18 | }; 19 | 20 | type destination = (tickets: list) => list; 21 | 22 | type ExportTicketsTo = 23 | // @layout comb 24 | { 25 | ticketsToExport : list; 26 | destination : destination; 27 | }; 28 | 29 | type ExportTicket = ExportTicketsTo; 30 | export type T = ExportTicket; 31 | 32 | type ProxyParameters = { 33 | tickets: list, 34 | action: (tickets: list) => list 35 | }; 36 | 37 | type balance_event_data = 38 | @layout("comb") 39 | { 40 | owner: address, 41 | token_id: nat, 42 | new_balance: nat, 43 | diff: int 44 | }; 45 | 46 | type allowance_update_type = 47 | @layout("comb") 48 | { 49 | owner: address, 50 | spender: address, 51 | token_id: nat, 52 | new_allowance: nat, 53 | diff: int 54 | }; 55 | 56 | type LedgerModule = Ledger.ledger_module; 57 | 58 | const create_ticket = ( 59 | storage: Storage, 60 | ticketToExport : TicketToExport, 61 | ledger: LedgerModule 62 | ) : [list, ExportedTicket, LedgerModule, Storage] => { 63 | const { from_, token_id, amount } = ticketToExport; 64 | 65 | let operations : list = list([]); 66 | 67 | /** Allowance & Operator checks **/ 68 | 69 | Storage.assert_token_exist(storage, token_id); 70 | const approvals = Storage.get_approvals(storage); 71 | 72 | const oldApprovals = Approvals.get_amount(approvals, from_, Tezos.get_sender(), token_id); 73 | const authorizedApprovals = Transfer.authorize_transfer(storage, approvals, from_, token_id, amount); 74 | const newApprovals = Approvals.get_amount(authorizedApprovals, from_, Tezos.get_sender(), token_id); 75 | 76 | if (oldApprovals != newApprovals) { 77 | const allowance_update: allowance_update_type = { 78 | owner: from_, 79 | spender: Tezos.get_sender(), 80 | token_id: token_id, 81 | new_allowance: newApprovals, 82 | diff: newApprovals - oldApprovals; 83 | }; 84 | const allowance_event_op = Tezos.emit("%allowance_update", allowance_update); 85 | operations = list([allowance_event_op, ...operations]); 86 | } 87 | const balance = Ledger.get_for_user(ledger, from_, token_id); 88 | const balance_update: balance_event_data = 89 | { 90 | owner: from_, 91 | token_id: token_id, 92 | new_balance: abs(balance - amount), 93 | diff: -amount, 94 | }; 95 | const event_balance = Tezos.emit("%balance_update", balance_update); 96 | operations = list([event_balance, ...operations]); 97 | const newLedger = Ledger.decrease_token_amount_for_user(ledger, from_, token_id, amount); 98 | let newStorage = Storage.set_approvals(storage, authorizedApprovals); 99 | newStorage = Storage.set_ledger(newStorage, newLedger.data); 100 | 101 | const ticket = Option.value_with_error( 102 | Errors.cannot_create_ticket, 103 | Tezos.Next.Ticket.create([token_id, None()], amount) 104 | ); 105 | return [operations, ticket, newLedger, newStorage]; 106 | }; 107 | 108 | export const lambda_export = ( 109 | exportTicket : ExportTicket, 110 | storage : Storage, 111 | ledger : LedgerModule 112 | ) : [list, Storage] => { 113 | let finalOperations : list = list([]); 114 | const { ticketsToExport, destination } = exportTicket; 115 | 116 | const [tickets, newLedger, newStorage, newOperations] = List.fold_left( 117 | ([[lt, l, s, ops], tk]) => { 118 | const [ticketOps, ticket, updatedLedger, updatedStorage] = create_ticket(s, tk, l); 119 | let combinedOps = list([...ops, ...ticketOps]); 120 | return [List.cons(ticket, lt), updatedLedger, updatedStorage, combinedOps]; 121 | }, 122 | [list([]), ledger, storage, finalOperations], 123 | ticketsToExport 124 | ); 125 | 126 | const operation = Tezos.Next.Operation.transaction( 127 | { 128 | tickets, 129 | action: destination 130 | }, 131 | Tezos.get_amount(), 132 | Option.value_with_error( 133 | Errors.invalid_proxy, 134 | Tezos.get_contract_opt(storage.proxy) 135 | ) 136 | ); 137 | finalOperations = list([operation, ...newOperations]); 138 | 139 | return [finalOperations, Storage.set_ledger(newStorage, newLedger.data)]; 140 | }; 141 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/mint.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/amount.jsligo" "Amount" 2 | #import "../data/errors.jsligo" "Errors" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/token.jsligo" "Token" 6 | #import "../data/tokenMetadata.jsligo" "TokenMetadata" 7 | 8 | type Storage = Storage.T; 9 | type LedgerModule = Ledger.ledger_module; 10 | 11 | type MintParam = { 12 | to_ : address; 13 | token_id : Token.T; 14 | amount : Amount.T; 15 | token_info : option>; 16 | }; 17 | 18 | type total_supply_update_type = 19 | @layout("comb") 20 | { 21 | token_id: Token.T, 22 | new_total_supply: Amount.T, 23 | diff: int 24 | }; 25 | 26 | type balance_event_data = 27 | @layout("comb") 28 | { 29 | owner: address, 30 | token_id: nat, 31 | new_balance: nat, 32 | diff: int 33 | }; 34 | 35 | type metadata_update_type = 36 | @layout("comb") 37 | { 38 | token_id: Token.T, 39 | new_metadata: option> 40 | }; 41 | 42 | type Mint = MintParam; 43 | export type T = MintParam; 44 | 45 | export const mint = ( 46 | mint: Mint, 47 | storage: Storage, 48 | ledger: LedgerModule 49 | ): [list, Storage] => { 50 | let operations = list([]); 51 | let finalOperations = list([]); 52 | // assert_with_error(storage.extension.admin == Tezos.get_sender(), Errors.not_admin); 53 | 54 | let oldSupply = Ledger.get_supply(ledger, mint.token_id); 55 | const balance = Ledger.get_for_user(ledger, mint.to_, mint.token_id); 56 | const supply_update: total_supply_update_type = 57 | { 58 | token_id: mint.token_id, 59 | new_total_supply: mint.amount+oldSupply, 60 | diff: int(mint.amount), 61 | }; 62 | const balance_update: balance_event_data = 63 | { 64 | owner: mint.to_, 65 | token_id: mint.token_id, 66 | new_balance: balance + mint.amount, 67 | diff: int(mint.amount), 68 | }; 69 | const event_balance = Tezos.emit("%balance_update", balance_update); 70 | const event_supply = Tezos.emit("%total_supply_update", supply_update); 71 | let new_token_info = match (mint.token_info) { 72 | when (Some(token_metadata)): do { return token_metadata; }; 73 | when (None): do { return Map.empty; }; 74 | }; 75 | let token_info = TokenMetadata.get_token_info(storage.token_metadata, mint.token_id); 76 | let newTokenMetadata = TokenMetadata.set_token_info(storage.token_metadata, mint.token_id, token_info); 77 | if (Map.size(token_info) == (0 as nat) && Map.size(new_token_info) == (0 as nat)) { 78 | token_info = Option.unopt_with_error(mint.token_info, "Token info must be provided"); 79 | }; 80 | if(Map.size(new_token_info) > (0 as nat)) { 81 | newTokenMetadata = TokenMetadata.set_token_info(storage.token_metadata, mint.token_id, new_token_info); 82 | const metadata_event_data: metadata_update_type = { 83 | token_id: mint.token_id, 84 | new_metadata: mint.token_info 85 | } 86 | const metadata_event = Tezos.emit("%token_metadata_update", { metadata_event_data }); 87 | operations = list([metadata_event, ...operations]); 88 | } 89 | const newLedger = Ledger.increase_token_amount_for_user(ledger, mint.to_, mint.token_id, mint.amount); 90 | operations = list([event_supply, ...operations]); 91 | operations = list([event_balance, ...operations]); 92 | 93 | /** Reverse operations list **/ 94 | 95 | for (const op of operations) { 96 | finalOperations = list([op, ...finalOperations]); 97 | } 98 | 99 | return [ 100 | finalOperations, 101 | Storage.set_ledger( 102 | Storage.set_token_metadata(storage, newTokenMetadata), 103 | newLedger.data 104 | ) 105 | ]; 106 | }; 107 | -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/transfer.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/token.jsligo" "Token" 2 | #import "../data/amount.jsligo" "Amount" 3 | #import "../data/approvals.jsligo" "Approvals" 4 | #import "../data/operators.jsligo" "Operators" 5 | #import "../data/ledger.jsligo" "Ledger" 6 | #import "../data/storage.jsligo" "Storage" 7 | #import "../data/errors.jsligo" "Errors" 8 | 9 | type storage = Storage.T; 10 | 11 | type atomic_trans = 12 | // @layout comb 13 | { 14 | to_ : address; 15 | amount : Amount.T; 16 | token_id : Token.T; 17 | }; 18 | 19 | type transfer_from = { 20 | from_ : address; 21 | txs : list; 22 | }; 23 | 24 | type balance_event_data = 25 | @layout("comb") 26 | { 27 | owner: address, 28 | token_id: nat, 29 | new_balance: nat, 30 | diff: int 31 | }; 32 | 33 | type allowance_update_type = 34 | @layout("comb") 35 | { 36 | owner: address, 37 | spender: address, 38 | token_id: nat, 39 | new_allowance: nat, 40 | diff: int 41 | }; 42 | 43 | type transfer_event_type = 44 | @layout("comb") 45 | { 46 | from_: address, 47 | to_: address, 48 | token_id: nat, 49 | amount: nat 50 | }; 51 | 52 | type transfer = list; 53 | export type t = transfer; 54 | 55 | type ledger_module = Ledger.ledger_module; 56 | 57 | export const authorize_transfer = ( 58 | storage: storage, 59 | approvals: Approvals.T, 60 | from_: address, 61 | token_id: Token.T, 62 | amount: Amount.T 63 | ): Approvals.T => { 64 | 65 | if (from_ == Tezos.get_sender()) { 66 | return approvals; 67 | } 68 | 69 | const current_allowance: nat = Approvals.get_amount(approvals, from_, Tezos.get_sender(), token_id); 70 | 71 | if (current_allowance > (0 as nat)) { 72 | if (current_allowance >= amount) { 73 | return Approvals.decrease_approved_amount(approvals, from_, Tezos.get_sender(), token_id, amount); 74 | } 75 | } 76 | 77 | return match(Storage.get_operators(storage)) { 78 | when(Some(operators)): do { 79 | Operators.assert_authorisation(operators, from_, token_id); 80 | return approvals; 81 | }; 82 | when(None()): do { 83 | failwith(Errors.insufficient_allowance); 84 | }; 85 | }; 86 | }; 87 | 88 | type event_map_key = [address, Token.T]; 89 | type allowance_event_map_key = [address, address, Token.T]; 90 | type event_map_value = [nat, int]; 91 | 92 | const atomic_trans = ( 93 | storage : storage, 94 | from_ : address, 95 | [[ledger, approvals, operations, event_balance, event_allowance], transfer] : [[ledger_module, Approvals.T, list, map, map], atomic_trans] 96 | ): [ledger_module, Approvals.T, list, map, map] => { 97 | let operationsEvent = operations; 98 | let event_balance_map: map = event_balance; 99 | let event_allowance_map: map = event_allowance; 100 | 101 | const { to_, token_id, amount } = transfer; 102 | Storage.assert_token_exist(storage, token_id); 103 | 104 | const oldApprovals = Approvals.get_amount(approvals, from_, Tezos.get_sender(), token_id); 105 | const authorizedApprovals = authorize_transfer(storage, approvals, from_, token_id, amount); 106 | const newApprovals = Approvals.get_amount(authorizedApprovals, from_, Tezos.get_sender(), token_id); 107 | const balance_from = Ledger.get_for_user(ledger, from_, token_id); 108 | const balance_to = Ledger.get_for_user(ledger, to_, token_id); 109 | 110 | let diff_from: int = 0; 111 | let diff_to: int = 0; 112 | let diff_allowance: int = 0; 113 | const key_from: event_map_key = [from_, token_id]; 114 | const key_to: event_map_key = [to_, token_id]; 115 | const key_allowance: allowance_event_map_key = [from_, Tezos.get_sender(), token_id]; 116 | 117 | const oldData_from: option = Map.find_opt(key_from, event_balance_map); 118 | const oldData_to: option = Map.find_opt(key_to, event_balance_map); 119 | const oldData_allowance: option = Map.find_opt(key_allowance, event_allowance_map); 120 | let new_balance_from: nat = abs(balance_from - amount); 121 | let new_balance_to: nat = balance_to + amount; 122 | 123 | match(oldData_from) { 124 | when(Some(data)): do { diff_from = data[1] - amount; 125 | event_balance_map = Map.update(key_from, Some([new_balance_from, diff_from]), event_balance_map)}; 126 | when(None): do { diff_from = - amount; 127 | event_balance_map = Map.add(key_from, [new_balance_from, diff_from], event_balance_map); }; 128 | }; 129 | 130 | match(oldData_to) { 131 | when(Some(data)): do { diff_to = data[1] + amount; 132 | event_balance_map = Map.update(key_to, Some([new_balance_to, diff_to]), event_balance_map);}; 133 | when(None): do { diff_to = int(amount); 134 | event_balance_map = Map.add(key_to, [new_balance_to, diff_to], event_balance_map); }; 135 | }; 136 | 137 | if (oldApprovals != newApprovals) { 138 | match(oldData_allowance) { 139 | when(Some(data)): do { diff_allowance = data[1] + (newApprovals - oldApprovals); 140 | event_allowance_map = Map.update(key_allowance, Some([newApprovals, diff_allowance]), event_allowance_map);}; 141 | when(None): do { diff_allowance = newApprovals - oldApprovals; 142 | event_allowance_map = Map.add(key_allowance, [newApprovals, diff_allowance], event_allowance_map);}; 143 | }; 144 | } 145 | const transfer_event_data: transfer_event_type = 146 | { 147 | from_: from_, 148 | to_: to_, 149 | token_id: token_id, 150 | amount: amount 151 | }; 152 | const transfer_event_op = Tezos.emit("%transfer_event", transfer_event_data); 153 | operationsEvent = list([transfer_event_op, ...operationsEvent]); 154 | 155 | const newLedger = Ledger.decrease_token_amount_for_user(ledger, from_, token_id, amount); 156 | 157 | return [Ledger.increase_token_amount_for_user(newLedger, to_, token_id, amount), authorizedApprovals, operationsEvent, event_balance_map, event_allowance_map]; 158 | }; 159 | 160 | const transfer_from = ( 161 | storage: storage, 162 | [[ledger, approvals, operations, event_balance, event_allowance], transfer] : [[ledger_module, Approvals.T, list, map, map], transfer_from] 163 | ): [ledger_module, Approvals.T, list, map, map] => { 164 | const { from_, txs } = transfer; 165 | let currentOperations = operations; 166 | let event_balance_map: map = event_balance; 167 | let event_allowance_map: map = event_allowance; 168 | 169 | return List.fold_left(atomic_trans(storage, from_), [ledger, approvals, currentOperations, event_balance_map, event_allowance_map], txs); 170 | } 171 | 172 | export const transfer = ( 173 | transfer: transfer, 174 | storage: storage, 175 | ledger: ledger_module 176 | ): [list, storage] => { 177 | let operations = list([]); 178 | let finalOperations = list([]); 179 | let event_balance_map: map = Map.empty; 180 | let event_allowance_map: map = Map.empty; 181 | 182 | const approvals = Storage.get_approvals(storage); 183 | const [newLedger, newApprovals, operationsEvent, event_balance, event_allowance] = List.fold_left(transfer_from(storage), [ledger, approvals, operations, event_balance_map, event_allowance_map ], transfer); 184 | 185 | let newStorage = Storage.set_approvals(storage, newApprovals); 186 | newStorage = Storage.set_ledger(newStorage, newLedger.data); 187 | 188 | let balance_updates: list = Map.fold( 189 | ([acc, [key, value]]: [list, [event_map_key, event_map_value]]) => { 190 | if (value[1] != 0) { 191 | const data: balance_event_data = 192 | { 193 | owner: key[0], 194 | token_id: key[1], 195 | new_balance: value[0], 196 | diff: value[1] 197 | }; 198 | const event_balance = Tezos.emit("%balance_update", data); 199 | return list([event_balance, ...acc]); 200 | } 201 | return acc; 202 | }, 203 | event_balance, 204 | list([]) 205 | ) 206 | 207 | let allowance_updates: list = Map.fold( 208 | ([acc, [key, value]]: [list, [allowance_event_map_key, event_map_value]]) => { 209 | if (value[1] != 0) { 210 | const data: allowance_update_type = 211 | { 212 | owner: key[0], 213 | spender: key[1], 214 | token_id: key[2], 215 | new_allowance: value[0], 216 | diff: value[1] 217 | }; 218 | const event_allowance_op = Tezos.emit("%allowance_update", data); 219 | return list([event_allowance_op, ...acc]); 220 | } 221 | return acc; 222 | }, 223 | event_allowance, 224 | list([]) 225 | ) 226 | 227 | /** Reverse operations list **/ 228 | 229 | for (const op of operationsEvent) { 230 | finalOperations = list([op, ...finalOperations]); 231 | } 232 | finalOperations = list([...allowance_updates, ...finalOperations]); 233 | finalOperations = list([...balance_updates, ...finalOperations]); 234 | return [finalOperations, newStorage]; 235 | }; -------------------------------------------------------------------------------- /lib/fa2.1/entrypoints/update.jsligo: -------------------------------------------------------------------------------- 1 | #import "../data/errors.jsligo" "Errors" 2 | #import "../data/operators.jsligo" "Operators" 3 | #import "../data/ledger.jsligo" "Ledger" 4 | #import "../data/storage.jsligo" "Storage" 5 | #import "../data/token.jsligo" "Token" 6 | 7 | type Storage = Storage.T; 8 | 9 | type operator = 10 | // @layout comb 11 | { 12 | owner : address; 13 | operator : address; 14 | token_id : nat; 15 | }; 16 | 17 | export type operator_update = 18 | @layout("comb") 19 | { 20 | owner : address; 21 | operator : address; 22 | token_id : Token.T; 23 | is_operator : bool; 24 | }; 25 | 26 | type operator_update_event = 27 | @layout("comb") 28 | { 29 | sender : address; 30 | operator_update: list; 31 | }; 32 | 33 | export type unit_update = 34 | | ["AddOperator", operator] 35 | | ["RemoveOperator", operator]; 36 | 37 | type update_operators = list; 38 | export type T = update_operators; 39 | 40 | const _update_ops = ( 41 | [updates, operators] : [list, Operators.t], 42 | update : unit_update 43 | ) : [list, Operators.t] => { 44 | return match(update) { 45 | when(AddOperator(operator)): do { 46 | const { owner, operator, token_id } = operator; 47 | const newOperators = Operators.add_operator(operators, owner, operator, token_id); 48 | const updatedOp: operator_update = { 49 | owner, 50 | operator, 51 | token_id, 52 | is_operator: true 53 | }; 54 | return [list([updatedOp, ...updates]), newOperators]; 55 | }; 56 | when(RemoveOperator(operator)): do { 57 | const { owner, operator, token_id } = operator; 58 | const newOperators = Operators.remove_operator(operators, owner, operator, token_id); 59 | const updatedOp: operator_update = { 60 | owner, 61 | operator, 62 | token_id, 63 | is_operator: true 64 | }; 65 | 66 | return [list([updatedOp, ...updates]), newOperators]; 67 | }; 68 | }; 69 | }; 70 | 71 | export const update_ops = ( 72 | updates: update_operators, 73 | storage: Storage 74 | ) : [list, Storage] => { 75 | const sender = Tezos.get_sender(); 76 | return match(Storage.get_operators(storage)) { 77 | when(Some(operators)): do { 78 | const [newUpdates, newOperators] = List.fold_left( 79 | ([acc, item]) => _update_ops(acc, item), 80 | [list([]), operators], 81 | updates 82 | ); 83 | 84 | return [ 85 | list([ Tezos.emit("%operator_update", { sender, newUpdates }) ]), 86 | Storage.set_operators(storage, newOperators) 87 | ]; 88 | }; 89 | when(None()): failwith(Errors.storage_has_no_operators); 90 | }; 91 | }; 92 | -------------------------------------------------------------------------------- /lib/fa2.1/fa2.1-NFT.jsligo: -------------------------------------------------------------------------------- 1 | #import "./data/ledger.jsligo" "Ledger" 2 | 3 | export #import "./fa2.1-generic.jsligo" "Generic" 4 | 5 | //// This is an NFT marketplace derived from the generic FA2.1 implementation. 6 | 7 | /// The main customisation happens in the two declarations below. 8 | /// 9 | /// The Ledger.NFT.L type and associated Ledger.NFT.ledger_module 10 | /// are used to keep track of token-specific information. An NFT 11 | /// it records the owner of each token, but, for example, in a simple 12 | /// currency-like financial asset (e.g. fa2.1-single-asset.jsligo), 13 | /// the ledger type and module would keep track of the amount owned by 14 | /// each "shareholder-like" account. 15 | 16 | export type Storage = Generic.Storage; 17 | 18 | const specialised : Generic.Interface = 19 | Generic.make(Ledger.NFT.ledger_module); 20 | 21 | const specialisedViews : Generic.InterfaceViews = 22 | Generic.makeViews(Ledger.NFT.ledger_module); 23 | 24 | /// Following that, the entry points and views are exported from the 25 | /// specialised implementation: 26 | @entry const transfer = (p, s) => specialised.transfer([p, s]); 27 | @entry const balance_of = (p, s) => specialised.balance_of([p, s]); 28 | @entry const update_operators = (p, s) => specialised.update_operators([p, s]); 29 | @entry const approve = (p, s) => specialised.approve([p, s]); 30 | @entry const export_ticket = (p, s) => specialised.export_ticket([p, s]); 31 | @entry const lambda_export = (p, s) => specialised.lambda_export([p, s]); 32 | @entry const import_ticket = (p, s) => specialised.import_ticket([p, s]); 33 | 34 | @view const get_balance = (p, s) => specialisedViews.get_balance([p, s]); 35 | @view const get_total_supply = (p, s) => specialisedViews.get_total_supply([p, s]); 36 | @view const is_operator = (p, s) => specialisedViews.is_operator([p, s]); 37 | @view const get_allowance = (p, s) => specialisedViews.get_allowance([p, s]); 38 | @view const get_token_metadata = (p, s) => specialisedViews.get_token_metadata([p, s]); 39 | @view const is_token = (p, s) => specialisedViews.is_token([p, s]); 40 | 41 | /// You can then add your own entry points like you would do in any contract, by adding e.g. 42 | @view const get_marketplace_name = (_params: unit, _s: Storage) : string => "My NFT Marketplace"; 43 | -------------------------------------------------------------------------------- /lib/fa2.1/fa2.1-generic.jsligo: -------------------------------------------------------------------------------- 1 | /* 2 | * DISCLAIMER 3 | * This implementation of FA2.1 norm is work in progress, this implementation is not yet ready for production. 4 | */ 5 | 6 | export #import "./data/errors.jsligo" "Errors" 7 | export #import "./data/metadata.jsligo" "Metadata" 8 | export #import "./data/token.jsligo" "Token" 9 | export #import "./data/ledger.jsligo" "Ledger" 10 | export #import "./data/operators.jsligo" "Operators" 11 | export #import "./data/approvals.jsligo" "Approvals" 12 | export #import "./data/tokenMetadata.jsligo" "TokenMetadata" 13 | export #import "./data/storage.jsligo" "Storage" 14 | export #import "./entrypoints/transfer.jsligo" "Transfer" 15 | export #import "./entrypoints/balance_of.jsligo" "Balance_of" 16 | export #import "./entrypoints/update.jsligo" "Update" 17 | export #import "./entrypoints/approve.jsligo" "Approve" 18 | export #import "./entrypoints/export_ticket.jsligo" "Export_ticket" 19 | export #import "./entrypoints/lambda_export.jsligo" "Lambda_export" 20 | export #import "./entrypoints/import_ticket.jsligo" "Import_ticket" 21 | export #import "views.jsligo" "Views" 22 | 23 | type LedgerModule = Ledger.ledger_module; 24 | export type Storage = Storage.T; 25 | 26 | // A is the extension for the storage 27 | // L is the ledger type 28 | 29 | export type Interface = { 30 | transfer: (ps : [Transfer.t, Storage]) => [list, Storage], 31 | balance_of: (ps: [Balance_of.T, Storage]) => [list, Storage], 32 | update_operators: (ps : [Update.T, Storage]) => [list, Storage], 33 | approve: (ps : [Approve.T, Storage]) => [list, Storage], 34 | export_ticket: (ps : [Export_ticket.T, Storage]) => [list, Storage], 35 | lambda_export: (ps : [Lambda_export.T, Storage]) => [list, Storage], 36 | import_ticket: (ps : [Import_ticket.T, Storage]) => [list, Storage], 37 | }; 38 | 39 | export type InterfaceViews = { 40 | get_balance: (ps: [Views.get_balance_T, Storage]) => Views.get_balance_R, 41 | get_total_supply: (ps: [Views.get_total_supply_T, Storage]) => Views.get_total_supply_R, 42 | is_operator: (ps: [Views.is_operator_T, Storage]) => Views.is_operator_R, 43 | get_allowance: (ps: [Views.get_allowance_T, Storage]) => Views.get_allowance_R, 44 | get_token_metadata: (ps: [Views.get_token_metadata_T, Storage]) => Views.get_token_metadata_R, 45 | is_token: (ps: [Views.is_token_T, Storage]) => Views.is_token_R, 46 | }; 47 | 48 | export const make = (makeLedger: (l: L) => LedgerModule) : Interface => { 49 | const ledger = (s : Storage) : LedgerModule => makeLedger(s.assets); 50 | 51 | return { 52 | transfer: ([p, s]) => Transfer.transfer(p, s, ledger(s)), 53 | balance_of: ([p, s]) => Balance_of.balance_of(p, s, ledger(s)), 54 | update_operators: ([p, s]) => Update.update_ops(p, s), 55 | approve: ([p, s]) => Approve.approve(p, s), 56 | export_ticket: ([p, s]) => Export_ticket.export_tickets(p, s, ledger(s)), 57 | lambda_export: ([p, s]) => Lambda_export.lambda_export(p, s, ledger(s)), 58 | import_ticket: ([p, s]) => Import_ticket.import_tickets(p, s, ledger(s)), 59 | }; 60 | }; 61 | 62 | export const makeViews = (makeLedger: (l: L) => LedgerModule) : InterfaceViews => { 63 | const ledger = (s : Storage) : LedgerModule => makeLedger(s.assets); 64 | 65 | return { 66 | get_balance: ([p, s]) => Views.get_balance(p, s, ledger(s)), 67 | get_total_supply: ([p, s]) => Views.get_total_supply(p, s, ledger(s)), 68 | is_operator: ([p, s]) => Views.is_operator(p, s, ledger(s)), 69 | get_allowance: ([p, s]) => Views.get_allowance(p, s, ledger(s)), 70 | get_token_metadata: ([p, s]) => Views.get_token_metadata(p, s, ledger(s)), 71 | is_token: ([p, s]) => Views.is_token(p, s, ledger(s)), 72 | }; 73 | }; 74 | -------------------------------------------------------------------------------- /lib/fa2.1/fa2.1-multi-asset.jsligo: -------------------------------------------------------------------------------- 1 | #import "./data/ledger.jsligo" "Ledger" 2 | 3 | #import "./fa2.1-generic.jsligo" "Generic" 4 | 5 | export type Storage = Generic.Storage; 6 | 7 | const specialised : Generic.Interface = 8 | Generic.make(Ledger.Multi_asset.ledger_module); 9 | 10 | const specialisedViews : Generic.InterfaceViews = 11 | Generic.makeViews(Ledger.Multi_asset.ledger_module); 12 | 13 | @entry const transfer = (p, s) => specialised.transfer([p, s]); 14 | @entry const balance_of = (p, s) => specialised.balance_of([p, s]); 15 | @entry const update_operators = (p, s) => specialised.update_operators([p, s]); 16 | @entry const approve = (p, s) => specialised.approve([p, s]); 17 | @entry const export_ticket = (p, s) => specialised.export_ticket([p, s]); 18 | @entry const lambda_export = (p, s) => specialised.lambda_export([p, s]); 19 | @entry const import_ticket = (p, s) => specialised.import_ticket([p, s]); 20 | 21 | @view const get_balance = (p, s) => specialisedViews.get_balance([p, s]); 22 | @view const get_total_supply = (p, s) => specialisedViews.get_total_supply([p, s]); 23 | @view const is_operator = (p, s) => specialisedViews.is_operator([p, s]); 24 | @view const get_allowance = (p, s) => specialisedViews.get_allowance([p, s]); 25 | @view const get_token_metadata = (p, s) => specialisedViews.get_token_metadata([p, s]); 26 | @view const is_token = (p, s) => specialisedViews.is_token([p, s]); 27 | -------------------------------------------------------------------------------- /lib/fa2.1/fa2.1-single-asset.jsligo: -------------------------------------------------------------------------------- 1 | #import "./data/ledger.jsligo" "Ledger" 2 | 3 | export #import "./fa2.1-generic.jsligo" "Generic" 4 | 5 | 6 | export type Storage = Generic.Storage; 7 | 8 | const specialised : Generic.Interface = 9 | Generic.make(Ledger.Single_asset.ledger_module); 10 | 11 | const specialisedViews : Generic.InterfaceViews = 12 | Generic.makeViews(Ledger.Single_asset.ledger_module) 13 | 14 | @entry const transfer = (p, s) => specialised.transfer([p, s]); 15 | @entry const balance_of = (p, s) => specialised.balance_of([p, s]); 16 | @entry const update_operators = (p, s) => specialised.update_operators([p, s]); 17 | @entry const approve = (p, s) => specialised.approve([p, s]); 18 | @entry const export_ticket = (p, s) => specialised.export_ticket([p, s]); 19 | @entry const lambda_export = (p, s) => specialised.lambda_export([p, s]); 20 | @entry const import_ticket = (p, s) => specialised.import_ticket([p, s]); 21 | 22 | @view const get_balance = (p, s) => specialisedViews.get_balance([p, s]); 23 | @view const get_total_supply = (p, s) => specialisedViews.get_total_supply([p, s]); 24 | @view const is_operator = (p, s) => specialisedViews.is_operator([p, s]); 25 | @view const get_allowance = (p, s) => specialisedViews.get_allowance([p, s]); 26 | @view const get_token_metadata = (p, s) => specialisedViews.get_token_metadata([p, s]); 27 | @view const is_token = (p, s) => specialisedViews.is_token([p, s]); 28 | -------------------------------------------------------------------------------- /lib/fa2.1/proxy.jsligo: -------------------------------------------------------------------------------- 1 | 2 | type Fa21Ticket = ticket<[nat, option]>; 3 | type Transfer = { 4 | tickets: list, 5 | action: (tickets: list) => list 6 | }; 7 | 8 | type Storage = unit; 9 | type Parameter = 10 | | ["Default", Transfer]; 11 | 12 | @entry 13 | const main = (p: Parameter, s: Storage) : [list, Storage] => { 14 | return match(p) { 15 | when(Default({ tickets, action })): do { return [action(tickets), s] }; 16 | }; 17 | }; 18 | -------------------------------------------------------------------------------- /lib/fa2.1/tzip12.datatypes.jsligo: -------------------------------------------------------------------------------- 1 | /** 2 | * Token-specific metadata is stored/presented as a Michelson value of type 3 | * (map string bytes). A few of the keys are reserved and predefined by 4 | * TZIP-012: 5 | * 6 | * 7 | * "" (empty-string): should correspond to a TZIP-016 URI which points to a JSON 8 | * representation of the token metadata. 9 | * 10 | * "name": should be a UTF-8 string giving a “display name” to the token. 11 | * 12 | * "symbol": should be a UTF-8 string for the short identifier of the token 13 | * (e.g. XTZ, EUR, …). 14 | * 15 | * "decimals": should be an integer (converted to a UTF-8 string in decimal) 16 | * which defines the position of the decimal point in token balances for display 17 | * purposes. 18 | * 19 | * In the case of a TZIP-016 URI pointing to a JSON blob, the JSON preserves the 20 | * same 3 reserved non-empty fields: 21 | * { "symbol": , "name": , "decimals": , ... } 22 | * Providing a value for "decimals" is required for all token types. "name" and "symbol" are not required but it is highly recommended for most tokens to provide the values either in the map or the JSON found via the TZIP-016 URI. 23 | * Other standards such as TZIP-021 describe token metadata schemas which reserve additional keys for different token types for greater compatibility across indexers, wallets, and tooling. 24 | **/ 25 | 26 | export type tokenMetadataData = { 27 | token_id: nat, 28 | token_info: map 29 | }; 30 | 31 | /** 32 | * Token metadata is intended for off-chain, user-facing contexts (e.g. wallets, 33 | * explorers, marketplaces). An earlier (superseded) specification of TZIP-012 token metadata is 34 | * contained in the Legacy Interface section of the Legacy FA2 document. 35 | * There is one @see tokenMetadataData per token_id 36 | **/ 37 | 38 | export type tokenMetadata = big_map; 39 | 40 | /** 41 | * A partial transaction (ignoring the from field) 42 | **/ 43 | 44 | export type atomic_trans = { to_: address, token_id: nat, amount: nat }; 45 | 46 | /** 47 | * Some transaction from a common owner address but multiple destinations @see atomic_trans 48 | **/ 49 | 50 | export type transfer_from = { from_: address, txs: list }; 51 | 52 | /** 53 | * Transfer entrypoint parameter 54 | * A batch of transaction represented by a list of @see transfer_from 55 | **/ 56 | 57 | export type transfer = list; 58 | 59 | /** 60 | * @see balance_of request 61 | **/ 62 | 63 | export type request = { owner: address, token_id: nat }; 64 | 65 | /** 66 | * A callback object is a response from a @see request with a balance 67 | **/ 68 | 69 | export type callback = { request, balance: nat }; 70 | 71 | /** 72 | * A default Main variant with a list of @see callback 73 | **/ 74 | 75 | export type callback_param = | ["Main", list]; 76 | 77 | /** 78 | * balance_of entrypoint parameter 79 | * Batch a list of balance requests @see request and @see callback_param contract 80 | **/ 81 | 82 | export type balance_of = { 83 | requests: list, 84 | callback: contract 85 | }; 86 | 87 | /** 88 | * Operator update 89 | **/ 90 | 91 | export type operator = { owner: address, operator: address, token_id: nat }; 92 | 93 | /** 94 | * Add or Remove token operators variant with an @see operator update 95 | **/ 96 | 97 | export type unit_update = 98 | ["Add_operator", operator] | ["Remove_operator", operator]; 99 | 100 | /** 101 | * update_operators entrypoint parameter 102 | * A list of @see unit_update for the specified token owners and token IDs 103 | **/ 104 | 105 | export type update_operators = list; 106 | -------------------------------------------------------------------------------- /lib/fa2.1/tzip26.datatypes.jsligo: -------------------------------------------------------------------------------- 1 | #import "tzip12.datatypes.jsligo" "TZIP12Datatypes" 2 | 3 | export type tokenMetadataData = TZIP12Datatypes.tokenMetadataData; 4 | 5 | export type operator = TZIP12Datatypes.operator; 6 | 7 | export type transfer = TZIP12Datatypes.transfer; 8 | 9 | /** 10 | * Approve entrypoint batch item 11 | **/ 12 | 13 | export type approve_allowance = { 14 | owner: address, 15 | spender: address, 16 | token_id: nat, 17 | action: [["Increase", nat], ["Decrease", nat]] 18 | }; 19 | 20 | /** 21 | * Approve entrypoint parameter 22 | * A batch of transactions represented by a list of @see approve_allowance 23 | **/ 24 | 25 | export type approve = list; 26 | 27 | export type ticket_to_export = { 28 | from_: address, 29 | token_id: nat, 30 | amount: nat 31 | }; 32 | 33 | export type export_tickets_to = { 34 | destination: contract]>>]>>, 35 | txs: { 36 | to_: address, 37 | tickets_to_export: list 38 | } 39 | }; 40 | 41 | /** 42 | * Export_ticket entrypoint parameter 43 | * A batch of transactions represented by a list of @see export_tickets_to 44 | **/ 45 | 46 | export type export_tickets = list; 47 | 48 | /** 49 | * Lambda_export entrypoint parameter 50 | * A batch of transactions represented by a list of @see approve_allowance 51 | **/ 52 | 53 | export type lambda_export = { 54 | tickets_to_export: list, 55 | action: ((p: list]>>) => list) 56 | }; 57 | 58 | export type import_ticket = [nat, option]; 59 | 60 | /** 61 | * Import_ticket entrypoint parameter 62 | * A batch of transactions represented by a list of @see import_ticket 63 | **/ 64 | export type import_tickets = { 65 | to_: address, 66 | tickets: list 67 | }; 68 | 69 | // Views: 70 | 71 | /** 72 | * get_allowance view parameter 73 | **/ 74 | export type get_allowance = {owner: address, spender: address, token_id: nat}; -------------------------------------------------------------------------------- /lib/fa2.1/tzip26.interfaces.jsligo: -------------------------------------------------------------------------------- 1 | #import "tzip26.datatypes.jsligo" "TZIP26Datatypes" 2 | 3 | export interface FA2_1 { 4 | // entry points 5 | 6 | /** 7 | The core behavior is the same as for FA2 with additional rules regarding allowances: 8 | 9 | To avoid conflicts between approve and update_operators, the operator's rights take precedence over those of the spender. 10 | If the allowance of the sender falls under zero, the transfer MUST fail with the "FA2.1_INSUFFICIENT_ALLOWANCE" error message. 11 | By default, transfer of zero amount is allowed but beware of the behavior regarding allowance equals to 0 (or not set at all). 12 | 13 | In the case of a self-transfer (%from_ == %to_), make sure to not allow one to transfer more than they own, i.e. do not update %to_'s balance before %from_'s balance or check %from_'s balance is enough to transfer %amount. 14 | Associated events: transfer_event, balance_update, allowance_update 15 | **/ 16 | @entry 17 | const transfer: (batch: TZIP26Datatypes.transfer, storage: storage) => ret; 18 | 19 | /** 20 | This entrypoint allows to increase or decrease the allowance set to %spender regarding a token %owner and a %token_id. The %spender can withdraw multiple times from the %owner's balance up to the set value. 21 | In case of a decrease below 0, no error is thrown, the allowance MUST be removed or set to 0. Beware of the case where anyone can transfer a 0 amount of tokens from anyone else because the default allowance is 0. 22 | Each call of transfer and export_ticket entrypoints decreases the allowance amount by the transferred amount of tokens, unless the transfer is called with %from_ being equal to sender or with %from_ being an operator on behalf of the sender. 23 | The standard does not specify who is permitted to approve on behalf of the token owner. Depending on the business use case, the particular implementation of the FA2.1 contract MAY limit allowance updates to a token owner (owner == SENDER) or it can be limited to an administrator. 24 | Associated events: allowance_update 25 | **/ 26 | @entry 27 | const approve : (batch: TZIP26Datatypes.approve, storage: storage) => ret; 28 | 29 | /** 30 | The ticket id MUST be a pair containing the %token_id and any additional information (for example: a timestamp, ...) as an option bytes. The ticket value MUST be the %amount. 31 | The entrypoint MUST comply with the same policy as the transfer entrypoint. The balance of %from_ MUST be decreased by %amount for each associated %token_id. The balance of %to_ and %destination MUST remain unchanged. The total supply MUST remain unchanged. 32 | In the case of the option %destination being: 33 | 34 | 35 | None, an operation is emitted for each exported ticket to its corresponding address %to_ and this address MUST be typed as a (contract (ticket (pair nat (option bytes))). This allows tz addresses to receive tickets directly. The contract MUST fail with the error mnemonic "FA2.1_INVALID_DESTINATION" if an address %to_ can't be correctly typed. 36 | 37 | (Some contract), only one operation is necessary to transfer the tickets to this contract. The contract parameter (list (pair address (list (ticket (pair nat (option bytes)))))) MUST be constructed using (address %to_) and (list %tickets_to_export). 38 | **/ 39 | @entry 40 | const export_ticket : (batch: TZIP26Datatypes.export_tickets, storage : storage) => ret; 41 | 42 | /** 43 | The ticket id MUST be a pair containing the %token_id and any additional information (for example: a timestamp, ...) as an option bytes. The ticket value MUST be the %amount. 44 | The entrypoint MUST comply with the same policy as the transfer entrypoint. The balance of %from_ MUST be decreased by %amount for each associated %token_id. The total supply MUST remain unchanged. 45 | Users must use the %action lambda to send exported tickets to the desired destination (tz addresses, smart contract entrypoints no matter the arguments, ...). 46 | ⚠ The lambda MUST NOT be executed by the FA2.1 contract itself but transferred to a sandbox contract instead whose sole purpose is to execute the lambda1. This avoids any security breach that could be caused by the operations resulting from the application of the lambda. Finally, the amount of tez transferred to this entrypoint should be forwarded to the sandbox as operations generated by the lambda may use it. 47 | Associated events: balance_update, allowance_update 48 | **/ 49 | @entry 50 | const lambda_export : (batch : TZIP26Datatypes.lambda_export, storage : storage) => ret; 51 | 52 | /** 53 | The nat value of the ticket id of type (pair nat (option bytes)) represents the token_id and the ticket value represents the amount to be credited to the to_ address for the corresponding token_id. The ticket SHOULD be destroyed upon import but some use cases may need to store or send them elsewhere. 54 | If the ticket has not been created by the contract in which it is imported (ticketer != SELF_ADDRESS), the entrypoint SHOULD fail but some use cases may accept to import tickets minted by other contracts. In case of failure, the error mnemonic MUST be "FA2.1_INVALID_TICKET". 55 | The total supply SHOULD remain unchanged but some use cases may accept tickets from other contracts, for example, and therefore require an increase in the total supply. 56 | Associated events: balance_update, total_supply_update 57 | **/ 58 | @entry 59 | const import_ticket: (batch: TZIP26Datatypes.import_tickets, storage : storage) => ret; 60 | 61 | 62 | 63 | // views 64 | 65 | /** 66 | Returns the number of tokens %token_id held for each %owner in the %requests list. 67 | If %owner isn't known by the contract or %token_id doesn't exist, the default returned value SHOULD be 0. 68 | **/ 69 | @view 70 | const get_balance: (p: [address, nat], storage: storage) => nat; 71 | 72 | /** 73 | Returns the total supply of token_id. The total supply accounts for the number of tokens stored by the contract and the ones exported in tickets. i.e., exporting tickets MUST not change the total supply. 74 | If %token_id doesn't exist, the default returned value SHOULD be 0. 75 | **/ 76 | @view 77 | const get_total_supply: (token_id: nat, storage: storage) => nat; 78 | 79 | /** 80 | Returns true if %operator is an operator of %owner for the token %token_id. 81 | If %owner or %operator isn't known by the contract or %token_id doesn't exist, the default returned value SHOULD be False. 82 | **/ 83 | @view 84 | const is_operator: (op: TZIP26Datatypes.operator, storage: storage) => bool; 85 | 86 | /** Returns the number of tokens %token_id allowed by %owner for %spender. 87 | If %owner or %spender isn't known by the contract or %token_id doesn't exist, the default returned value SHOULD be 0. 88 | **/ 89 | @view 90 | const get_allowance: (p: TZIP26Datatypes.get_allowance, storage: storage) => nat /*allowance*/; 91 | 92 | /** Returns the metadata associated to %token_id. 93 | If %token_id doesn't exist, the default returned value SHOULD be an empty map %token_info. 94 | **/ 95 | 96 | @view 97 | const get_token_metadata: (p: nat, storage: storage) => TZIP26Datatypes.tokenMetadataData; 98 | 99 | /** 100 | Returns True if %token_id exists and False otherwise. 101 | **/ 102 | @view 103 | const is_token: (token_id: nat, storage: storage) => bool /* token_exist */; 104 | } 105 | 106 | export interface FA2_1ImplementationDetails { 107 | // events 108 | 109 | /** 110 | MUST be triggered when a transfer occurs by any mechanism (a call to transfer but also custom entrypoints implementing a concept of transfer). 111 | In particular for the transfer entrypoint, each element of the list MUST trigger an event. 112 | The entrypoint export_ticket and import_ticket SHOULD NOT trigger this event. 113 | Associated entrypoints: transfer 114 | **/ 115 | const transfer_event: (from_: address, to_: address, token_id: nat, amount: nat) => operation; 116 | 117 | /** 118 | MUST be triggered when the balance of a token owner is modified by any mechanism. 119 | Only one balance_update event is allowed per pair (%owner, %token_id) per transaction. 120 | int %diff represents the difference between nat %new_balance and the previous value (new_balance = old_balance + diff). 121 | Associated entrypoints: transfer, export_ticket, lambda_export, import_ticket 122 | **/ 123 | const balance_update: (owner: address, token_id: nat, new_balance: nat, diff: int) => operation; 124 | 125 | /** 126 | MUST be triggered when the total supply of a %token_id is modified by any mechanism. 127 | int %diff represents the difference between nat %new_total_supply and the previous value (new_total_supply = old_total_supply + diff). So a positive %diff is a mint and a negative %diff is a burn. 128 | Only one total_supply_update event is allowed per %token_id per transaction. 129 | Associated entrypoints: import_ticket, mint (non standard) 130 | **/ 131 | const total_supply_update: (token_id: nat, new_total_supply: nat, diff: int) => operation; 132 | 133 | /** 134 | MUST be triggered when an operator is updated by any mechanism. 135 | Only one operator_update event is allowed per triplet (%owner, %operator, %token_id) per transaction. 136 | %is_operator is true if %operator has been set as an operator of owner and false otherwise. 137 | Associated entrypoints: update_operators 138 | **/ 139 | const operator_update: (owner: address, operator: address, token_id: nat, is_operator: bool) => operation; 140 | 141 | /** 142 | MUST be triggered when a spender's allowance is updated by any mechanism (including when the allowance is consumed by a call to transfer). 143 | int %diff represents the difference between nat %new_allowance and the previous value (new_allowance = old_allowance + diff). 144 | Only one allowance_update event is allowed per triplet (%owner, %spender, %token_id) per transaction. 145 | Associated entrypoints: approve, transfer, export_ticket, lambda_export 146 | **/ 147 | const allowance_update: (owner: address, spender: address, token_id: nat, new_allowance: nat, diff: int) => operation; 148 | 149 | /** 150 | MUST be triggered when a token metadata is updated by any mechanism, for example mint, burn, reveal, etc. 151 | In case of the token is burned the option %new_metadata (map string bytes) MUST be none. 152 | Only one token_metadata_update event is allowed per %token_id per transaction. 153 | Associated entrypoints: mint (non standard) 154 | **/ 155 | const token_metadata_update: (token_id: nat, new_metadata: map) => operation; 156 | 157 | 158 | 159 | // Errors 160 | 161 | /** A ticket import failed because one of the tickets doesn't belong to the contract (ticketer != SELF_ADDRESS) 162 | **/ 163 | const error_FA2_1_INVALID_TICKET : string; // = "FA2.1_INVALID_TICKET"; 164 | 165 | /** A %destination address or contract can't be correctly typed to receive the exported tickets 166 | **/ 167 | const error_FA2_1_INVALID_DESTINATION : string; // = "FA2.1_INVALID_DESTINATION"; 168 | 169 | /** Ticket export/import isn't supported 170 | **/ 171 | const error_FA2_1_TICKETS_UNSUPPORTED : string; // = "FA2.1_TICKETS_UNSUPPORTED"; 172 | 173 | /** A token spender does not have sufficient allowance to transfer tokens from the owner's account 174 | **/ 175 | const error_FA2_1_INSUFFICIENT_ALLOWANCE : string; // = "FA2.1_INSUFFICIENT_ALLOWANCE"; 176 | 177 | /// Token metadata as an extension of TZIP-21 178 | /** One of the following "fungible", "semi-fungible" or "non-fungible". 179 | **/ 180 | 181 | type tokenMetadata = { 182 | tokenKind: string; 183 | } 184 | } -------------------------------------------------------------------------------- /lib/fa2.1/views.jsligo: -------------------------------------------------------------------------------- 1 | // Import necessary modules 2 | #import "./data/token.jsligo" "Token" 3 | #import "./data/ledger.jsligo" "Ledger" 4 | #import "./data/storage.jsligo" "Storage" 5 | #import "./data/admin.jsligo" "Admin" 6 | 7 | //type ExtendedStorage = { 8 | // admin: Admin.T; 9 | //}; 10 | //type ParametricStorage = Storage.T; 11 | //type Ledger = Ledger.ledger_module; 12 | //export type Storage = ParametricStorage; 13 | 14 | export type Storage = Storage.T; 15 | type LedgerModule = Ledger.ledger_module; 16 | 17 | export type get_total_supply_T = Token.T; 18 | export type get_total_supply_R = nat; 19 | 20 | export const get_total_supply = ( 21 | token_id: get_total_supply_T, 22 | s: Storage, 23 | ledgerModule: LedgerModule 24 | ) : get_total_supply_R => 25 | Option.value(0n, ledgerModule.supply([s.assets, token_id])); 26 | 27 | /*{ 28 | return match(Big_map.find_opt(token_id, s.assets.supply)) { 29 | when(Some(n)): do { return n }; 30 | when(None): do { return 0n }; 31 | }; 32 | };*/ 33 | 34 | export type get_balance_T = {owner: address; token_id: Token.T}; 35 | export type get_balance_R = nat; 36 | 37 | export const get_balance = ( 38 | params: get_balance_T, 39 | s: Storage, 40 | ledgerModule: LedgerModule 41 | ) : get_balance_R => 42 | ledgerModule.balance_of([s.assets, params.owner, params.token_id]); 43 | 44 | export type is_token_T = Token.T; 45 | export type is_token_R = bool; 46 | 47 | export const is_token = ( 48 | token_id: is_token_T, 49 | s: Storage, 50 | ledgerModule: LedgerModule 51 | ) : is_token_R => 52 | match (ledgerModule.supply([s.assets, token_id])) { 53 | when(Some(_n)): true; 54 | when(None): false; 55 | }; 56 | 57 | export type get_token_metadata_T = Token.T; 58 | export type get_token_metadata_R = map; 59 | 60 | export const get_token_metadata = ( 61 | token_id: get_token_metadata_T, 62 | s: Storage, 63 | _ledgerModule: LedgerModule 64 | ) : get_token_metadata_R => { 65 | return match(Big_map.find_opt(token_id, s.token_metadata)) { 66 | when(Some(token)): do { return token.token_info }; 67 | when(None): do { return Map.empty }; 68 | }; 69 | }; 70 | 71 | export type get_allowance_T = {owner: address; spender: address; token_id: nat}; 72 | export type get_allowance_R = nat; 73 | 74 | export const get_allowance = ( 75 | params: get_allowance_T, 76 | s: Storage, 77 | _ledgerModule: LedgerModule 78 | ) : get_allowance_R => { 79 | const key = [params.owner, params.spender, params.token_id]; 80 | return match(Big_map.find_opt(key, s.approvals)) { 81 | when(Some(n)): do { return n }; 82 | when(None): do { return 0n }; 83 | }; 84 | }; 85 | 86 | export type is_operator_T = {owner: address; operator: address; token_id: nat}; 87 | export type is_operator_R = bool; 88 | 89 | export const is_operator = ( 90 | params: is_operator_T, 91 | s: Storage, 92 | _ledgerModule: LedgerModule 93 | ) : is_operator_R => { 94 | return match(s.operators) { 95 | when(Some(operators)): do { 96 | return match(Big_map.find_opt([params.owner, params.token_id], operators)) { 97 | when(Some(operators)): do { return Set.mem(params.operator, operators) }; 98 | when(None): do { return false }; 99 | }; 100 | }; 101 | when(None): do { return false }; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /lib/fa2/asset/extendable_multi_asset.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | export type ledger = big_map<[address, nat], nat>; 7 | 8 | export type operator = address; 9 | 10 | export type operators = big_map<[address, operator], set>; 11 | 12 | export type storage = { 13 | ledger: ledger, 14 | operators: operators, 15 | token_metadata: TZIP12.tokenMetadata, 16 | metadata: TZIP16.metadata, 17 | extension: T 18 | }; 19 | 20 | type ret = [list, storage]; 21 | 22 | const make_storage = (extension) => 23 | ( 24 | { 25 | ledger: Big_map.empty, 26 | operators: Big_map.empty, 27 | token_metadata: Big_map.empty, 28 | metadata: Big_map.empty, 29 | extension: extension 30 | } 31 | ) 32 | 33 | //operators 34 | export const assert_authorisation = ( 35 | [operators, from_, token_id]: [operators, address, nat] 36 | ): unit => { 37 | const sender_ = (Tezos.get_sender()); 38 | if (sender_ != from_) { 39 | const authorized = 40 | match((Big_map.find_opt([from_, sender_], operators))) { 41 | when (Some(a)): 42 | a 43 | when (None()): 44 | Set.empty 45 | }; 46 | if (! (Set.mem(token_id, authorized))) { 47 | return failwith(Errors.not_operator) 48 | } 49 | } 50 | }; 51 | 52 | export const add_operator = ( 53 | [operators, owner, operator, token_id]: [operators, address, operator, nat] 54 | ): operators => { 55 | if (owner == operator) { 56 | return operators 57 | } // assert_authorisation always allow the owner so this case is not relevant 58 | else { 59 | Assertions.assert_update_permission(owner); 60 | let auth_tokens = 61 | match(Big_map.find_opt([owner, operator], operators)) { 62 | when (Some(ts)): 63 | ts 64 | when (None()): 65 | Set.empty 66 | }; 67 | auth_tokens = Set.add(token_id, auth_tokens); 68 | return Big_map.update([owner, operator], Some(auth_tokens), operators) 69 | } 70 | }; 71 | 72 | export const remove_operator = ( 73 | [operators, owner, operator, token_id]: [operators, address, operator, nat] 74 | ): operators => { 75 | if (owner == operator) { 76 | return operators 77 | } // assert_authorisation always allow the owner so this case is not relevant 78 | else { 79 | Assertions.assert_update_permission(owner); 80 | const auth_tokens: option> = 81 | match(Big_map.find_opt([owner, operator], operators)) { 82 | when (Some(toks)): 83 | do { 84 | const ts = Set.remove(token_id, toks); 85 | if (Set.cardinal(ts) == 0n) { 86 | return None() 87 | } else { 88 | return Some(ts) 89 | } 90 | } 91 | when (None()): 92 | None() 93 | }; 94 | return Big_map.update([owner, operator], auth_tokens, operators) 95 | } 96 | } 97 | 98 | // ledger 99 | export const get_for_user = ([ledger, owner, token_id]: [ledger, address, nat]): nat => 100 | match((Big_map.find_opt([owner, token_id], ledger))) { 101 | when (Some(a)): 102 | a 103 | when (None()): 104 | 0 as nat 105 | }; 106 | 107 | const set_for_user = ( 108 | [ledger, owner, token_id, amount_]: [ledger, address, nat, nat] 109 | ): ledger => 110 | Big_map.update([owner, token_id], Some(amount_), ledger); 111 | 112 | export const decrease_token_amount_for_user = ( 113 | [ledger, from_, token_id, amount_]: [ledger, address, nat, nat] 114 | ): ledger => { 115 | let balance_ = get_for_user([ledger, from_, token_id]); 116 | Assert.Error.assert((balance_ >= amount_), Errors.ins_balance); 117 | balance_ = abs(balance_ - amount_); 118 | return set_for_user([ledger, from_, token_id, balance_]) 119 | }; 120 | 121 | export const increase_token_amount_for_user = ( 122 | [ledger, to_, token_id, amount_]: [ledger, address, nat, nat] 123 | ): ledger => { 124 | let balance_ = get_for_user([ledger, to_, token_id]); 125 | balance_ = balance_ + amount_; 126 | return set_for_user([ledger, to_, token_id, balance_]) 127 | } 128 | 129 | // storage 130 | export const set_ledger = ([s, ledger]: [storage, ledger]): storage => 131 | ({ ...s, ledger: ledger }); 132 | 133 | export const get_operators = (s: storage): operators => s.operators; 134 | 135 | export const set_operators = ([s, operators]: [storage, operators]): storage< 136 | T 137 | > => 138 | ({ ...s, operators: operators }) 139 | 140 | export const transfer = (t: TZIP12.transfer, s: storage): ret => { 141 | const process_atomic_transfer = (from_: address) => 142 | ([l, t]: [ledger, TZIP12.atomic_trans]): ledger => { 143 | const { to_, token_id, amount } = t; 144 | Assertions.assert_token_exist(s.token_metadata, token_id); 145 | assert_authorisation([s.operators, from_, token_id]); 146 | let ledger = 147 | decrease_token_amount_for_user([l, from_, token_id, amount]); 148 | ledger 149 | = increase_token_amount_for_user([ledger, to_, token_id, amount]); 150 | return ledger 151 | }; 152 | const process_single_transfer = ([l, t]: [ledger, TZIP12.transfer_from]): ledger => { 153 | const { from_, txs } = t; 154 | const ledger = List.fold_left(process_atomic_transfer(from_), l, txs); 155 | return ledger 156 | }; 157 | const ledger = List.fold_left(process_single_transfer, s.ledger, t); 158 | return [list([]), set_ledger([s, ledger])] 159 | }; 160 | 161 | export const balance_of = (b: TZIP12.balance_of, s: storage): ret => { 162 | const { requests, callback } = b; 163 | const get_balance_info = (request: TZIP12.request): TZIP12.callback => { 164 | const { owner, token_id } = request; 165 | Assertions.assert_token_exist(s.token_metadata, token_id); 166 | const balance_ = get_for_user([s.ledger, owner, token_id]); 167 | return ({ request: request, balance: balance_ }) 168 | }; 169 | const callback_param = List.map(get_balance_info, requests); 170 | const operation = Tezos.Next.Operation.transaction(Main(callback_param), 0mutez, callback); 171 | return [list([operation]), s] 172 | }; 173 | 174 | export const update_operators = ( 175 | updates: TZIP12.update_operators, 176 | s: storage 177 | ): ret => { 178 | const update_operator = ( 179 | [operators, update]: [operators, TZIP12.unit_update] 180 | ): operators => 181 | match(update) { 182 | when (Add_operator(operator)): 183 | add_operator( 184 | [operators, operator.owner, operator.operator, operator.token_id] 185 | ) 186 | when (Remove_operator(operator)): 187 | remove_operator( 188 | [operators, operator.owner, operator.operator, operator.token_id] 189 | ) 190 | }; 191 | let operators = get_operators(s); 192 | operators = List.fold_left(update_operator, operators, updates); 193 | const store = set_operators([s, operators]); 194 | return [list([]), store] 195 | }; 196 | 197 | export const get_balance = (p: [address, nat], s: storage): nat => { 198 | const [owner, token_id] = p; 199 | Assertions.assert_token_exist(s.token_metadata, token_id); 200 | return match(Big_map.find_opt([owner, token_id], s.ledger)) { 201 | when (None()): 202 | 0n 203 | when (Some(n)): 204 | n 205 | } 206 | }; 207 | 208 | export const total_supply = (_token_id: nat, _s: storage): nat => 209 | failwith(Errors.not_available); 210 | 211 | export const all_tokens = (_: unit, _s: storage): set => 212 | failwith(Errors.not_available); 213 | 214 | export const is_operator = (op: TZIP12.operator, s: storage): bool => { 215 | const authorized = 216 | match(Big_map.find_opt([op.owner, op.operator], s.operators)) { 217 | when (Some(opSet)): 218 | opSet 219 | when (None()): 220 | Set.empty 221 | }; 222 | return (Set.size(authorized) > 0n || op.owner == op.operator) 223 | }; 224 | 225 | export const token_metadata = (p: nat, s: storage): TZIP12. 226 | tokenMetadataData => { 227 | return match(Big_map.find_opt(p, s.token_metadata)) { 228 | when (Some(data)): 229 | data 230 | when (None()): 231 | failwith(Errors.undefined_token) 232 | } 233 | }; 234 | -------------------------------------------------------------------------------- /lib/fa2/asset/extendable_multi_asset.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | type ledger = ((address * nat), nat) big_map 7 | 8 | type operator = address 9 | 10 | type operators = ((address * operator), nat set) big_map 11 | 12 | type 'a storage = 13 | { 14 | ledger : ledger; 15 | operators : operators; 16 | token_metadata : TZIP12.tokenMetadata; 17 | metadata : TZIP16.metadata; 18 | extension : 'a 19 | } 20 | 21 | type 'a ret = operation list * 'a storage 22 | 23 | let make_storage (type a) (extension : a) : a storage = 24 | { 25 | ledger = Big_map.empty; 26 | operators = Big_map.empty; 27 | token_metadata = Big_map.empty; 28 | metadata = Big_map.empty; 29 | extension = extension 30 | } 31 | 32 | // Operators 33 | let assert_authorisation 34 | (operators : operators) 35 | (from_ : address) 36 | (token_id : nat) 37 | : unit = 38 | let sender_ = (Tezos.get_sender ()) in 39 | if (sender_ = from_) 40 | then () 41 | else 42 | let authorized = 43 | match Big_map.find_opt (from_, sender_) operators with 44 | Some (a) -> a 45 | | None -> Set.empty in 46 | if Set.mem token_id authorized then () else failwith Errors.not_operator 47 | 48 | let add_operator 49 | (operators : operators) 50 | (owner : address) 51 | (operator : operator) 52 | (token_id : nat) 53 | : operators = 54 | if owner = operator 55 | then operators 56 | (* assert_authorisation always allow the owner so this case is not relevant *) 57 | else 58 | let () = Assertions.assert_update_permission owner in 59 | let auth_tokens = 60 | match Big_map.find_opt (owner, operator) operators with 61 | Some (ts) -> ts 62 | | None -> Set.empty in 63 | let auth_tokens = Set.add token_id auth_tokens in 64 | Big_map.update (owner, operator) (Some auth_tokens) operators 65 | 66 | let remove_operator 67 | (operators : operators) 68 | (owner : address) 69 | (operator : operator) 70 | (token_id : nat) 71 | : operators = 72 | if owner = operator 73 | then operators 74 | (* assert_authorisation always allow the owner so this case is not relevant *) 75 | else 76 | let () = Assertions.assert_update_permission owner in 77 | let auth_tokens = 78 | match Big_map.find_opt (owner, operator) operators with 79 | None -> None 80 | | Some (ts) -> 81 | let ts = Set.remove token_id ts in 82 | if (Set.size ts = 0n) then None else Some (ts) in 83 | Big_map.update (owner, operator) auth_tokens operators 84 | 85 | // Ledger 86 | let get_for_user (ledger : ledger) (owner : address) (token_id : nat) : nat = 87 | match Big_map.find_opt (owner, token_id) ledger with 88 | Some (a) -> a 89 | | None -> 0n 90 | 91 | let set_for_user 92 | (ledger : ledger) 93 | (owner : address) 94 | (token_id : nat) 95 | (amount_ : nat) 96 | : ledger = Big_map.update (owner, token_id) (Some amount_) ledger 97 | 98 | let decrease_token_amount_for_user 99 | (ledger : ledger) 100 | (from_ : address) 101 | (token_id : nat) 102 | (amount_ : nat) 103 | : ledger = 104 | let balance_ = get_for_user ledger from_ token_id in 105 | let () = Assert.Error.assert (balance_ >= amount_) Errors.ins_balance in 106 | let balance_ = abs (balance_ - amount_) in 107 | let ledger = set_for_user ledger from_ token_id balance_ in 108 | ledger 109 | 110 | let increase_token_amount_for_user 111 | (ledger : ledger) 112 | (to_ : address) 113 | (token_id : nat) 114 | (amount_ : nat) 115 | : ledger = 116 | let balance_ = get_for_user ledger to_ token_id in 117 | let balance_ = balance_ + amount_ in 118 | let ledger = set_for_user ledger to_ token_id balance_ in 119 | ledger 120 | 121 | // Storage 122 | let assert_token_exist (type a) (s : a storage) (token_id : nat) : unit = 123 | let _ = 124 | Option.value_with_error 125 | Errors.undefined_token 126 | (Big_map.find_opt token_id s.token_metadata) 127 | in 128 | () 129 | 130 | let set_ledger (type a) (s : a storage) (ledger : ledger) = 131 | {s with ledger = ledger} 132 | 133 | let get_operators (type a) (s : a storage) = s.operators 134 | 135 | let set_operators (type a) (s : a storage) (operators : operators) = 136 | {s with operators = operators} 137 | 138 | let transfer (type a) (t : TZIP12.transfer) (s : a storage) : a ret = 139 | (* This function process the "txs" list. Since all transfer share the same "from_" address, we use a se *) 140 | let process_atomic_transfer 141 | (from_ : address) 142 | (ledger, t : ledger * TZIP12.atomic_trans) = 143 | let { 144 | to_; 145 | token_id; 146 | amount = amount_ 147 | } = t in 148 | let () = assert_token_exist s token_id in 149 | let () = assert_authorisation s.operators from_ token_id in 150 | let ledger = decrease_token_amount_for_user ledger from_ token_id amount_ in 151 | let ledger = increase_token_amount_for_user ledger to_ token_id amount_ in 152 | ledger in 153 | let process_single_transfer (ledger, t : ledger * TZIP12.transfer_from) = 154 | let { 155 | from_; 156 | txs 157 | } = t in 158 | let ledger = List.fold_left (process_atomic_transfer from_) ledger txs in 159 | ledger in 160 | let ledger = List.fold_left process_single_transfer s.ledger t in 161 | ([] : operation list), set_ledger s ledger 162 | 163 | let balance_of (type a) (b : TZIP12.balance_of) (s : a storage) : a ret = 164 | let { 165 | requests; 166 | callback 167 | } = b in 168 | let get_balance_info (request : TZIP12.request) : TZIP12.callback = 169 | let { 170 | owner; 171 | token_id 172 | } = request in 173 | let () = assert_token_exist s token_id in 174 | let balance_ = get_for_user s.ledger owner token_id in 175 | { 176 | request = request; 177 | balance = balance_ 178 | } in 179 | let callback_param = List.map get_balance_info requests in 180 | let operation = Tezos.Next.Operation.transaction (Main callback_param) 0mutez callback in 181 | ([operation] : operation list), s 182 | 183 | let update_operators (type a) 184 | (updates : TZIP12.update_operators) 185 | (s : a storage) 186 | : a ret = 187 | let update_operator (operators, update : operators * TZIP12.unit_update) = 188 | match update with 189 | Add_operator 190 | { 191 | owner = owner; 192 | operator = operator; 193 | token_id = token_id 194 | } -> add_operator operators owner operator token_id 195 | | Remove_operator 196 | { 197 | owner = owner; 198 | operator = operator; 199 | token_id = token_id 200 | } -> remove_operator operators owner operator token_id in 201 | let operators = get_operators s in 202 | let operators = List.fold_left update_operator operators updates in 203 | let s = set_operators s operators in 204 | ([] : operation list), s 205 | 206 | let get_balance (type a) (p : (address * nat)) (s : a storage) : nat = 207 | let (owner, token_id) = p in 208 | let () = assert_token_exist s token_id in 209 | match Big_map.find_opt (owner, token_id) s.ledger with 210 | None -> 0n 211 | | Some (n) -> n 212 | 213 | let total_supply (type a) (_token_id : nat) (_s : a storage) : nat = 214 | failwith Errors.not_available 215 | 216 | let all_tokens (type a) (_ : unit) (_s : a storage) : nat set = 217 | failwith Errors.not_available 218 | 219 | let is_operator (type a) (op : TZIP12.operator) (s : a storage) : bool = 220 | let authorized = 221 | match Big_map.find_opt (op.owner, op.operator) s.operators with 222 | Some (opSet) -> opSet 223 | | None -> Set.empty in 224 | Set.size authorized > 0n || op.owner = op.operator 225 | 226 | let token_metadata (type a) (p : nat) (s : a storage) : TZIP12.tokenMetadataData = 227 | match Big_map.find_opt p s.token_metadata with 228 | Some (data) -> data 229 | | None () -> failwith Errors.undefined_token 230 | -------------------------------------------------------------------------------- /lib/fa2/asset/extendable_single_asset.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | export type ledger = big_map; 7 | 8 | export type operator = address; 9 | 10 | export type operators = big_map>; 11 | 12 | export type storage = { 13 | ledger: ledger, 14 | operators: operators, 15 | token_metadata: TZIP12.tokenMetadata, 16 | metadata: TZIP16.metadata, 17 | extension: T 18 | }; 19 | 20 | type ret = [list, storage]; 21 | 22 | const make_storage = (extension) => 23 | ( 24 | { 25 | ledger: Big_map.empty, 26 | operators: Big_map.empty, 27 | token_metadata: Big_map.empty, 28 | metadata: Big_map.empty, 29 | extension: extension 30 | } 31 | ) 32 | 33 | // operators 34 | export const assert_authorisation = (operators: operators, from_: address): unit => { 35 | const sender_ = Tezos.get_sender(); 36 | if (sender_ == from_) return unit else { 37 | const authorized = 38 | match(Big_map.find_opt(from_, operators)) { 39 | when (Some(a)): 40 | a 41 | when (None()): 42 | Set.empty 43 | }; 44 | if (Set.mem(sender_, authorized)) return unit else failwith( 45 | Errors.not_operator 46 | ) 47 | } 48 | }; 49 | 50 | export const add_operator = ( 51 | operators: operators, 52 | owner: address, 53 | operator: operator 54 | ): operators => { 55 | if (owner == operator) return operators else { 56 | const _ = Assertions.assert_update_permission(owner); 57 | let auths = 58 | match(Big_map.find_opt(owner, operators)) { 59 | when (Some(os)): 60 | os 61 | when (None()): 62 | Set.empty 63 | }; 64 | auths = Set.add(operator, auths); 65 | return Big_map.update(owner, Some(auths), operators) 66 | } 67 | }; 68 | 69 | export const remove_operator = ( 70 | operators: operators, 71 | owner: address, 72 | operator: operator 73 | ): operators => { 74 | if (owner == operator) return operators else { 75 | const _ = Assertions.assert_update_permission(owner); 76 | const auths = 77 | match(Big_map.find_opt(owner, operators)) { 78 | when (None()): 79 | None() 80 | when (Some(ops)): 81 | do { 82 | let os = Set.remove(operator, ops); 83 | if (Set.size(os) == 0n) return None() else return Some(os) 84 | } 85 | }; 86 | return Big_map.update(owner, auths, operators) 87 | } 88 | } 89 | 90 | // ledger 91 | export const get_for_user = (ledger: ledger, owner: address): nat => 92 | match(Big_map.find_opt(owner, ledger)) { 93 | when (Some(tokens)): 94 | tokens 95 | when (None()): 96 | 0 as nat 97 | }; 98 | 99 | const update_for_user = (ledger: ledger, owner: address, amount_: nat): ledger => 100 | Big_map.update(owner, Some(amount_), ledger); 101 | 102 | export const decrease_token_amount_for_user = ( 103 | ledger: ledger, 104 | from_: address, 105 | amount_: nat 106 | ): ledger => { 107 | let tokens = get_for_user(ledger, from_); 108 | const _ = Assert.Error.assert(tokens >= amount_, Errors.ins_balance); 109 | tokens = abs(tokens - amount_); 110 | return update_for_user(ledger, from_, tokens) 111 | }; 112 | 113 | export const increase_token_amount_for_user = ( 114 | ledger: ledger, 115 | to_: address, 116 | amount_: nat 117 | ): ledger => { 118 | let tokens = get_for_user(ledger, to_); 119 | tokens = tokens + amount_; 120 | return update_for_user(ledger, to_, tokens) 121 | } 122 | 123 | // Storage 124 | export const get_amount_for_owner = (s: storage, owner: address) => 125 | get_for_user(s.ledger, owner); 126 | 127 | export const set_ledger = ([s, ledger]: [storage, ledger]): storage => 128 | ({ ...s, ledger: ledger }); 129 | 130 | export const get_operators = (s: storage): operators => s.operators; 131 | 132 | export const set_operators = ([s, operators]: [storage, operators]): storage< 133 | T 134 | > => 135 | ({ ...s, operators: operators }) 136 | 137 | export const transfer = (t: TZIP12.transfer, s: storage): ret => { 138 | /* This function process the "txs" list. Since all transfer share the same "from_" address, we use a se */ 139 | const process_atomic_transfer = (from_: address) => 140 | ([ledger, t]: [ledger, TZIP12.atomic_trans]): ledger => { 141 | const { to_, token_id, amount } = t; 142 | ignore(token_id); // Review: should be assert(token_id == 0); 143 | const _ = assert_authorisation(s.operators, from_); 144 | let l = decrease_token_amount_for_user(ledger, from_, amount); 145 | return increase_token_amount_for_user(l, to_, amount) 146 | }; 147 | const process_single_transfer = ([ledger, t]: [ledger, TZIP12.transfer_from]): ledger => { 148 | const { from_, txs } = t; 149 | return List.fold_left(process_atomic_transfer(from_), ledger, txs) 150 | }; 151 | const ledger = List.fold_left(process_single_transfer, s.ledger, t); 152 | const store = set_ledger([s, ledger]); 153 | return [list([]), store] 154 | }; 155 | 156 | /** balance_of entrypoint 157 | */ 158 | export const balance_of = (b: TZIP12.balance_of, s: storage): ret => { 159 | const { requests, callback } = b; 160 | const get_balance_info = (request: TZIP12.request): TZIP12.callback => { 161 | const { owner, token_id } = request; 162 | ignore(token_id); 163 | const balance_ = get_amount_for_owner(s, owner); 164 | return { request: request, balance: balance_ } 165 | }; 166 | const callback_param = List.map(get_balance_info, requests); 167 | const operation = Tezos.Next.Operation.transaction(Main(callback_param), 0mutez, callback); 168 | return [list([operation]), s] 169 | }; 170 | 171 | /** 172 | Add or Remove token operators for the specified token owners and token IDs. 173 | 174 | 175 | The entrypoint accepts a list of update_operator commands. If two different 176 | commands in the list add and remove an operator for the same token owner and 177 | token ID, the last command in the list MUST take effect. 178 | 179 | 180 | It is possible to update operators for a token owner that does not hold any token 181 | balances yet. 182 | 183 | 184 | Operator relation is not transitive. If C is an operator of B and if B is an 185 | operator of A, C cannot transfer tokens that are owned by A, on behalf of B. 186 | 187 | 188 | */ 189 | export const update_operators = ( 190 | updates: TZIP12.update_operators, 191 | s: storage 192 | ): ret => { 193 | const update_operator = ( 194 | [operators, update]: [operators, TZIP12.unit_update] 195 | ): operators => 196 | match(update) { 197 | when (Add_operator(operator)): 198 | add_operator(operators, operator.owner, operator.operator) 199 | when (Remove_operator(operator)): 200 | remove_operator(operators, operator.owner, operator.operator) 201 | }; 202 | const operators = List.fold_left(update_operator, get_operators(s), updates); 203 | const store = set_operators([s, operators]); 204 | return [list([]), store] 205 | }; 206 | 207 | export const get_balance = (p: [address, nat], s: storage): nat => { 208 | const [owner, token_id] = p; 209 | Assertions.assert_token_exist(s.token_metadata, token_id); 210 | return match(Big_map.find_opt(owner, s.ledger)) { 211 | when (None()): 212 | 0n 213 | when (Some(n)): 214 | n 215 | } 216 | }; 217 | 218 | export const total_supply = (_token_id: nat, _s: storage): nat => 219 | failwith(Errors.not_available); 220 | 221 | export const all_tokens = (_: unit, _s: storage): set => 222 | failwith(Errors.not_available); 223 | 224 | export const is_operator = (op: TZIP12.operator, s: storage): bool => { 225 | const authorized = 226 | match(Big_map.find_opt(op.owner, s.operators)) { 227 | when (Some(opSet)): 228 | opSet 229 | when (None()): 230 | Set.empty 231 | }; 232 | return (Set.mem(op.operator, authorized) || op.owner == op.operator) 233 | }; 234 | 235 | export const token_metadata = (p: nat, s: storage): TZIP12. 236 | tokenMetadataData => { 237 | return match(Big_map.find_opt(p, s.token_metadata)) { 238 | when (Some(data)): 239 | data 240 | when (None()): 241 | failwith(Errors.undefined_token) 242 | } 243 | }; 244 | -------------------------------------------------------------------------------- /lib/fa2/asset/extendable_single_asset.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | type ledger = (address, nat) big_map 7 | 8 | type operator = address 9 | 10 | type operators = (address, operator set) big_map 11 | 12 | type 'a storage = 13 | { 14 | ledger : ledger; 15 | operators : operators; 16 | token_metadata : TZIP12.tokenMetadata; 17 | metadata : TZIP16.metadata; 18 | extension : 'a 19 | } 20 | 21 | type 'a ret = operation list * 'a storage 22 | 23 | let make_storage (type a) (extension : a) : a storage = 24 | { 25 | ledger = Big_map.empty; 26 | operators = Big_map.empty; 27 | token_metadata = Big_map.empty; 28 | metadata = Big_map.empty; 29 | extension = extension 30 | } 31 | 32 | // Operators 33 | let assert_authorisation (operators : operators) (from_ : address) : unit = 34 | let sender_ = (Tezos.get_sender ()) in 35 | if (sender_ = from_) 36 | then () 37 | else 38 | let authorized = 39 | match Big_map.find_opt from_ operators with 40 | Some (a) -> a 41 | | None -> Set.empty in 42 | if Set.mem sender_ authorized then () else failwith Errors.not_operator 43 | 44 | let add_operator (operators : operators) (owner : address) (operator : operator) 45 | : operators = 46 | if owner = operator 47 | then operators 48 | (* assert_authorisation always allow the owner so this case is not relevant *) 49 | else 50 | let () = Assertions.assert_update_permission owner in 51 | let auths = 52 | match Big_map.find_opt owner operators with 53 | Some (os) -> os 54 | | None -> Set.empty in 55 | let auths = Set.add operator auths in 56 | Big_map.update owner (Some auths) operators 57 | 58 | let remove_operator 59 | (operators : operators) 60 | (owner : address) 61 | (operator : operator) 62 | : operators = 63 | if owner = operator 64 | then operators 65 | (* assert_authorisation always allow the owner so this case is not relevant *) 66 | else 67 | let () = Assertions.assert_update_permission owner in 68 | let auths = 69 | match Big_map.find_opt owner operators with 70 | None -> None 71 | | Some (os) -> 72 | let os = Set.remove operator os in 73 | if (Set.size os = 0n) then None else Some (os) in 74 | Big_map.update owner auths operators 75 | 76 | // Ledger 77 | let get_for_user (ledger : ledger) (owner : address) : nat = 78 | match Big_map.find_opt owner ledger with 79 | Some (tokens) -> tokens 80 | | None -> 0n 81 | 82 | let update_for_user (ledger : ledger) (owner : address) (amount_ : nat) : ledger = 83 | Big_map.update owner (Some amount_) ledger 84 | 85 | let decrease_token_amount_for_user 86 | (ledger : ledger) 87 | (from_ : address) 88 | (amount_ : nat) 89 | : ledger = 90 | let tokens = get_for_user ledger from_ in 91 | let () = Assert.Error.assert (tokens >= amount_) Errors.ins_balance in 92 | let tokens = abs (tokens - amount_) in 93 | let ledger = update_for_user ledger from_ tokens in 94 | ledger 95 | 96 | let increase_token_amount_for_user 97 | (ledger : ledger) 98 | (to_ : address) 99 | (amount_ : nat) 100 | : ledger = 101 | let tokens = get_for_user ledger to_ in 102 | let tokens = tokens + amount_ in 103 | let ledger = update_for_user ledger to_ tokens in 104 | ledger 105 | 106 | // Storage 107 | let get_amount_for_owner (type a) (s : a storage) (owner : address) = 108 | get_for_user s.ledger owner 109 | 110 | let set_ledger (type a) (s : a storage) (ledger : ledger) = 111 | {s with ledger = ledger} 112 | 113 | let get_operators (type a) (s : a storage) = s.operators 114 | 115 | let set_operators (type a) (s : a storage) (operators : operators) = 116 | {s with operators = operators} 117 | 118 | let transfer (type a) (t : TZIP12.transfer) (s : a storage) : a ret = 119 | (* This function process the "txs" list. Since all transfer share the same "from_" address, we use a se *) 120 | let process_atomic_transfer 121 | (from_ : address) 122 | (ledger, t : ledger * TZIP12.atomic_trans) = 123 | let { 124 | to_; 125 | token_id = _token_id; 126 | amount = amount_ 127 | } = t in 128 | let () = assert_authorisation s.operators from_ in 129 | let ledger = decrease_token_amount_for_user ledger from_ amount_ in 130 | let ledger = increase_token_amount_for_user ledger to_ amount_ in 131 | ledger in 132 | let process_single_transfer (ledger, t : ledger * TZIP12.transfer_from) = 133 | let { 134 | from_; 135 | txs 136 | } = t in 137 | let ledger = List.fold_left (process_atomic_transfer from_) ledger txs in 138 | ledger in 139 | let ledger = List.fold_left process_single_transfer s.ledger t in 140 | let s = set_ledger s ledger in 141 | ([] : operation list), s 142 | 143 | let balance_of (type a) (b : TZIP12.balance_of) (s : a storage) : a ret = 144 | let { 145 | requests; 146 | callback 147 | } = b in 148 | let get_balance_info (request : TZIP12.request) : TZIP12.callback = 149 | let { 150 | owner; 151 | token_id = _token_id 152 | } = request in 153 | let balance_ = get_amount_for_owner s owner in 154 | { 155 | request = request; 156 | balance = balance_ 157 | } in 158 | let callback_param = List.map get_balance_info requests in 159 | let operation = Tezos.Next.Operation.transaction (Main callback_param) 0mutez callback in 160 | ([operation] : operation list), s 161 | 162 | (** 163 | Add or Remove token operators for the specified token owners and token IDs. 164 | 165 | 166 | The entrypoint accepts a list of update_operator commands. If two different 167 | commands in the list add and remove an operator for the same token owner and 168 | token ID, the last command in the list MUST take effect. 169 | 170 | 171 | It is possible to update operators for a token owner that does not hold any token 172 | balances yet. 173 | 174 | 175 | Operator relation is not transitive. If C is an operator of B and if B is an 176 | operator of A, C cannot transfer tokens that are owned by A, on behalf of B. 177 | 178 | 179 | *) 180 | let update_operators (type a) 181 | (updates : TZIP12.update_operators) 182 | (s : a storage) 183 | : a ret = 184 | let update_operator (operators, update : operators * TZIP12.unit_update) = 185 | match update with 186 | Add_operator 187 | { 188 | owner = owner; 189 | operator = operator; 190 | token_id = _token_id 191 | } -> add_operator operators owner operator 192 | | Remove_operator 193 | { 194 | owner = owner; 195 | operator = operator; 196 | token_id = _token_id 197 | } -> remove_operator operators owner operator in 198 | let operators = get_operators s in 199 | let operators = List.fold_left update_operator operators updates in 200 | let s = set_operators s operators in 201 | ([] : operation list), s 202 | 203 | let get_balance (type a) (p : (address * nat)) (s : a storage) : nat = 204 | let (owner, token_id) = p in 205 | let () = Assertions.assert_token_exist s.token_metadata token_id in 206 | match Big_map.find_opt owner s.ledger with 207 | None -> 0n 208 | | Some (n) -> n 209 | 210 | let total_supply (type a) (_token_id : nat) (_s : a storage) : nat = 211 | failwith Errors.not_available 212 | 213 | let all_tokens (type a) (_ : unit) (_s : a storage) : nat set = 214 | failwith Errors.not_available 215 | 216 | let is_operator (type a) (op : TZIP12.operator) (s : a storage) : bool = 217 | let authorized = 218 | match Big_map.find_opt (op.owner) s.operators with 219 | Some (opSet) -> opSet 220 | | None -> Set.empty in 221 | Set.mem op.operator authorized || op.owner = op.operator 222 | 223 | let token_metadata (type a) (p : nat) (s : a storage) : TZIP12.tokenMetadataData = 224 | match Big_map.find_opt p s.token_metadata with 225 | Some (data) -> data 226 | | None () -> failwith Errors.undefined_token 227 | -------------------------------------------------------------------------------- /lib/fa2/asset/multi_asset.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | #import "./extendable_multi_asset.impl.jsligo" "MultiAssetExtendable" 7 | 8 | export type ledger = MultiAssetExtendable.ledger; 9 | 10 | export type operator = MultiAssetExtendable.operator; 11 | 12 | export type operators = MultiAssetExtendable.operators; 13 | 14 | export type storage = { 15 | ledger: ledger, 16 | operators: operators, 17 | token_metadata: TZIP12.tokenMetadata, 18 | metadata: TZIP16.metadata, 19 | } 20 | 21 | type ret = [list, storage]; 22 | 23 | const empty_storage: storage = { 24 | ledger: Big_map.empty, 25 | operators: Big_map.empty, 26 | token_metadata: Big_map.empty, 27 | metadata: Big_map.empty 28 | } 29 | 30 | @inline 31 | const lift = (s: storage): MultiAssetExtendable.storage => { 32 | return { 33 | extension: unit, 34 | ledger: s.ledger, 35 | operators: s.operators, 36 | token_metadata: s.token_metadata, 37 | metadata: s.metadata 38 | }; 39 | } 40 | 41 | @inline 42 | const unlift = ([ops, s]: [list, MultiAssetExtendable.storage]): ret => { 43 | let storage = { 44 | ledger: s.ledger, 45 | operators: s.operators, 46 | token_metadata: s.token_metadata, 47 | metadata: s.metadata 48 | }; 49 | return [ops, storage]; 50 | } 51 | 52 | @entry 53 | const transfer = (t: TZIP12.transfer, s: storage): ret => 54 | unlift(MultiAssetExtendable.transfer(t, lift(s))) 55 | 56 | @entry 57 | const balance_of = (b: TZIP12.balance_of, s: storage): ret => 58 | unlift(MultiAssetExtendable.balance_of(b, lift(s))) 59 | 60 | @entry 61 | const update_operators = (updates: TZIP12.update_operators, s: storage): ret => 62 | unlift(MultiAssetExtendable.update_operators(updates, lift(s))) 63 | 64 | @view 65 | const get_balance = (p: [address, nat], s: storage): nat => 66 | MultiAssetExtendable.get_balance(p, lift(s)) 67 | 68 | @view 69 | const total_supply = (token_id: nat, s: storage): nat => 70 | MultiAssetExtendable.total_supply(token_id, lift(s)) 71 | 72 | @view 73 | const all_tokens = (_: unit, s: storage): set => 74 | MultiAssetExtendable.all_tokens(unit, lift(s)) 75 | 76 | @view 77 | const is_operator = (op: TZIP12.operator, s: storage): bool => 78 | MultiAssetExtendable.is_operator(op, lift(s)) 79 | 80 | @view 81 | const token_metadata = (p: nat, s: storage): TZIP12.tokenMetadataData => 82 | MultiAssetExtendable.token_metadata(p, lift(s)) 83 | -------------------------------------------------------------------------------- /lib/fa2/asset/multi_asset.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip12.interfaces.jsligo" "TZIP12Interface" 5 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 6 | #import "./extendable_multi_asset.impl.mligo" "MultiAssetExtendable" 7 | 8 | type ledger = MultiAssetExtendable.ledger 9 | 10 | type operator = MultiAssetExtendable.operator 11 | 12 | type operators = MultiAssetExtendable.operators 13 | 14 | type storage = 15 | { 16 | ledger : ledger; 17 | operators : operators; 18 | token_metadata : TZIP12.tokenMetadata; 19 | metadata : TZIP16.metadata 20 | } 21 | 22 | type ret = operation list * storage 23 | 24 | let empty_storage : storage = 25 | { 26 | ledger = Big_map.empty; 27 | operators = Big_map.empty; 28 | token_metadata = Big_map.empty; 29 | metadata = Big_map.empty 30 | } 31 | 32 | [@inline] 33 | let lift (s : storage) : unit MultiAssetExtendable.storage = 34 | { 35 | extension = (); 36 | ledger = s.ledger; 37 | operators = s.operators; 38 | token_metadata = s.token_metadata; 39 | metadata = s.metadata 40 | } 41 | 42 | [@inline] 43 | let unlift (ret : operation list * unit MultiAssetExtendable.storage) : ret = 44 | let ops, s = ret in 45 | ops, 46 | { 47 | ledger = s.ledger; 48 | operators = s.operators; 49 | token_metadata = s.token_metadata; 50 | metadata = s.metadata 51 | } 52 | 53 | [@entry] 54 | let transfer (t : TZIP12.transfer) (s : storage) : ret = 55 | unlift (MultiAssetExtendable.transfer t (lift s)) 56 | 57 | [@entry] 58 | let balance_of (b : TZIP12.balance_of) (s : storage) : ret = 59 | unlift (MultiAssetExtendable.balance_of b (lift s)) 60 | 61 | [@entry] 62 | let update_operators (updates : TZIP12.update_operators) (s : storage) : ret = 63 | unlift (MultiAssetExtendable.update_operators updates (lift s)) 64 | 65 | [@view] 66 | let get_balance (p : (address * nat)) (s : storage) : nat = 67 | MultiAssetExtendable.get_balance p (lift s) 68 | 69 | [@view] 70 | let total_supply (token_id : nat) (s : storage) : nat = 71 | MultiAssetExtendable.total_supply token_id (lift s) 72 | 73 | [@view] 74 | let all_tokens (_ : unit) (s : storage) : nat set = 75 | MultiAssetExtendable.all_tokens () (lift s) 76 | 77 | [@view] 78 | let is_operator (op : TZIP12.operator) (s : storage) : bool = 79 | MultiAssetExtendable.is_operator op (lift s) 80 | 81 | [@view] 82 | let token_metadata (p : nat) (s : storage) : TZIP12.tokenMetadataData = 83 | MultiAssetExtendable.token_metadata p (lift s) 84 | -------------------------------------------------------------------------------- /lib/fa2/asset/single_asset.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip12.interfaces.jsligo" "TZIP12Interface" 5 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 6 | 7 | #import "./extendable_single_asset.impl.jsligo" "SingleAssetExtendable" 8 | 9 | export type ledger = SingleAssetExtendable.ledger; 10 | 11 | export type operator = SingleAssetExtendable.operator; 12 | 13 | export type operators = SingleAssetExtendable.operators; 14 | 15 | export type storage = SingleAssetExtendable.storage; 16 | 17 | export type storage = { 18 | ledger: ledger, 19 | operators: operators, 20 | token_metadata: TZIP12.tokenMetadata, 21 | metadata: TZIP16.metadata, 22 | } 23 | 24 | type ret = [list, storage]; 25 | 26 | const empty_storage: storage = { 27 | ledger: Big_map.empty, 28 | operators: Big_map.empty, 29 | token_metadata: Big_map.empty, 30 | metadata: Big_map.empty 31 | } 32 | 33 | @inline 34 | const lift = (s: storage): SingleAssetExtendable.storage => { 35 | return { 36 | extension: unit, 37 | ledger: s.ledger, 38 | operators: s.operators, 39 | token_metadata: s.token_metadata, 40 | metadata: s.metadata 41 | }; 42 | } 43 | 44 | @inline 45 | const unlift = ( 46 | [ops, s]: [list, SingleAssetExtendable.storage] 47 | ): ret => { 48 | let storage = { 49 | ledger: s.ledger, 50 | operators: s.operators, 51 | token_metadata: s.token_metadata, 52 | metadata: s.metadata 53 | }; 54 | return [ops, storage]; 55 | } 56 | 57 | @entry 58 | const transfer = (t: TZIP12.transfer, s: storage): ret => 59 | unlift(SingleAssetExtendable.transfer(t, lift(s))) 60 | 61 | @entry 62 | const balance_of = (b: TZIP12.balance_of, s: storage): ret => 63 | unlift(SingleAssetExtendable.balance_of(b, lift(s))) 64 | 65 | @entry 66 | const update_operators = (updates: TZIP12.update_operators, s: storage): ret => 67 | unlift(SingleAssetExtendable.update_operators(updates, lift(s))) 68 | 69 | @view 70 | const get_balance = (p: [address, nat], s: storage): nat => 71 | SingleAssetExtendable.get_balance(p, lift(s)) 72 | 73 | @view 74 | const total_supply = (token_id: nat, s: storage): nat => 75 | SingleAssetExtendable.total_supply(token_id, lift(s)) 76 | 77 | @view 78 | const all_tokens = (_: unit, s: storage): set => 79 | SingleAssetExtendable.all_tokens(unit, lift(s)) 80 | 81 | @view 82 | const is_operator = (op: TZIP12.operator, s: storage): bool => 83 | SingleAssetExtendable.is_operator(op, lift(s)) 84 | 85 | @view 86 | const token_metadata = (p: nat, s: storage): TZIP12.tokenMetadataData => 87 | SingleAssetExtendable.token_metadata(p, lift(s)) 88 | -------------------------------------------------------------------------------- /lib/fa2/asset/single_asset.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip12.interfaces.jsligo" "TZIP12Interface" 5 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 6 | #import "./extendable_single_asset.impl.mligo" "SingleAssetExtendable" 7 | 8 | type ledger = SingleAssetExtendable.ledger 9 | 10 | type operator = SingleAssetExtendable.operator 11 | 12 | type operators = SingleAssetExtendable.operators 13 | 14 | type storage = unit SingleAssetExtendable.storage 15 | 16 | type storage = 17 | { 18 | ledger : ledger; 19 | operators : operators; 20 | token_metadata : TZIP12.tokenMetadata; 21 | metadata : TZIP16.metadata 22 | } 23 | 24 | type ret = operation list * storage 25 | 26 | let empty_storage : storage = 27 | { 28 | ledger = Big_map.empty; 29 | operators = Big_map.empty; 30 | token_metadata = Big_map.empty; 31 | metadata = Big_map.empty 32 | } 33 | 34 | [@inline] 35 | let lift (s : storage) : unit SingleAssetExtendable.storage = 36 | { 37 | extension = (); 38 | ledger = s.ledger; 39 | operators = s.operators; 40 | token_metadata = s.token_metadata; 41 | metadata = s.metadata 42 | } 43 | 44 | [@inline] 45 | let unlift (ret : operation list * unit SingleAssetExtendable.storage) : ret = 46 | let ops, s = ret in 47 | ops, 48 | { 49 | ledger = s.ledger; 50 | operators = s.operators; 51 | token_metadata = s.token_metadata; 52 | metadata = s.metadata 53 | } 54 | 55 | [@entry] 56 | let transfer (t : TZIP12.transfer) (s : storage) : ret = 57 | unlift (SingleAssetExtendable.transfer t (lift s)) 58 | 59 | [@entry] 60 | let balance_of (b : TZIP12.balance_of) (s : storage) : ret = 61 | unlift (SingleAssetExtendable.balance_of b (lift s)) 62 | 63 | [@entry] 64 | let update_operators (updates : TZIP12.update_operators) (s : storage) : ret = 65 | unlift (SingleAssetExtendable.update_operators updates (lift s)) 66 | 67 | [@view] 68 | let get_balance (p : (address * nat)) (s : storage) : nat = 69 | SingleAssetExtendable.get_balance p (lift s) 70 | 71 | [@view] 72 | let total_supply (token_id : nat) (s : storage) : nat = 73 | SingleAssetExtendable.total_supply token_id (lift s) 74 | 75 | [@view] 76 | let all_tokens (_ : unit) (s : storage) : nat set = 77 | SingleAssetExtendable.all_tokens () (lift s) 78 | 79 | [@view] 80 | let is_operator (op : TZIP12.operator) (s : storage) : bool = 81 | SingleAssetExtendable.is_operator op (lift s) 82 | 83 | [@view] 84 | let token_metadata (p : nat) (s : storage) : TZIP12.tokenMetadataData = 85 | SingleAssetExtendable.token_metadata p (lift s) 86 | -------------------------------------------------------------------------------- /lib/fa2/common/assertions.jsligo: -------------------------------------------------------------------------------- 1 | #import "./errors.mligo" "Errors" 2 | 3 | #import "tzip12.datatypes.jsligo" "TZIP12Datatypes" 4 | 5 | #import "tzip12.interfaces.jsligo" "TZIP12Interface" 6 | 7 | /** 8 | * Check if the Tx sender is the token's owner 9 | * @param owner : the address of the actual owner 10 | */ 11 | 12 | export const assert_update_permission = (owner: address): unit => { 13 | return Assert.Error.assert( 14 | (owner == Tezos.get_sender()), 15 | Errors.only_sender_manage_operators 16 | ) 17 | } 18 | 19 | /** 20 | * Check if the token id is already defined 21 | * @param token_metadata : bigmap of token definitions 22 | * @param token_id : the token id to test 23 | */ 24 | 25 | export const assert_token_exist = ( 26 | token_metadata: TZIP12Datatypes.tokenMetadata, 27 | token_id: nat 28 | ): unit => { 29 | const _ = 30 | Option.value_with_error( 31 | Errors.undefined_token, 32 | Big_map.find_opt(token_id, token_metadata) 33 | ) 34 | }; 35 | -------------------------------------------------------------------------------- /lib/fa2/common/errors.mligo: -------------------------------------------------------------------------------- 1 | type t = string 2 | 3 | [@no_mutation] let undefined_token = "FA2_TOKEN_UNDEFINED" 4 | [@no_mutation] let ins_balance = "FA2_INSUFFICIENT_BALANCE" 5 | [@no_mutation] let no_transfer = "FA2_TX_DENIED" 6 | [@no_mutation] let not_owner = "FA2_NOT_OWNER" 7 | [@no_mutation] let not_operator = "FA2_NOT_OPERATOR" 8 | [@no_mutation] let not_supported = "FA2_OPERATORS_UNSUPPORTED" 9 | [@no_mutation] let rec_hook_fail = "FA2_RECEIVER_HOOK_FAILED" 10 | [@no_mutation] let send_hook_fail = "FA2_SENDER_HOOK_FAILED" 11 | [@no_mutation] let rec_hook_undef = "FA2_RECEIVER_HOOK_UNDEFINED" 12 | [@no_mutation] let send_hook_under = "FA2_SENDER_HOOK_UNDEFINED" 13 | [@no_mutation] let wrong_amount = "WRONG_AMOUNT" 14 | 15 | 16 | [@no_mutation] let only_sender_manage_operators = "The sender can only manage operators for his own token" 17 | [@no_mutation] let not_available = "Feature not available. Maybe use an indexer instead" 18 | -------------------------------------------------------------------------------- /lib/fa2/common/tzip12.datatypes.jsligo: -------------------------------------------------------------------------------- 1 | /** 2 | * Token-specific metadata is stored/presented as a Michelson value of type 3 | * (map string bytes). A few of the keys are reserved and predefined by 4 | * TZIP-012: 5 | * 6 | * 7 | * "" (empty-string): should correspond to a TZIP-016 URI which points to a JSON 8 | * representation of the token metadata. 9 | * 10 | * "name": should be a UTF-8 string giving a “display name” to the token. 11 | * 12 | * "symbol": should be a UTF-8 string for the short identifier of the token 13 | * (e.g. XTZ, EUR, …). 14 | * 15 | * "decimals": should be an integer (converted to a UTF-8 string in decimal) 16 | * which defines the position of the decimal point in token balances for display 17 | * purposes. 18 | * 19 | * In the case of a TZIP-016 URI pointing to a JSON blob, the JSON preserves the 20 | * same 3 reserved non-empty fields: 21 | * { "symbol": , "name": , "decimals": , ... } 22 | * Providing a value for "decimals" is required for all token types. "name" and "symbol" are not required but it is highly recommended for most tokens to provide the values either in the map or the JSON found via the TZIP-016 URI. 23 | * Other standards such as TZIP-021 describe token metadata schemas which reserve additional keys for different token types for greater compatibility across indexers, wallets, and tooling. 24 | **/ 25 | 26 | export type tokenMetadataData = { 27 | token_id: nat, 28 | token_info: map 29 | }; 30 | 31 | /** 32 | * Token metadata is intended for off-chain, user-facing contexts (e.g. wallets, 33 | * explorers, marketplaces). An earlier (superseded) specification of TZIP-012 token metadata is 34 | * contained in the Legacy Interface section of the Legacy FA2 document. 35 | * There is one @see tokenMetadataData per token_id 36 | **/ 37 | 38 | export type tokenMetadata = big_map; 39 | 40 | /** 41 | * A partial transaction (ignoring the from field) 42 | **/ 43 | 44 | export type atomic_trans = { to_: address, token_id: nat, amount: nat }; 45 | 46 | /** 47 | * Some transaction from a common owner address but multiple destinations @see atomic_trans 48 | **/ 49 | 50 | export type transfer_from = { from_: address, txs: list }; 51 | 52 | /** 53 | * Transfer entrypoint parameter 54 | * A batch of transaction represented by a list of @see transfer_from 55 | **/ 56 | 57 | export type transfer = list; 58 | 59 | /** 60 | * @see balance_of request 61 | **/ 62 | 63 | export type request = { owner: address, token_id: nat }; 64 | 65 | /** 66 | * A callback object is a response from a @see request with a balance 67 | **/ 68 | 69 | export type callback = { request, balance: nat }; 70 | 71 | /** 72 | * A default Main variant with a list of @see callback 73 | **/ 74 | 75 | export type callback_param = | ["Main", list]; 76 | 77 | /** 78 | * balance_of entrypoint parameter 79 | * Batch a list of balance requests @see request and @see callback_param contract 80 | **/ 81 | 82 | export type balance_of = { 83 | requests: list, 84 | callback: contract 85 | }; 86 | 87 | /** 88 | * Operator update 89 | **/ 90 | 91 | export type operator = { owner: address, operator: address, token_id: nat }; 92 | 93 | /** 94 | * Add or Remove token operators variant with an @see operator update 95 | **/ 96 | 97 | export type unit_update = 98 | ["Add_operator", operator] | ["Remove_operator", operator]; 99 | 100 | /** 101 | * update_operators entrypoint parameter 102 | * A list of @see unit_update for the specified token owners and token IDs 103 | **/ 104 | 105 | export type update_operators = list; 106 | -------------------------------------------------------------------------------- /lib/fa2/common/tzip12.interfaces.jsligo: -------------------------------------------------------------------------------- 1 | #import "tzip12.datatypes.jsligo" "TZIP12Datatypes" 2 | 3 | #import "tzip16.datatypes.jsligo" "TZIP16Datatypes" 4 | 5 | export interface FA2 { 6 | /** The ledger stores user<->token ownership 7 | * @see storage.ledger 8 | **/ 9 | 10 | type ledger; 11 | /** A group of operators. An operator is a Tezos address that originates token transfer operation on behalf of the owner. 12 | * @see storage.operators 13 | **/ 14 | 15 | type operators; 16 | /* Mandatory storage to be recognized as TZIP12 contract*/ 17 | 18 | type storage = { 19 | extension: unit, 20 | ledger: ledger, 21 | operators: operators, 22 | token_metadata: TZIP12Datatypes.tokenMetadata, 23 | metadata: TZIP16Datatypes.metadata, 24 | }; 25 | type ret = [list, storage]; 26 | /** 27 | * Each transfer in the batch is specified between one source (from_) address and 28 | * a list of destinations. Each transfer_destination specifies token type and the 29 | * amount to be transferred from the source address to the destination (to_) address. 30 | * 31 | * FA2 does NOT specify an interface for mint and burn operations; however, if an 32 | * FA2 token contract implements mint and burn operations, it SHOULD, when possible, 33 | * enforce the same logic (core transfer behavior and transfer permission logic) 34 | * applied to the token transfer operation. Mint and burn can be considered special 35 | * cases of the transfer. Although, it is possible that mint and burn have more or 36 | * less restrictive rules than the regular transfer. For instance, mint and burn 37 | * operations may be invoked by a special privileged administrative address only. 38 | * In this case, regular operator restrictions may not be applicable. 39 | * 40 | * Core Transfer Behavior 41 | * FA2 token contracts MUST always implement this behavior. 42 | * 43 | * 44 | * - Every transfer operation MUST happen atomically and in order. If at least one 45 | * transfer in the batch cannot be completed, the whole transaction MUST fail, all 46 | * token transfers MUST be reverted, and token balances MUST remain unchanged. 47 | * 48 | * 49 | * - Each transfer in the batch MUST decrement token balance of the source (from_) 50 | * address by the amount of the transfer and increment token balance of the destination 51 | * (to_) address by the amount of the transfer. 52 | * 53 | * 54 | * - If the transfer amount exceeds current token balance of the source address, 55 | * the whole transfer operation MUST fail with the error mnemonic "FA2_INSUFFICIENT_BALANCE". 56 | * 57 | * 58 | * - If the token owner does not hold any tokens of type token_id, the owner's balance 59 | * is interpreted as zero. No token owner can have a negative balance. 60 | * 61 | * 62 | * - The transfer MUST update token balances exactly as the operation 63 | * parameters specify it. Transfer operations MUST NOT try to adjust transfer 64 | * amounts or try to add/remove additional transfers like transaction fees. 65 | * 66 | * 67 | * - Transfers of zero amount MUST be treated as normal transfers. 68 | * 69 | * 70 | * - Transfers with the same address (from_ equals to_) MUST be treated as normal 71 | * transfers. 72 | * 73 | * 74 | * -If one of the specified token_ids is not defined within the FA2 contract, the 75 | * entrypoint MUST fail with the error mnemonic "FA2_TOKEN_UNDEFINED". 76 | * 77 | * 78 | * - Transfer implementations MUST apply transfer permission policy logic (either 79 | * default transfer permission policy or 80 | * customized one). 81 | * If permission logic rejects a transfer, the whole operation MUST fail. 82 | * 83 | * 84 | * - Core transfer behavior MAY be extended. If additional constraints on tokens 85 | * transfer are required, FA2 token contract implementation MAY invoke additional 86 | * permission policies. If the additional permission fails, the whole transfer 87 | * operation MUST fail with a custom error mnemonic. 88 | * 89 | * 90 | * 91 | * Default Transfer Permission Policy 92 | * 93 | * 94 | * - Token owner address MUST be able to perform a transfer of its own tokens (e. g. 95 | * SENDER equals to from_ parameter in the transfer). 96 | * 97 | * 98 | * - An operator (a Tezos address that performs token transfer operation on behalf 99 | * of the owner) MUST be permitted to manage the specified owner's tokens before 100 | * it invokes a transfer transaction (see update_operators). 101 | * 102 | * 103 | * - If the address that invokes a transfer operation is neither a token owner nor 104 | * one of the permitted operators, the transaction MUST fail with the error mnemonic 105 | * "FA2_NOT_OPERATOR". If at least one of the transfers in the batch is not permitted, 106 | * the whole transaction MUST fail. 107 | * @param p : @see TZIP12Datatypes.transfer 108 | * @param s : the storage 109 | **/ 110 | 111 | @entry 112 | const transfer: (p: TZIP12Datatypes.transfer, s: storage) => ret; 113 | /** 114 | * Gets the balance of multiple account/token pairs. Accepts a list of 115 | * balance_of_requests and a callback contract callback which accepts a list of 116 | * balance_of_response records. 117 | * 118 | * 119 | * - There may be duplicate balance_of_request's, in which case they should not be 120 | * deduplicated nor reordered. 121 | * 122 | * 123 | * - If the account does not hold any tokens, the account 124 | * balance is interpreted as zero. 125 | * 126 | * 127 | * - If one of the specified token_ids is not defined within the FA2 contract, the 128 | * entrypoint MUST fail with the error mnemonic "FA2_TOKEN_UNDEFINED". 129 | * @param p : @see TZIP12Datatypes.balance_of 130 | * @param s : the storage 131 | **/ 132 | 133 | @entry 134 | const balance_of: (p: TZIP12Datatypes.balance_of, s: storage) => ret; 135 | /** 136 | * Add or Remove token operators for the specified token owners and token IDs. 137 | * 138 | * 139 | * - The entrypoint accepts a list of update_operator commands. If two different 140 | * commands in the list add and remove an operator for the same token owner and 141 | * token ID, the last command in the list MUST take effect. 142 | * 143 | * 144 | * - It is possible to update operators for a token owner that does not hold any token 145 | * balances yet. 146 | * 147 | * 148 | * - Operator relation is not transitive. If C is an operator of B and if B is an 149 | * operator of A, C cannot transfer tokens that are owned by A, on behalf of B. 150 | * 151 | * 152 | * The standard does not specify who is permitted to update operators on behalf of 153 | * the token owner. Depending on the business use case, the particular implementation 154 | * of the FA2 contract MAY limit operator updates to a token owner (owner == SENDER) 155 | * or be limited to an administrator. 156 | * @param p : @see TZIP12Datatypes.update_operators 157 | * @param s : the storage 158 | **/ 159 | 160 | @entry 161 | const update_operators: (p: TZIP12Datatypes.update_operators, s: storage) => ret; 162 | /** OPTIONAL VIEWS **/ 163 | //TODO waiting for next ligo feature : Interface optional fields to uncomment these lines 164 | /** return the balance corresponding to the owner/token pair 165 | **/ 166 | 167 | @view 168 | const get_balance: (p: [address, nat], s: storage) => nat; 169 | /** return to total number of tokens for the given token-id if known or fail if not. 170 | **/ 171 | 172 | @view 173 | const total_supply: (token_id: nat, s: storage) => nat; 174 | /** returns the list of all the token IDs 175 | **/ 176 | 177 | @view 178 | const all_tokens: (_: unit, s: storage) => set; 179 | /** return whether %operator is allowed to transfer %token_id tokens owned by owner 180 | **/ 181 | 182 | @view 183 | const is_operator: (op: TZIP12Datatypes.operator, s: storage) => bool; 184 | /** It is one of the 2 ways of providing token-specific metadata, it 185 | is defined in section Token Metadata and is not optional if 186 | the contract does not have a %token_metadata big-map 187 | **/ 188 | 189 | @view 190 | const token_metadata: (p: nat, s: storage) => TZIP12Datatypes.tokenMetadataData; 191 | }; 192 | -------------------------------------------------------------------------------- /lib/fa2/common/tzip16.datatypes.jsligo: -------------------------------------------------------------------------------- 1 | /** 2 | * This file implement the TZIP-16 protocol (Contract Metadata) for Tezos 3 | * defined here : https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-16/tzip-16.md 4 | * 5 | * An FA2-compliant contract SHOULD provide contract-level metadata via TZIP-016: 6 | * 7 | * - If a contract does not contain the TZIP-016 %metadata big-map, it must provide token-specific-metadata through the %token_metadata big-map method described above in Token Metadata. 8 | * 9 | * The TZIP-016 contract metadata JSON structure is described below: 10 | * 11 | * 12 | * - The TZIP-016 "interfaces" field MUST be present 13 | * 14 | * - It should contain "TZIP-012[-]" 15 | * 16 | * 17 | * - version-info is an optional string extension, precising which version of 18 | * this document is implemented by the contract (commit hash prefix, 19 | * e.g. 6883675 or an RFC-3339 date, 20 | * e.g. 2020-10-23). 21 | * 22 | * TZIP-016 : JSON nomenclature 23 | * { 24 | * "name":"", (* A single string, free format.It is recommended to have “name” strings help identifying the purpose of the contract. *) 25 | * "description":"", (* A single string, free format.Preferably a set of proper natural language paragraphs. *) 26 | * "version":"", (* A single string, free format. It is recommended to have version strings which attempt at uniquely identifying the exact Michelson contract, or at least its behavior, as a precision w.r.t the name field. Of course, none of that can be enforced. *) 27 | * "license":{"name":"", "details":""}, (* An extensible object { "name": , "details" : }, "details" being optional. It is recommended to use de facto standard short names when possible, see the Debian guidelines for instance. *) 28 | * "authors":[], (* A list of strings. Each author should obey the "Print Name <'contact'>", where the 'contact' string is either an email address, or a web URI. *) 29 | * "homepage":"", (* A single string, representing a “web” URL (e.g. HTTPS). The homepage is for human-consumption, it may be the location of the source of the contract, how to submit issue tickets, or just a more elaborate description. *) 30 | * "source":{"tools":[], "location":""}, (* An object { "tools": [], "location": } describing the source code which was transformed or generated the Michelson code of the contract. "tools" is an informal list of compilers/code-generators/libraries/post-processors used to generate the originated code, if possible with version information. The goal is to attempt to provide enough information for interested parties to reproduce the Michelson from its source, or at least to inspect it. *) 31 | * "interfaces":[], (* A list of strings. Each string should allow the consumer of the metadata to know which interfaces and behaviors the contract claims to obey (other than the obvious TZIP-016). In the case of standards defined as TZIPs in the present repository, the string should obey the pattern "TZIP-" where is additional information prefixed with a space character. Example: an FA2 contract would (at least) have an "interfaces" field containing ["TZIP-012"] or ["TZIP-012 git 6544de32"]. *) 32 | * "errors":[], (* A list of “error translation” objects, which allow one to interpret error values output by the contract using the FAILWITH instruction. The interpretation is a larger data-structure, for instance, to provide to a wallet-user with a more understandable error message to act on (usually a string or bytes natural language text value). They are either: *) 33 | * "views":[] (* A list of off-chain-view objects, defined in the following section. *) 34 | * } 35 | **/ 36 | 37 | export type metadata = big_map; 38 | -------------------------------------------------------------------------------- /lib/fa2/nft/extendable_nft.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | export type ledger = big_map; 7 | 8 | export type operator = address; 9 | 10 | export type operators = big_map<[address, operator], set>; //[owner,operator] 11 | 12 | 13 | export type storage = { 14 | extension: T, 15 | ledger: ledger, 16 | operators: operators, 17 | token_metadata: TZIP12.tokenMetadata, 18 | metadata: TZIP16.metadata 19 | }; 20 | 21 | type ret = [list, storage]; 22 | 23 | const make_storage = (extension: T) : storage => 24 | ( 25 | { 26 | ledger: Big_map.empty, 27 | operators: Big_map.empty, 28 | token_metadata: Big_map.empty, 29 | metadata: Big_map.empty, 30 | extension: extension 31 | } 32 | ) 33 | 34 | //** LIGOFA_NFT 35 | export const assert_authorisation = ( 36 | operators: operators, 37 | from_: address, 38 | token_id: nat 39 | ): unit => { 40 | const sender_ = (Tezos.get_sender()); 41 | if (sender_ != from_) { 42 | const authorized = 43 | match((Big_map.find_opt([from_, sender_], operators))) { 44 | when (Some(a)): 45 | a 46 | when (None()): 47 | Set.empty 48 | }; 49 | if (! (Set.mem(token_id, authorized))) { 50 | return failwith(Errors.not_operator) 51 | } 52 | } else { 53 | return unit 54 | } 55 | }; 56 | 57 | export const add_operator = ( 58 | operators: operators, 59 | owner: address, 60 | operator: operator, 61 | token_id: nat 62 | ): operators => { 63 | if (owner == operator) { 64 | return operators 65 | // assert_authorisation always allow the owner so this case is not relevant 66 | } else { 67 | Assertions.assert_update_permission(owner); 68 | let auth_tokens = 69 | match(Big_map.find_opt([owner, operator], operators)) { 70 | when (Some(ts)): 71 | ts 72 | when (None()): 73 | Set.empty 74 | }; 75 | auth_tokens = Set.add(token_id, auth_tokens); 76 | return Big_map.update([owner, operator], Some(auth_tokens), operators) 77 | } 78 | }; 79 | 80 | export const remove_operator = ( 81 | operators: operators, 82 | owner: address, 83 | operator: operator, 84 | token_id: nat 85 | ): operators => { 86 | if (owner == operator) { 87 | return operators 88 | // assert_authorisation always allow the owner so this case is not relevant 89 | } else { 90 | Assertions.assert_update_permission(owner); 91 | const auth_tokens: option> = 92 | match(Big_map.find_opt([owner, operator], operators)) { 93 | when (Some(ts)): 94 | do { 95 | const toks = Set.remove(token_id, ts); 96 | if (Set.cardinal(toks) == 0n) { 97 | return None() 98 | } else { 99 | return Some(toks) 100 | } 101 | } 102 | when (None()): 103 | None() 104 | }; 105 | return Big_map.update([owner, operator], auth_tokens, operators) 106 | } 107 | } 108 | 109 | // ledger 110 | export const is_owner_of = (ledger: ledger, token_id: nat, owner: address): bool => { 111 | const current_owner = Option.value_with_error ("option is None", Big_map.find_opt(token_id, ledger)); 112 | return (current_owner == owner) 113 | }; 114 | 115 | export const assert_owner_of = (ledger: ledger, token_id: nat, owner: address): unit => 116 | Assert.Error.assert(is_owner_of(ledger, token_id, owner), Errors.ins_balance); 117 | 118 | 119 | export const transfer_token_from_user_to_user = ( 120 | ledger: ledger, 121 | token_id: nat, 122 | from_: address, 123 | to_: address 124 | ): ledger => { 125 | assert_owner_of(ledger, token_id, from_); 126 | return Big_map.update(token_id, Some(to_), ledger) 127 | } 128 | 129 | export const set_ledger = ([s, ledger]: [storage, ledger]): storage => 130 | ({ ...s, ledger: ledger }); 131 | 132 | export const get_operators = (s: storage): operators => s.operators; 133 | 134 | export const set_operators = ([s, operators]: [storage, operators]): storage< 135 | T 136 | > => 137 | ({ ...s, operators: operators }) 138 | 139 | //** TZIP12Interface.FA2 140 | // operators 141 | /** 142 | * Check if the intented transfer is sent from the same sender as from field, otherwise check if the sender is part of the operator authorized to receive this token 143 | * @param operators : operator bigmap 144 | * @param from_ : transfer from address 145 | * @param token_id : token_id to test 146 | */ 147 | export const transfer = (t: TZIP12.transfer, s: storage): ret => { 148 | const process_atomic_transfer = (from_: address) => 149 | ([ledger, t]: [ledger, TZIP12.atomic_trans]): ledger => { 150 | const { to_, token_id, amount } = t; 151 | ignore(amount); 152 | Assertions.assert_token_exist(s.token_metadata, token_id); 153 | assert_authorisation(s.operators, from_, token_id); 154 | return transfer_token_from_user_to_user(ledger, token_id, from_, to_) 155 | }; 156 | const process_single_transfer = ([ledger, t]: [ledger, TZIP12.transfer_from]): ledger => { 157 | const { from_, txs } = t; 158 | return List.fold_left(process_atomic_transfer(from_), ledger, txs) 159 | }; 160 | const ledger = List.fold_left(process_single_transfer, s.ledger, t); 161 | const store = set_ledger([s, ledger]); 162 | return [list([]), store] 163 | }; 164 | 165 | export const balance_of = (b: TZIP12.balance_of, s: storage): ret => { 166 | const { requests, callback } = b; 167 | const get_balance_info = (request: TZIP12.request): TZIP12.callback => { 168 | const { owner, token_id } = request; 169 | Assertions.assert_token_exist(s.token_metadata, token_id); 170 | let balance_ = 0 as nat; 171 | if (is_owner_of(s.ledger, token_id, owner)) balance_ = 1 as nat; 172 | return ({ request: request, balance: balance_ }) 173 | }; 174 | const callback_param = List.map(get_balance_info, requests); 175 | const operation = Tezos.Next.Operation.transaction(Main(callback_param), 0mutez, callback); 176 | return [list([operation]), s] 177 | }; 178 | 179 | export const update_operators = ( 180 | updates: TZIP12.update_operators, 181 | s: storage 182 | ): ret => { 183 | const update_operator = ( 184 | [operators, update]: [operators, TZIP12.unit_update] 185 | ): operators => 186 | match(update) { 187 | when (Add_operator(operator)): 188 | add_operator( 189 | operators, 190 | operator.owner, 191 | operator.operator, 192 | operator.token_id 193 | ) 194 | when (Remove_operator(operator)): 195 | remove_operator( 196 | operators, 197 | operator.owner, 198 | operator.operator, 199 | operator.token_id 200 | ) 201 | }; 202 | let operators = get_operators(s); 203 | operators = List.fold_left(update_operator, operators, updates); 204 | const store = set_operators([s, operators]); 205 | return [list([]), store] 206 | }; 207 | 208 | export const get_balance = (p: [address, nat], s: storage): nat => { 209 | const [owner, token_id] = p; 210 | Assertions.assert_token_exist(s.token_metadata, token_id); 211 | if (is_owner_of(s.ledger, token_id, owner)) { 212 | return 1n 213 | } else { 214 | return 0n 215 | } 216 | }; 217 | 218 | export const total_supply = (token_id: nat, s: storage): nat => { 219 | Assertions.assert_token_exist(s.token_metadata, token_id); 220 | return 1n 221 | }; 222 | 223 | export const all_tokens = (_: unit, _s: storage): set => 224 | failwith(Errors.not_available); 225 | 226 | export const is_operator = (op: TZIP12.operator, s: storage): bool => { 227 | const authorized = 228 | match(Big_map.find_opt([op.owner, op.operator], s.operators)) { 229 | when (Some(a)): 230 | a 231 | when (None()): 232 | Set.empty 233 | }; 234 | return (Set.mem(op.token_id, authorized) || op.owner == op.operator) 235 | }; 236 | 237 | export const token_metadata = (p: nat, s: storage): TZIP12. 238 | tokenMetadataData => { 239 | return match(Big_map.find_opt(p, s.token_metadata)) { 240 | when (Some(data)): 241 | data 242 | when (None()): 243 | failwith(Errors.undefined_token) 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /lib/fa2/nft/extendable_nft.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | type ledger = (nat, address) big_map 7 | 8 | type operator = address 9 | 10 | type operators = ((address * operator), nat set) big_map 11 | 12 | type 'a storage = 13 | { 14 | extension : 'a; 15 | ledger : ledger; 16 | operators : operators; 17 | token_metadata : TZIP12.tokenMetadata; 18 | metadata : TZIP16.metadata 19 | } 20 | 21 | type 'a ret = operation list * 'a storage 22 | 23 | let make_storage (type a) (extension : a) : a storage = 24 | { 25 | ledger = Big_map.empty; 26 | operators = Big_map.empty; 27 | token_metadata = Big_map.empty; 28 | metadata = Big_map.empty; 29 | extension = extension 30 | } 31 | 32 | // Operators 33 | let assert_authorisation 34 | (operators : operators) 35 | (from_ : address) 36 | (token_id : nat) 37 | : unit = 38 | let sender_ = Tezos.get_sender () in 39 | if sender_ <> from_ 40 | then 41 | let authorized = 42 | match Big_map.find_opt (from_, sender_) operators with 43 | Some (a) -> a 44 | | None -> Set.empty in 45 | if not (Set.mem token_id authorized) then failwith Errors.not_operator 46 | 47 | let is_operator 48 | (operators, owner, operator, token_id : (operators * address * address * nat)) 49 | : bool = 50 | let authorized = 51 | match Big_map.find_opt (owner, operator) operators with 52 | Some (a) -> a 53 | | None -> Set.empty in 54 | (owner = operator || Set.mem token_id authorized) 55 | 56 | let add_operator 57 | (operators : operators) 58 | (owner : address) 59 | (operator : operator) 60 | (token_id : nat) 61 | : operators = 62 | if owner = operator 63 | then operators 64 | (* assert_authorisation always allow the owner so this case is not relevant *) 65 | else 66 | let () = Assertions.assert_update_permission owner in 67 | let auth_tokens = 68 | match Big_map.find_opt (owner, operator) operators with 69 | Some (ts) -> ts 70 | | None -> Set.empty in 71 | let auth_tokens = Set.add token_id auth_tokens in 72 | Big_map.update (owner, operator) (Some auth_tokens) operators 73 | 74 | let remove_operator 75 | (operators : operators) 76 | (owner : address) 77 | (operator : operator) 78 | (token_id : nat) 79 | : operators = 80 | if owner = operator 81 | then operators 82 | (* assert_authorisation always allow the owner so this case is not relevant *) 83 | else 84 | let () = Assertions.assert_update_permission owner in 85 | let auth_tokens = 86 | match Big_map.find_opt (owner, operator) operators with 87 | None -> None 88 | | Some (ts) -> 89 | let ts = Set.remove token_id ts in 90 | [@no_mutation] 91 | let is_empty = Set.size ts = 0n in 92 | if is_empty then None else Some (ts) in 93 | Big_map.update (owner, operator) auth_tokens operators 94 | 95 | //module Ledger = struct 96 | let is_owner_of (ledger : ledger) (token_id : nat) (owner : address) : bool = 97 | let current_owner: address = Option.value_with_error "option is None" (Big_map.find_opt token_id ledger) in 98 | current_owner = owner 99 | 100 | let assert_owner_of (ledger : ledger) (token_id : nat) (owner : address) : unit = 101 | Assert.Error.assert (is_owner_of ledger token_id owner) Errors.ins_balance 102 | 103 | let transfer_token_from_user_to_user 104 | (ledger : ledger) 105 | (token_id : nat) 106 | (from_ : address) 107 | (to_ : address) 108 | : ledger = 109 | let () = assert_owner_of ledger token_id from_ in 110 | let ledger = Big_map.update token_id (Some to_) ledger in 111 | ledger 112 | 113 | //module Storage = struct 114 | let is_owner_of (type a) (s : a storage) (owner : address) (token_id : nat) 115 | : bool = is_owner_of s.ledger token_id owner 116 | 117 | let set_ledger (type a) (s : a storage) (ledger : ledger) = 118 | {s with ledger = ledger} 119 | 120 | let get_operators (type a) (s : a storage) = s.operators 121 | 122 | let set_operators (type a) (s : a storage) (operators : operators) = 123 | {s with operators = operators} 124 | 125 | let get_balance (type a) (s : a storage) (owner : address) (token_id : nat) 126 | : nat = 127 | let () = Assertions.assert_token_exist s.token_metadata token_id in 128 | if is_owner_of s owner token_id then 1n else 0n 129 | 130 | let set_balance (type a) (s : a storage) (owner : address) (token_id : nat) 131 | : a storage = 132 | let () = Assertions.assert_token_exist s.token_metadata token_id in 133 | let new_ledger = Big_map.update token_id (Some owner) s.ledger in 134 | set_ledger s new_ledger 135 | 136 | let transfer (type a) (t : TZIP12.transfer) (s : a storage) : a ret = 137 | (* This function process the "txs" list. Since all transfer share the same "from_" address, we use a se *) 138 | let process_atomic_transfer 139 | (from_ : address) 140 | (ledger, t : ledger * TZIP12.atomic_trans) = 141 | let { 142 | to_; 143 | token_id; 144 | amount = _ 145 | } = t in 146 | let () = Assertions.assert_token_exist s.token_metadata token_id in 147 | let () = assert_authorisation s.operators from_ token_id in 148 | let ledger = transfer_token_from_user_to_user ledger token_id from_ to_ in 149 | ledger in 150 | let process_single_transfer (ledger, t : ledger * TZIP12.transfer_from) = 151 | let { 152 | from_; 153 | txs 154 | } = t in 155 | let ledger = List.fold_left (process_atomic_transfer from_) ledger txs in 156 | ledger in 157 | let ledger = List.fold_left process_single_transfer s.ledger t in 158 | let s = set_ledger s ledger in 159 | ([] : operation list), s 160 | 161 | let balance_of (type a) (b : TZIP12.balance_of) (s : a storage) : a ret = 162 | let { 163 | requests; 164 | callback 165 | } = b in 166 | let get_balance_info (request : TZIP12.request) : TZIP12.callback = 167 | let { 168 | owner; 169 | token_id 170 | } = request in 171 | let balance_ = get_balance s owner token_id in 172 | { 173 | request = request; 174 | balance = balance_ 175 | } in 176 | let callback_param = List.map get_balance_info requests in 177 | let operation = Tezos.Next.Operation.transaction (Main callback_param) 0mutez callback in 178 | ([operation] : operation list), s 179 | 180 | let update_operators (type a) 181 | (updates : TZIP12.update_operators) 182 | (s : a storage) 183 | : a ret = 184 | let update_operator (operators, update : operators * TZIP12.unit_update) = 185 | match update with 186 | Add_operator 187 | { 188 | owner = owner; 189 | operator = operator; 190 | token_id = token_id 191 | } -> add_operator operators owner operator token_id 192 | | Remove_operator 193 | { 194 | owner = owner; 195 | operator = operator; 196 | token_id = token_id 197 | } -> remove_operator operators owner operator token_id in 198 | let operators = get_operators s in 199 | let operators = List.fold_left update_operator operators updates in 200 | let s = set_operators s operators in 201 | ([] : operation list), s 202 | 203 | let get_balance (type a) (p : address * nat) (s : a storage) = 204 | let (owner, token_id) = p in 205 | let balance_ = get_balance s owner token_id in 206 | balance_ 207 | 208 | (* FIXME? Dynamic supply *) 209 | let total_supply (type a) (token_id : nat) (s : a storage) = 210 | let () = Assertions.assert_token_exist s.token_metadata token_id in 211 | 1n 212 | 213 | let all_tokens (type a) (() : unit) (_s : a storage) : nat set = 214 | failwith Errors.not_available 215 | 216 | let is_operator (type a) (op : TZIP12.operator) (s : a storage) : bool = 217 | let authorized = 218 | match Big_map.find_opt (op.owner, op.operator) s.operators with 219 | Some (opSet) -> opSet 220 | | None -> Set.empty in 221 | Set.size authorized > 0n || op.owner = op.operator 222 | 223 | let token_metadata (type a) (p : nat) (s : a storage) : TZIP12.tokenMetadataData = 224 | match Big_map.find_opt p s.token_metadata with 225 | Some (data) -> data 226 | | None () -> failwith Errors.undefined_token 227 | -------------------------------------------------------------------------------- /lib/fa2/nft/nft.impl.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../common/assertions.jsligo" "Assertions" 2 | export #import "../common/errors.mligo" "Errors" 3 | export #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | export #import "../common/tzip16.datatypes.jsligo" "TZIP16" 5 | 6 | #import "./extendable_nft.impl.jsligo" "NFTExtendable" 7 | 8 | export type ledger = NFTExtendable.ledger; 9 | 10 | export type operator = NFTExtendable.operator; 11 | 12 | export type operators = NFTExtendable.operators; 13 | 14 | export type storage = { 15 | ledger: ledger, 16 | operators: operators, 17 | token_metadata: TZIP12.tokenMetadata, 18 | metadata: TZIP16.metadata, 19 | } 20 | 21 | type ret = [list, storage]; 22 | 23 | const empty_storage: storage = { 24 | ledger: Big_map.empty, 25 | operators: Big_map.empty, 26 | token_metadata: Big_map.empty, 27 | metadata: Big_map.empty 28 | } 29 | 30 | @inline 31 | const lift = (s: storage): NFTExtendable.storage => { 32 | return { 33 | extension: unit, 34 | ledger: s.ledger, 35 | operators: s.operators, 36 | token_metadata: s.token_metadata, 37 | metadata: s.metadata 38 | }; 39 | } 40 | 41 | @inline 42 | const unlift = ([ops, s]: [list, NFTExtendable.storage]): ret => { 43 | let storage = { 44 | ledger: s.ledger, 45 | operators: s.operators, 46 | token_metadata: s.token_metadata, 47 | metadata: s.metadata 48 | }; 49 | return [ops, storage]; 50 | } 51 | 52 | @entry 53 | const transfer = (t: TZIP12.transfer, s: storage): ret => 54 | unlift(NFTExtendable.transfer(t, lift(s))) 55 | 56 | @entry 57 | const balance_of = (b: TZIP12.balance_of, s: storage): ret => 58 | unlift(NFTExtendable.balance_of(b, lift(s))) 59 | 60 | @entry 61 | const update_operators = (updates: TZIP12.update_operators, s: storage): ret => 62 | unlift(NFTExtendable.update_operators(updates, lift(s))) 63 | 64 | @view 65 | const get_balance = (p: [address, nat], s: storage): nat => 66 | NFTExtendable.get_balance(p, lift(s)) 67 | 68 | @view 69 | const total_supply = (token_id: nat, s: storage): nat => 70 | NFTExtendable.total_supply(token_id, lift(s)) 71 | 72 | @view 73 | const all_tokens = (_: unit, s: storage): set => 74 | NFTExtendable.all_tokens(unit, lift(s)) 75 | 76 | @view 77 | const is_operator = (op: TZIP12.operator, s: storage): bool => 78 | NFTExtendable.is_operator(op, lift(s)) 79 | 80 | @view 81 | const token_metadata = (p: nat, s: storage): TZIP12.tokenMetadataData => 82 | NFTExtendable.token_metadata(p, lift(s)) 83 | -------------------------------------------------------------------------------- /lib/fa2/nft/nft.impl.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../common/assertions.jsligo" "Assertions" 2 | [@public] #import "../common/errors.mligo" "Errors" 3 | [@public] #import "../common/tzip12.datatypes.jsligo" "TZIP12" 4 | [@public] #import "../common/tzip12.interfaces.jsligo" "TZIP12Interface" 5 | [@public] #import "../common/tzip16.datatypes.jsligo" "TZIP16" 6 | 7 | #import "./extendable_nft.impl.jsligo" "NFTExtendable" 8 | 9 | type ledger = NFTExtendable.ledger 10 | 11 | type operator = NFTExtendable.operator 12 | 13 | type operators = NFTExtendable.operators 14 | 15 | type storage = 16 | { 17 | ledger : ledger; 18 | operators : operators; 19 | token_metadata : TZIP12.tokenMetadata; 20 | metadata : TZIP16.metadata 21 | } 22 | 23 | type ret = operation list * storage 24 | 25 | let empty_storage : storage = 26 | { 27 | ledger = Big_map.empty; 28 | operators = Big_map.empty; 29 | token_metadata = Big_map.empty; 30 | metadata = Big_map.empty 31 | } 32 | 33 | [@inline] 34 | let lift (s : storage) : unit NFTExtendable.storage = 35 | { 36 | ledger = s.ledger; 37 | operators = s.operators; 38 | token_metadata = s.token_metadata; 39 | metadata = s.metadata; 40 | extension = () 41 | } 42 | 43 | [@inline] 44 | let unlift (ops, s : operation list * unit NFTExtendable.storage) : ret = 45 | ops, 46 | { 47 | ledger = s.ledger; 48 | operators = s.operators; 49 | token_metadata = s.token_metadata; 50 | metadata = s.metadata 51 | } 52 | 53 | [@entry] 54 | let transfer (t : TZIP12.transfer) (s : storage) : ret = 55 | unlift (NFTExtendable.transfer t (lift s)) 56 | 57 | [@entry] 58 | let balance_of (b : TZIP12.balance_of) (s : storage) : ret = 59 | unlift (NFTExtendable.balance_of b (lift s)) 60 | 61 | [@entry] 62 | let update_operators (updates : TZIP12.update_operators) (s : storage) : ret = 63 | unlift (NFTExtendable.update_operators updates (lift s)) 64 | 65 | [@view] 66 | let get_balance (p : (address * nat)) (s : storage) : nat = 67 | NFTExtendable.get_balance p (lift s) 68 | 69 | [@view] 70 | let total_supply (token_id : nat) (s : storage) : nat = 71 | NFTExtendable.total_supply token_id (lift s) 72 | 73 | [@view] 74 | let all_tokens (_ : unit) (s : storage) : nat set = 75 | NFTExtendable.all_tokens () (lift s) 76 | 77 | [@view] 78 | let is_operator (op : TZIP12.operator) (s : storage) : bool = 79 | NFTExtendable.is_operator op (lift s) 80 | 81 | [@view] 82 | let token_metadata (p : nat) (s : storage) : TZIP12.tokenMetadataData = 83 | NFTExtendable.token_metadata p (lift s) 84 | -------------------------------------------------------------------------------- /lib/main.jsligo: -------------------------------------------------------------------------------- 1 | // An implementaion of FA2 Interface (TZIP-12) for NFT 2 | export #import "./fa2/nft/nft.impl.jsligo" "NFT" 3 | export #import "./fa2/nft/extendable_nft.impl.jsligo" "NFTExtendable" 4 | 5 | // An implementaion of FA2 Interface (TZIP-12) for a Single Asset Token 6 | export #import "./fa2/asset/single_asset.impl.jsligo" "SingleAsset" 7 | export #import "./fa2/asset/extendable_single_asset.impl.jsligo" "SingleAssetExtendable" 8 | 9 | // An implementaion of FA2 Interface (TZIP-12) for a Multi Asset Token 10 | export #import "./fa2/asset/multi_asset.impl.jsligo" "MultiAsset" 11 | export #import "./fa2/asset/extendable_multi_asset.impl.jsligo" "MultiAssetExtendable" 12 | -------------------------------------------------------------------------------- /lib/main.mligo: -------------------------------------------------------------------------------- 1 | (* An implementaion of FA2 Interface (TZIP-12) for NFT *) 2 | [@public] #import "./fa2/nft/nft.impl.mligo" "NFT" 3 | [@public] #import "./fa2/nft/extendable_nft.impl.mligo" "NFTExtendable" 4 | 5 | (* An implementaion of FA2 Interface (TZIP-12) for a Single Asset Token *) 6 | [@public] #import "./fa2/asset/single_asset.impl.mligo" "SingleAsset" 7 | [@public] #import "./fa2/asset/extendable_single_asset.impl.mligo" "SingleAssetExtendable" 8 | 9 | (* An implementaion of FA2 Interface (TZIP-12) for a Multi Asset Token *) 10 | [@public] #import "./fa2/asset/multi_asset.impl.mligo" "MultiAsset" 11 | [@public] #import "./fa2/asset/extendable_multi_asset.impl.mligo" "MultiAssetExtendable" 12 | -------------------------------------------------------------------------------- /ligo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@ligo/fa", 3 | "version": "1.5.0", 4 | "description": "FA2 interface and types compliant with TZIP12. The library is also providing 3 Ligo contract implementations for nft, single asset & multi asset contracts", 5 | "directories": { 6 | "lib": "lib", 7 | "test": "test" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+ssh://git@github.com/ligolang/contract-catalogue.git" 12 | }, 13 | "main": "lib/main.mligo", 14 | "keywords": ["ligo", "ligo@^1.7.0", "tezos", "jsligo", "cameligo", "nft"], 15 | "author": "ligoLANG ", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/ligolang/contract-catalogue/issues" 19 | }, 20 | "homepage": "https://github.com/ligolang/contract-catalogue#readme" 21 | } 22 | -------------------------------------------------------------------------------- /test/fa2/balance_of_callback_contract.mligo: -------------------------------------------------------------------------------- 1 | type storage = nat list 2 | 3 | type request = { 4 | owner : address; 5 | token_id : nat; 6 | } 7 | 8 | type callback = { 9 | request : request; 10 | balance : nat; 11 | } 12 | 13 | type parameter = callback list 14 | 15 | [@entry] 16 | let main (responses : parameter) (_ : storage) = 17 | let balances = List.map (fun (r : callback) -> r.balance) responses in 18 | ([]: operation list), balances -------------------------------------------------------------------------------- /test/fa2/nft/e2e_mutation.test.mligo: -------------------------------------------------------------------------------- 1 | #import "../../../lib/fa2/nft/nft.impl.mligo" "FA2_NFT" 2 | #import "./nft.test.mligo" "Test_FA2_NFT" 3 | 4 | let originate_and_test_e2e contract = 5 | let () = Test_FA2_NFT._test_atomic_transfer_operator_success contract in 6 | let () = Test_FA2_NFT._test_atomic_transfer_owner_success contract in 7 | let () = Test_FA2_NFT._test_transfer_token_undefined contract in 8 | let () = Test_FA2_NFT._test_atomic_transfer_failure_not_operator contract in 9 | let () = 10 | Test_FA2_NFT._test_atomic_transfer_success_zero_amount_and_self_transfer 11 | contract in 12 | let () = Test_FA2_NFT._test_transfer_failure_transitive_operators contract in 13 | let () = Test_FA2_NFT._test_empty_transfer_and_balance_of contract in 14 | let () = Test_FA2_NFT._test_balance_of_token_undefines contract in 15 | let () = Test_FA2_NFT._test_balance_of_requests_with_duplicates contract in 16 | let () = 17 | Test_FA2_NFT._test_balance_of_0_balance_if_address_does_not_hold_tokens 18 | contract in 19 | let () = 20 | Test_FA2_NFT._test_update_operator_remove_operator_and_transfer contract in 21 | let () = Test_FA2_NFT._test_update_operator_add_operator_and_transfer contract in 22 | let () = Test_FA2_NFT._test_only_sender_manage_operators contract in 23 | let () = 24 | Test_FA2_NFT._test_update_operator_remove_operator_and_transfer1 contract in 25 | let () = 26 | Test_FA2_NFT._test_update_operator_add_operator_and_transfer1 contract in 27 | () 28 | 29 | let test_mutation = 30 | match Test.Next.Mutation.All.func (contract_of FA2_NFT) originate_and_test_e2e 31 | with 32 | [] -> () 33 | | ms -> 34 | let () = 35 | List.iter 36 | (fun ((_, mutation) : unit * mutation) -> let () = Test.Next.IO.log mutation in 37 | ()) 38 | ms in 39 | Test.Next.Assert.failwith "Some mutation also passes the tests! ^^" 40 | -------------------------------------------------------------------------------- /test/fa2/nft/views.test.mligo: -------------------------------------------------------------------------------- 1 | #import "../../helpers/nft_helpers.mligo" "TestHelpers" 2 | #import "../../helpers/list.mligo" "List_helper" 3 | #import "../../../lib/fa2/nft/nft.impl.mligo" "FA2_NFT" 4 | #import "./views_test_contract.mligo" "ViewsTestContract" 5 | 6 | (* Tests for views *) 7 | 8 | type orig_nft = (FA2_NFT parameter_of, FA2_NFT.storage) origination_result 9 | 10 | (* Test get_balance view *) 11 | let test_get_balance_view = 12 | let initial_storage, owners, _ = TestHelpers.get_initial_storage () in 13 | let owner1 = List_helper.nth_exn 0 owners in 14 | 15 | let orig = Test.Next.Originate.contract (contract_of FA2_NFT) initial_storage 0tez in 16 | 17 | 18 | let initial_storage : ViewsTestContract.storage = { 19 | main_contract = Test.Next.Typed_address.to_address orig.taddr; 20 | get_balance = (None : nat option); 21 | total_supply = (None : nat option); 22 | is_operator = (None : bool option); 23 | all_tokens = (None : nat set option); 24 | } in 25 | 26 | let orig_v = Test.Next.Originate.contract (contract_of ViewsTestContract) initial_storage 0tez in 27 | 28 | let _ = Test.Next.Typed_address.transfer_exn orig_v.taddr 29 | (Get_balance (owner1,1n) : ViewsTestContract parameter_of) 0tez 30 | in 31 | let storage = Test.Next.Typed_address.get_storage orig_v.taddr in 32 | let get_balance = storage.get_balance in 33 | Test.Next.Assert.assert (get_balance = Some 1n) 34 | 35 | (* Test total_supply view *) 36 | let test_total_supply_view = 37 | let initial_storage, _, _ = TestHelpers.get_initial_storage () in 38 | 39 | let orig = Test.Next.Originate.contract (contract_of FA2_NFT) initial_storage 0tez in 40 | 41 | 42 | let initial_storage : ViewsTestContract.storage = { 43 | main_contract = Test.Next.Typed_address.to_address orig.taddr; 44 | get_balance = (None : nat option); 45 | total_supply = (None : nat option); 46 | is_operator = (None : bool option); 47 | all_tokens = (None : nat set option); 48 | } in 49 | 50 | let orig_v = Test.Next.Originate.contract (contract_of ViewsTestContract) initial_storage 0tez in 51 | 52 | let _ = Test.Next.Typed_address.transfer_exn orig_v.taddr 53 | (Total_supply 2n : ViewsTestContract parameter_of) 0tez 54 | in 55 | let storage = Test.Next.Typed_address.get_storage orig_v.taddr in 56 | let total_supply = storage.total_supply in 57 | Test.Next.Assert.assert (total_supply = Some 1n) 58 | 59 | 60 | (* Test total_supply view - undefined token *) 61 | let test_total_supply_undefined_token_view = 62 | let initial_storage, _, _ = TestHelpers.get_initial_storage () in 63 | 64 | let orig = Test.Next.Originate.contract (contract_of FA2_NFT) initial_storage 0tez in 65 | 66 | 67 | let initial_storage : ViewsTestContract.storage = { 68 | main_contract = Test.Next.Typed_address.to_address orig.taddr; 69 | get_balance = (None : nat option); 70 | total_supply = (None : nat option); 71 | is_operator = (None : bool option); 72 | all_tokens = (None : nat set option); 73 | } in 74 | 75 | let orig_v = Test.Next.Originate.contract (contract_of ViewsTestContract) initial_storage 0tez in 76 | 77 | let result = Test.Next.Typed_address.transfer orig_v.taddr 78 | (Total_supply 15n : ViewsTestContract parameter_of) 0tez 79 | in 80 | TestHelpers.assert_error result FA2_NFT.Errors.undefined_token 81 | 82 | (* Test is_operator view *) 83 | let test_is_operator_view = 84 | let initial_storage, owners, operators = TestHelpers.get_initial_storage () in 85 | let owner1 = List_helper.nth_exn 0 owners in 86 | let op1 = List_helper.nth_exn 0 operators in 87 | 88 | let orig = Test.Next.Originate.contract (contract_of FA2_NFT) initial_storage 0tez in 89 | 90 | 91 | let initial_storage : ViewsTestContract.storage = { 92 | main_contract = Test.Next.Typed_address.to_address orig.taddr; 93 | get_balance = (None : nat option); 94 | total_supply = (None : nat option); 95 | is_operator = (None : bool option); 96 | all_tokens = (None : nat set option); 97 | } in 98 | 99 | let orig_v = Test.Next.Originate.contract (contract_of ViewsTestContract) initial_storage 0tez in 100 | 101 | let _ = Test.Next.Typed_address.transfer_exn orig_v.taddr 102 | (Is_operator { 103 | owner = owner1; 104 | operator = op1; 105 | token_id = 1n; 106 | } : ViewsTestContract parameter_of) 0tez 107 | in 108 | let storage = Test.Next.Typed_address.get_storage orig_v.taddr in 109 | let is_operator = storage.is_operator in 110 | Assert.assert (is_operator = Some true) 111 | 112 | 113 | -------------------------------------------------------------------------------- /test/fa2/nft/views_test_contract.mligo: -------------------------------------------------------------------------------- 1 | type storage = { 2 | main_contract : address; 3 | get_balance : nat option; 4 | total_supply : nat option; 5 | is_operator : bool option; 6 | all_tokens : nat set option; 7 | } 8 | 9 | type operator_request ={ 10 | owner : address; 11 | operator : address; 12 | token_id : nat; 13 | } 14 | 15 | [@entry] let get_balance (p : address * nat) (s: storage) : operation list * storage = 16 | let get_balance : nat option = Tezos.call_view "get_balance" p s.main_contract in 17 | [],{s with get_balance = get_balance} 18 | 19 | [@entry] let total_supply (p : nat) (s : storage) : operation list * storage = 20 | let total_supply : nat option = Tezos.call_view "total_supply" p s.main_contract in 21 | [],{s with total_supply = total_supply} 22 | 23 | [@entry] let is_operator (p : operator_request) (s : storage) : operation list * storage = 24 | let is_operator : bool option = Tezos.call_view "is_operator" p s.main_contract in 25 | [],{s with is_operator = is_operator} 26 | 27 | [@entry] let all_tokens () (s : storage) : operation list * storage = 28 | let all_tokens : nat set option = Tezos.call_view "all_tokens" () s.main_contract in 29 | [],{s with all_tokens = all_tokens} 30 | -------------------------------------------------------------------------------- /test/helpers/list.mligo: -------------------------------------------------------------------------------- 1 | 2 | let nth_exn (type a) (i: int) (a: a list) : a = 3 | let rec aux (remaining: a list) (cur: int) : a = 4 | match remaining with 5 | [] -> 6 | failwith "Not found in list" 7 | | hd :: tl -> 8 | if cur = i then 9 | hd 10 | else aux tl (cur + 1) 11 | in 12 | aux a 0 13 | -------------------------------------------------------------------------------- /test/helpers/nft_helpers.jsligo: -------------------------------------------------------------------------------- 1 | export #import "../../lib/fa2/nft/nft.impl.jsligo" "FA2_NFT" 2 | 3 | export function get_initial_storage () { 4 | Test.Next.State.reset(8n, [ 5 | 1000000tez, 6 | 1000000tez, 7 | 1000000tez, 8 | 1000000tez, 9 | 1000000tez, 10 | 1000000tez, 11 | 1000000tez, 12 | 1000000tez, 13 | ] as list); 14 | 15 | const baker = Test.Next.Account.address(7n); 16 | Test.Next.State.set_baker(baker); 17 | 18 | const owner1 = Test.Next.Account.address(0n); 19 | const owner2 = Test.Next.Account.address(1n); 20 | const owner3 = Test.Next.Account.address(2n); 21 | const owner4 = Test.Next.Account.address(6n); 22 | 23 | const owners = list([owner1, owner2, owner3, owner4]); 24 | 25 | const op1 = Test.Next.Account.address(3n); 26 | const op2 = Test.Next.Account.address(4n); 27 | const op3 = Test.Next.Account.address(5n); 28 | 29 | const ops = list([op1, op2, op3]); 30 | 31 | const ledger = Big_map.literal([ 32 | [1n, owner1], 33 | [2n, owner2], 34 | [3n, owner3], 35 | [4n, owner4], 36 | [5n, owner4] 37 | ]); 38 | 39 | const operators = Big_map.literal([ 40 | [[owner1, op1], Set.literal([1n])], 41 | [[owner2, op1], Set.literal([2n])], 42 | [[owner3, op1], Set.literal([3n])], 43 | [[op1 , op3], Set.literal([1n])], 44 | [[owner4, op1], Set.literal([4n, 5n])] 45 | ]); 46 | 47 | const token_metadata = Big_map.literal([ 48 | [1n, ({token_id: 1n, token_info: Map.empty as map} 49 | as FA2_NFT.TZIP12.tokenMetadataData)], 50 | [2n, ({token_id: 2n, token_info: Map.empty as map} 51 | as FA2_NFT.TZIP12.tokenMetadataData)], 52 | [3n, ({token_id: 3n, token_info: Map.empty as map} 53 | as FA2_NFT.TZIP12.tokenMetadataData)], 54 | [4n, ({token_id: 3n, token_info: Map.empty as map} 55 | as FA2_NFT.TZIP12.tokenMetadataData)], 56 | [5n, ({token_id: 3n, token_info: Map.empty as map} 57 | as FA2_NFT.TZIP12.tokenMetadataData)] 58 | ]) as FA2_NFT.TZIP12.tokenMetadata; 59 | 60 | const metadata = Big_map.literal([ 61 | ["", bytes`tezos-storage:data`], 62 | ["data", bytes`{ 63 | "name":"FA2", 64 | "description":"Example FA2 implementation", 65 | "version":"0.1.0", 66 | "license":{"name":"MIT"}, 67 | "authors":["Benjamin Fuentes"], 68 | "homepage":"", 69 | "source":{"tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, 70 | "interfaces":["TZIP-012"], 71 | "errors":[], 72 | "views":[] 73 | 74 | }`]]); 75 | 76 | const initial_storage : FA2_NFT.storage = { 77 | ledger : ledger, 78 | token_metadata : token_metadata, 79 | operators : operators, 80 | metadata : metadata 81 | }; 82 | 83 | return [initial_storage, owners, ops]; 84 | } 85 | 86 | export function assert_balances 87 | (contract_address: 88 | typed_address , 89 | [a, b, c]: 90 | [[address, nat], [address, nat], [address, nat]]) { 91 | const [owner1, token_id_1] = a; 92 | const [owner2, token_id_2] = b; 93 | const [owner3, token_id_3] = c; 94 | const storage = Test.Next.Typed_address.get_storage(contract_address); 95 | const ledger = storage.ledger; 96 | const _x = 97 | match(Big_map.find_opt(token_id_1, ledger)) { 98 | when(Some(amt)) : Assert.assert(amt == owner1); 99 | when(None) : Test.Next.Assert.failwith("incorrect address"); 100 | }; 101 | 102 | const _y = 103 | match(Big_map.find_opt(token_id_2, ledger)) { 104 | when(Some(amt)) : Assert.assert (amt == owner2); 105 | when(None) : Test.Next.Assert.failwith("incorrect address"); 106 | }; 107 | 108 | const _z = 109 | match(Big_map.find_opt(token_id_3, ledger)) { 110 | when(Some(amt)) : Assert.assert (amt == owner3); 111 | when(None) : Test.Next.Assert.failwith("incorrect address"); 112 | }; 113 | 114 | return unit; 115 | }; 116 | 117 | export const assert_error = 118 | (result: test_exec_result, error : FA2_NFT.Errors.t) => 119 | match(result) { 120 | when(Success(_)) : Test.Next.Assert.failwith("This test should fail"); 121 | when(Fail(Rejected(err, _))) : 122 | Assert.assert (Test.Next.Compare.eq(err, Test.Next.Michelson.eval(error))) 123 | when(Fail(_)) : Test.Next.Assert.failwith("invalid test failure") 124 | }; 125 | -------------------------------------------------------------------------------- /test/helpers/nft_helpers.mligo: -------------------------------------------------------------------------------- 1 | [@public] #import "../../lib/fa2/nft/nft.impl.mligo" "FA2_NFT" 2 | 3 | let get_initial_storage () = 4 | let () = Test.Next.State.reset 8n ([ 5 | 1000000tez; 6 | 1000000tez; 7 | 1000000tez; 8 | 1000000tez; 9 | 1000000tez; 10 | 1000000tez; 11 | 1000000tez; 12 | 1000000tez; 13 | ] : tez list) in 14 | 15 | let baker = Test.Next.Account.address 7n in 16 | let () = Test.Next.State.set_baker baker in 17 | 18 | let owner1 = Test.Next.Account.address 0n in 19 | let owner2 = Test.Next.Account.address 1n in 20 | let owner3 = Test.Next.Account.address 2n in 21 | let owner4 = Test.Next.Account.address 6n in 22 | 23 | let owners = [owner1; owner2; owner3; owner4] in 24 | 25 | let op1 = Test.Next.Account.address 3n in 26 | let op2 = Test.Next.Account.address 4n in 27 | let op3 = Test.Next.Account.address 5n in 28 | 29 | let ops = [op1; op2; op3] in 30 | 31 | let ledger = Big_map.literal ([ 32 | (1n, owner1); 33 | (2n, owner2); 34 | (3n, owner3); 35 | (4n, owner4); 36 | (5n, owner4); 37 | ]) 38 | in 39 | 40 | let operators = Big_map.literal ([ 41 | ((owner1, op1), Set.literal [1n]); 42 | ((owner2, op1), Set.literal [2n]); 43 | ((owner3, op1), Set.literal [3n]); 44 | ((op1 , op3), Set.literal [1n]); 45 | ((owner4, op1), Set.literal [4n; 5n]); 46 | ]) 47 | in 48 | 49 | let token_metadata = (Big_map.literal [ 50 | (1n, ({token_id=1n;token_info=(Map.empty : (string, bytes) map);} : FA2_NFT.TZIP12.tokenMetadataData)); 51 | (2n, ({token_id=2n;token_info=(Map.empty : (string, bytes) map);} : FA2_NFT.TZIP12.tokenMetadataData)); 52 | (3n, ({token_id=3n;token_info=(Map.empty : (string, bytes) map);} : FA2_NFT.TZIP12.tokenMetadataData)); 53 | (4n, ({token_id=3n;token_info=(Map.empty : (string, bytes) map);} : FA2_NFT.TZIP12.tokenMetadataData)); 54 | (5n, ({token_id=3n;token_info=(Map.empty : (string, bytes) map);} : FA2_NFT.TZIP12.tokenMetadataData)); 55 | ] : FA2_NFT.TZIP12.tokenMetadata) in 56 | 57 | let metadata =Big_map.literal [ 58 | ("", [%bytes {|tezos-storage:data|}]); 59 | ("data", [%bytes 60 | {|{ 61 | "name":"FA2", 62 | "description":"Example FA2 implementation", 63 | "version":"0.1.0", 64 | "license":{"name":"MIT"}, 65 | "authors":["Benjamin Fuentes"], 66 | "homepage":"", 67 | "source":{"tools":["Ligo"], "location":"https://github.com/ligolang/contract-catalogue/tree/main/lib/fa2"}, 68 | "interfaces":["TZIP-012"], 69 | "errors":[], 70 | "views":[] 71 | 72 | }|}]); 73 | ] in 74 | 75 | 76 | let initial_storage : FA2_NFT.storage = { 77 | ledger = ledger; 78 | token_metadata = token_metadata; 79 | operators = operators; 80 | metadata = metadata; 81 | } in 82 | 83 | initial_storage, owners, ops 84 | 85 | 86 | let assert_balances 87 | (contract_address : (FA2_NFT parameter_of, FA2_NFT.storage) typed_address ) 88 | (a, b, c : (address * nat) * (address * nat) * (address * nat)) = 89 | let (owner1, token_id_1) = a in 90 | let (owner2, token_id_2) = b in 91 | let (owner3, token_id_3) = c in 92 | let storage = Test.Next.Typed_address.get_storage contract_address in 93 | let ledger = storage.ledger in 94 | let () = match (Big_map.find_opt token_id_1 ledger) with 95 | Some amt -> Assert.assert (amt = owner1) 96 | | None -> Test.Next.Assert.failwith "incorrect address" 97 | in 98 | let () = match (Big_map.find_opt token_id_2 ledger) with 99 | Some amt -> Assert.assert (amt = owner2) 100 | | None -> Test.Next.Assert.failwith "incorrect address" 101 | in 102 | let () = match (Big_map.find_opt token_id_3 ledger) with 103 | Some amt -> Assert.assert (amt = owner3) 104 | | None -> Test.Next.Assert.failwith "incorrect address" 105 | in 106 | () 107 | 108 | let assert_error (result : test_exec_result) (error : FA2_NFT.Errors.t) = 109 | match result with 110 | Success _ -> Test.Next.Assert.failwith "This test should fail" 111 | | Fail (Rejected (err, _)) -> Assert.assert (Test.Next.Compare.eq err (Test.Next.Michelson.eval error)) 112 | | Fail _ -> Test.Next.Assert.failwith "invalid test failure" 113 | --------------------------------------------------------------------------------