From dfebaacc3e2cd28d6e2e8edfcb265dbb180bdac4 Mon Sep 17 00:00:00 2001 From: Tim McMackin Date: Fri, 21 Mar 2025 14:44:53 -0400 Subject: [PATCH 1/2] 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/2] 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"