-
Notifications
You must be signed in to change notification settings - Fork 47
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%","buy_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 2 values:
%call succeeded%
and%last result%
You can use that information to conditionally execute commands
Examples:
["try call","%contract%","transfer","%to%","10.5"] ["if","%call succeeded%"] ["...","%last result%"] ... ["end if"]
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 2 values:
%call succeeded%
and%last result%
You can use that information to conditionally execute commands
Examples:
["try call + send","1.5 aergo","%contract%","buy_something","%arg%"] ["if","%call succeeded%"] ["...","%last result%"] ... ... ["end if"]
get aergo balance
["get aergo balance",address]Returns the current balance in AERGO tokens from the given account address.
It is also possible to direclty use the
%my aergo balance%
variable
send
["send",address,amount]Transfer an amount of native AERGO tokens to the informed address
The amount can be a string or a bignumber
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.
Here are some examples:
["let","min_amount","0.05","%token2%"] ["let","min_amount","1.5","%token2%"] ["let","min_amount","100","Am..."]
store result as
["store result as",variable_name]Store the result of the last operation 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
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
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"] ["to big number",value]Converts a value to a bignumber
If no argument is given, it converts the last result
to number
["to number"] ["to number",value]Converts a value to a number
If no argument is given, it converts the last result
to string
["to string"] ["to string",value]Converts a value to a string
If no argument is given, it converts the last result
to json
["to json"] ["to json",list_or_object]Converts a value to a json string
If no argument is given, it converts the last result
from json
["from json"] ["from json",json_string]Converts a json string to a table
If no argument is given, it converts the last result
assert
["assert",<expression>]If the assertion fails, the whole transaction is reverted and marked as failed
If the first value is a big number, the second value can be a string representation of an amount
The expression can be a single boolean value
Examples:
["assert","%call succeeded%"] ["assert","%variable_name%",">","10.5 aergo"] ["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>]Executes the following commands only if the expression is true
The expression can be a single boolean value
Example expressions:
["if","%call succeeded%"] ["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 if"]
⚠️ Important:if
statements can NOT be nested!
else if
["else if",<expression>]Executes the following commands only if the expression is true
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 if"]
else
["else"]Example:
["if","%amount%",">",20], ... ["else"], ... ["end if"]
end if
["end if"]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
Iterate on the items from a list or a dictionary
To iterate on a list:
["for each",item,"in",list]Example:
["let","list",[11,22,33]], ["for each","item","in","%list%"], ... [...,"%item%"], ... ["loop"]To iterate on a dictionary:
["for each",key,value,"in",table]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 if"], ... ["loop"]
return
["return"] ["return",value]Example:
["return","%last result%"]
There is also these predefined variables:
%my account address%
%my aergo balance%
Example:
["assert","%my aergo balance%",">","10 aergo"],
["call","%token address%","balanceOf","%my account address%"],
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