Atomic transfers
In traditional finance, trading assets generally requires a trusted intermediary, like a bank or an exchange, to make sure that both sides receive what they agreed to. On the Algorand blockchain, this type of trade is implemented within the protocol as an Atomic Transfer. This simply means that transactions that are part of the transfer either all succeed or all fail. Atomic transfers allow complete strangers to trade assets without the need for a trusted intermediary, all while guaranteeing that each party will receive what they agreed to.
On Algorand, atomic transfers are implemented as irreducible batch operations, where a group of transactions are submitted as a unit and all transactions in the batch either pass or fail. This also eliminates the need for more complex solutions like hashed timelock contracts that are implemented on other blockchains. An atomic transfer on Algorand is confirmed in less than 4 seconds, just like any other transaction. Transactions can contain Algos or Algorand Standard Assets and may also be governed by Algorand Smart Contracts.
Quick start videos¶
If you prefer videos, take a look at this 10 minute guide to learn about Atomic Transactions.
Use cases¶
Atomic transfers enable use cases such as:
Circular trades - Alice pays Bob if and only if Bob pays Claire if and only if Claire pays Alice.
Group payments - Everyone pays or no one pays.
Decentralized exchanges - Trade one asset for another without going through a centralized exchange.
Distributed payments - Payments to multiple recipients.
Pooled transaction fees - One transaction pays the fees of others.
Process overview¶
To implement an atomic transfer, generate all of the transactions that will be involved in the transfer and then group those transactions together. The result of grouping is that each transaction is assigned the same group ID. Once all transactions contain this group ID, the transactions can be split up and sent to their respective senders to be authorized. A single party can then collect all the authorized transactions and submit them to the network together.
Info
An individual account involved in an atomic transfer can verify that all the correct transfers are involved by creating the same set of [unsigned] transactions and grouping them in the same order. The group ID is a hash of the group of transactions and should match if the configuration is the same.
Below you will find examples for creating and sending group transactions to the network in each of the available SDKs and with goal
.
Tip
The following processes may be simplified with the SDKs use of the atomic transaction composer (ATC). The ATC supports ABI compliant smart contract calls with Argument and Return Value encoding/decoding.
Step-by-step guide¶
Create transactions¶
Create two or more (up to 16 total) unsigned transactions of any type. Read about transaction types in the Transactions Overview section.
This could be done by a service or by each party involved in the transaction. For example, an asset exchange application can create the entire atomic transfer and allow individual parties to sign from their location.
The example below illustrates creating, grouping, and signing transactions atomically and submitting to the network.
const suggestedParams = await client.getTransactionParams().do();
const alicesTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
from: acct1.addr,
to: acct2.addr,
amount: 1e6,
suggestedParams,
});
const bobsTxn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
from: acct2.addr,
to: acct1.addr,
amount: 1e6,
suggestedParams,
});
# payment from account 1 to account 2
txn_1 = transaction.PaymentTxn(addr1, suggested_params, addr2, 100000)
# payment from account 2 to account 1
txn_2 = transaction.PaymentTxn(addr2, suggested_params, addr1, 200000)
Response<TransactionParametersResponse> rsp = algodClient.TransactionParams().execute();
// payment from account 1 to account 2
Transaction ptxn1 = Transaction.PaymentTransactionBuilder().sender(acct1.getAddress())
.amount(1000000).receiver(acct2.getAddress()).suggestedParams(rsp.body()).build();
// txn_1 = transaction.PaymentTxn(addr1, suggested_params, addr2, 100000)
// payment from account 2 to account 1
Transaction ptxn2 = Transaction.PaymentTransactionBuilder().sender(acct2.getAddress())
.amount(2000000).receiver(acct1.getAddress()).suggestedParams(rsp.body()).build();
sp, err := algodClient.SuggestedParams().Do(context.Background())
if err != nil {
log.Fatalf("failed to get suggested params: %s", err)
}
tx1, err := transaction.MakePaymentTxn(acct1.Address.String(), acct2.Address.String(), 100000, nil, "", sp)
if err != nil {
log.Fatalf("failed creating transaction: %s", err)
}
// from account 2 to account 1
tx2, err := transaction.MakePaymentTxn(acct2.Address.String(), acct1.Address.String(), 100000, nil, "", sp)
if err != nil {
log.Fatalf("failed creating transaction: %s", err)
}
$ goal clerk send --from=my-account-a<PLACEHOLDER> --to=my-account-c<PLACEHOLDER> --fee=1000 --amount=100000 --out=unsginedtransaction1.txn"
$ goal clerk send --from=my-account-b<PLACEHOLDER> --to=my-account-a<PLACEHOLDER> --fee=1000 --amount=200000 --out=unsginedtransaction2.txn"
At this point, these are just individual transactions. The next critical step is to combine them and then calculate the group ID.
See Authorizing Transactions Offline to learn how to create and save individual unsigned transactions to a file. This method can be used to distribute group transactions for signing.
Group transactions¶
The result of this step is what ultimately guarantees that a particular transaction belongs to a group and is not valid if sent alone (even if properly signed). A group-id is calculated by hashing the concatenation of a set of related transactions. The resulting hash is assigned to the Group field within each transaction. This mechanism allows anyone to recreate all transactions and recalculate the group ID to verify that the contents are as agreed upon by all parties. Ordering of the transaction set must be maintained.
const txnArray = [alicesTxn, bobsTxn];
// assignGroupID returns the same txns with the group ID set
const txnGroup = algosdk.assignGroupID(txnArray);
# Assign group id to the transactions (order matters!)
transaction.assign_group_id([txn_1, txn_2])
# Or, equivalently
# get group id and assign it to transactions
# gid = transaction.calculate_group_id([txn_1, txn_2])
# txn_1.group = gid
# txn_2.group = gid
// Assign group id to the transactions (order matters!)
Transaction[] txs = TxGroup.assignGroupID(ptxn1, ptxn2);
// Or, equivalently
// compute group id and assign it to transactions
Digest gid = TxGroup.computeGroupID(txs);
ptxn1.group = gid;
ptxn2.group = gid;
// compute group id and put it into each transaction
gid, _ := crypto.ComputeGroupID([]types.Transaction{tx1, tx2})
tx1.Group = gid
tx2.Group = gid
$ cat unsignedtransaction1.tx unsignedtransaction2.tx > combinedtransactions.tx
$ goal clerk group -i combinedtransactions.tx -o groupedtransactions.tx -d data -w yourwallet
Split transactions (Goal only)¶
At this point the transaction set must be split to allow distributing each component transaction to the appropriate wallet for authorization.
# keys in distinct wallets
$ goal clerk split -i groupedtransactions.tx -o splitfiles -d data -w yourwallet
Wrote transaction 0 to splitfiles-0
Wrote transaction 1 to splitfiles-1
# distribute files for authorization
Sign transactions¶
With a group ID assigned, each transaction sender must authorize their respective transaction.
const alicesSignedTxn = txnGroup[0].signTxn(acct1.privateKey);
const bobsSignedTxn = txnGroup[1].signTxn(acct2.privateKey);
// sign transactions
SignedTransaction signedPtxn1 = acct1.signTransaction(ptxn1);
SignedTransaction signedPtxn2 = acct2.signTransaction(ptxn2);
_, stx1, err := crypto.SignTransaction(acct1.PrivateKey, tx1)
if err != nil {
fmt.Printf("Failed to sign transaction: %s\n", err)
return
}
_, stx2, err := crypto.SignTransaction(acct2.PrivateKey, tx2)
if err != nil {
fmt.Printf("Failed to sign transaction: %s\n", err)
}
# sign from single wallet containing all keys
$ goal clerk sign -i groupedtransactions.tx -o signout.tx -d data -w yourwallet
# -- OR --
# sign from distinct wallets
$ goal clerk sign -i splitfiles-0 -o splitfiles-0.sig -d data -w my_wallet_1
$ goal clerk sign -i splitfiles-1 -o splitfiles-1.sig -d data -w my_wallet_2
Assemble transaction group¶
All authorized transactions are now assembled into an array, maintaining the original transaction ordering, which represents the transaction group.
// combine the signed transactions into a single list
SignedTransaction[] stxns = new SignedTransaction[] { signedPtxn1, signedPtxn2 };
var signedGroup []byte
signedGroup = append(signedGroup, stx1...)
signedGroup = append(signedGroup, stx2...)
# combine signed transactions files
cat splitfiles-0.sig splitfiles-1.sig > signout.tx
Send transaction group¶
The transaction group is now broadcast to the network.
await client.sendRawTransaction(signedTxns).do();
await algosdk.waitForConfirmation(client, alicesTxn.txID().toString(), 3);
# Only the first transaction id is returned
tx_id = algod_client.send_transactions(signed_group)
# wait for confirmation
result: Dict[str, Any] = transaction.wait_for_confirmation(
algod_client, tx_id, 4
)
print(f"txID: {tx_id} confirmed in round: {result.get('confirmed-round', 0)}")
// Only the first transaction id is returned
Response<PostTransactionsResponse> txResponse = algodClient.RawTransaction()
.rawtxn(Encoder.encodeToMsgPack(stxns)).execute();
String txid = txResponse.body().txId;
// Wait for the transaction id to be confirmed
// If the results from other transactions are needed, grab the txid from those
// directly and
// call waitForConfirmation on each
PendingTransactionResponse txResult = Utils.waitForConfirmation(algodClient, txid, 4);
System.out.printf("Transaction %s confirmed in round %d\n", txid, txResult.confirmedRound);
pendingTxID, err := algodClient.SendRawTransaction(signedGroup).Do(context.Background())
if err != nil {
fmt.Printf("failed to send transaction: %s\n", err)
return
}
confirmedTxn, err := transaction.WaitForConfirmation(algodClient, pendingTxID, 4, context.Background())
if err != nil {
fmt.Printf("Error waiting for confirmation on txID: %s\n", pendingTxID)
return
}
fmt.Printf("Confirmed Transaction: %s in Round %d\n", pendingTxID, confirmedTxn.ConfirmedRound)
goal clerk rawsend -f signout.tx -d data -w yourwallet