This tutorial will be focus on how to deal with your storage with bigmap.
Improve https://github.com/Laucans/Intezos-tutorial-1 by adding a transfer entry instead of creating a new contract for a new transfer. It'll impact the way to manage the storage and so others entrypoints
That's better because :
- Origination fees are paid only once.
- Easier to integrate your contract into dApp with only one contract address
- Only one contract to monitor
Contract : Using Ligo
- Work with
big_map
- Split your code with
import
and understand difference withinclude
- Work with
timestamp
- Be sure you followed or understand concepts of https://github.com/Laucans/Intezos-tutorial-1
- If you don't have the code, run
cp initial_contracts contracts
into your terminal. - Be sure to use
dry-run
and others command see in the precedent tutorial. You can get thecontracts_final/intezos.runner.jsligo
file if you want someparameters
andstorage
instance.
Start by extracting your current storage which represent a transfer into a new type named transfer
:
type transfer = {
amount: tez, // The transfered amount (xtz)
sender: address, // Address of the sender (tzxxx)
receiver: address, // Address of the recipient (tzxxx)
secret, // Type secret
pending: bool // Is the has been already claimed or redeem ?
};
And your storage have to mutate contain multiple transfer
type storage = list<transfer>
list
which is common into others languages. That's bad in blockchain ecosystem.
Remember you pay for your execution, at the startup of your contract call, the storage is loaded, load a big list
can consume a lot of gas
and so fees
can be expensive for nothing. Worse, an attacker can growth your list
and sature memory of the contract making it unusable.
To avoid it Ligo has Bigmap
lazily loaded. From ligolang.org :
A lazily deserialised map that is intended to store large amounts of data. Here, "lazily" means that storage is read or written per key on demand. Therefore there are no
map
,fold
, anditer
operations as there are inMap
.
Because it is a map
, an unique key will be necessary, couple sender
receiver
is a good candidate, but not unique, to ensure unicity we will introduce the timestamp
of the transfer :
type sender = address;
type receiver = address;
type transfer_key = {
sender,
receiver,
transfer_timestamp: timestamp
};
type storage = {
transfer_ledger: big_map<transfer_key, transfer>,
};
Our intezos.jsligo
start to be a big fuzzy file containing our contract entrypoints and our datatype, it's time to split it to keep our code clean.
Create a new file named storage.types.jsligo
and move your storage types into it.
I like to keep my parameters type near to my entrypoints but it is personnal preference.
Now you need to import your new file in your intezos.jsligo
.
One way like used before is to use :
#include "storage.types.jsligo"
#include
will literally copy your file and paste it into your current file. Code collision (variable or function which has been defined twice) is possible.
Prefer
#import "storage.types.jsligo" "Storage"
It will surround your file with a namespace Storage
to avoid collision. Now to use your type definition, you'll have to prefix it with Storage
.
Update your signatures like this to fix your errors :
const claim = (parameter: claim_parameter, store: Storage.t): [list<operation>, Storage.t]
For you information, each
#
instructions are executed by ligo preprocessor. You can see it like a script which is doing operation onto your files before to pass it through compiler.
Because our storage has been mutated, we have to redo all our entries.
We will do the Transfer
one together, comments other entries for now.
When you are doing a transfer operation first step is to verify request parameters are compliant
Get the amount sent by the user with Tezos.get_amount() from stdlib.
const transaction_amount = Tezos.get_amount();
Then verify that the amount
is valid, and parameters are filled.
assert_with_error((transaction_amount > (0 as mutez)), "You have to send tokens to initiate a transaction");
assert_with_error(
(parameter.secret.question != "" && parameter.secret.encrypted_answer != Bytes.pack("")),
"Question/answer are mandatory"
);
Check if the contract exist and is a wallet, if not emit an error.
type contract<unit>
is how wallet can be identified in Ligo
const _wallet : contract<unit> = Tezos.get_contract_with_error(address, "Not an existing address");
We want to generate the new entry which gonna be inserted onto the ledger. Because it is a map we need a key and a value. To instantiate the timestamp use Tezos.get_now()
which return the current timestamp of the chain.
const transfer_key: Storage.transfer_key =
{ sender: Tezos.get_source(), receiver: parameter.receiver, transfer_timestamp: Tezos.get_now() };
const transfer: Storage.transfer =
{ amount: transaction_amount, secret: parameter.secret, reason: parameter.reason, pending: true };
And then update your Big_map
by using Big_map.update before to return the mutated instance.
This is how to use the update
function :
Big_map(key_of_the_entry_to_update, Some(New_value) | None(), bigmap_to_update)
Note: when None
is used as a value, the value is removed from the big_map
.
const updated_ledger =
Big_map.update(
transfer_key,
Some(transfer),
store.transfer_ledger
);
return [
list([]) as list<operation>,
{ transfer_ledger: updated_ledger }
]
First step is to retrieve the corresponding entry from your ledger, to do it, you will need to build the searched key from parameters.
The type of your parameter need to be updated :
export type claim_parameter = {
sender: Storage.sender,
transfer_timestamp: timestamp,
answer: bytes
};
And in your claim
function let's build the key :
// Define the key that you want to retrieve
const transfer_to_claim_key =
{ sender: parameter.sender, receiver: Tezos.get_source(), transfer_timestamp: parameter.transfer_timestamp };
Then you will need to search for the key onto your map. For it I create a function onto a new utils.jsligo
file which gonna use Big_map.find_opt(key, Big_map)
to search the transfer into the ledger :
export const get_transfer_from_storage = (key: Storage.transfer_key, store: Storage.t) => {
return match(Big_map.find_opt(key, store.transfer_ledger))
{
when(None()) : failwith("Transfer doesn't exist")
when(Some (transfer)): transfer
}
};
And I use it in my claim
function. Like seen before add this code on top of your intezos.jsligo
file :
#import "./utils.jsligo" "Utils"
Then call it with Utils.get_transfer_from_storage
:
const transfer_to_claim = Utils.get_transfer_from_storage(transfer_to_claim_key, store);
Like for transfer, we will do verification step :
assert_with_error(
transfer_to_claim.pending && Utils.encrypt(parameter.answer) == transfer_to_claim.secret.encrypted_answer, "Failed to claim the transfer"
);
Last step is create the tezos transaction
which gonna be returned to the chain and update your storage, like in transfer
entry.
const transferOperation: operation =
Tezos.transaction(unit, transfer_to_claim.amount, Tezos.get_contract_with_error(Tezos.get_source(), "Not an existing address"));
const updated_ledger =
Big_map.update(
transfer_to_claim_key,
Some({ ...transfer_to_claim, pending: false }),
store.transfer_ledger
);
return [list([transferOperation]), { transfer_ledger: updated_ledger }]
The redeem doesn't introduce new concepts you can try to do it alone or check one solution in contracts_final
folder.
Congrats ! The next step will be to integrate it into your first dApp https://github.com/Laucans/Intezos-tutorial-3