Last updated
Last updated
The previous chapter left us with a minimal contract as a starting point.
The first thing we need to do is to configure the desired target amount and the deadline. The deadline will be expressed as the block timestamp after which the contract can no longer be funded. We will be adding 2 more storage fields and arguments to the constructor.
The deadline being a block timestamp can be expressed as a regular 64-bits unsigned int. The target, however, being a sum of EGLD cannot. Note that 1 EGLD = 10^18 EGLD-wei (also known as atto-EGLD), the smallest unit of currency, and all payments are expressed in wei. So you can see that even for small payments the numbers get large. Luckily, the framework offers support for big numbers out of the box.
Signed big numbers, a.k.a BigInt, are not currently supported by mx-sdk-as.
Also note that BigUint logic does not reside in the contract, but is built into the MultiversX VM API, to not bloat the contract code.
Let's test that initialization works.
Note the added "arguments"
field in scDeploy
and the added fields in storage.
Run the following commands:
You should once again see this:
It is not enough to receive the funds, the contract also needs to keep track of who donated how much.
This storage mapper has an extra argument, for an address. This is how we define a map in the storage. The donor argument will become part of the storage key. Any number of such key arguments can be added, but in this case we only need one. The resulting storage key will be a concatenation of the specified base key "deposit"
and the serialized argument.
AssemblyScript is statically typed, you should explicit the return type of every function, even if it is void
.
Instead of Rust contracts, you don't have to mark a method as endpoint
and/or payable
. Every public method of the class contract will be reachable and will accept payments.
If you don't want to make a method reachable, set is visibility to private
.
To test the function, we'll add a new test file, in the same mandos
folder. Let's call it crowdfunding-fund.scen.json
.
To avoid duplicating the deployment code, we import it from crowdfunding-init.scen.json
.
Explanation:
"externalSteps"
allows us to import steps from another json file. This is very handy, because we can write test scenarios that branch out from each other without having to duplicate code. Here we will be reusing the deployment steps in all tests. These imported steps get executed again each time they are imported.
We need a donor, so we add another account using a new "setState"
step.
The actual simulated transaction. Note that we use "scCall"
instead of "scDeploy"
. There is a "to"
field, and no "contractCode"
. The rest functions the same. The "egldValue"
field indicates the amount paid to the function.
When checking the state, we have a new user, we see that the donor's balance is decreased by the amount paid, and the contract balance increased by the same amount.
There is another entry in the contract storage. The pipe symbol|
in the key means concatenation. The addresses are serialized as itself, and we can represent it in the same readable format.
Test it by running the commands again:
You should then see that both tests pass:
It doesn't make sense to fund after the deadline has passed, so fund transactions after a certain block timestamp must be rejected. The idiomatic way to do this is:
this.require(expression, errorMessage)
is the same as if (!expression) { throw new Error(errorMessage) }
this.require
may be replaced by a global function require
in the future.
We'll create another test file to verify that the validation works: test-fund-too-late.scen.json
.
We branch this time from crowdfunding-fund.scen.json
, where we already had a donor. Now the same donor wants to donate, again, but in the meantime the current block timestamp has become 123,001, one block later than the deadline. The transaction fails with status 4 (user error - all errors from within the contract will return this status). The testing environment allows us to also check that the correct message was returned.
By building and testing the contract again, you should see that all three tests pass:
The contract status can be known by anyone by looking into the storage and on the blockchain, but it is really inconvenient right now. Let's create an endpoint that gives this status directly. The status will be one of: FundingPeriod
, Successful
or Failed
. We could use a number to represent it in code, but the nice way to do it is with an enum. We will take this opportunity to show how to create a serializable type that can be taken as argument, returned as result or saved in storage.
This is the enum:
Make sure to add it outside the contract class.
The @enumtype
annotation tells the framework to automatically implement features like storage encoding.
At compile time enums the framework transforms enums
into classes
. This leads to some issues with switch
statements, make sure to use if/else if/else
instead
We can now use the type Status just like we use the other types, so we can write the following method in the contract class:
Remember, these two methods are publics, so reachable by anyone on-chain.
To test this method, we append one more step to the last test we worked on, test-fund-too-late.scen.json
:
Since the function we're trying to call is a view function, we use the scQuery
step instead of the scCall
step. The difference is that for scQuery
, there is no caller
, no payment, and gas price/gas limit. On the real blockchain, a smart contract query does not create a transaction on the blockchain, so no account is needed. scQuery
simulates this exact behaviour.
Note the call to "status" at the end and the result "out": [ "2" ]
, which is the encoding for Status::Failure
. Enums are encoded as an index of their values. In this example, Status::FundingPeriod
is "0"
(or ""
), Status::Successful
is "1"
and, as you've already seen, Status::Failure
is "2"
.
Contract functions can return in principle any number of results, that is why "out"
is a list.
Finally, let's add the claim
method. The status
method we just implemented helps us keep the code tidy:
The only new function here is this.send().directEgld()
, which simply forwards EGLD from the contract to the given address.
If you followed all the steps presented until now, you should have ended up with a contract that looks something like:
As an exercise, try to add some more tests, especially ones involving the claim function.
This concludes the first AssemblyScript MultiversX tutorial.
If you have any issue you can open one in the GitHub repo :
Define contract arguments, handle storage, process payments, define new types, write better tests