This page describes testing of distributor smart contract. The goal of contract is to distribute some ton among users. Code of contract located here. Code of contract wrapper located here.
First of all let's make sure our contract is deployed.
Note that we are using toHaveTransaction
from @ton/test-utils
to make sure result have transaction that matches pattern.
it('should deploy', async () => {
const result = await distributor.sendDeploy(owner.getSender(), toNano('0.05'));
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
deploy: true,
success: true
});
});
Let's check that our get methods are working.
Note that we are using toEqualAddress
from @ton/test-utils
. Under the hood it uses Address.equals
to compare addresses.
it('should get owner', async () => {
const ownerFromContract = await distributor.getOwner();
expect(ownerFromContract).toEqualAddress(owner.address);
});
it('should get shares dict', async () => {
const shares = await distributor.getShares();
expect(shares.keys().length).toEqual(0);
});
To share our coins we need to add some users to shares dict
it('should add firstUser', async () => {
const result = await distributor.sendAddUser(owner.getSender(), {
value: toNano('0.05'),
userAddress: firstUser.address,
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: true,
});
const shares = await distributor.getShares();
expect(shares.keys()[0]).toEqualAddress(firstUser.address);
});
After all preparations we now can finally share coins to users.
it('should share coins to one user', async () => {
const result = await distributor.sendShareCoins(owner.getSender(), {
value: toNano('10'),
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
outMessagesCount: 1,
success: true
});
expect(result.transactions).toHaveTransaction({
from: distributor.address,
on: firstUser.address,
op: 0x0,
success: true
});
});
Additionally, lets validate that our contract share coins to multiple users correctly.
Function findTransactionRequired
from @ton/test-utils
will throw error if tx is not found. Also, it has silent version findTransaction
.
it('should add secondUser', async () => {
const result = await distributor.sendAddUser(owner.getSender(), {
value: toNano('0.05'),
userAddress: secondUser.address
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: true
});
const shares = await distributor.getShares();
expect(
shares.keys().some((addr) => secondUser.address.equals(addr))
).toBeTruthy();
});
it('should share coins to 2 users', async () => {
const result = await distributor.sendShareCoins(owner.getSender(), {
value: toNano('10')
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: true,
outMessagesCount: 2
});
expect(result.transactions).toHaveTransaction({
from: distributor.address,
on: firstUser.address,
op: 0x0,
success: true
});
expect(result.transactions).toHaveTransaction({
from: distributor.address,
on: secondUser.address,
op: 0x0,
success: true
});
const firstUserTransaction = findTransactionRequired(result.transactions, { on: firstUser.address });
const secondUserTransaction = findTransactionRequired(result.transactions, { on: secondUser.address });
expect(flattenTransaction(firstUserTransaction).value).toEqual(flattenTransaction(secondUserTransaction).value);
});
By this simple steps positive flow was validated. But what if someone other than the owner tries to add user to shares? Negative testing comes in place.
Below we make sure that no one except admin can call add_user
. Function randomAddress
from @ton/test-utils
is useful for random address generation.
it('should not add user as not owner', async () => {
const notOwner = await blockchain.treasury(`not-owner`);
const result = await distributor.sendAddUser(notOwner.getSender(), {
value: toNano('0.5'),
userAddress: randomAddress(),
});
expect(result.transactions).toHaveTransaction({
from: notOwner.address,
on: distributor.address,
success: false,
exitCode: ExitCode.MUST_BE_OWNER,
});
});
We should put impure specifier in function below because it throws an exception. If impure is not specified compiler will delete this function call. This can lead to security issues so do not forget to test it!
() throw_unless_owner(slice address) impure inline {
throw_unless(err::must_be_owner, equal_slice_bits(address, storage::owner));
}
Make sure that your contract does not output more than 255 out actions, or else action phase will fail with 33 exit code.
it('should add 255 users', async () => {
for (let i = 0; i < 255; ++i) {
const userAddress = randomAddress();
const result = await distributor.sendAddUser(owner.getSender(), {
value: toNano('0.5'),
userAddress,
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: true
});
}
});
it('should not add one more user', async () => {
const userAddress = randomAddress();
const result = await distributor.sendAddUser(owner.getSender(), {
value: toNano('0.5'),
userAddress,
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: false,
exitCode: ExitCode.SHARES_SIZE_EXCEEDED_LIMIT
});
});
Always test edge cases too. In this case we test 255 out actions. Function filterTransactions
from @ton/test-utils
provides nice interface for filtering transactions.
it('should share money to 255 users', async () => {
const result = await distributor.sendShareCoins(owner.getSender(), {
value: toNano('1000')
});
expect(result.transactions).toHaveTransaction({
from: owner.address,
on: distributor.address,
success: true
});
printTransactionFees(result.transactions);
const transferTransaction = filterTransactions(result.transactions, { op: 0x0 });
expect(transferTransaction.length).toEqual(255);
});
Function printTransactionFees
is useful for debugging costs of transactions. It provides output like this:
┌─────────┬─────────────┬────────────────┬────────────────┬────────────────┬────────────────┬───────────────┬────────────┬────────────────┬──────────┬────────────┐
│ (index) │ op │ valueIn │ valueOut │ totalFees │ inForwardFee │ outForwardFee │ outActions │ computeFee │ exitCode │ actionCode │
├─────────┼─────────────┼────────────────┼────────────────┼────────────────┼────────────────┼───────────────┼────────────┼────────────────┼──────────┼────────────┤
│ 0 │ 'N/A' │ 'N/A' │ '1000 TON' │ '0.004007 TON' │ 'N/A' │ '0.001 TON' │ 1 │ '0.001937 TON' │ 0 │ 0 │
│ 1 │ '0x45ab564' │ '1000 TON' │ '998.8485 TON' │ '1.051473 TON' │ '0.000667 TON' │ '0.255 TON' │ 255 │ '0.966474 TON' │ 0 │ 0 │
│ 2 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │
│ 3 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │
│ 4 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │
│ 5 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │
│ 6 │ '0x0' │ '3.917053 TON' │ '0 TON' │ '0.00031 TON' │ '0.000667 TON' │ 'N/A' │ 0 │ '0.000309 TON' │ 0 │ 0 │
...