Skip to main content
Version: Next

Part 3: Getting the payouts

Now that the customer-facing entrypoint of the contract is ready, you can set up the administrator-related entrypoint. In this case, Pedro needs a way to reset the stock of tacos and send the tez from the contract to his account. You could do this in two entrypoints, but for simplicity this tutorial shows how to do both of these things in one entrypoint named payout.

Adding administrator information

Also for the sake of simplicity, the contract provides no way to change Pedro's account address after the contract is deployed. In production applications, the address of the administrator should be in the contract storage and an entrypoint should allow the current administrator to change the administrator address. As it is, this contract cannot change the administrator address after it is deployed, so use caution.

  1. In the payout entrypoint, add this code to verify that the administrator is calling the entrypoint:

    // Ensure that only the admin can call this entrypoint
    if (Tezos.get_sender() != storage.admin_address) {
    failwith("Only the admin can call this entrypoint");
    }

    The function Tezos.get_sender returns the address of the account that called the smart contract.

  2. Add this code to generate the operation that sends tez to the administrator account:

    // Create contract object that represents the target 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);

    Sending tez to a user account means treating the user account as though it is a smart contract account. This way, sending tez to a user account works in the same way as sending tez to a smart contract.

    The Tezos.Operation.transaction function creates a Tezos transaction. There are many kinds of internal transactions in Tezos, but most smart contracts deal with these transactions:

    • Transferring tez to another account
    • Calling an entrypoint on a smart contract

    Calling an entrypoint on a smart contract (either the current contract or another contract) is beyond the scope of this tutorial. For information, see Calling a contract.

    The Tezos.Operation.transaction function takes these parameters:

    1. The parameter to pass, in this case unit, which means no value
    2. The amount of tez to include with the transaction, in this case all of the tez the contract has, denoted by the Tezos.get_balance function
    3. The address of the target contract
  3. Add this code to calculate the new value of the storage, using the existing admin address and the default taco data:

    // Restore stock of tacos
    const new_storage: storage = {
    admin_address: storage.admin_address,
    taco_data: default_taco_data,
    };
  4. Replace the payout entrypoint's return statement with this code:

    return [[payout_operation], new_storage];

    Creating the transaction is not enough to run it; you must return it in the list of operations at the end of the entrypoint.

The complete entrypoint looks like this:

// @entry
const payout = (_u: unit, storage: storage): [
list<operation>,
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), {
"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);
// Restore stock of tacos
const new_storage: storage = {
admin_address: storage.admin_address,
taco_data: default_taco_data,
};
return [[payout_operation], new_storage];
}

That's all you need to do to reset the storage and send the contract's tez to the administrator. If you want to extend this logic, try separating the payout entrypoint into separate entrypoints for paying out the tez and resetting the stock of tacos.

Testing the new entrypoint

Of course, after you implement the payout entrypoint, you should add tests for it.

  1. At the end of the test function, add this code to get the current balance of Pedro's account before calling the entrypoint:

    // Test the payout entrypoint as the administrator
    const admin_balance_before = Test.Address.get_balance(admin_address);
  2. Add this code to set the account that smart contract calls come from in the test scenario:

    Test.State.set_source(admin_address);

    Now when you call the Test.Contract.transfer function, the transaction comes from Pedro's account.

  3. Add this code to call the payout entrypoint and verify that the storage was updated, as in previous tests:

    const payout_result =
    Test.Contract.transfer(
    Test.Typed_address.get_entrypoint("payout", contract.taddr),
    unit,
    0 as tez
    );
    $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(1 as nat, TacoShop.default_taco_data),
    storage.taco_data,
    1 as nat
    ));
    Assert.assert(
    eq_in_map(
    Map.find(2 as nat, TacoShop.default_taco_data),
    storage.taco_data,
    2 as nat
    ));
    Test.IO.log("Successfully reset taco storage");
    })(),
    "Fail": (_err) => failwith("Failed to reset taco storage"),
    });
  4. Add this code to verify that Pedro's account received the tez from the contract:

    // 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));

    The exact amounts differ because calling the payout entrypoint costs a small fee, but this code verifies that Pedro's account has more tez in it after calling the payout entrypoint.

  5. Add this code to generate a test account and verify that it can't call the payout entrypoint because it is not the administrator:

    // Verify that the entrypoint fails if called by someone else
    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,
    0 as tez
    );
    $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"),
    });
  6. Run the test with ligo run test taco_shop.jsligo and verify that the test runs successfully.

The complete contract and tests looks like this:

namespace TacoShop {
export type taco_supply = { current_stock: nat, max_price: tez };
export type taco_data = map<nat, taco_supply>;
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([
[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), {
"Some": (kind) => kind,
"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<operation>,
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), {
"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);
// 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 == (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" as "Some", {...taco_kind, current_stock: abs(taco_kind.current_stock - 1) }],
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<operation>,
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), {
"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);
// 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<tez> = Tezos.View.call("get_taco_price", taco_kind_index, untyped_address);
return $match(view_result_option, {
"Some": (cost_mutez) => cost_mutez,
"None": () => Test.Assert.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), {
"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(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, 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, 1 as nat);
// Purchase a taco
const success_result =
Test.Contract.transfer(
Test.Typed_address.get_entrypoint("buy_taco", contract.taddr),
1 as nat,
current_price
);
// Verify that the stock was updated
$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: 49 as nat, max_price: 50000000 as mutez },
storage.taco_data,
1 as nat
));
// Check that the amount of the other taco type has not changed
Assert.assert(eq_in_map(
{ current_stock: 20 as nat, max_price: 75000000 as mutez },
storage.taco_data,
2 as nat
)
);
Test.IO.log("Successfully bought a taco");
})(),
"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),
1 as nat,
1 as mutez
);
$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);
Test.State.set_source(admin_address);
const payout_result =
Test.Contract.transfer(
Test.Typed_address.get_entrypoint("payout", contract.taddr),
unit,
0 as tez
);
$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(1 as nat, TacoShop.default_taco_data),
storage.taco_data,
1 as nat
));
Assert.assert(
eq_in_map(
Map.find(2 as nat, TacoShop.default_taco_data),
storage.taco_data,
2 as nat
));
Test.IO.log("Successfully 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(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,
0 as tez
);
$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"),
});
}) ();

Now you can allow different users to do different things in the contract.

Conclusion

Now you have a contract that Pedro can use to sell tacos and manage the profits and the taco stock. From here you can expand the contract in many ways, such as:

  • Adding more types of tacos
  • Changing how the price of tacos is calculated
  • Expanding the administrator functionality
  • Accepting more than the price of the taco as a tip
  • Adding more tests

You can also try deploying the contract to a test network and trying it in a real Tezos environment. For a tutorial that covers deploying a contract, see Deploy a smart contract on docs.tezos.com.