From dfebaacc3e2cd28d6e2e8edfcb265dbb180bdac4 Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Fri, 21 Mar 2025 14:44:53 -0400 Subject: [PATCH 1/3] First draft of files for updated TacoShop tutorial --- ligo-tacoshop/.gitignore | 2 + ligo-tacoshop/README.md | 5 + ligo-tacoshop/taco_shop_1.jsligo | 82 +++++++++++ ligo-tacoshop/taco_shop_1.mligo | 74 ++++++++++ ligo-tacoshop/taco_shop_2.jsligo | 162 ++++++++++++++++++++++ ligo-tacoshop/taco_shop_2.mligo | 158 +++++++++++++++++++++ ligo-tacoshop/taco_shop_3.jsligo | 229 +++++++++++++++++++++++++++++++ ligo-tacoshop/taco_shop_3.mligo | 195 ++++++++++++++++++++++++++ 8 files changed, 907 insertions(+) create mode 100644 ligo-tacoshop/.gitignore create mode 100644 ligo-tacoshop/README.md create mode 100644 ligo-tacoshop/taco_shop_1.jsligo create mode 100644 ligo-tacoshop/taco_shop_1.mligo create mode 100644 ligo-tacoshop/taco_shop_2.jsligo create mode 100644 ligo-tacoshop/taco_shop_2.mligo create mode 100644 ligo-tacoshop/taco_shop_3.jsligo create mode 100644 ligo-tacoshop/taco_shop_3.mligo diff --git a/ligo-tacoshop/.gitignore b/ligo-tacoshop/.gitignore new file mode 100644 index 0000000..80aafad --- /dev/null +++ b/ligo-tacoshop/.gitignore @@ -0,0 +1,2 @@ +*.tz +.ligo diff --git a/ligo-tacoshop/README.md b/ligo-tacoshop/README.md new file mode 100644 index 0000000..5ef630b --- /dev/null +++ b/ligo-tacoshop/README.md @@ -0,0 +1,5 @@ +# LIGO Taco Shop tutorial + +These are the completed contract files for the LIGO Taco Shop tutorial here: + +https://ligolang.org/docs/tutorials/taco-shop/selling-tacos diff --git a/ligo-tacoshop/taco_shop_1.jsligo b/ligo-tacoshop/taco_shop_1.jsligo new file mode 100644 index 0000000..83685e3 --- /dev/null +++ b/ligo-tacoshop/taco_shop_1.jsligo @@ -0,0 +1,82 @@ +namespace TacoShop { + export type taco_supply = { current_stock: nat, max_price: tez }; + export type taco_data = map; + export type admin_address = address; + export type storage = { + admin_address: admin_address, + taco_data: taco_data, + }; + + export const default_taco_data: taco_data = Map.literal([ + [1n, { current_stock: 50n, max_price: 50tez }], + [2n, { current_stock: 20n, max_price: 75tez }] + ]); + + // Internal function to get the price of a taco + const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco") + }; + return taco_kind.max_price / taco_kind.current_stock; + } + + @view + const get_taco_price = (taco_kind_index: nat, storage: storage): tez => + get_taco_price_internal(taco_kind_index, storage.taco_data); + + // Buy a taco + @entry + const buy_taco = (taco_kind_index: nat, storage: storage): [ + list, + storage + ] => { + + const { admin_address, taco_data } = storage; + + // Retrieve the kind of taco from the contracts storage or fail + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco"); + }; + + // Get the current price of this type of taco + const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); + + // Verify that the caller sent the correct amount of tez + if ((Tezos.get_amount()) != current_purchase_price) { + return failwith("Sorry, the taco you are trying to purchase has a different price"); + } + + // Verify that there is at least one of this type of taco + if (taco_kind.current_stock == 0n) { + return failwith("Sorry, we are out of this type of taco"); + } + + // Update the storage with the new quantity of tacos + const updated_taco_data: taco_data = Map.update( + taco_kind_index, + (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + taco_data); + + const updated_storage: storage = { + admin_address: admin_address, + taco_data: updated_taco_data, + }; + + return [[], updated_storage]; + } + + @entry + const payout = (_u: unit, storage: storage): [ + list, + storage + ] => { + + // Entrypoint logic goes here + + return [[], storage]; + } +}; \ No newline at end of file diff --git a/ligo-tacoshop/taco_shop_1.mligo b/ligo-tacoshop/taco_shop_1.mligo new file mode 100644 index 0000000..c165b31 --- /dev/null +++ b/ligo-tacoshop/taco_shop_1.mligo @@ -0,0 +1,74 @@ +module TacoShop = struct + + type taco_supply = { current_stock: nat; max_price: tez } + type taco_data = (nat, taco_supply) map + type admin_address = address + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + + (* Internal function to get the price of a taco *) + let get_taco_price_internal (taco_kind_index : nat) (taco_data : taco_data) : tez = + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" + in + taco_kind.max_price / taco_kind.current_stock + + [@view] + let get_taco_price (taco_kind_index : nat) (storage : storage) : tez = + get_taco_price_internal taco_kind_index storage.taco_data + + (* Buy a taco *) + [@entry] + let buy_taco (taco_kind_index : nat) (storage : storage) : operation list * storage = + + let { admin_address; taco_data } = storage in + + (* Retrieve the kind of taco from the contracts storage or fail *) + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" in + + (* Get the current price of this type of taco *) + let current_purchase_price = get_taco_price_internal taco_kind_index taco_data in + + (* Verify that the caller sent the correct amount of tez *) + let _ = if (Tezos.get_amount () <> current_purchase_price) then + failwith "Sorry, the taco you are trying to purchase has a different price" in + + (* Verify that there is at least one of this type of taco *) + let _ = if (taco_kind.current_stock = 0n) then + failwith "Sorry, we are out of this type of taco" in + + + (* Update the storage with the new quantity of tacos *) + let updated_taco_data : taco_data = Map.update + taco_kind_index + (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) + taco_data in + + + let updated_storage : storage = { + admin_address = admin_address; + taco_data = updated_taco_data; + } in + + [], updated_storage + + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Entrypoint logic goes here *) + + [], storage + + end \ No newline at end of file diff --git a/ligo-tacoshop/taco_shop_2.jsligo b/ligo-tacoshop/taco_shop_2.jsligo new file mode 100644 index 0000000..2c301f2 --- /dev/null +++ b/ligo-tacoshop/taco_shop_2.jsligo @@ -0,0 +1,162 @@ +import Test = Test.Next; +import Tezos = Tezos.Next; + +namespace TacoShop { + export type taco_supply = { current_stock: nat, max_price: tez }; + export type taco_data = map; + export type admin_address = address; + export type storage = { + admin_address: admin_address, + taco_data: taco_data, + }; + + export const default_taco_data: taco_data = Map.literal ([ + [1n, { current_stock: 50n, max_price: 50tez }], + [2n, { current_stock: 20n, max_price: 75tez }] + ]); + + // Internal function to get the price of a taco + const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco") + }; + return taco_kind.max_price / taco_kind.current_stock; + } + + @view + const get_taco_price = (taco_kind_index: nat, storage: storage): tez => + get_taco_price_internal(taco_kind_index, storage.taco_data); + + // Buy a taco + @entry + const buy_taco = (taco_kind_index: nat, storage: storage): [ + list, + storage + ] => { + + const { admin_address, taco_data } = storage; + + // Retrieve the kind of taco from the contracts storage or fail + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco"); + }; + + // Get the current price of this type of taco + const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); + + // Verify that the caller sent the correct amount of tez + if ((Tezos.get_amount()) != current_purchase_price) { + return failwith("Sorry, the taco you are trying to purchase has a different price"); + } + + // Verify that there is at least one of this type of taco + if (taco_kind.current_stock == 0n) { + return failwith("Sorry, we are out of this type of taco"); + } + + // Update the storage with the new quantity of tacos + const updated_taco_data: taco_data = Map.update( + taco_kind_index, + (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + taco_data); + + const updated_storage: storage = { + admin_address: admin_address, + taco_data: updated_taco_data, + }; + + return [[], updated_storage]; + } + + @entry + const payout = (_u: unit, storage: storage): [ + list, + storage + ] => { + + // Entrypoint logic goes here + + return [[], storage]; + } +}; + +// Convenience function to get current taco price +const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return match(view_result_option) { + when(Some(cost_mutez)): cost_mutez; + when(None()): Test.failwith("Couldn't get the price of the taco.") + }; +} + +// Convenience function for testing equality in maps +const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => + match(Map.find_opt(k, m)) { + when(None): + false + when(Some(v)): + v.current_stock == r.current_stock && v.max_price == r.max_price + }; + +const test = (() => { + + // Set the initial storage and deploy the contract + const admin_address: address = Test.Account.address(0n); + const initial_storage: TacoShop.storage = { + admin_address: admin_address, + taco_data: TacoShop.default_taco_data, + } + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0tez); + + // Get the current price of a taco + const untyped_address = Test.Typed_address.to_address(contract.taddr); + const current_price = get_taco_price(untyped_address, 1n); + + // Purchase a taco + const success_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1n, + current_price + ); + + // Verify that the stock was updated + match(success_result) { + when(Success(_s)): + do { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been updated correctly + Assert.assert( + eq_in_map( + { current_stock: 49n, max_price: 50000000mutez }, + storage.taco_data, + 1n + )); + // Check that the amount of the other taco type has not changed + Assert.assert(eq_in_map( + { current_stock: 20n, max_price: 75000000mutez }, + storage.taco_data, + 2n + ) + ); + Test.IO.log("Successfully bought a taco"); + } + when(Fail(err)): failwith(err); + }; + + // Fail to purchase a taco without sending enough tez + const fail_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1n, + 1mutez + ); + match(fail_result) { + when(Success(_s)): failwith("Test was able to buy a taco for the wrong price"); + when(Fail(_err)): Test.IO.log("Contract successfully blocked purchase with incorrect price"); + }; +}) (); diff --git a/ligo-tacoshop/taco_shop_2.mligo b/ligo-tacoshop/taco_shop_2.mligo new file mode 100644 index 0000000..0277d8a --- /dev/null +++ b/ligo-tacoshop/taco_shop_2.mligo @@ -0,0 +1,158 @@ +module Test = Test.Next +module Tezos = Tezos.Next + +module TacoShop = struct + + type taco_supply = { current_stock: nat; max_price: tez } + type taco_data = (nat, taco_supply) map + type admin_address = address + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + + (* Internal function to get the price of a taco *) + let get_taco_price_internal (taco_kind_index : nat) (taco_data : taco_data) : tez = + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" + in + taco_kind.max_price / taco_kind.current_stock + + [@view] + let get_taco_price (taco_kind_index : nat) (storage : storage) : tez = + get_taco_price_internal taco_kind_index storage.taco_data + + (* Buy a taco *) + [@entry] + let buy_taco (taco_kind_index : nat) (storage : storage) : operation list * storage = + + let { admin_address; taco_data } = storage in + + (* Retrieve the kind of taco from the contracts storage or fail *) + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" in + + (* Get the current price of this type of taco *) + let current_purchase_price = get_taco_price_internal taco_kind_index taco_data in + + (* Verify that the caller sent the correct amount of tez *) + let _ = if (Tezos.get_amount () <> current_purchase_price) then + failwith "Sorry, the taco you are trying to purchase has a different price" in + + (* Verify that there is at least one of this type of taco *) + let _ = if (taco_kind.current_stock = 0n) then + failwith "Sorry, we are out of this type of taco" in + + + (* Update the storage with the new quantity of tacos *) + let updated_taco_data : taco_data = Map.update + taco_kind_index + (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) + taco_data in + + + let updated_storage : storage = { + admin_address = admin_address; + taco_data = updated_taco_data; + } in + + [], updated_storage + + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Ensure that only the admin can call this entrypoint *) + let _ = if (Tezos.get_sender () <> storage.admin_address) then + failwith "Only the admin can call this entrypoint" in + + (* Create contract object that represents the target account *) + let receiver_contract = match Tezos.get_contract_opt storage.admin_address with + | Some contract -> contract + | None -> failwith "Couldn't find account" in + + (* Create operation to send tez *) + let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + + (* Restore stock of tacos *) + let new_storage : storage = { + admin_address = storage.admin_address; + taco_data = default_taco_data + } in + + [payout_operation], new_storage + +end + +(* Convenience function to get current taco price *) +let get_taco_price (untyped_address : address) (taco_kind_index : nat) : tez = + let view_result_option : tez option = Tezos.View.call + "get_taco_price" + taco_kind_index + untyped_address in + match view_result_option with + | Some cost_mutez -> cost_mutez + | None -> Test.failwith "Couldn't get the price of a taco" + +(* Convenience function for testing equality in maps *) +let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_data) (k : nat) = + match Map.find_opt k m with + | None -> false + | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price + +let test = + + (* Set the initial storage and deploy the contract *) + let admin_address : address = Test.Account.address 0n in + let initial_storage : TacoShop.storage = { + admin_address = admin_address; + taco_data = TacoShop.default_taco_data + } in + let contract = Test.Originate.contract (contract_of TacoShop) initial_storage 0tez in + + (* Get the current price of a taco *) + let untyped_address = Test.Typed_address.to_address contract.taddr in + let current_price = get_taco_price untyped_address 1n in + + (* Purchase a taco *) + let success_result = + Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + current_price + in + + (* Verify that the stock was updated *) + let () = match success_result with + | Success _s -> + let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert (eq_in_map + { current_stock = 49n; max_price = 50000000mutez } + storage.taco_data + 1n + ) in + let () = Assert.assert (eq_in_map + { current_stock = 20n; max_price = 75000000mutez } + storage.taco_data + 2n + ) in + Test.IO.log "Successfully bought a taco" + | Fail err -> failwith err + in + + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" \ No newline at end of file diff --git a/ligo-tacoshop/taco_shop_3.jsligo b/ligo-tacoshop/taco_shop_3.jsligo new file mode 100644 index 0000000..bdf41cc --- /dev/null +++ b/ligo-tacoshop/taco_shop_3.jsligo @@ -0,0 +1,229 @@ +import Test = Test.Next; +import Tezos = Tezos.Next; + +namespace TacoShop { + export type taco_supply = { current_stock: nat, max_price: tez }; + export type taco_data = map; + export type admin_address = address; + export type storage = { + admin_address: admin_address, + taco_data: taco_data, + }; + + export const default_taco_data: taco_data = Map.literal ([ + [1n, { current_stock: 50n, max_price: 50tez }], + [2n, { current_stock: 20n, max_price: 75tez }] + ]); + + // Internal function to get the price of a taco + const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco") + }; + return taco_kind.max_price / taco_kind.current_stock; + } + + @view + const get_taco_price = (taco_kind_index: nat, storage: storage): tez => + get_taco_price_internal(taco_kind_index, storage.taco_data); + + // Buy a taco + @entry + const buy_taco = (taco_kind_index: nat, storage: storage): [ + list, + storage + ] => { + + const { admin_address, taco_data } = storage; + + // Retrieve the kind of taco from the contracts storage or fail + const taco_kind: taco_supply = + match (Map.find_opt(taco_kind_index, taco_data)) { + when(Some(kind)): kind; + when(None()): failwith("Unknown kind of taco"); + }; + + // Get the current price of this type of taco + const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); + + // Verify that the caller sent the correct amount of tez + if ((Tezos.get_amount()) != current_purchase_price) { + return failwith("Sorry, the taco you are trying to purchase has a different price"); + } + + // Verify that there is at least one of this type of taco + if (taco_kind.current_stock == 0n) { + return failwith("Sorry, we are out of this type of taco"); + } + + // Update the storage with the new quantity of tacos + const updated_taco_data: taco_data = Map.update( + taco_kind_index, + (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + taco_data); + + const updated_storage: storage = { + admin_address: admin_address, + taco_data: updated_taco_data, + }; + + return [[], updated_storage]; + } + + @entry + const payout = (_u: unit, storage: storage): [ + list, + storage + ] => { + + // Ensure that only the admin can call this entrypoint + if (Tezos.get_sender() != storage.admin_address) { + failwith("Only the admin can call this entrypoint"); + } + + // Create contract object that represents the target account + const receiver_contract = match(Tezos.get_contract_opt(storage.admin_address)) { + when(Some(contract)): contract; + when(None): failwith("Couldn't find account"); + }; + + // Create operation to send tez + const payout_operation = Tezos.Operation.transaction(unit, Tezos.get_balance(), receiver_contract); + + // Restore stock of tacos + const new_storage: storage = { + admin_address: storage.admin_address, + taco_data: default_taco_data, + }; + + return [[payout_operation], new_storage]; + } +}; + +// Convenience function to get current taco price +const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return match(view_result_option) { + when(Some(cost_mutez)): cost_mutez; + when(None()): Test.failwith("Couldn't get the price of the taco.") + }; +} + +// Convenience function for testing equality in maps +const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => + match(Map.find_opt(k, m)) { + when(None): + false + when(Some(v)): + v.current_stock == r.current_stock && v.max_price == r.max_price + }; + +const test = (() => { + + // Set the initial storage and deploy the contract + const admin_address: address = Test.Account.address(0n); + const initial_storage: TacoShop.storage = { + admin_address: admin_address, + taco_data: TacoShop.default_taco_data, + } + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0tez); + + // Get the current price of a taco + const untyped_address = Test.Typed_address.to_address(contract.taddr); + const current_price = get_taco_price(untyped_address, 1n); + + // Purchase a taco + const success_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1n, + current_price + ); + + // Verify that the stock was updated + match(success_result) { + when(Success(_s)): + do { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been updated correctly + Assert.assert( + eq_in_map( + { current_stock: 49n, max_price: 50000000mutez }, + storage.taco_data, + 1n + )); + // Check that the amount of the other taco type has not changed + Assert.assert(eq_in_map( + { current_stock: 20n, max_price: 75000000mutez }, + storage.taco_data, + 2n + ) + ); + Test.IO.log("Successfully bought a taco"); + } + when(Fail(err)): failwith(err); + }; + + // Fail to purchase a taco without sending enough tez + const fail_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), + 1n, + 1mutez + ); + match(fail_result) { + when(Success(_s)): failwith("Test was able to buy a taco for the wrong price"); + when(Fail(_err)): Test.IO.log("Contract successfully blocked purchase with incorrect price"); + }; + + // Test the payout entrypoint as the administrator + const admin_balance_before = Test.Address.get_balance(admin_address); + Test.State.set_source(admin_address); + const payout_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("payout", contract.taddr), + unit, + 0tez + ); + match(payout_result) { + when(Success(_s)): + do { + const storage = Test.Typed_address.get_storage(contract.taddr); + // Check that the stock has been reset + Assert.assert( + eq_in_map( + Map.find(1n, TacoShop.default_taco_data), + storage.taco_data, + 1n + )); + Assert.assert( + eq_in_map( + Map.find(2n, TacoShop.default_taco_data), + storage.taco_data, + 2n + )); + Test.IO.log("Successfully reset taco storage"); + } + when(Fail(_err)): failwith("Failed to reset taco storage"); + }; + // Check that the admin account got a payout + const admin_balance_after = Test.Address.get_balance(admin_address); + Assert.assert(Test.Compare.lt(admin_balance_before, admin_balance_after)); + + // Verify that the entrypoint fails if called by someone else + const other_user_account = Test.Account.address(1n); + Test.State.set_source(other_user_account); + const failed_payout_result = + Test.Contract.transfer( + Test.Typed_address.get_entrypoint("payout", contract.taddr), + unit, + 0tez + ); + match(failed_payout_result) { + when(Success(_s)): failwith("A non-admin user was able to call the payout entrypoint"); + when(Fail(_err)): Test.IO.log("Successfully prevented a non-admin user from calling the payout entrypoint"); + }; + +}) (); diff --git a/ligo-tacoshop/taco_shop_3.mligo b/ligo-tacoshop/taco_shop_3.mligo new file mode 100644 index 0000000..8235526 --- /dev/null +++ b/ligo-tacoshop/taco_shop_3.mligo @@ -0,0 +1,195 @@ +module Test = Test.Next +module Tezos = Tezos.Next + +module TacoShop = struct + + type taco_supply = { current_stock: nat; max_price: tez } + type taco_data = (nat, taco_supply) map + type admin_address = address + type storage = { + admin_address: admin_address; + taco_data: taco_data; + } + + let default_taco_data: taco_data = Map.literal [ + (1n, { current_stock = 50n; max_price = 50tez }); + (2n, { current_stock = 20n; max_price = 75tez }); + ] + + (* Internal function to get the price of a taco *) + let get_taco_price_internal (taco_kind_index : nat) (taco_data : taco_data) : tez = + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" + in + taco_kind.max_price / taco_kind.current_stock + + [@view] + let get_taco_price (taco_kind_index : nat) (storage : storage) : tez = + get_taco_price_internal taco_kind_index storage.taco_data + + (* Buy a taco *) + [@entry] + let buy_taco (taco_kind_index : nat) (storage : storage) : operation list * storage = + + let { admin_address; taco_data } = storage in + + (* Retrieve the kind of taco from the contracts storage or fail *) + let taco_kind : taco_supply = + match Map.find_opt taco_kind_index taco_data with + | Some kind -> kind + | None -> failwith "Unknown kind of taco" in + + (* Get the current price of this type of taco *) + let current_purchase_price = get_taco_price_internal taco_kind_index taco_data in + + (* Verify that the caller sent the correct amount of tez *) + let _ = if (Tezos.get_amount () <> current_purchase_price) then + failwith "Sorry, the taco you are trying to purchase has a different price" in + + (* Verify that there is at least one of this type of taco *) + let _ = if (taco_kind.current_stock = 0n) then + failwith "Sorry, we are out of this type of taco" in + + + (* Update the storage with the new quantity of tacos *) + let updated_taco_data : taco_data = Map.update + taco_kind_index + (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) + taco_data in + + + let updated_storage : storage = { + admin_address = admin_address; + taco_data = updated_taco_data; + } in + + [], updated_storage + + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Ensure that only the admin can call this entrypoint *) + let _ = if (Tezos.get_sender () <> storage.admin_address) then + failwith "Only the admin can call this entrypoint" in + + (* Create contract object that represents the target account *) + let receiver_contract = match Tezos.get_contract_opt storage.admin_address with + | Some contract -> contract + | None -> failwith "Couldn't find account" in + + (* Create operation to send tez *) + let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + + (* Restore stock of tacos *) + let new_storage : storage = { + admin_address = storage.admin_address; + taco_data = default_taco_data + } in + + [payout_operation], new_storage + +end + +(* Convenience function to get current taco price *) +let get_taco_price (untyped_address : address) (taco_kind_index : nat) : tez = + let view_result_option : tez option = Tezos.View.call + "get_taco_price" + taco_kind_index + untyped_address in + match view_result_option with + | Some cost_mutez -> cost_mutez + | None -> Test.failwith "Couldn't get the price of a taco" + +(* Convenience function for testing equality in maps *) +let eq_in_map (r : TacoShop.taco_supply) (m : TacoShop.taco_data) (k : nat) = + match Map.find_opt k m with + | None -> false + | Some v -> v.current_stock = r.current_stock && v.max_price = r.max_price + +let test = + + (* Set the initial storage and deploy the contract *) + let admin_address : address = Test.Account.address 0n in + let initial_storage : TacoShop.storage = { + admin_address = admin_address; + taco_data = TacoShop.default_taco_data + } in + let contract = Test.Originate.contract (contract_of TacoShop) initial_storage 0tez in + + (* Get the current price of a taco *) + let untyped_address = Test.Typed_address.to_address contract.taddr in + let current_price = get_taco_price untyped_address 1n in + + (* Purchase a taco *) + let success_result = + Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + current_price + in + + (* Verify that the stock was updated *) + let () = match success_result with + | Success _s -> + let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert (eq_in_map + { current_stock = 49n; max_price = 50000000mutez } + storage.taco_data + 1n + ) in + let () = Assert.assert (eq_in_map + { current_stock = 20n; max_price = 75000000mutez } + storage.taco_data + 2n + ) in + Test.IO.log "Successfully bought a taco" + | Fail err -> failwith err + in + + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + let () = match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" in + + (* Test the payout entrypoint as the administrator *) + let admin_balance_before = Test.Address.get_balance admin_address in + let () = Test.State.set_source admin_address in + let payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + let () = match payout_result with + | Success _s -> let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert + (eq_in_map (Map.find 1n TacoShop.default_taco_data) + storage.taco_data + 1n) in + let () = Assert.assert + (eq_in_map (Map.find 2n TacoShop.default_taco_data) + storage.taco_data + 2n) in + Test.IO.log "Successfully reset taco storage" + | Fail _err -> failwith "Failed to reset taco storage" in + + (* Check that the admin account got a payout *) + let admin_balance_after = Test.Address.get_balance admin_address in + let () = Assert.assert (Test.Compare.lt admin_balance_before admin_balance_after) in + + (* Verify that the entrypoint fails if called by someone else *) + let other_user_account = Test.Account.address 1n in + let _ = Test.State.set_source other_user_account in + let failed_payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + match failed_payout_result with + | Success _s -> failwith "A non-admin user was able to call the payout entrypoint" + | Fail _err -> Test.IO.log "Successfully prevented a non-admin user from calling the payout entrypoint" From 96e62106135adf48a97da2c69e83377961bfd7ff Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Tue, 25 Mar 2025 14:11:23 -0400 Subject: [PATCH 2/3] Updates and fixes --- ligo-tacoshop/taco_shop_1.jsligo | 4 +- ligo-tacoshop/taco_shop_1.mligo | 13 ++-- ligo-tacoshop/taco_shop_2.jsligo | 22 +++--- ligo-tacoshop/taco_shop_2.mligo | 42 +++------- ligo-tacoshop/taco_shop_3.mligo | 128 +++++++++++++++---------------- 5 files changed, 95 insertions(+), 114 deletions(-) diff --git a/ligo-tacoshop/taco_shop_1.jsligo b/ligo-tacoshop/taco_shop_1.jsligo index 83685e3..2035a2d 100644 --- a/ligo-tacoshop/taco_shop_1.jsligo +++ b/ligo-tacoshop/taco_shop_1.jsligo @@ -58,7 +58,7 @@ namespace TacoShop { // Update the storage with the new quantity of tacos const updated_taco_data: taco_data = Map.update( taco_kind_index, - (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + (Some (({...taco_kind, current_stock: abs(taco_kind.current_stock - 1n) }))), taco_data); const updated_storage: storage = { @@ -79,4 +79,4 @@ namespace TacoShop { return [[], storage]; } -}; \ No newline at end of file +}; diff --git a/ligo-tacoshop/taco_shop_1.mligo b/ligo-tacoshop/taco_shop_1.mligo index c165b31..e18c76c 100644 --- a/ligo-tacoshop/taco_shop_1.mligo +++ b/ligo-tacoshop/taco_shop_1.mligo @@ -49,14 +49,12 @@ module TacoShop = struct let _ = if (taco_kind.current_stock = 0n) then failwith "Sorry, we are out of this type of taco" in - (* Update the storage with the new quantity of tacos *) let updated_taco_data : taco_data = Map.update taco_kind_index (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) taco_data in - let updated_storage : storage = { admin_address = admin_address; taco_data = updated_taco_data; @@ -64,11 +62,12 @@ module TacoShop = struct [], updated_storage - [@entry] - let payout (_u : unit) (storage : storage) : operation list * storage = + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = + + (* Entrypoint logic goes here *) - (* Entrypoint logic goes here *) + [], storage - [], storage +end - end \ No newline at end of file diff --git a/ligo-tacoshop/taco_shop_2.jsligo b/ligo-tacoshop/taco_shop_2.jsligo index 2c301f2..3e2b76c 100644 --- a/ligo-tacoshop/taco_shop_2.jsligo +++ b/ligo-tacoshop/taco_shop_2.jsligo @@ -10,7 +10,7 @@ namespace TacoShop { taco_data: taco_data, }; - export const default_taco_data: taco_data = Map.literal ([ + export const default_taco_data: taco_data = Map.literal([ [1n, { current_stock: 50n, max_price: 50tez }], [2n, { current_stock: 20n, max_price: 75tez }] ]); @@ -61,7 +61,7 @@ namespace TacoShop { // Update the storage with the new quantity of tacos const updated_taco_data: taco_data = Map.update( taco_kind_index, - (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + (Some (({...taco_kind, current_stock: abs(taco_kind.current_stock - 1n) }))), taco_data); const updated_storage: storage = { @@ -74,9 +74,9 @@ namespace TacoShop { @entry const payout = (_u: unit, storage: storage): [ - list, - storage - ] => { + list, + storage + ] => { // Entrypoint logic goes here @@ -86,11 +86,11 @@ namespace TacoShop { // Convenience function to get current taco price const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { - const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); - return match(view_result_option) { - when(Some(cost_mutez)): cost_mutez; - when(None()): Test.failwith("Couldn't get the price of the taco.") - }; + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return match(view_result_option) { + when(Some(cost_mutez)): cost_mutez; + when(None()): Test.failwith("Couldn't get the price of the taco.") + }; } // Convenience function for testing equality in maps @@ -100,7 +100,7 @@ const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => false when(Some(v)): v.current_stock == r.current_stock && v.max_price == r.max_price - }; +}; const test = (() => { diff --git a/ligo-tacoshop/taco_shop_2.mligo b/ligo-tacoshop/taco_shop_2.mligo index 0277d8a..0f5999b 100644 --- a/ligo-tacoshop/taco_shop_2.mligo +++ b/ligo-tacoshop/taco_shop_2.mligo @@ -52,14 +52,12 @@ module TacoShop = struct let _ = if (taco_kind.current_stock = 0n) then failwith "Sorry, we are out of this type of taco" in - (* Update the storage with the new quantity of tacos *) let updated_taco_data : taco_data = Map.update taco_kind_index (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) taco_data in - let updated_storage : storage = { admin_address = admin_address; taco_data = updated_taco_data; @@ -67,28 +65,12 @@ module TacoShop = struct [], updated_storage - [@entry] - let payout (_u : unit) (storage : storage) : operation list * storage = - - (* Ensure that only the admin can call this entrypoint *) - let _ = if (Tezos.get_sender () <> storage.admin_address) then - failwith "Only the admin can call this entrypoint" in - - (* Create contract object that represents the target account *) - let receiver_contract = match Tezos.get_contract_opt storage.admin_address with - | Some contract -> contract - | None -> failwith "Couldn't find account" in - - (* Create operation to send tez *) - let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = - (* Restore stock of tacos *) - let new_storage : storage = { - admin_address = storage.admin_address; - taco_data = default_taco_data - } in + (* Entrypoint logic goes here *) - [payout_operation], new_storage + [], storage end @@ -148,11 +130,11 @@ let test = | Fail err -> failwith err in - (* Fail to purchase a taco without sending enough tez *) - let fail_result = Test.Contract.transfer - (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) - 1n - 1mutez in - match fail_result with - | Success _s -> failwith "Test was able to buy a taco for the wrong price" - | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" \ No newline at end of file + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" diff --git a/ligo-tacoshop/taco_shop_3.mligo b/ligo-tacoshop/taco_shop_3.mligo index 8235526..f2841ee 100644 --- a/ligo-tacoshop/taco_shop_3.mligo +++ b/ligo-tacoshop/taco_shop_3.mligo @@ -52,14 +52,12 @@ module TacoShop = struct let _ = if (taco_kind.current_stock = 0n) then failwith "Sorry, we are out of this type of taco" in - (* Update the storage with the new quantity of tacos *) let updated_taco_data : taco_data = Map.update taco_kind_index (Some { taco_kind with current_stock = abs (taco_kind.current_stock - 1n) }) taco_data in - let updated_storage : storage = { admin_address = admin_address; taco_data = updated_taco_data; @@ -67,28 +65,28 @@ module TacoShop = struct [], updated_storage - [@entry] - let payout (_u : unit) (storage : storage) : operation list * storage = + [@entry] + let payout (_u : unit) (storage : storage) : operation list * storage = - (* Ensure that only the admin can call this entrypoint *) - let _ = if (Tezos.get_sender () <> storage.admin_address) then - failwith "Only the admin can call this entrypoint" in + (* Ensure that only the admin can call this entrypoint *) + let _ = if (Tezos.get_sender () <> storage.admin_address) then + failwith "Only the admin can call this entrypoint" in - (* Create contract object that represents the target account *) - let receiver_contract = match Tezos.get_contract_opt storage.admin_address with - | Some contract -> contract - | None -> failwith "Couldn't find account" in + (* Create contract object that represents the target account *) + let receiver_contract = match Tezos.get_contract_opt storage.admin_address with + | Some contract -> contract + | None -> failwith "Couldn't find account" in - (* Create operation to send tez *) - let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in + (* Create operation to send tez *) + let payout_operation = Tezos.Operation.transaction unit (Tezos.get_balance ()) receiver_contract in - (* Restore stock of tacos *) - let new_storage : storage = { - admin_address = storage.admin_address; - taco_data = default_taco_data - } in + (* Restore stock of tacos *) + let new_storage : storage = { + admin_address = storage.admin_address; + taco_data = default_taco_data + } in - [payout_operation], new_storage + [payout_operation], new_storage end @@ -148,48 +146,50 @@ let test = | Fail err -> failwith err in - (* Fail to purchase a taco without sending enough tez *) - let fail_result = Test.Contract.transfer - (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) - 1n - 1mutez in - let () = match fail_result with - | Success _s -> failwith "Test was able to buy a taco for the wrong price" - | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" in - - (* Test the payout entrypoint as the administrator *) - let admin_balance_before = Test.Address.get_balance admin_address in - let () = Test.State.set_source admin_address in - let payout_result = Test.Contract.transfer - (Test.Typed_address.get_entrypoint "payout" contract.taddr) - unit - 0tez - in - let () = match payout_result with - | Success _s -> let storage = Test.Typed_address.get_storage contract.taddr in - let () = Assert.assert - (eq_in_map (Map.find 1n TacoShop.default_taco_data) - storage.taco_data - 1n) in - let () = Assert.assert - (eq_in_map (Map.find 2n TacoShop.default_taco_data) - storage.taco_data - 2n) in - Test.IO.log "Successfully reset taco storage" - | Fail _err -> failwith "Failed to reset taco storage" in - - (* Check that the admin account got a payout *) - let admin_balance_after = Test.Address.get_balance admin_address in - let () = Assert.assert (Test.Compare.lt admin_balance_before admin_balance_after) in - - (* Verify that the entrypoint fails if called by someone else *) - let other_user_account = Test.Account.address 1n in - let _ = Test.State.set_source other_user_account in - let failed_payout_result = Test.Contract.transfer - (Test.Typed_address.get_entrypoint "payout" contract.taddr) - unit - 0tez - in - match failed_payout_result with - | Success _s -> failwith "A non-admin user was able to call the payout entrypoint" - | Fail _err -> Test.IO.log "Successfully prevented a non-admin user from calling the payout entrypoint" + (* Fail to purchase a taco without sending enough tez *) + let fail_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "buy_taco" contract.taddr) + 1n + 1mutez in + let () = match fail_result with + | Success _s -> failwith "Test was able to buy a taco for the wrong price" + | Fail _err -> Test.IO.log "Contract successfully blocked purchase with incorrect price" in + + (* Test the payout entrypoint as the administrator *) + let admin_balance_before = Test.Address.get_balance admin_address in + + let () = Test.State.set_source admin_address in + + let payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + let () = match payout_result with + | Success _s -> let storage = Test.Typed_address.get_storage contract.taddr in + let () = Assert.assert + (eq_in_map (Map.find 1n TacoShop.default_taco_data) + storage.taco_data + 1n) in + let () = Assert.assert + (eq_in_map (Map.find 2n TacoShop.default_taco_data) + storage.taco_data + 2n) in + Test.IO.log "Successfully reset taco storage" + | Fail _err -> failwith "Failed to reset taco storage" in + + (* Check that the admin account got a payout *) + let admin_balance_after = Test.Address.get_balance admin_address in + let () = Assert.assert (Test.Compare.lt admin_balance_before admin_balance_after) in + + (* Verify that the entrypoint fails if called by someone else *) + let other_user_account = Test.Account.address 1n in + let _ = Test.State.set_source other_user_account in + let failed_payout_result = Test.Contract.transfer + (Test.Typed_address.get_entrypoint "payout" contract.taddr) + unit + 0tez + in + match failed_payout_result with + | Success _s -> failwith "A non-admin user was able to call the payout entrypoint" + | Fail _err -> Test.IO.log "Successfully prevented a non-admin user from calling the payout entrypoint" From 96ae63466beeb19cb00855c9e866507f6a3cf3a1 Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Tue, 15 Apr 2025 16:06:56 -0400 Subject: [PATCH 3/3] Upgrade to JsLIGO v2 --- ligo-tacoshop/taco_shop_1.jsligo | 31 +++---- ligo-tacoshop/taco_shop_2.jsligo | 94 +++++++++++----------- ligo-tacoshop/taco_shop_3.jsligo | 133 +++++++++++++++---------------- 3 files changed, 127 insertions(+), 131 deletions(-) diff --git a/ligo-tacoshop/taco_shop_1.jsligo b/ligo-tacoshop/taco_shop_1.jsligo index 2035a2d..eed4e03 100644 --- a/ligo-tacoshop/taco_shop_1.jsligo +++ b/ligo-tacoshop/taco_shop_1.jsligo @@ -8,26 +8,26 @@ namespace TacoShop { }; export const default_taco_data: taco_data = Map.literal([ - [1n, { current_stock: 50n, max_price: 50tez }], - [2n, { current_stock: 20n, max_price: 75tez }] + [1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }], + [2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }] ]); // Internal function to get the price of a taco const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco") - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); return taco_kind.max_price / taco_kind.current_stock; } - @view + // @view const get_taco_price = (taco_kind_index: nat, storage: storage): tez => get_taco_price_internal(taco_kind_index, storage.taco_data); // Buy a taco - @entry + // @entry const buy_taco = (taco_kind_index: nat, storage: storage): [ list, storage @@ -37,10 +37,10 @@ namespace TacoShop { // Retrieve the kind of taco from the contracts storage or fail const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco"); - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); // Get the current price of this type of taco const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); @@ -51,14 +51,14 @@ namespace TacoShop { } // Verify that there is at least one of this type of taco - if (taco_kind.current_stock == 0n) { + if (taco_kind.current_stock == (0 as nat)) { return failwith("Sorry, we are out of this type of taco"); } // Update the storage with the new quantity of tacos const updated_taco_data: taco_data = Map.update( taco_kind_index, - (Some (({...taco_kind, current_stock: abs(taco_kind.current_stock - 1n) }))), + ["Some" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }], taco_data); const updated_storage: storage = { @@ -69,7 +69,7 @@ namespace TacoShop { return [[], updated_storage]; } - @entry + // @entry const payout = (_u: unit, storage: storage): [ list, storage @@ -79,4 +79,5 @@ namespace TacoShop { return [[], storage]; } + }; diff --git a/ligo-tacoshop/taco_shop_2.jsligo b/ligo-tacoshop/taco_shop_2.jsligo index 3e2b76c..c62ddae 100644 --- a/ligo-tacoshop/taco_shop_2.jsligo +++ b/ligo-tacoshop/taco_shop_2.jsligo @@ -11,26 +11,26 @@ namespace TacoShop { }; export const default_taco_data: taco_data = Map.literal([ - [1n, { current_stock: 50n, max_price: 50tez }], - [2n, { current_stock: 20n, max_price: 75tez }] + [1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }], + [2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }] ]); // Internal function to get the price of a taco const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco") - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); return taco_kind.max_price / taco_kind.current_stock; } - @view + // @view const get_taco_price = (taco_kind_index: nat, storage: storage): tez => get_taco_price_internal(taco_kind_index, storage.taco_data); // Buy a taco - @entry + // @entry const buy_taco = (taco_kind_index: nat, storage: storage): [ list, storage @@ -40,10 +40,10 @@ namespace TacoShop { // Retrieve the kind of taco from the contracts storage or fail const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco"); - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); // Get the current price of this type of taco const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); @@ -54,14 +54,14 @@ namespace TacoShop { } // Verify that there is at least one of this type of taco - if (taco_kind.current_stock == 0n) { + if (taco_kind.current_stock == (0 as nat)) { return failwith("Sorry, we are out of this type of taco"); } // Update the storage with the new quantity of tacos const updated_taco_data: taco_data = Map.update( taco_kind_index, - (Some (({...taco_kind, current_stock: abs(taco_kind.current_stock - 1n) }))), + ["Some" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }], taco_data); const updated_storage: storage = { @@ -72,91 +72,89 @@ namespace TacoShop { return [[], updated_storage]; } - @entry + // @entry const payout = (_u: unit, storage: storage): [ - list, - storage - ] => { + list, + storage + ] => { // Entrypoint logic goes here return [[], storage]; } + }; // Convenience function to get current taco price const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); - return match(view_result_option) { - when(Some(cost_mutez)): cost_mutez; - when(None()): Test.failwith("Couldn't get the price of the taco.") - }; + return $match(view_result_option, { + "Some": (cost_mutez) => cost_mutez, + "None": () => Test.failwith("Couldn't get the price of the taco."), + }); } // Convenience function for testing equality in maps const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => - match(Map.find_opt(k, m)) { - when(None): - false - when(Some(v)): - v.current_stock == r.current_stock && v.max_price == r.max_price -}; + $match(Map.find_opt(k, m), { + "None": () => false, + "Some": (v) => v.current_stock == r.current_stock && v.max_price == r.max_price + }); const test = (() => { // Set the initial storage and deploy the contract - const admin_address: address = Test.Account.address(0n); + const admin_address: address = Test.Account.address(0 as nat); const initial_storage: TacoShop.storage = { admin_address: admin_address, taco_data: TacoShop.default_taco_data, } - const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0tez); + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0 as tez); // Get the current price of a taco const untyped_address = Test.Typed_address.to_address(contract.taddr); - const current_price = get_taco_price(untyped_address, 1n); + const current_price = get_taco_price(untyped_address, 1 as nat); // Purchase a taco const success_result = Test.Contract.transfer( Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), - 1n, + 1 as nat, current_price ); // Verify that the stock was updated - match(success_result) { - when(Success(_s)): - do { + $match(success_result, { + "Success": (_s) => (() => { const storage = Test.Typed_address.get_storage(contract.taddr); // Check that the stock has been updated correctly Assert.assert( eq_in_map( - { current_stock: 49n, max_price: 50000000mutez }, + { current_stock: 49 as nat, max_price: 50000000 as mutez }, storage.taco_data, - 1n + 1 as nat )); // Check that the amount of the other taco type has not changed Assert.assert(eq_in_map( - { current_stock: 20n, max_price: 75000000mutez }, + { current_stock: 20 as nat, max_price: 75000000 as mutez }, storage.taco_data, - 2n + 2 as nat ) ); Test.IO.log("Successfully bought a taco"); - } - when(Fail(err)): failwith(err); - }; + })(), + "Fail": (err) => failwith(err), + }); // Fail to purchase a taco without sending enough tez const fail_result = Test.Contract.transfer( Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), - 1n, - 1mutez + 1 as nat, + 1 as mutez ); - match(fail_result) { - when(Success(_s)): failwith("Test was able to buy a taco for the wrong price"); - when(Fail(_err)): Test.IO.log("Contract successfully blocked purchase with incorrect price"); - }; + $match(fail_result, { + "Success": (_s) => failwith("Test was able to buy a taco for the wrong price"), + "Fail": (_err) => Test.IO.log("Contract successfully blocked purchase with incorrect price"), + }); }) (); diff --git a/ligo-tacoshop/taco_shop_3.jsligo b/ligo-tacoshop/taco_shop_3.jsligo index bdf41cc..93ca7a7 100644 --- a/ligo-tacoshop/taco_shop_3.jsligo +++ b/ligo-tacoshop/taco_shop_3.jsligo @@ -10,27 +10,27 @@ namespace TacoShop { taco_data: taco_data, }; - export const default_taco_data: taco_data = Map.literal ([ - [1n, { current_stock: 50n, max_price: 50tez }], - [2n, { current_stock: 20n, max_price: 75tez }] + export const default_taco_data: taco_data = Map.literal([ + [1 as nat, { current_stock: 50 as nat, max_price: 50 as tez }], + [2 as nat, { current_stock: 20 as nat, max_price: 75 as tez }] ]); // Internal function to get the price of a taco const get_taco_price_internal = (taco_kind_index: nat, taco_data: taco_data): tez => { const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco") - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); return taco_kind.max_price / taco_kind.current_stock; } - @view + // @view const get_taco_price = (taco_kind_index: nat, storage: storage): tez => get_taco_price_internal(taco_kind_index, storage.taco_data); // Buy a taco - @entry + // @entry const buy_taco = (taco_kind_index: nat, storage: storage): [ list, storage @@ -40,10 +40,10 @@ namespace TacoShop { // Retrieve the kind of taco from the contracts storage or fail const taco_kind: taco_supply = - match (Map.find_opt(taco_kind_index, taco_data)) { - when(Some(kind)): kind; - when(None()): failwith("Unknown kind of taco"); - }; + $match (Map.find_opt(taco_kind_index, taco_data), { + "Some": (kind) => kind, + "None": () => failwith("Unknown kind of taco"), + }); // Get the current price of this type of taco const current_purchase_price = get_taco_price_internal(taco_kind_index, taco_data); @@ -54,14 +54,14 @@ namespace TacoShop { } // Verify that there is at least one of this type of taco - if (taco_kind.current_stock == 0n) { + if (taco_kind.current_stock == (0 as nat)) { return failwith("Sorry, we are out of this type of taco"); } // Update the storage with the new quantity of tacos const updated_taco_data: taco_data = Map.update( taco_kind_index, - (Some (({...taco_kind, current_stock: abs (taco_kind.current_stock - 1n) }))), + ["Some" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }], taco_data); const updated_storage: storage = { @@ -72,7 +72,7 @@ namespace TacoShop { return [[], updated_storage]; } - @entry + // @entry const payout = (_u: unit, storage: storage): [ list, storage @@ -84,10 +84,10 @@ namespace TacoShop { } // Create contract object that represents the target account - const receiver_contract = match(Tezos.get_contract_opt(storage.admin_address)) { - when(Some(contract)): contract; - when(None): failwith("Couldn't find account"); - }; + const receiver_contract = $match(Tezos.get_contract_opt(storage.admin_address), { + "Some": (contract) => contract, + "None": () => failwith("Couldn't find account"), + }); // Create operation to send tez const payout_operation = Tezos.Operation.transaction(unit, Tezos.get_balance(), receiver_contract); @@ -104,79 +104,76 @@ namespace TacoShop { // Convenience function to get current taco price const get_taco_price = (untyped_address: address, taco_kind_index: nat): tez => { - const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); - return match(view_result_option) { - when(Some(cost_mutez)): cost_mutez; - when(None()): Test.failwith("Couldn't get the price of the taco.") - }; + const view_result_option: option = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address); + return $match(view_result_option, { + "Some": (cost_mutez) => cost_mutez, + "None": () => Test.failwith("Couldn't get the price of the taco."), + }); } // Convenience function for testing equality in maps const eq_in_map = (r: TacoShop.taco_supply, m: TacoShop.taco_data, k: nat) => - match(Map.find_opt(k, m)) { - when(None): - false - when(Some(v)): - v.current_stock == r.current_stock && v.max_price == r.max_price - }; + $match(Map.find_opt(k, m), { + "None": () => false, + "Some": (v) => v.current_stock == r.current_stock && v.max_price == r.max_price + }); const test = (() => { // Set the initial storage and deploy the contract - const admin_address: address = Test.Account.address(0n); + const admin_address: address = Test.Account.address(0 as nat); const initial_storage: TacoShop.storage = { admin_address: admin_address, taco_data: TacoShop.default_taco_data, } - const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0tez); + const contract = Test.Originate.contract(contract_of(TacoShop), initial_storage, 0 as tez); // Get the current price of a taco const untyped_address = Test.Typed_address.to_address(contract.taddr); - const current_price = get_taco_price(untyped_address, 1n); + const current_price = get_taco_price(untyped_address, 1 as nat); // Purchase a taco const success_result = Test.Contract.transfer( Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), - 1n, + 1 as nat, current_price ); // Verify that the stock was updated - match(success_result) { - when(Success(_s)): - do { + $match(success_result, { + "Success": (_s) => (() => { const storage = Test.Typed_address.get_storage(contract.taddr); // Check that the stock has been updated correctly Assert.assert( eq_in_map( - { current_stock: 49n, max_price: 50000000mutez }, + { current_stock: 49 as nat, max_price: 50000000 as mutez }, storage.taco_data, - 1n + 1 as nat )); // Check that the amount of the other taco type has not changed Assert.assert(eq_in_map( - { current_stock: 20n, max_price: 75000000mutez }, + { current_stock: 20 as nat, max_price: 75000000 as mutez }, storage.taco_data, - 2n + 2 as nat ) ); Test.IO.log("Successfully bought a taco"); - } - when(Fail(err)): failwith(err); - }; + })(), + "Fail": (err) => failwith(err), + }); // Fail to purchase a taco without sending enough tez const fail_result = Test.Contract.transfer( Test.Typed_address.get_entrypoint("buy_taco", contract.taddr), - 1n, - 1mutez + 1 as nat, + 1 as mutez ); - match(fail_result) { - when(Success(_s)): failwith("Test was able to buy a taco for the wrong price"); - when(Fail(_err)): Test.IO.log("Contract successfully blocked purchase with incorrect price"); - }; + $match(fail_result, { + "Success": (_s) => failwith("Test was able to buy a taco for the wrong price"), + "Fail": (_err) => Test.IO.log("Contract successfully blocked purchase with incorrect price"), + }); // Test the payout entrypoint as the administrator const admin_balance_before = Test.Address.get_balance(admin_address); @@ -185,45 +182,45 @@ const test = (() => { Test.Contract.transfer( Test.Typed_address.get_entrypoint("payout", contract.taddr), unit, - 0tez + 0 as tez ); - match(payout_result) { - when(Success(_s)): - do { + $match(payout_result, { + "Success": (_s) => (() => { const storage = Test.Typed_address.get_storage(contract.taddr); // Check that the stock has been reset Assert.assert( eq_in_map( - Map.find(1n, TacoShop.default_taco_data), + Map.find(1 as nat, TacoShop.default_taco_data), storage.taco_data, - 1n + 1 as nat )); Assert.assert( eq_in_map( - Map.find(2n, TacoShop.default_taco_data), + Map.find(2 as nat, TacoShop.default_taco_data), storage.taco_data, - 2n + 2 as nat )); Test.IO.log("Successfully reset taco storage"); - } - when(Fail(_err)): failwith("Failed to reset taco storage"); - }; + })(), + "Fail": (_err) => failwith("Failed to reset taco storage"), + }); + // Check that the admin account got a payout const admin_balance_after = Test.Address.get_balance(admin_address); Assert.assert(Test.Compare.lt(admin_balance_before, admin_balance_after)); // Verify that the entrypoint fails if called by someone else - const other_user_account = Test.Account.address(1n); + const other_user_account = Test.Account.address(1 as nat); Test.State.set_source(other_user_account); const failed_payout_result = Test.Contract.transfer( Test.Typed_address.get_entrypoint("payout", contract.taddr), unit, - 0tez + 0 as tez ); - match(failed_payout_result) { - when(Success(_s)): failwith("A non-admin user was able to call the payout entrypoint"); - when(Fail(_err)): Test.IO.log("Successfully prevented a non-admin user from calling the payout entrypoint"); - }; + $match(failed_payout_result, { + "Success": (_s) => failwith("A non-admin user was able to call the payout entrypoint"), + "Fail": (_err) => Test.IO.log("Successfully prevented a non-admin user from calling the payout entrypoint"), + }); }) ();