-
Notifications
You must be signed in to change notification settings - Fork 48
Composable Transactions
Normal transactions can make only a single call to a smart contract.
This means that when we need to call a function many times, or interact with different smart contracts, we need a separate transaction for each smart contract interaction.
Composable Transactions bring the ability to make many calls in a single transaction, either to the same smart contract or to different ones.
- Multiple calls
- Atomicity
- Sequential execution
- Act on current state
- Scripting
- No blind signing
If any of the calls fail, the entire transaction is aborted.
This means that either all calls succeed or none of them.
Execute a series of contract calls with the guarantee that they happen in sequence order with no external action occurring between them.
Do actions using the current values from contract and account states (by making queries to contracts and using the returned value on the same transaction) instead of a previously hardcoded value acquired at transaction building time.
This is useful when another transaction arrives first and changes the contract state.
Instead of just a list of calls, it is possible to process the returned value and act according to them.
The language supports data conversion and manipulation, assertion, conditional execution, loops, and more.
The transaction can be reviewed even on hardware wallets before approval or rejection
1) Purchase airplane tickets and hotel reservation from different contracts, only if both succeed. If some of the purchases fail, the other is reverted (the user does not want one of them if the other is not acquired).
2) Swap token and buy something with it. If the purchase fails, the user does not want that token.
3) Swap tokens using a split route. If one of the routes fail, revert the other(s). Also check the minimum output in the transaction itself (by adding individual outputs) and revert if not satisfied.
4) Swap to an exact amount of the output token when using a multi-hop route, by either: a. doing a reverse swap of the surplus amount, or b. querying a contract for the right amount to send (between approved range) in the same transaction.
5) Add liquidity to a pool (2 tokens) in a single transaction
6) Trustless swap or purchase - check if the purchased token was received, otherwise revert the transaction
7) Transferring tokens (fungible and/or non-fungible) to multiple recipients at the same time
8) Transferring different tokens (fungible and/or non-fungible) to one recipient in a single transaction
9) Mint many non-fungible tokens (NFTs) in a single transaction
10) Approve contract B to use contract A (token) and then call a function on contract B that handles the resources on contract A (eg: approve and swap, approve and add liquidity) on a single transaction
11) Approve, use resource, and remove approval on a single transaction, with the guarantee that no other operation would happen in between while the resource/token is approved to contract B
The call information is stored in the transaction payload in JSON format.
You can view it as a list of calls processed in order:
[
<call 1>
<call 2>
<call 3>
]
Each call containing the contract address, function to call, and arguments:
[
[call, contract, function, arg1, arg2, arg3...],
[call, contract, function, arg1, arg2, arg3...],
[call, contract, function, arg1, arg2, arg3...]
]
Buy 2 products or services on a single transaction:
[
["call","<token 1>","transfer","<amount 1>","<shop 1>","buy","product 1"],
["call","<token 2>","transfer","<amount 2>","<shop 2>","buy","product 2"],
]
Acquire token2 via swap and buy a product/service with it:
[
["call","<token 1>","transfer","<amount 1>","<swap pair>","swap",{"exact_output":"<amount 2>"}],
["call","<token 2>","transfer","<amount 2>","<shop>","buy","product"],
]
Add liquidity to a swap pair:
[
["call","<token 1>","transfer","<amount 1>","<swap_pair>","store_token"],
["call","<token 2>","transfer","<amount 2>","<swap_pair>","add_liquidity"],
]
Another feature is the use of variables, in the format %name%
When a string in the above format is found, it is replaced by the corresponding value.
One hard-coded variable is the returned value from the last operation: %last_result%
Example usage:
[
["call","<address 1>","function1","arg"],
["call","<address 2>","function2","%last_result%"]
]
This feature uses a new simple scripting language based on JSON.
The first element of each operation is a command. Here are some of them:
- call
- store (result)
- assert
The call
is a normal smart contract call
The store
command is used to store the returned value in a temporary variable
The assert
is used to ensure that a value is within expected bounds, otherwise the whole transaction is reverted
Example 1:
[
["call","<address>","function","arg"],
["assert","%last_result%",">=","250"]
]
Example 2:
[
["call","<address 1>","function1","arg"],
["call","<address 2>","function2","arg"],
["assert","%last_result%",">=","250"]
]
Example 3:
[
["call","<address 1>","function","arg"],
["store result as","result1"],
["call","<address 2>","function","arg"],
["assert","%last_result%",">=","%result1%"]
]
Example 4:
[
["call","<address 1>","function1","arg"],
["store result as","result1"],
["call","<address 2>","function2","arg"],
["store result as","result2"],
["call","<address 3>","function3","%result1%","%result2%"]
]
To support a variety of use cases, composable transactions contain many different commands:
- Smart contract call
- Sending Aergo tokens
- Checking Aergo balance
- Mathematical operations
- String operations
- Table operations (lists and dictionaries)
- Variables
- Conversions
- Assertion
- Conditional execution
- Loops
- Returning a result
(click on a command to expand)
call
["call",address,function,args...]Calls a function on a smart contract
Examples:
["call","%contract%","transfer","%to%","10.5"] ["call","Ammlaklkfld","transfer","Amafpoajf","10.5"]If the call fails, the entire transaction is reverted
call + send
["call + send",amount,address,function,args...]Calls a function on a smart contract, also sending an amount of native AERGO tokens
Examples:
["call + send","1.5 aergo","%contract%","do_something","%arg%"]If the call fails, the entire transaction is reverted
try call
["try call",address,function,args...]Makes a protected call to a function on a smart contract.
A "protected call" means that if the call fails, it continues execution on the next command.
It returns a list with 2 elements:
[success, result]
We can check if the call succeeded by checking the first returned value
Examples:
["try call","%contract%","transfer","%to%","10.5"] ["store result as","result"] ["get","%result%",1] ["if","%last_result%","=",true] ["get","%result%",2] ... ["end"]
try call + send
["try call + send",amount,address,function,args...]Makes a protected call to a function on a smart contract, also sending an amount of native AERGO tokens.
A "protected call" means that if the call fails, it continues execution on the next command.
It returns a list with 2 elements:
[success, result]
We can check if the call succeeded by checking the first returned value
Examples:
["try call + send","1.5 aergo","%contract%","do_something","%arg%"] ["store result as","result"] ["get","%result%",1] ["if","%last_result%","=",true] ["get","%result%",2] ... ["end"]
get balance
["get balance"] ["get balance",address]Returns the current balance in AERGO tokens from the given account address.
If no address is given, it returns the balance of the caller account.
send
["send",address,amount]Transfer an amount of native AERGO tokens to the informed address
let
["let",variable_name,value] ["let",variable_name,value,token_address]Assign the value to the variable
The value can be a number, bignumber, string, boolean, list or object
It is also possible to convert an amount in decimal format to bignumber, by supplying the address of the token.
⚠️ The decimal format must always contain a.
Here are some examples:
["let","min_amount","0.05","%token2%"] ["let","min_amount","1.5","%token2%"] ["let","min_amount","100.","Am..."]
store
["store result as",variable_name]Store the last result in a variable with the given name
Example:
["store result as","amount"]
get
["get",table,key_or_pos]Retrieve an element from a table (list or object)
For lists we inform the position:
["get","%list%",3]For dictionaries we inform the key in string format:
["get","%result%","price"]
set
["set",table,key_or_pos,value]Set a value in a table (list or object)
For lists we inform the position:
["set","%list%",2,"%value%"]For dictionaries we inform the key in string format:
["set","%obj%","price","%last_result%"]
insert
["insert",table,item] ["insert",table,pos,item]Inserts an element on a list
Other elements are moved up
If no position is informed, the element is inserted at the end
remove
["remove",table,position]Removes an element from a list, by position
It returns the removed item
get size
["get size",table_or_string]Retrieve the number of elements on a table (array) or the length of a string
get keys
["get keys",table]Retrieve a list of keys from a table (dictionary)
add
["add",value1,value2]Adds 2 values and return the result
The values must be of the same type
They can be number or bignumber
subtract
["subtract",value1,value2]Subtract one value from the other and return the result
The values must be of the same type
They can be number or bignumber
multiply
["multiply",value1,value2]Multiplies 2 values and return the result
The values must be of the same type
They can be number or bignumber
divide
["divide",value1,value2]Divides the first value by the second and returns the result
The values must be of the same type
They can be number or bignumber
remainder
["remainder",value1,value2]Returns the modulo of the division of the first value by the second
The values must be of the same type
They can be number or bignumber
pow
["pow",value1,value2]Elevates the first value by the power of the second and returns the result
The values must be of the same type
They can be number or bignumber
sqrt
["sqrt",value]Returns the square root of the given bignumber value
For numbers use:
["pow",value,0.5]
combine
["combine",str1,str2,...]Concatenates all the given strings into one
format
["format",formatstring,...]The same as string.format in Lua
It can also be used for concatenation:
["format","%s %s","hello","world"]
extract
["extract",string,start] ["extract",string,start,end]The same as string.sub in Lua
find
["find",string,pattern] ["find",string,pattern,init]The same as string.match in Lua
replace
["replace",string,pattern,replace] ["replace",string,pattern,replace,n]The same as string.gsub in Lua
to big number
["to big number",value]
to number
["to number",value]
to string
["to string",value]
to json
["to json",list_or_object]
from json
["from json",json_string]
assert
["assert",<expression>]If the assertion fails, the whole transaction is reverted and marked as failed
Examples:
["assert","%variable_name%",">",10] ["assert","%var1%","<=","%var2%"] ["assert","%var1%","<","%var2%","and","%var1%",">=",1500] ["assert","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]The operators can be:
=
!=
>
>=
<
<=
match
The logic operators:
and
or
if
["if",<expression>]Example expressions:
["if","%variable_name%",">",10] ["if","%var1%","<=","%var2%"] ["if","%var1%","<","%var2%","and","%var1%",">=",1500] ["if","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]The operators can be:
=
!=
>
>=
<
<=
match
The logic operators:
and
or
Example:
["if","%amount%",">",20], ... ["end"]
⚠️ Important:if
statements can NOT be nested!
else if
["else if",<expression>]Example expressions:
["else if","%variable_name%",">",10] ["else if","%var1%","<=","%var2%"] ["else if","%var1%","<","%var2%","and","%var1%",">=",1500] ["else if","%var1%","=","text1","or","%var1%","=","text2","or","%var1%","=","text3"]The operators can be:
=
!=
>
>=
<
<=
match
The logic operators:
and
or
Example:
["if","%amount%",">",20], ... ["else if","%amount%",">",10], ... ["end"]
else
["else"]Example:
["if","%amount%",">",20], ... ["else"], ... ["end"]
end
["end"]Used to close an
if
statement. Check examples above
for
["for",variable,start,end,increment]Example:
["for","n",1,10], ... [...,"%n%"], ... ["loop"]With a decrement:
["for","n",50,10,-10], ... [...,"%n%"], ... ["loop"]
for each
- With lists
["for each",item,"in",list]Iterate on items from a list
Example:
["let","list",[11,22,33]], ["for each","item","in","%list%"], ... [...,"%item%"], ... ["loop"]
- With tables
["for each",key,value,"in",table]Iterate on items from a dictionary
Example:
["let","payments",{"Am..":"10.5","Am..":"12.3"}], ["for each","address","amount","in","%payments%"], ["send","%address%","%amount%"], ["loop"]
loop
["loop"]This command can be used in 3 ways:
- With a
for
command- With a
for each
command- Alone as the last command
When used alone, it will loop to the first command on the list.
We can exit the loop using the
break
command or areturn
inside of anif
statement.
break
["break"] ["break","if",<expression>]Used to exit from a loop
Example:
["for each","item","in","%list%"], ... ["break","if",...], ... ["loop"]Or if already using an if statement:
["for each","item","in","%list%"], ... ["if",...], ["break"], ["end"], ... ["loop"]
return
["return"] ["return",value]Example:
["return","%last_result%"]
Here is an example script (payload) that would be used to check the returned amount of tokens on a token swap:
["call","<tokenB>","balanceOf"], <-- get the previous balance of token B
["store result as","balance_before"], <-- store it in a variable
["call","<tokenA>","transfer","<to>","<amount>","swap"], <-- swap token A for token B
["call","<tokenB>","balanceOf"], <-- get the new balance of token B
["subtract","%last_result%","%balance_before%"], <-- subtract one balance by the other
["assert","%last_result%",">=","<minimum_amount>"] <-- assert that we got the minimum amount
This feature allows real TRUSTLESS TRANSACTIONS, where the user does not trust even the swap contract!
It can allow for direct token swaps between users A and B, in a complete trustless way
To build a transaction with composable transactions we:
- Set the type to
MULTICALL
(7) - Set the recipient to null/none
- Set the amount to 0
- Put the JSON script in the payload