diff --git a/ticketing_cameligo/.gitignore b/ticketing_cameligo/.gitignore new file mode 100644 index 00000000..8258c3ea --- /dev/null +++ b/ticketing_cameligo/.gitignore @@ -0,0 +1,2 @@ +.vscode/ +compiled/ \ No newline at end of file diff --git a/ticketing_cameligo/Makefile b/ticketing_cameligo/Makefile new file mode 100644 index 00000000..e06096b5 --- /dev/null +++ b/ticketing_cameligo/Makefile @@ -0,0 +1,111 @@ +default: help + +# Perl Colors, with fallback if tput command not available +GREEN := $(shell command -v tput >/dev/null 2>&1 && tput -Txterm setaf 2 || echo "") +BLUE := $(shell command -v tput >/dev/null 2>&1 && tput -Txterm setaf 4 || echo "") +WHITE := $(shell command -v tput >/dev/null 2>&1 && tput -Txterm setaf 7 || echo "") +YELLOW := $(shell command -v tput >/dev/null 2>&1 && tput -Txterm setaf 3 || echo "") +RESET := $(shell command -v tput >/dev/null 2>&1 && tput -Txterm sgr0 || echo "") + +# Add help text after each target name starting with '\#\#' +# A category can be added with @category +HELP_FUN = \ + %help; \ + while(<>) { push @{$$help{$$2 // 'options'}}, [$$1, $$3] if /^([a-zA-Z\-]+)\s*:.*\#\#(?:@([a-zA-Z\-]+))?\s(.*)$$/ }; \ + print "usage: make [target]\n\n"; \ + for (sort keys %help) { \ + print "${WHITE}$$_:${RESET}\n"; \ + for (@{$$help{$$_}}) { \ + $$sep = " " x (32 - length $$_->[0]); \ + print " ${YELLOW}$$_->[0]${RESET}$$sep${GREEN}$$_->[1]${RESET}\n"; \ + }; \ + print "\n"; } + +help: + @perl -e '$(HELP_FUN)' $(MAKEFILE_LIST) + +####################################### +# PROJECT # +####################################### + +all: install compile deploy ##@Project - Runs all the deployment chain from scratch + +# nuke-all: ##@Project - Deletes IMPORTANT FILES & FOLDERS to reset to initial state +# @echo "Are you sure you want to UNPIN ALL FILES from your Pinata Cloud account ? [y/N]" && read ans && if [ $${ans:-'N'} = 'y' ]; then npx ts-node ./scripts/unpin; fi +# @echo "Are you sure you want to DELETE IMPORTANT FILES from this folder to RESET EVERYTHING to initial state ? [y/N]" && read ans && if [ $${ans:-'N'} = 'y' ]; then rm -rf node_modules/ assets/* compiled/* deployments/* ; fi + +####################################### +# CONTRACTS # +####################################### +ifndef LIGO +LIGO=docker run --platform linux/amd64 --rm -v "$(PWD)":"$(PWD)" -w "$(PWD)" ligolang/ligo:0.49.0 +endif + +compile = $(LIGO) compile contract ./src/$(1) -o ./compiled/$(2) $(3) +# ^ Compile contracts to Michelson or Micheline + +test-ligo = $(LIGO) run test ./test/ligo/$(1) +# ^ Run the given LIGO Test file + +compile: ##@Contracts - Compile LIGO contracts + @if [ ! -d ./compiled ]; then mkdir ./compiled ; fi + @echo "Compiling contracts..." +## @$(call compile,generic_fa2/core/instance/NFT.mligo,fa2_nft.tz) +## @$(call compile,generic_fa2/core/instance/NFT.mligo,fa2_nft.json,--michelson-format json) + @$(call compile,main.mligo,ticketing.tz) + @$(call compile,main.mligo,ticketing.json,--michelson-format json) + +.PHONY: test +test-ligo: ##@Contracts - Run LIGO tests + @$(call test-ligo,ticketing.test.mligo) +# @$(call test-ligo,nft.premint.test.mligo) +# @$(call test-ligo,nft.mint.test.mligo) +# @$(call test-ligo,nft.airdrop.test.mligo) +# @$(call test-ligo,nft.changeallocation.test.mligo) +# @$(call test-ligo,nft.changeminteedwallet.test.mligo) + +test-integration: ##@Contracts - Run integration tests + $(MAKE) deploy + @npm run test + +clean: ##@Contracts - Contracts clean up + @echo "Are you sure you want to DELETE ALL COMPILED CONTRACT FILES from your Compiled folder ? [y/N]" && read ans && if [ $${ans:-'N'} = 'y' ]; then rm -rf compiled/* ; fi + +####################################### +# SCRIPTS # +####################################### +install: ##@Scripts - Install NPM dependencies + @if [ ! -f ./.env ]; then cp .env.dist .env ; fi + @npm i + +deploy: ##@Scripts - Deploy contracts + @./scripts/deploy + +# fixtures: ##@Scripts - Generate image fixtures +# @npx ts-node ./scripts/fixtures.ts + +# prepare: ##@Scripts - Prepare a collection JSON +# @if [ ! -d ./assets/thumbnail ]; then mkdir assets/thumbnail ; fi +# @if [ ! -d ./assets/display ]; then mkdir assets/display ; fi +# @npx ts-node ./scripts/prepare.ts + +# upload: ##@Scripts - Upload assets to IPFS and make their Metadata +# @npx ts-node ./scripts/upload.ts + +# generate: ##@Scripts - Generate a collection (FACTORY=KTxx) +# @npx ts-node ./migrations/2_collection.ts $(FACTORY) + +# local: ##@Scripts - Generate a JSON to avoid fetching IPFS +# @npx ts-node ./scripts/local.test + +# unpin: ##@Scripts - Unpin (delete) all IPFS files on Pinata +# @echo "Are you sure you want to UNPIN ALL FILES from your Pinata Cloud account ? [y/N]" && read ans && if [ $${ans:-'N'} = 'y' ]; then npx ts-node ./scripts/unpin; fi + +####################################### +# SANDBOX # +####################################### +sandbox-start: ##@Sandbox - Start Flextesa sandbox + @./scripts/run-sandbox + +sandbox-stop: ##@Sandbox - Stop Flextesa sandbox + @docker stop sandbox diff --git a/ticketing_cameligo/README.md b/ticketing_cameligo/README.md new file mode 100644 index 00000000..e69de29b diff --git a/ticketing_cameligo/src/errors.mligo b/ticketing_cameligo/src/errors.mligo new file mode 100644 index 00000000..fd2075fc --- /dev/null +++ b/ticketing_cameligo/src/errors.mligo @@ -0,0 +1,2 @@ +let only_admin : string = "Only Admin" +let uuid_already_used : string = "The given optional UUID has already been used" \ No newline at end of file diff --git a/ticketing_cameligo/src/generic_fa2/core/common/NFT.mligo b/ticketing_cameligo/src/generic_fa2/core/common/NFT.mligo new file mode 100644 index 00000000..c9340850 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/NFT.mligo @@ -0,0 +1,114 @@ +(** + This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos + copyright Wulfman Corporation 2021 +*) + +#import "errors.mligo" "Errors" +#import "address.mligo" "Address" +#import "operators.mligo" "Operators" +#import "tokenMetadata.mligo" "TokenMetadata" +#import "ledger.mligo" "Ledger" +#import "storage.mligo" "Storage" + +type storage = Storage.t + +(** Transfer entrypoint *) +type atomic_trans = [@layout:comb] { + to_ : Address.t; + token_id : nat; + amount : nat; +} + +type transfer_from = { + from_ : Address.t; + tx : atomic_trans list +} +type transfer = transfer_from list + +let transfer (type a) (t:transfer) (s:a storage) : operation list * a storage = + (* This function process the "tx" list. Since all transfer share the same "from_" address, we use a se *) + let process_atomic_transfer (from_:Address.t) (ledger, t:Ledger.t * atomic_trans) = + // let {to_;token_id} = t in + // let () = Storage.assert_token_exist s token_id in + // let () = Operators.assert_authorisation s.operators from_ token_id in + // let ledger = Ledger.transfer_token_from_user_to_user ledger token_id from_ to_ in + // ledger + let {to_; token_id; amount=amount_} = t in + let () = Storage.assert_token_exist s token_id in + let () = Operators.assert_authorisation s.operators from_ token_id in + let ledger = Ledger.decrease_token_amount_for_user ledger from_ token_id amount_ in + let ledger = Ledger.increase_token_amount_for_user ledger to_ token_id amount_ in + ledger + in + let process_single_transfer (ledger, t:Ledger.t * transfer_from ) = + let {from_;tx} = t in + let ledger = List.fold_left (process_atomic_transfer from_) ledger tx in + ledger + in + let ledger = List.fold_left process_single_transfer s.ledger t in + let s = Storage.set_ledger s ledger in + ([] : operation list), s + +type request = { + owner : Address.t; + token_id : nat; +} + +type callback = [@layout:comb] { + request : request; + balance : nat; +} + +type balance_of = [@layout:comb] { + requests : request list; + callback : callback list contract; +} + +(** Balance_of entrypoint *) +let balance_of (type a) (b: balance_of) (s: a storage) : operation list * a storage = + let {requests;callback} = b in + let get_balance_info (request : request) : callback = + let {owner;token_id} = request in + let balance_ = Storage.get_balance s owner token_id in + {request=request;balance=balance_} + in + let callback_param = List.map get_balance_info requests in + let operation = Tezos.transaction callback_param 0tez callback in + ([operation]: operation list),s + +(** Update_operators entrypoint *) +type operator = [@layout:comb] { + owner : Address.t; + operator : Address.t; + token_id : nat; +} +type unit_update = Add_operator of operator | Remove_operator of operator +type update_operators = unit_update list + +let update_ops (type a) (updates: update_operators) (s: a storage) : operation list * a storage = + let update_operator (operators,update : Operators.t * unit_update) = match update with + Add_operator {owner=owner;operator=operator;token_id=token_id} -> Operators.add_operator operators owner operator token_id + | Remove_operator {owner=owner;operator=operator;token_id=token_id} -> Operators.remove_operator operators owner operator token_id + in + let operators = Storage.get_operators s in + let operators = List.fold_left update_operator operators updates in + let s = Storage.set_operators s operators in + ([]: operation list),s + +[@view] let get_balance (type a) (p, s : (Address.t * nat) * a storage) : nat = + let (owner, token_id) = p in + let balance_ = Storage.get_balance s owner token_id in + balance_ + +[@view] let total_supply (type a) ((token_id, s) : (nat * a storage)): nat = + let () = Storage.assert_token_exist s token_id in + 1n + +[@view] let all_tokens (type a) ((_, s) : (unit * a storage)): nat list = + s.token_ids + +[@view] let is_operator (type a) ((op, s) : (operator * a storage)): bool = + Operators.is_operator (s.operators, op.owner, op.operator, op.token_id) + +[@view] let token_metadata (type a) ((p, s) : (nat * a storage)): TokenMetadata.data = + TokenMetadata.get_token_metadata p s.token_metadata \ No newline at end of file diff --git a/ticketing_cameligo/src/generic_fa2/core/common/address.mligo b/ticketing_cameligo/src/generic_fa2/core/common/address.mligo new file mode 100644 index 00000000..d147d67a --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/address.mligo @@ -0,0 +1,8 @@ +(** + This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos + copyright Wulfman Corporation 2021 +*) + +type t = address + +let equal (a : t) (b : t) = a = b \ No newline at end of file diff --git a/ticketing_cameligo/src/generic_fa2/core/common/errors.mligo b/ticketing_cameligo/src/generic_fa2/core/common/errors.mligo new file mode 100644 index 00000000..ea503458 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/errors.mligo @@ -0,0 +1,12 @@ +type t = string + +let undefined_token = "FA2_TOKEN_UNDEFINED" +let ins_balance = "FA2_INSUFFICIENT_BALANCE" +let not_owner = "FA2_NOT_OWNER" +let not_operator = "FA2_NOT_OPERATOR" + +// Following error might be used in commented block Operators.mligo : 30 +// let no_transfer = "FA2_TX_DENIED" + +let only_sender_manage_operators = "The sender can only manage operators for his own token" +let only_admin = "FA2_NOT_ADMIN" \ No newline at end of file diff --git a/ticketing_cameligo/src/generic_fa2/core/common/ledger.mligo b/ticketing_cameligo/src/generic_fa2/core/common/ledger.mligo new file mode 100644 index 00000000..9bc59357 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/ledger.mligo @@ -0,0 +1,44 @@ +(** + This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos + copyright Wulfman Corporation 2021 +*) + +#import "errors.mligo" "Errors" +#import "address.mligo" "Address" + +type token_id = nat +type owner = Address.t +type amount_ = nat +type t = ((owner * token_id), amount_) big_map + +// let is_owner_of (ledger:t) (token_id : token_id) (owner: Address.t) : bool = +// (** We already sanitized token_id, a failwith here indicated a patological storage *) +// let current_owner = Option.unopt (Big_map.find_opt token_id ledger) in +// Address.equal current_owner owner + +// let assert_owner_of (ledger:t) (token_id : token_id) (owner: Address.t) : unit = +// assert_with_error (is_owner_of ledger token_id owner) Errors.ins_balance + +// let transfer_token_from_user_to_user (ledger : t) (token_id : token_id) (from_ : owner) (to_ : owner) : t = +// let () = assert_owner_of ledger token_id from_ in +// let ledger = Big_map.update token_id (Some to_) ledger in +// ledger + +let get_for_user (ledger:t) (owner: owner) (token_id : token_id) : amount_ = + match Big_map.find_opt (owner,token_id) ledger with Some (a) -> a | None -> 0n + +let set_for_user (ledger:t) (owner: owner) (token_id : token_id ) (amount_:amount_) : t = + Big_map.update (owner,token_id) (Some amount_) ledger + +let decrease_token_amount_for_user (ledger : t) (from_ : owner) (token_id : nat) (amount_ : nat) : t = + let balance_ = get_for_user ledger from_ token_id in + let () = assert_with_error (balance_ >= amount_) Errors.ins_balance in + let balance_ = abs (balance_ - amount_) in + let ledger = set_for_user ledger from_ token_id balance_ in + ledger + +let increase_token_amount_for_user (ledger : t) (to_ : owner) (token_id : nat) (amount_ : nat) : t = + let balance_ = get_for_user ledger to_ token_id in + let balance_ = balance_ + amount_ in + let ledger = set_for_user ledger to_ token_id balance_ in + ledger diff --git a/ticketing_cameligo/src/generic_fa2/core/common/operators.mligo b/ticketing_cameligo/src/generic_fa2/core/common/operators.mligo new file mode 100644 index 00000000..69a7297d --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/operators.mligo @@ -0,0 +1,62 @@ +(** +This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos +copyright Wulfman Corporation 2021 +*) + +#import "errors.mligo" "Errors" +#import "address.mligo" "Address" + +type owner = Address.t +type operator = Address.t +type token_id = nat +type t = ((owner * operator), token_id set) big_map + +(** if transfer policy is Owner_or_operator_transfer *) +let assert_authorisation (operators : t) (from_ : Address.t) (token_id : nat) : unit = + let sender_ = Tezos.get_sender() in + if (Address.equal sender_ from_) then () + else + let authorized = match Big_map.find_opt (from_,sender_) operators with + Some (a) -> a | None -> Set.empty + in if Set.mem token_id authorized then () + else failwith Errors.not_operator +(** if transfer policy is Owner_transfer +let assert_authorisation (operators : t) (from_ : Address.t) : unit = + let sender_ = Tezos.sender in + if (sender_ = from_) then () + else failwith Errors.not_owner +*) + +(** if transfer policy is No_transfer +let assert_authorisation (operators : t) (from_ : Address.t) : unit = + failwith Errors.no_owner +*) + +let is_operator (operators, owner, operator, token_id : (t * Address.t * Address.t * nat)) : bool = + let authorized = match Big_map.find_opt (owner,operator) operators with + Some (a) -> a | None -> Set.empty in + (owner = operator || Set.mem token_id authorized) + +let assert_update_permission (owner : owner) : unit = + assert_with_error (Address.equal owner (Tezos.get_sender())) Errors.only_sender_manage_operators + +let add_operator (operators : t) (owner : owner) (operator : operator) (token_id : token_id) : t = + if owner = operator then operators (* assert_authorisation always allow the owner so this case is not relevant *) + else + let () = assert_update_permission owner in + let auth_tokens = match Big_map.find_opt (owner,operator) operators with + Some (ts) -> ts | None -> Set.empty in + let auth_tokens = Set.add token_id auth_tokens in + Big_map.update (owner,operator) (Some auth_tokens) operators + +let remove_operator (operators : t) (owner : owner) (operator : operator) (token_id : token_id) : t = + if owner = operator then operators (* assert_authorisation always allow the owner so this case is not relevant *) + else + let () = assert_update_permission owner in + let auth_tokens = match Big_map.find_opt (owner,operator) operators with + None -> None | Some (ts) -> + let ts = Set.remove token_id ts in + let is_empty = Set.cardinal ts = 0n in + if is_empty then None else Some (ts) + in + Big_map.update (owner,operator) auth_tokens operators diff --git a/ticketing_cameligo/src/generic_fa2/core/common/storage.mligo b/ticketing_cameligo/src/generic_fa2/core/common/storage.mligo new file mode 100644 index 00000000..6e286af9 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/storage.mligo @@ -0,0 +1,38 @@ +(** + This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos + copyright Wulfman Corporation 2021 +*) + +#import "errors.mligo" "Errors" +#import "address.mligo" "Address" +#import "operators.mligo" "Operators" +#import "tokenMetadata.mligo" "TokenMetadata" +#import "ledger.mligo" "Ledger" + +type token_id = nat +type 'a t = { + extension : 'a; + ledger : Ledger.t; + operators : Operators.t; + token_ids : token_id list; + token_metadata : TokenMetadata.t; +} + +// let is_owner_of (type a) (s:a t) (owner : Address.t) (token_id : token_id) : bool = +// Ledger.is_owner_of s.ledger token_id owner + +let assert_token_exist (type a) (s:a t) (token_id : nat) : unit = + let _ = Option.unopt_with_error (Big_map.find_opt token_id s.token_metadata) + Errors.undefined_token in + () + +let set_ledger (type a) (s:a t) (ledger:Ledger.t) = {s with ledger = ledger} + +let get_operators (type a) (s:a t) = s.operators + +let set_operators (type a) (s:a t) (operators:Operators.t) = {s with operators = operators} + +let get_balance (type a) (s:a t) (owner : Address.t) (token_id : nat) : nat = + let () = assert_token_exist s token_id in + //if is_owner_of s owner token_id then 1n else 0n + Ledger.get_for_user s.ledger owner token_id diff --git a/ticketing_cameligo/src/generic_fa2/core/common/tokenMetadata.mligo b/ticketing_cameligo/src/generic_fa2/core/common/tokenMetadata.mligo new file mode 100644 index 00000000..06f6bcc2 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/common/tokenMetadata.mligo @@ -0,0 +1,40 @@ +(** + This file implement the TZIP-12 protocol (a.k.a FA2) for NFT on Tezos + copyright Wulfman Corporation 2021 +*) + +#import "errors.mligo" "Errors" + +(** + This should be initialized at origination, conforming to either + TZIP-12 : https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#token-metadata + or TZIP-16 : https://gitlab.com/tezos/tzip/-/blob/master/proposals/tzip-12/tzip-12.md#contract-metadata-tzip-016 +*) +type data = { + token_id : nat; + token_info : (string,bytes)map +} +type t = (nat, data) big_map + +let get_token_metadata (token_id : nat) (tm : t) = + match Big_map.find_opt token_id tm with + Some data -> data + | None -> failwith Errors.undefined_token + +let helper_add_global_metadata(acc , elt : (t * (string, bytes)map) * nat) : (t * (string, bytes)map) = + let current_metadata = match Big_map.find_opt elt acc.0 with + | None -> (failwith(Errors.undefined_token) : data) + | Some c -> c + in + let add_field(infos, field: (string, bytes)map * (string * bytes)) : (string, bytes)map = + match Map.find_opt field.0 infos with + | None -> Map.add field.0 field.1 infos + | Some _old -> infos + in + let modified_current_token_info = Map.fold add_field acc.1 current_metadata.token_info in + let modified_current_metadata : data = { current_metadata with token_info=modified_current_token_info } in + (Big_map.update elt (Some(modified_current_metadata)) acc.0, acc.1) + +let add_global_metadata (token_ids : nat list) (metadatas : t) (global_metadatas : (string, bytes) map) : t = + let result, _gl_metas = List.fold helper_add_global_metadata token_ids (metadatas, global_metadatas) in + result \ No newline at end of file diff --git a/ticketing_cameligo/src/generic_fa2/core/instance/NFT.mligo b/ticketing_cameligo/src/generic_fa2/core/instance/NFT.mligo new file mode 100644 index 00000000..afdbf61b --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/instance/NFT.mligo @@ -0,0 +1,462 @@ +#import "../common/errors.mligo" "Errors" +#import "../common/address.mligo" "Address" +#import "../common/storage.mligo" "Storage" +#import "../common/ledger.mligo" "Ledger" +#import "../common/tokenMetadata.mligo" "TokenMetadata" +#import "../common/NFT.mligo" "NFT" + +#import "errors.mligo" "ErrorsExtension" + +(* + Specialization corner +*) + +module TotalSupply = struct + type token_id = nat + type amount_ = nat + type distribution = { + total : nat; + available: nat; + reserved : nat; + } + type t = (token_id, distribution) big_map + + let get_distribution_for_token_id (totalsupplies:t) (token_id : token_id) : distribution = + match Big_map.find_opt token_id totalsupplies with + | Some (a) -> a + | None -> (failwith(ErrorsExtension.missing_distribution) : distribution) + + let set_distribution_for_token_id (totalsupplies:t) (token_id : token_id) (distrib: distribution) : t = + Big_map.update token_id (Some distrib) totalsupplies + + let set_total_for_token_id (totalsupplies:t) (token_id : token_id) (new_total: nat) : t = + match Big_map.find_opt token_id totalsupplies with + | Some distrib -> Big_map.update token_id (Some { distrib with total=new_total}) totalsupplies + | None -> (failwith(ErrorsExtension.missing_distribution) : t) + + let set_available_for_token_id (totalsupplies:t) (token_id : token_id) (new_available: nat) : t = + match Big_map.find_opt token_id totalsupplies with + | Some distrib -> Big_map.update token_id (Some { distrib with available=new_available}) totalsupplies + | None -> (failwith(ErrorsExtension.missing_distribution) : t) + + let set_reserved_for_token_id (totalsupplies:t) (token_id : token_id) (new_reserve: nat) : t = + match Big_map.find_opt token_id totalsupplies with + | Some distrib -> Big_map.update token_id (Some { distrib with reserved=new_reserve}) totalsupplies + | None -> (failwith(ErrorsExtension.missing_distribution) : t) + + let increase_total_for_token_id (totalsupplies:t) (token_id : token_id) (extra_total: nat) : t = + let distrib = get_distribution_for_token_id totalsupplies token_id in + let new_distrib = { distrib with total=distrib.total + extra_total } in + set_distribution_for_token_id totalsupplies token_id new_distrib + + let decrease_available_for_token_id (totalsupplies:t) (token_id : token_id) (amount_minted: nat) : t = + let distrib = get_distribution_for_token_id totalsupplies token_id in + let _check_limit_reached : unit = assert_with_error (distrib.available >= amount_minted) ErrorsExtension.insuffisant_available_editions in + let new_distrib = { distrib with available=abs(distrib.available - amount_minted) } in + set_distribution_for_token_id totalsupplies token_id new_distrib + + let decrease_reserved_for_token_id (totalsupplies:t) (token_id : token_id) (amount_minted: nat) : t = + let distrib = get_distribution_for_token_id totalsupplies token_id in + let _check_limit_reached : unit = assert_with_error (distrib.reserved >= amount_minted) ErrorsExtension.insuffisant_reserved_editions in + let new_distrib = { distrib with reserved=abs(distrib.reserved - amount_minted) } in + set_distribution_for_token_id totalsupplies token_id new_distrib +end + +type currency = XTZ | EURL | USDT | ETH | EUR +//type license = NO_LICENSE | CCO | CCBY | CCBYSA +type sale = FCFS | DUTCH_AUCTION | ENGLISH_AUCTION | OFFERS | RAFFLE +//type uuid = bytes + +type asset_info = { + initial_price : tez; + is_mintable : bool; + max_per_wallet : nat; + currency : currency; + salemode : sale; + allocations : (address, nat) map; + allocations_decimals : nat; + uuid : string option; +} + +type extension = { + admin : Address.t; + creator : Address.t; + minteed : Address.t; + minteed_ratio : nat; + total_supply : TotalSupply.t; + metadata: (string, bytes) big_map; + next_token_id : nat; + minted_per_wallet : (address, (nat, nat)map) big_map; + asset_infos : (nat, asset_info) big_map; +} + +type storage = Storage.t + +let authorize_admin (s:extension storage): unit = + let sender_ = Tezos.get_sender() in + assert_with_error (sender_ = s.extension.admin) Errors.only_admin + +let authorize_admin_and_creator (s:extension storage): unit = + let sender_ = Tezos.get_sender() in + assert_with_error ((sender_ = s.extension.admin) or (sender_ = s.extension.creator)) ErrorsExtension.only_admin_or_creator + +let authorize_minteed (s:extension storage): unit = + let sender_ = Tezos.get_sender() in + assert_with_error (sender_ = s.extension.minteed) ErrorsExtension.only_minteed + + + +let transfer_extension (_t: NFT.transfer) (s:extension storage): operation list * extension storage = + // let process_atomic_usage (usage, t:TokenUsage.t * NFT.atomic_trans) = TokenUsage.update_usage_token usage t.token_id in + // let update_usage_single_transfer (usage, t:TokenUsage.t * NFT.transfer_from ) = List.fold process_atomic_usage t.tx usage in + // let usage = List.fold update_usage_single_transfer t s.extension.token_usage in + // let s = set_usage s usage in + ([]: operation list),s + +type premint_asset_info = { + quantity : nat; + quantity_reserved : nat; + initial_price : tez; + is_mintable : bool; + max_per_wallet : nat; + currency : currency; + salemode : sale; + allocations : (address, nat) map; + allocations_decimals : nat; + uuid : string option; + metas : (string, bytes) map; +} + +type premint_param = premint_asset_info list + +type tokenmetadata_param = [@layout:comb] { + tokenid : nat; + ipfsuri : bytes +} + +let premint (param: premint_param) (s: extension storage) : operation list * extension storage = + + let create_asset(acc, elt: (nat list * TokenMetadata.t * extension) * premint_asset_info) : (nat list * TokenMetadata.t * extension) = + let current_token_id = acc.2.next_token_id in + // TODO : to be reviewed for production launch + let _check_currency : unit = assert_with_error(elt.currency = XTZ) ErrorsExtension.unsupported_currency in + let _check_salemode : unit = assert_with_error(elt.salemode = FCFS) ErrorsExtension.unsupported_sale_mode in + ( + current_token_id :: acc.0, + Big_map.update current_token_id (Some { token_id=current_token_id; token_info=elt.metas } ) acc.1, + { acc.2 with + total_supply = TotalSupply.set_distribution_for_token_id acc.2.total_supply current_token_id { total=elt.quantity; available=abs(elt.quantity - elt.quantity_reserved); reserved=elt.quantity_reserved }; + next_token_id = current_token_id + 1n; + asset_infos = Big_map.update current_token_id (Some { + initial_price=elt.initial_price; + is_mintable=elt.is_mintable; + max_per_wallet=elt.max_per_wallet; + currency=elt.currency; + salemode=elt.salemode; + allocations=elt.allocations; + allocations_decimals=elt.allocations_decimals; + uuid=elt.uuid }) acc.2.asset_infos; + } + ) + in + let (new_token_ids, new_token_metadata, new_extension) : (nat list * TokenMetadata.t * extension) = + List.fold create_asset param (s.token_ids, s.token_metadata, s.extension) in + (([]: operation list), { s with token_ids=new_token_ids; token_metadata=new_token_metadata; extension=new_extension }) + +type mint_param = [@layout:comb] { + ids : Storage.token_id list; + quantity : (Storage.token_id, nat) map; +} + +let mint (param: mint_param) (s: extension storage) : operation list * extension storage = + // The mint implementation supports only payments in XTZ + let verify_currency_is_xtz(acc, tok: bool * Storage.token_id) : bool = + match Big_map.find_opt tok s.extension.asset_infos with + | None -> failwith ErrorsExtension.missing_currency + | Some info -> (info.currency = XTZ) && acc + in + // TODO : to be reviewed for production launch + let verify_currencies : bool = List.fold verify_currency_is_xtz param.ids true in + // TODO : to be reviewed for production launch + let _check_currencies : unit = assert_with_error(verify_currencies = true) ErrorsExtension.unsupported_currency in + + // verify token are mintable + [@inline] let verify_is_mintable(acc, tok: bool * Storage.token_id) = + match Big_map.find_opt tok s.extension.asset_infos with + | None -> failwith ErrorsExtension.missing_mintable_token_id + | Some info -> info.is_mintable && acc + in + let verify_mintable : bool = List.fold verify_is_mintable param.ids true in + let _check_mintable : unit = assert_with_error(verify_mintable = true) ErrorsExtension.token_not_mintable in + + // get total expected amount from initial price and quantity + let get_price(acc, id : ((nat,tez)map * tez) * nat) : (nat,tez)map * tez = + match Big_map.find_opt id s.extension.asset_infos with + | None -> (failwith(ErrorsExtension.missing_asset_info) : (nat,tez)map * tez) + | Some info -> + let requested_amount : nat = match (Map.find_opt id param.quantity : nat option) with + | None -> (failwith(ErrorsExtension.missing_quantity) : nat) + | Some val -> val + in + let amount_by_id = requested_amount * info.initial_price in + ( + Map.add id amount_by_id acc.0, + acc.1 + amount_by_id + ) + in + let (amounts_by_id, expected_total_amount) : (nat,tez)map * tez = List.fold get_price param.ids ((Map.empty : (nat,tez)map), 0tez) in + let amount_ = Tezos.get_amount() in + let _check_amount : unit = assert_with_error(amount_ = expected_total_amount) ErrorsExtension.mint_ins_amount in + + // update ledger + let set_token_owner (acc, elt : (Ledger.t * TotalSupply.t) * Storage.token_id) : (Ledger.t * TotalSupply.t) = + let total_amount : nat = match (Map.find_opt elt param.quantity : nat option) with + | None -> (failwith("quantity has not been defined for this token id") : nat) + | Some val -> val + in + let sender_ = Tezos.get_sender() in + ( + Ledger.increase_token_amount_for_user acc.0 sender_ elt total_amount, + TotalSupply.decrease_available_for_token_id acc.1 elt total_amount + ) + in + let (new_ledger, new_total_supply) : (Ledger.t * TotalSupply.t) = List.fold set_token_owner param.ids (s.ledger, s.extension.total_supply) in + + // update minted_per_wallet map + let update_minted_per_wallet (acc, elt : (address, (nat, nat)map) big_map * Storage.token_id) : (address, (nat, nat)map) big_map = + let max : nat = match (Big_map.find_opt elt s.extension.asset_infos : asset_info option) with + | None -> (failwith("max per wallet has not been defined for this token id") : nat) + | Some info -> info.max_per_wallet + in + let minted_amount : nat = match (Map.find_opt elt param.quantity : nat option) with + | None -> (failwith("quantity has not been defined for this token id") : nat) + | Some val -> val + in + let _check_max_minted : unit = assert_with_error (minted_amount <= max) ErrorsExtension.max_minted_reached in + let sender_ = Tezos.get_sender() in + let new_inner_map : (nat, nat)map = match (Big_map.find_opt sender_ acc : (nat, nat)map option) with + | None -> Map.add elt minted_amount (Map.empty : (nat, nat)map) + | Some val_map -> ( + let inner_value_opt : nat option = Map.find_opt elt val_map in + match inner_value_opt with + | None -> Map.update elt (Some(minted_amount)) val_map + | Some val -> + let _check_max_minted : unit = assert_with_error (val + minted_amount <= max) ErrorsExtension.max_minted_reached in + Map.update elt (Some(val + minted_amount)) val_map + ) + in + Big_map.update sender_ (Some(new_inner_map)) acc + in + let new_minted_per_wallet = List.fold update_minted_per_wallet param.ids s.extension.minted_per_wallet in + + let new_extension : extension = { s.extension with total_supply=new_total_supply; minted_per_wallet = new_minted_per_wallet; } in + + let power (x, y : nat * nat) : nat = + let rec multiply(acc, elt, last: nat * nat * nat ) : nat = if last = 0n then acc else multiply(acc * elt, elt, abs(last - 1n)) in + multiply(1n, x, y) + in + + // take 5% for minteed + let initial_royalties = (Map.empty : (address, tez)map) in + let apply_minteed_share(acc, elt: ((address, tez)map * (nat,tez)map) * (nat * tez)) : ((address, tez)map * (nat,tez)map) = + let current_token_id = elt.0 in + let current_amount = elt.1 in + let minteed_ratio = s.extension.minteed_ratio in + let _check_ratio_outofbound : unit = assert_with_error(minteed_ratio <= 100n) ErrorsExtension.invalid_minteed_ratio in + let minteed_amount = current_amount * minteed_ratio / 100n in + let minteed_address = s.extension.minteed in + let alloc_to_spread = abs(100n - minteed_ratio) in + let alloc_amount = current_amount * alloc_to_spread / 100n in + //update computed royalties for minteed + let acc_royalties = match (Map.find_opt minteed_address acc.0) with + | None -> Map.add minteed_address minteed_amount acc.0 + | Some val -> Map.update minteed_address (Some (val + minteed_amount)) acc.0 + in + //decrease amounts that will be taken into account for allocations + let acc_amounts_by_id = match (Map.find_opt current_token_id acc.1) with + | None -> failwith("ERROR should have an amount for this token_id") + | Some _val -> Map.update current_token_id (Some (alloc_amount)) acc.1 + in + (acc_royalties, acc_amounts_by_id) + in + let (royalties_with_minteed, amounts_by_id_after_minteed) = Map.fold apply_minteed_share amounts_by_id (initial_royalties, amounts_by_id) in + + // compute royalties by recipient + let compute_royalties(acc, elt: (address, tez)map * (nat * tez)) : (address, tez)map = + let current_token_id = elt.0 in + let current_amount = elt.1 in + //retrieve allocation for current token_id + let (allocations, allocations_decimals) = match (Big_map.find_opt current_token_id s.extension.asset_infos) with + | None -> (failwith(ErrorsExtension.missing_asset_info) : (address, nat)map * nat) + | Some info -> (info.allocations, info.allocations_decimals) + in + // apply allocation to each token_id and aggregate by recipient + let compute_distribution(acc_distrib, alloc : (address, tez)map * (address * nat)) : (address, tez)map = + let recipient : address = alloc.0 in + let recipient_ratio : nat = alloc.1 in + let recipient_amount : tez = recipient_ratio * current_amount / power(10n, allocations_decimals) in + match (Map.find_opt recipient acc_distrib) with + | None -> Map.add recipient recipient_amount acc_distrib + | Some val -> Map.update recipient (Some (val+recipient_amount)) acc_distrib + in + Map.fold compute_distribution allocations acc + in + let royalties = Map.fold compute_royalties amounts_by_id_after_minteed royalties_with_minteed in + + // prepare operation to send royalties + let compute_operations(acc, elt : operation list * (address * tez)) : operation list = + let dest_address : address = elt.0 in + let dest_amount : tez = elt.1 in + if (dest_amount > 0mutez) then + let destination_opt : unit contract option = Tezos.get_contract_opt(dest_address) in + let destination : unit contract = match destination_opt with + | None -> (failwith("unknown address") : unit contract) + | Some dest -> dest + in + (Tezos.transaction unit dest_amount destination) :: acc + else + acc + in + let ops : operation list = Map.fold compute_operations royalties ([] : operation list) in + + // redistribute to creator + // let compute_distribution(acc, elt : operation list * (address * nat)) : operation list = + // let dest_address : address = elt.0 in + // let dest_amount : tez = (elt.1 * expected_amount / power(10n, s.extension.allocations_decimals)) in + // let destination_opt : unit contract option = Tezos.get_contract_opt(dest_address) in + // let destination : unit contract = match destination_opt with + // | None -> (failwith("unknown address") : unit contract) + // | Some dest -> dest + // in + // (Tezos.transaction unit dest_amount destination) :: acc + // in + // let ops : operation list = Map.fold compute_distribution s.extension.allocations ([] : operation list) in + (ops, { s with ledger=new_ledger; extension=new_extension }) + //(([] : operation list), s) + +type airdrop_param = [@layout:comb] { + ids : Storage.token_id list; + quantity : (Storage.token_id, nat) map; + to : address +} + +let airdrop (param: airdrop_param) (s: extension storage) : operation list * extension storage = + let amount_ = Tezos.get_amount() in + let _check_amount : unit = assert_with_error(amount_ = 0tez) ErrorsExtension.expects_0_tez in + // update ledger and total_supply + let set_token_owner (acc, elt : (Ledger.t * TotalSupply.t) * Storage.token_id) : (Ledger.t * TotalSupply.t) = + let transfer_amount : nat = match (Map.find_opt elt param.quantity : nat option) with + | None -> (failwith("quantity has not been defined for this token id") : nat) + | Some val -> val + in + ( + Ledger.increase_token_amount_for_user acc.0 param.to elt transfer_amount, + TotalSupply.decrease_reserved_for_token_id acc.1 elt transfer_amount + ) + in + let (new_ledger, new_total_supply) : (Ledger.t * TotalSupply.t) = List.fold set_token_owner param.ids (s.ledger, s.extension.total_supply) in + let new_extension : extension = { s.extension with total_supply=new_total_supply } in + (([]: operation list), { s with ledger=new_ledger; extension=new_extension }) + +let changeCollectionMetadata (new_metadata: bytes) (s: extension storage) : operation list * extension storage = + // TODO: prevent this change if an asset has been sold + let newMetadataMap : (string, bytes) big_map = Big_map.literal [(("contents" : string), new_metadata)] in + let new_extension : extension = { s.extension with metadata=newMetadataMap; } in + (([]: operation list), { s with extension = new_extension }) + +let changeTokenMetadata (p_tokenID, p_newIpfsURI : nat * bytes)(s: extension storage) : operation list * extension storage = + // TODO: prevent this change if an asset has been sold + let new_tokenID : nat = p_tokenID in + let token_newIpfsURI : bytes = p_newIpfsURI in + let new_tokenInfoMap : (string, bytes) map = Map.literal [(("" : string), (token_newIpfsURI))] in + let token_infoRecord : TokenMetadata.data = {token_id = new_tokenID; token_info = new_tokenInfoMap} in + (([]: operation list), { s with token_metadata = Big_map.literal [(new_tokenID, token_infoRecord)] }) + +let switchOpenMint (p_tokenID : nat)(s: extension storage) : operation list * extension storage = + let current_asset_info : asset_info = match Big_map.find_opt p_tokenID s.extension.asset_infos with + | None -> failwith ErrorsExtension.missing_asset_info + | Some info -> info + in + let modified = Big_map.update p_tokenID (Some { current_asset_info with is_mintable=(not current_asset_info.is_mintable) }) s.extension.asset_infos in + let new_extension : extension = { s.extension with asset_infos=modified; } in + (([]: operation list), { s with extension=new_extension }) + + +type changeallocation_param = { + token_id : nat; + allocations : (address, nat)map; + allocations_decimals : nat; +} + +let changeAllocation (param : changeallocation_param)(s: extension storage) : operation list * extension storage = + let current_asset_info : asset_info = match Big_map.find_opt param.token_id s.extension.asset_infos with + | None -> failwith ErrorsExtension.missing_asset_info + | Some info -> info + in + let modified = Big_map.update param.token_id (Some { current_asset_info with allocations=param.allocations; allocations_decimals=param.allocations_decimals }) s.extension.asset_infos in + let new_extension : extension = { s.extension with asset_infos=modified; } in + (([]: operation list), { s with extension=new_extension }) + +type changeMinteedWallet_param = { + addr : address; + ratio : nat +} + +let changeMinteedWallet (param : changeMinteedWallet_param)(s: extension storage) : operation list * extension storage = + let new_extension : extension = { s.extension with minteed=param.addr; minteed_ratio=param.ratio} in + (([]: operation list), { s with extension=new_extension }) + +type parameter = [@layout:comb] + | Transfer of NFT.transfer + | Balance_of of NFT.balance_of + | Update_operators of NFT.update_operators + | Premint of premint_param + | Mint of mint_param + | Airdrop of airdrop_param + | ChangeCollectionMetadata of bytes + | ChangeTokenMetadata of (nat * bytes) + | SwitchOpenMint of nat + | ChangeAllocation of changeallocation_param + | ChangeMinteedWallet of changeMinteedWallet_param + +let main (p, s : parameter * extension storage) : operation list * extension storage = + match p with + Transfer p -> let o1, s = NFT.transfer p s in + let o2, s = transfer_extension p s in + let o = List.fold_left (fun ((a,x):operation list * operation) -> x :: a) o2 o1 in + o, s + | Balance_of p -> NFT.balance_of p s + | Update_operators p -> NFT.update_ops p s + | Mint p -> mint p s + | Airdrop p -> let _ = authorize_admin s in + airdrop p s + | Premint p -> let _ = authorize_admin_and_creator s in + premint p s + | ChangeCollectionMetadata p -> let _ = authorize_admin_and_creator s in + changeCollectionMetadata p s + | ChangeTokenMetadata p -> let _ = authorize_admin_and_creator s in + changeTokenMetadata p s + | SwitchOpenMint p -> let _ = authorize_admin_and_creator s in + switchOpenMint p s + | ChangeAllocation p -> let _ = authorize_admin s in + changeAllocation p s + | ChangeMinteedWallet p -> let _ = authorize_minteed s in + changeMinteedWallet p s + +[@view] let get_balance (p, s : (Address.t * nat) * extension storage) : nat = + let (owner, token_id) = p in + let balance_ = NFT.Storage.get_balance s owner token_id in + balance_ + +// [@view] let total_supply ((token_id, s) : (nat * extension storage)): nat = +// let () = NFT.Storage.assert_token_exist s token_id in +// 1n + +// [@view] let all_tokens ((_, s) : (unit * extension storage)): nat list = +// s.token_ids + +// [@view] let is_operator ((op, s) : (NFT.operator * extension storage)): bool = +// NFT.Operators.is_operator (s.operators, op.owner, op.operator, op.token_id) + +// [@view] let token_metadata ((p, s) : (nat * extension storage)): NFT.TokenMetadata.data = +// NFT.TokenMetadata.get_token_metadata p s.token_metadata diff --git a/ticketing_cameligo/src/generic_fa2/core/instance/errors.mligo b/ticketing_cameligo/src/generic_fa2/core/instance/errors.mligo new file mode 100644 index 00000000..0395b383 --- /dev/null +++ b/ticketing_cameligo/src/generic_fa2/core/instance/errors.mligo @@ -0,0 +1,20 @@ +let mint_ins_amount : string = "MINT_INSUFFICIENT_AMOUNT" +let expects_0_tez : string = "The Airdrop entrypoint expects 0 tez" +let missing_quantity : string = "quantity has not been defined for this token id" +let missing_quantity_reserved : string = "reserved quantity has not been defined for this token id" +let already_preminted : string = "This token_id has already been preminted" +let missing_token_info : string = "Missing token_info" +let only_admin_or_creator : string = "Only admin or creator" +let missing_mintable_token_id : string = "mintable flag has not been defined for this token id" +let token_not_mintable : string = "Token is not flagged as mintable" +let max_minted_reached : string = "Reached maximum minted assets" + +let insuffisant_available_editions : string = "Not enough available editions for this token id" +let insuffisant_reserved_editions : string = "Not enough reserved editions for this token id" +let missing_distribution : string = "Missing distribution for this token id" +let missing_currency : string = "Currency has not been defined for this token id" +let unsupported_currency : string = "Assets can be bought only in XTZ" +let unsupported_sale_mode : string = "Only FCFS (First Come First Served) sale mode is supported" +let missing_asset_info : string = "Missing asset info for this token id" +let only_minteed : string = "Only minteed" +let invalid_minteed_ratio : string = "Invalid minteed fee ratio (should be between 0 and 100)" \ No newline at end of file diff --git a/ticketing_cameligo/src/main.mligo b/ticketing_cameligo/src/main.mligo new file mode 100644 index 00000000..103172e5 --- /dev/null +++ b/ticketing_cameligo/src/main.mligo @@ -0,0 +1,86 @@ +#import "storage.mligo" "Storage" +#import "parameter.mligo" "Parameter" +#import "errors.mligo" "Errors" +//#import "generic_fa2/core/instance/NFT.mligo" "NFT_FA2" +//#import "generic_fa2/core/common/ledger.mligo" "NFT_FA2_LEDGER" + +type storage = Storage.t +type parameter = Parameter.t +type return = operation list * storage + +//type store = NFT_FA2.Storage.t +//type ext = NFT_FA2.extension +//type ext_storage = ext store + + +let buyTicket(param, store : Parameter.buyTicketParam * (Storage.data_storage * Storage.tickets)) : return = + let (data_storage, tickets_map) = store in + let { + ticket_type = ticket_type; + ticket_amount = ticket_amount; + ticket_owner = ticket_owner; + } = param in + + // verify amount + let _check_amount : unit = assert_with_error(ticket_amount >= 1n) "Invalid ticket amount" in + let price_per_type : tez = match Big_map.find_opt ticket_type data_storage.prices with + | None -> failwith("No price specified for this ticket_type") + | Some price -> price + in + let _check_zero_amount = assert_with_error (Tezos.get_amount() > 0mutez) "Expects some tez !" in + let _check_given_amount = assert_with_error (Tezos.get_amount() = ticket_amount * price_per_type) "Insuffisant amount" in + + // retrieve ticket from ticket_map + let (ticket, ticket_map) = Big_map.get_and_update (ticket_owner, ticket_type) (None : Storage.ticket_value option) tickets_map in + + // create/join new ticket + let new_ticket = match ticket with + | None -> Tezos.create_ticket ticket_type ticket_amount + | Some t -> + let nt = Tezos.create_ticket ticket_type ticket_amount in + ( + match Tezos.join_tickets (t.1, nt) with + | None -> failwith("could not join tickets") + | Some t_j -> t_j + ) + in + // update ticket_map + let (_, new_ticket_map) = Big_map.get_and_update (ticket_owner, ticket_type) ((Some(Tezos.get_now(), new_ticket)) : Storage.ticket_value option) ticket_map in + (([] : operation list), { data = data_storage; all_tickets = new_ticket_map }) + + +let redeemTicket(param, store : Parameter.redeemTicketParam * (Storage.data_storage * Storage.tickets)) : return = + let (data_storage, tickets_map) = store in + let { ticket_type = ticket_type; ticket_amount = ticket_amount } = param in + // retrieve ticket from ticket_map + let (ticket_opt, ticket_map) = Big_map.get_and_update (Tezos.get_sender(), ticket_type) (None : Storage.ticket_value option) tickets_map in + let modified_tickets_map : Storage.tickets = match ticket_opt with + | None -> (failwith("No tickets") : Storage.tickets) + | Some tkt -> + let (ticket_creation_timestamp, ticket) = tkt in + let _check_ticket_validity : unit = assert_with_error + (ticket_creation_timestamp + int(data_storage.ticket_duration) > Tezos.get_now()) + "Ticket expired" + in + let ((_ticketer, (_value, amt)), ticket) = Tezos.read_ticket ticket in + let _check_zero_amount : unit = assert_with_error (amt > 0n) "Ticket amount is zero" in + if (amt = ticket_amount) then + ticket_map + else + ( + match Tezos.split_ticket ticket (abs(amt - ticket_amount),ticket_amount) with + | None -> failwith("unsplittable ticket") + | Some splitted_tickets -> + let (resulting_ticket, _to_delete_ticket) = splitted_tickets in + let (_, new_ticket_map) = Big_map.get_and_update (Tezos.get_sender(), ticket_type) ((Some(ticket_creation_timestamp, resulting_ticket)) : Storage.ticket_value option) ticket_map in + new_ticket_map + ) + in + (([] : operation list), { data = data_storage; all_tickets = modified_tickets_map }) + +let main(p, store : parameter * storage) : return = + let { data = d; all_tickets = tickets_map} = store in + match p with + | BuyTicket param -> buyTicket(param, (d, tickets_map)) + | RedeemTicket param -> redeemTicket(param, (d, tickets_map)) + diff --git a/ticketing_cameligo/src/parameter.mligo b/ticketing_cameligo/src/parameter.mligo new file mode 100644 index 00000000..4744b311 --- /dev/null +++ b/ticketing_cameligo/src/parameter.mligo @@ -0,0 +1,14 @@ +#import "generic_fa2/core/instance/NFT.mligo" "NFT_FA2" + +type buyTicketParam = [@layout:comb] { + ticket_amount : nat; + ticket_owner : address; + ticket_type : string; +} + +type redeemTicketParam = [@layout:comb] { + ticket_type : string; + ticket_amount : nat; +} + +type t = BuyTicket of buyTicketParam | RedeemTicket of redeemTicketParam \ No newline at end of file diff --git a/ticketing_cameligo/src/storage.mligo b/ticketing_cameligo/src/storage.mligo new file mode 100644 index 00000000..efb65f6a --- /dev/null +++ b/ticketing_cameligo/src/storage.mligo @@ -0,0 +1,22 @@ +type owner = address +type ticket_type = string +type ticket_value = timestamp * ticket_type ticket + +type 'a tickets_ = ((owner * ticket_type), 'a) big_map +type tickets = ticket_value tickets_ + +type ticket_prices = (ticket_type, tez) big_map + +type data_storage = { + prices : ticket_prices; + ticket_duration : nat; + metadata: (string, bytes) big_map; +} + +type 'a t_ = [@layout:comb] { + all_tickets : 'a tickets_; + data : data_storage; +} + +type t = ticket_value t_ + diff --git a/ticketing_cameligo/test/ligo/bootstrap/bootstrap.mligo b/ticketing_cameligo/test/ligo/bootstrap/bootstrap.mligo new file mode 100644 index 00000000..9f44b87e --- /dev/null +++ b/ticketing_cameligo/test/ligo/bootstrap/bootstrap.mligo @@ -0,0 +1,32 @@ +#import "../helpers/ticketing.mligo" "Ticketing_helper" +#import "../helpers/nft.mligo" "NFT_helper" + +(* Boostrapping of the test environment for Factory *) +let boot_ticketing () = + let () = Test.reset_state 6n ([] : tez list) in + + let accounts = + Test.nth_bootstrap_account 1, + Test.nth_bootstrap_account 2 + in + + let ticketing = Ticketing_helper.originate(Ticketing_helper.base_storage) in + (accounts, ticketing) + +(* Boostrapping of the test environment for NFT *) +let boot_nft () = + let () = Test.reset_state 6n ([] : tez list) in + + let (admin, creator) = + Test.nth_bootstrap_account 1, + Test.nth_bootstrap_account 2 + in + let accounts = admin, creator, + Test.nth_bootstrap_account 3, + Test.nth_bootstrap_account 4, + Test.nth_bootstrap_account 5 + in + + //let nft = NFT_helper.originate(NFT_helper.base_storage(admin, creator)) in + let nft = NFT_helper.originate_from_file(NFT_helper.base_storage(admin, creator, accounts.4)) in + (accounts, nft) diff --git a/ticketing_cameligo/test/ligo/helpers/assert.mligo b/ticketing_cameligo/test/ligo/helpers/assert.mligo new file mode 100644 index 00000000..4c3ce540 --- /dev/null +++ b/ticketing_cameligo/test/ligo/helpers/assert.mligo @@ -0,0 +1,15 @@ +(* Assert contract call results in failwith with given string *) +let string_failure (res : test_exec_result) (expected : string) : unit = + let expected = Test.eval expected in + match res with + | Fail (Rejected (actual,_)) -> assert (actual = expected) + | Fail (Balance_too_low _err) -> failwith "contract failed: balance too low" + | Fail (Other s) -> failwith s + | Success _ -> failwith "Transaction should fail" + +(* Assert contract result is successful *) +let tx_success (res: test_exec_result) : unit = + match res with + | Success(_) -> () + | Fail (Rejected (error,_)) -> let () = Test.log(error) in Test.failwith "Transaction should not fail" + | Fail _ -> failwith "Transaction should not fail" diff --git a/ticketing_cameligo/test/ligo/helpers/log.mligo b/ticketing_cameligo/test/ligo/helpers/log.mligo new file mode 100644 index 00000000..99db4e0e --- /dev/null +++ b/ticketing_cameligo/test/ligo/helpers/log.mligo @@ -0,0 +1,18 @@ +(* Return str repeated n times *) +let repeat (str, n : string * nat) : string = + let rec loop (n, acc: nat * string) : string = + if n = 0n then acc else loop (abs(n - 1n), acc ^ str) + in loop(n, "") + +(* + Log boxed lbl + + "+-----------+" + "| My string |" + "+-----------+" +*) +let describe (lbl : string) = + let hr = "+" ^ repeat("-", String.length(lbl) + 2n) ^ "+" in + let () = Test.log hr in + let () = Test.log ("| " ^ lbl ^ " |") in + Test.log hr diff --git a/ticketing_cameligo/test/ligo/helpers/nft.mligo b/ticketing_cameligo/test/ligo/helpers/nft.mligo new file mode 100644 index 00000000..c43201c7 --- /dev/null +++ b/ticketing_cameligo/test/ligo/helpers/nft.mligo @@ -0,0 +1,174 @@ +#import "../../../src/generic_fa2/core/instance/NFT.mligo" "NFT" +#import "./assert.mligo" "Assert" + +(* Some types for readability *) +type nft_ext = NFT.extension +type nft_storage = NFT.storage +type extended_storage = nft_ext nft_storage + +type taddr = (NFT.parameter, extended_storage) typed_address +type contr = NFT.parameter contract +type originated = { + addr: address; + taddr: taddr; + contr: contr; +} + +(* Some dummy values intended to be used as placeholders *) +let dummy_token_info = + Map.literal [("", + Bytes.pack "ipfs://QmbKq7QriWWU74NSq35sDSgUf24bYWTgpBq3Lea7A3d7jU")] + +(* Base NFT storage *) +let base_storage (admin, creator, minteed : address * address * address) : extended_storage = { + ledger = (Big_map.empty : NFT.Ledger.t); + token_metadata = (Big_map.empty : NFT.TokenMetadata.t); + operators = (Big_map.empty : NFT.Storage.Operators.t); + token_ids = ([] : NFT.Storage.token_id list); + + (* extension *) + extension = { + admin = admin; + creator = creator; + minteed = minteed; + minteed_ratio = 5n; + total_supply = (Big_map.empty : NFT.TotalSupply.t); + metadata = (Big_map.empty : (string, bytes) big_map); + next_token_id=0n; + minted_per_wallet = (Big_map.empty : (address, (nat, nat)map) big_map); + asset_infos = (Big_map.empty : (nat, NFT.asset_info) big_map); + } +} + +(* Originate an NFT contract with given init_storage storage *) +let originate (init_storage : extended_storage) = + let (taddr, _, _) = Test.originate NFT.main init_storage 0mutez in + let contr = Test.to_contract taddr in + let addr = Tezos.address contr in + {addr = addr; taddr = taddr; contr = contr} + +(* + Originate an NFT contract with given init_storage storage + Use this one if you need access to views +*) +let originate_from_file (init_storage : extended_storage) = + let f = "./src/generic_fa2/core/instance/NFT.mligo" in + let v_mich = Test.run (fun (x:extended_storage) -> x) init_storage in + let (addr, _, _) = Test.originate_from_file f "main" ["get_balance"] v_mich 0tez in + let taddr : taddr = Test.cast_address addr in + let contr = Test.to_contract taddr in + {addr = addr; taddr = taddr; contr = contr} + +(* Call entry point of NFT contr contract *) +let call (p, contr : NFT.parameter * contr) = + Test.transfer_to_contract contr p 0mutez + +(* Call entry point of NFT contr contract with amount *) +let call_with_amount (p, amount_, contr : NFT.parameter * tez * contr) = + Test.transfer_to_contract contr p amount_ + +(* Entry points call helpers *) +let premint (p, contr : NFT.premint_param * contr) = call(Premint(p), contr) +let mint (p, amount_, contr : NFT.mint_param * tez * contr) = + call_with_amount(Mint(p), amount_, contr) +let airdrop (p, contr : NFT.airdrop_param * contr) = + call(Airdrop(p), contr) +let changeallocation (p, contr : NFT.changeallocation_param * contr) = + call(ChangeAllocation(p), contr) +let changeminteedwallet (p, contr : NFT.changeMinteedWallet_param * contr) = + call(ChangeMinteedWallet(p), contr) + +let update_operators (p, contr : NFT.NFT.update_operators * contr) = + call(Update_operators(p), contr) + +(* Asserter helper for successful entry point calls *) +let premint_success (p, contr : NFT.premint_param * contr) = + Assert.tx_success (premint(p, contr)) + +let mint_success (p, amount_, contr : NFT.mint_param * tez * contr) = + Assert.tx_success (mint(p, amount_, contr)) + +let airdrop_success (p, contr : NFT.airdrop_param * contr) = + Assert.tx_success (airdrop(p, contr)) + +let changeallocation_success (p, contr : NFT.changeallocation_param * contr) = + Assert.tx_success (changeallocation(p, contr)) + +let changeminteedwallet_success(p, contr : NFT.changeMinteedWallet_param * contr) = + Assert.tx_success (changeminteedwallet(p, contr)) + +let update_operators_success (p, contr : NFT.NFT.update_operators * contr) = + Assert.tx_success (update_operators(p, contr)) + +(* assert NFT contract at [taddr] have [owner] address, token id pair with [amount_] in its ledger *) +let assert_balance (taddr, owned, amount_ : + taddr * (NFT.Ledger.owner * NFT.Ledger.token_id) * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt owned s.ledger with + Some tokens -> assert(tokens = amount_) + | None -> failwith("Big_map key should not be missing") + +(* assert NFT contract at [taddr] have [token_id] initial price of [amount_] *) +let assert_price (taddr, token_id, expected_price : taddr * nat * tez) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.asset_infos with + | Some asset_info -> assert(asset_info.initial_price = expected_price) + | None -> failwith("Big_map key should not be missing") + + +(* assert NFT contract at [taddr] have [token_id] initial supply of [amount_] *) +let assert_total_supply (taddr, token_id, amount_ : taddr * nat * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.total_supply with + Some supply -> assert(supply.total = amount_) + | None -> failwith("Big_map key should not be missing") + +let assert_available_supply (taddr, token_id, amount_ : taddr * nat * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.total_supply with + Some supply -> assert(supply.available = amount_) + | None -> failwith("Big_map key should not be missing") + +let assert_reserved_supply (taddr, token_id, amount_ : taddr * nat * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.total_supply with + Some supply -> assert(supply.reserved = amount_) + | None -> failwith("Big_map key should not be missing") + +(* assert NFT contract at [taddr] have [token_id] initial mintable of [expected_mintable] *) +let assert_is_mintable (taddr, token_id, expected_mintable : taddr * nat * bool) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.asset_infos with + | Some asset_info -> assert(asset_info.is_mintable = expected_mintable) + | None -> failwith("Big_map key should not be missing") + +(* assert NFT contract at [taddr] have [token_id] initial max_per_wallet of [expected_max_per_wallet] *) +let assert_max_per_wallet (taddr, token_id, expected_max_per_wallet : taddr * nat * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.asset_infos with + | Some asset_info -> assert(asset_info.max_per_wallet = expected_max_per_wallet) + | None -> failwith("Big_map key should not be missing") + + +(* assert NFT contract at [taddr] have [token_id] allocations for a [recipient] address of [expected_ratio] *) +let assert_allocation (taddr, token_id, recipient, expected_ratio : taddr * nat * address * nat) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.asset_infos with + | None -> failwith("Big_map key should not be missing") + | Some asset_info -> ( + match Map.find_opt recipient asset_info.allocations with + | None -> failwith("Recipient is not registered in allocations") + | Some ratio -> assert(ratio = expected_ratio) + ) + +(* assert NFT contract at [taddr] have [token_id] initialuuid of [expected_uuid] *) +let assert_uuid (taddr, token_id, expected_uuid : taddr * nat * string option) = + let s = Test.get_storage taddr in + match Big_map.find_opt token_id s.extension.asset_infos with + | Some asset_info -> assert(asset_info.uuid = expected_uuid) + | None -> failwith("Big_map key should not be missing") + +(* assert NFT contract at [taddr] have [token_id] initial mintable of [expected_mintable] *) +let assert_minteedwallet (taddr, expected_minteed_wallet : taddr * address) = + let s = Test.get_storage taddr in + assert(s.extension.minteed = expected_minteed_wallet) diff --git a/ticketing_cameligo/test/ligo/helpers/ticketing.mligo b/ticketing_cameligo/test/ligo/helpers/ticketing.mligo new file mode 100644 index 00000000..6c1b5076 --- /dev/null +++ b/ticketing_cameligo/test/ligo/helpers/ticketing.mligo @@ -0,0 +1,82 @@ +#import "../../../src/main.mligo" "Ticketing" +#import "./assert.mligo" "Assert" +#import "./nft.mligo" "NFT_helper" + +type unforged_ticket_value = timestamp * Ticketing.Storage.ticket_type unforged_ticket + +type ticketStoragePolymorphiq = Ticketing.Storage.t_ +type unforged_t = unforged_ticket_value ticketStoragePolymorphiq + + +(* Some types for readability *) +type taddr = (Ticketing.parameter, Ticketing.storage) typed_address +type contr = Ticketing.parameter contract +type originated = { + addr: address; + taddr: taddr; + contr: contr; +} + +let metadata_empty : (string, bytes) big_map = (Big_map.empty : (string, bytes) big_map) + +(* Base Ticketing storage *) +let base_storage : Ticketing.storage = { + all_tickets = (Big_map.empty : Ticketing.Storage.tickets); + data = { + prices = Big_map.literal[("PARKING", 10tez)]; + ticket_duration = 1000n; + metadata = metadata_empty + } +} + +(* Originate a Ticketing contract with given init_storage storage *) +let originate (init_storage : Ticketing.storage) = + let (taddr, _, _) = Test.originate Ticketing.main init_storage 0mutez in + let contr = Test.to_contract taddr in + let addr = Tezos.address contr in + {addr = addr; taddr = taddr; contr = contr} + +(* Call entry point of Ticketing contr contract *) +let call (p, contr : Ticketing.parameter * contr) = + Test.transfer_to_contract contr (p) 0mutez + +(* Call entry point of Ticketing contr contract with amount *) +let call_with_amount (p, amount_, contr : Ticketing.parameter * tez * contr) = + Test.transfer_to_contract contr p amount_ + +let call_success (p, contr: Ticketing.parameter * contr) = + Assert.tx_success (call(p, contr)) + +(* Entry points call helpers *) +let redeem_ticket (p, contr : Ticketing.Parameter.redeemTicketParam * contr) = + call(RedeemTicket(p), contr) + +let buy_ticket (p, amount_, contr : Ticketing.Parameter.buyTicketParam * tez * contr) = + call_with_amount(BuyTicket(p), amount_, contr) + +(* Asserter helper for successful entry point calls *) +let redeem_ticket_success (p, contr : Ticketing.Parameter.redeemTicketParam * contr) = + Assert.tx_success (redeem_ticket(p, contr)) + +let buy_ticket_success (p, amount_, contr : Ticketing.Parameter.buyTicketParam * tez * contr) = + Assert.tx_success (buy_ticket(p, amount_, contr)) + +(* assert Ticketing contract at [taddr] have [owner] address with [amount] tickets *) +let assert_owned_ticket_amount (addr, taddr, owner, ticket_type, _amount : address * taddr * Ticketing.Storage.owner * Ticketing.Storage.ticket_type * nat) = + let storage : michelson_program = Test.get_storage_of_address addr in + let unforged_storage = (Test.decompile storage : unforged_t) in + let () = Test.log(unforged_storage) in + + + let s = Test.get_storage taddr in + let (ticket, _ticket_map) = Big_map.get_and_update (owner, ticket_type) (None : Ticketing.Storage.ticket_value option) s.all_tickets in + let ticket_to_decompile : michelson_program = match ticket with + | None -> Test.failwith("ticket not found") + | Some tkt -> Test.eval tkt.1 + in + let decompiled_ticket = (Test.decompile ticket_to_decompile : unforged_ticket_value) in + Test.log(decompiled_ticket) + + //let { ticketer = ticketer ; value =value ; amount = amt } = tval in + //let (tt, (ticketer, (value, amt))) : string unforged_ticket = tval in + // //let ((_,(_, amt)), tval) = Tezos.read_ticket tval in diff --git a/ticketing_cameligo/test/ligo/ticketing.test.mligo b/ticketing_cameligo/test/ligo/ticketing.test.mligo new file mode 100644 index 00000000..f546998f --- /dev/null +++ b/ticketing_cameligo/test/ligo/ticketing.test.mligo @@ -0,0 +1,59 @@ +#import "./helpers/assert.mligo" "Assert" +#import "./bootstrap/bootstrap.mligo" "Bootstrap" +#import "./helpers/log.mligo" "Log" +#import "./helpers/ticketing.mligo" "Ticketing_helper" +#import "./helpers/nft.mligo" "NFT_helper" +#import "../../src/main.mligo" "Ticketing" + +let () = Log.describe("[Ticketing] test suite") + +let bootstrap () = Bootstrap.boot_ticketing() + +let test_success_buy_ticket = + let (accounts, ticketing) = bootstrap() in + let (creator, _operator) = accounts in + let () = Test.set_source creator in + let ret = Ticketing_helper.buy_ticket({ + ticket_type = "PARKING"; + ticket_amount = 2n; + ticket_owner = creator; + }, 20tez, ticketing.contr) in + let () = Test.log(ret) in + Ticketing_helper.assert_owned_ticket_amount(ticketing.addr, ticketing.taddr, creator, "PARKING", 1n) + +// let test_success_nft_origination_with_uuid = +// let (accounts, ticketing) = bootstrap() in +// let (creator, operator) = accounts in +// let () = Test.set_source operator in +// let uuid : string = "abcdef" in +// let () = Ticketing_helper.call_success({ +// name = "collection_name"; +// creator = creator; +// metadata = metadata_empty; +// uuid = (Some(uuid) : string option); +// assets_infos = Ticketing_helper.dummy_assets_info(creator, operator); +// }, ticketing.contr) in +// let () = Ticketing_helper.assert_owned_collections_size(ticketing.taddr, creator, 1n) in +// Ticketing_helper.assert_uuid_collection(ticketing.taddr, uuid, true) + +// let test_failure_nft_origination_with_already_used_uuid = +// let (accounts, ticketing) = bootstrap() in +// let (creator, operator) = accounts in +// let () = Test.set_source operator in +// let uuid : string = "abcdef" in +// let () = Ticketing_helper.call_success({ +// name = "collection_name"; +// creator = creator; +// metadata = metadata_empty; +// uuid = (Some(uuid) : string option); +// assets_infos = Ticketing_helper.dummy_assets_info(creator, operator); +// }, ticketing.contr) in +// let () = Ticketing_helper.assert_owned_collections_size(ticketing.taddr, creator, 1n) in +// let r2 = Ticketing_helper.call({ +// name = "collection_name_2"; +// creator = creator; +// metadata = metadata_empty; +// uuid = (Some(uuid) : string option); +// assets_infos = Ticketing_helper.dummy_assets_info(creator, operator); +// }, ticketing.contr) in +// Assert.string_failure r2 Ticketing.Errors.uuid_already_used