Executing Swaps Through Optimal Path
Executing swaps is the most common use of Aquarius protocol. This article explains how to prepare, find the best path and execute swap.
Last updated
Executing swaps is the most common use of Aquarius protocol. This article explains how to prepare, find the best path and execute swap.
Last updated
This is the recommended way to execute swaps with Aquarius, as it provides the best swap results. It supports swaps through multiple pools (multi-hop, up to 4 pools).
In this example we will swap 1 XLM to AQUA with Aquarius AMM. The swap will be executed using router and will be optimized for best results.
Please make sure account has established trustline for asset to receive. Otherwise, swap will fail until trustline is created. For more information please refer to documentation on Stellar.org.
Scroll here to see the complete code.
To perform a swap, you need to follow these steps:
1. Specify user secret key, input token, output token and input token amount: You need to specify the input and output tokens and the amount of the input token you want to swap in stroops.
# This account must have at least 3 XLM and a trustline to AQUA.
user_secret_key = "S..."
# XLM
token_in = Asset.native()
# AQUA
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")
# 1 XLM in stroops
amount_in = 1_0000000
const userSecretKey = 'S...';
// XLM
const tokenIn = Asset.native();
// AQUA
const tokenOut = new Asset('AQUA','GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA');
// 1 XLM in stroops
const amount = 10000000;
2. Call Find Path API to calculate swap chain and XDR: Send a POST request to the find-path endpoint (/api/external/v1/find-path/). The request body should be a JSON object with the following fields:
token_in_address: The address of the token you want to swap from.
token_out_address: The address of the token you want to swap to.
amount: The amount of the input token you want to swap.
def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount_in: int) -> (int, str):
data = {
'token_in_address': token_in_address,
'token_out_address': token_out_address,
'amount': amount_in
}
response = requests.post(f'{base_api}/find-path/', json=data)
swap_result = response.json()
assert swap_result['success']
return int(swap_result['amount']), swap_result['swap_chain_xdr']
async function findSwapPath() {
const headers = { 'Content-Type': 'application/json' };
const body = JSON.stringify({
token_in_address: tokenIn.contractId(Networks.PUBLIC),
token_out_address: tokenOut.contractId(Networks.PUBLIC),
amount: amount.toString(),
});
const estimateResponse = await fetch(`${baseApi}/find-path/`, { method: 'POST', body, headers });
const estimateResult = await estimateResponse.json();
console.log(estimateResult);
// {
// success: true,
// swap_chain_xdr: 'AAAAEAAAAAE...SEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
// pools: [ 'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC' ],
// tokens: [
// 'native',
// 'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
// ],
// amount: 1724745895
// }
if (!estimateResult.success) {
throw new Error('Estimate failed');
}
return estimateResult;
}
3. Make AMM Router smart contract call of the method swap_chained using XDR from Find Path API: Use the XDR from the Find Path API to make a smart contract call to the swap_chained method in the AMM Router smart contract.
def execute_swap(
network: str,
soroban_rpc_server: SorobanServer,
horizon_server: Server,
keypair: Keypair,
router_contract_id: str,
token_in_address: str,
amount_in: int,
min_amount_out: int,
swap_path: str,
) -> int:
tx = soroban_rpc_server.prepare_transaction(
TransactionBuilder(
horizon_server.load_account(keypair.public_key),
network_passphrase=network,
base_fee=10000
).set_timeout(300)
.append_invoke_contract_function_op(
contract_id=router_contract_id,
function_name="swap_chained",
parameters=[
scval.to_address(keypair.public_key),
SCVal.from_xdr(swap_path),
scval.to_address(token_in_address),
scval.to_uint128(amount_in),
scval.to_uint128(min_amount_out),
],
)
.build()
)
tx.sign(keypair)
submit_response = horizon_server.submit_transaction(tx)
assert submit_response['successful']
result_meta = soroban_server.get_transaction(submit_response['id']).result_meta_xdr
transaction_meta = TransactionMeta.from_xdr(result_meta)
result = transaction_meta.v3.soroban_meta.return_value
return u128_to_int(result.u128)
async function executeSwap(estimateResult) {
const keypair = Keypair.fromSecret(userSecretKey);
const sorobanServer = new rpc.Server(sorobanServerUrl);
const horizonServer = new Horizon.Server(horizonServerUrl);
// No need to generate swapsChain manually, use value received from find-path api
const swapsChain = xdr.ScVal.fromXDR(estimateResult.swap_chain_xdr, 'base64');
const tokenInScVal = Address.contract(StrKey.decodeContract(tokenIn.contractId(Networks.PUBLIC))).toScVal()
const amountU128 = new XdrLargeInt('u128', amount.toFixed()).toU128();
const amountWithSlippage = estimateResult.amount * 0.99; // slippage 1%
const amountWithSlippageU128 = new XdrLargeInt('u128', amountWithSlippage.toFixed()).toU128();
const account = await sorobanServer.getAccount(keypair.publicKey());
const tx = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.PUBLIC,
})
.addOperation(
new StellarSdk.Contract(routerContractId).call(
"swap_chained",
xdr.ScVal.scvAddress(Address.fromString(keypair.publicKey()).toScAddress()),
swapsChain,
tokenInScVal,
amountU128,
amountWithSlippageU128
)
)
.setTimeout(TimeoutInfinite)
.build();
const preparedTx = await sorobanServer.prepareTransaction(tx);
preparedTx.sign(keypair);
const result = await horizonServer.submitTransaction(preparedTx);
const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr
const returnValue = meta.v3().sorobanMeta().returnValue();
const swapResult = u128ToInt(returnValue.value());
console.log('Swap successful!');
console.log(`Swapped: ${amount / 1e7} ${tokenIn.code} => ${swapResult / 1e7} ${tokenOut.code}`);
}
This code swaps 1 XLM to AQUA with Aquarius AMM on mainnet.
To successfully execute the code, provide the secret key of a Stellar account with at least 3 XLM and an established trustline for AQUA.
import requests
from stellar_sdk import scval, SorobanServer, TransactionBuilder, Network, Server, Keypair, Asset
from stellar_sdk.xdr import SCVal, TransactionMeta, UInt128Parts
# =========================================
# Configuration & Setup
# =========================================
# This account must have at least 3 XLM and a trustline to AQUA.
user_secret_key = "S....."
keypair = Keypair.from_secret(user_secret_key)
# Input and output tokens
token_in = Asset.native() # XLM
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")
# Amount to swap: 1 XLM in stroops (1 XLM = 10^7 stroops)
amount_in = 1_0000000
# Router contract for AMM swaps on mainnet
router_contract_id = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK"
# Soroban and Horizon servers
soroban_server = SorobanServer("https://mainnet.sorobanrpc.com")
horizon_server = Server("https://horizon.stellar.org/")
network = Network.PUBLIC_NETWORK_PASSPHRASE
# AQUA AMM API endpoint
base_api = 'https://amm-api.aqua.network/api/external/v1'
# =========================================
# Utility Function
# =========================================
def u128_to_int(value: UInt128Parts) -> int:
"""Convert Uint128Parts to Python int."""
return (value.hi.uint64 << 64) + value.lo.uint64
# =========================================
# Functions
# =========================================
def find_swap_path(base_api: str, token_in_address: str, token_out_address: str, amount_in: int) -> (int, str):
"""
Call the Find Path API to retrieve the swap chain and estimated output amount.
"""
print("Requesting swap path from AMM API...")
data = {
'token_in_address': token_in_address,
'token_out_address': token_out_address,
'amount': amount_in
}
response = requests.post(f'{base_api}/find-path/', json=data)
swap_result = response.json()
print(swap_result)
"""
{
'success': True,
'swap_chain_xdr': 'AAAAEAAAAAEAAAABAAAAEAAAAAEAAAADAAAAEAAAAAEAAAACAAAAEgAAAAEltPzYWa7C+mNIQ4xImzw8EMmLbSG+T9PLMMtolT75dwAAABIAAAABKIUvaMGYSI40b7EhLtUCkFN2HMJPRTOS41OYIBsIJecAAAANAAAAILLgL8/KbJb4rVy9hOd4Snd7NtnJaiRZQCxPRYRiqrfwAAAAEgAAAAEohS9owZhIjjRvsSEu1QKQU3Ycwk9FM5LjU5ggGwgl5w==',
'pools': [
'CDE57N6XTUPBKYYDGQMXX7E7SLNOLFY3JEQB4MULSMR2AKTSAENGX2HC'
],
'tokens': [
'native',
'AQUA:GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
],
'amount': 3627808902
}
"""
if not swap_result.get('success', False):
raise Exception("Failed to retrieve swap path from the API.")
print("Swap path retrieved. Estimated output amount:", swap_result['amount'])
return int(swap_result['amount']), swap_result['swap_chain_xdr']
def execute_swap(
network: str,
soroban_rpc_server: SorobanServer,
horizon_server: Server,
keypair: Keypair,
router_contract_id: str,
token_in_address: str,
amount_in: int,
min_amount_out: int,
swap_path: str,
) -> int:
"""
Executes the chained swap transaction on Soroban and returns the final amount out.
"""
print("Preparing and building swap transaction...")
source_account = horizon_server.load_account(keypair.public_key)
# Build the transaction to invoke `swap_chained`
tx = (
TransactionBuilder(
source_account=source_account,
network_passphrase=network,
base_fee=10000
)
.set_timeout(300)
.append_invoke_contract_function_op(
contract_id=router_contract_id,
function_name="swap_chained",
parameters=[
scval.to_address(keypair.public_key),
SCVal.from_xdr(swap_path),
scval.to_address(token_in_address),
scval.to_uint128(amount_in),
scval.to_uint128(min_amount_out),
],
)
.build()
)
# Prepare transaction to get Soroban-specific data (footprint, etc.)
print("Preparing transaction on Soroban...")
prepared_tx = soroban_rpc_server.prepare_transaction(tx)
# Sign the prepared transaction
print("Signing transaction...")
prepared_tx.sign(keypair)
# Submit the transaction to Horizon
print("Submitting transaction to Horizon...")
submit_response = horizon_server.submit_transaction(prepared_tx)
if not submit_response.get('successful', False):
raise Exception("Transaction failed: " + str(submit_response))
print("Transaction submitted successfully. Fetching result...")
# Get the transaction result from Soroban server to access Soroban metadata
tx_info = soroban_server.get_transaction(submit_response['id'])
if not tx_info or not tx_info.result_meta_xdr:
raise Exception("No transaction metadata found.")
# Extract the result from the Soroban metadata
transaction_meta = TransactionMeta.from_xdr(tx_info.result_meta_xdr)
return_val = transaction_meta.v3.soroban_meta.return_value
final_amount_out = u128_to_int(return_val.u128)
print("Swap executed successfully.")
return final_amount_out
# =========================================
# Entry Point
# =========================================
print("Starting swap process...")
print(f"Swapping {amount_in / 1e7} {token_in.code} for {token_out.code}...")
# 1. Find the swap path and estimated output
amount_out_estimate, swap_path_xdr = find_swap_path(
base_api,
token_in.contract_id(network),
token_out.contract_id(network),
amount_in
)
# Apply 1% slippage tolerance
min_amount_out = int(amount_out_estimate * 0.99)
print(f"Applying 1% slippage. Minimum expected amount out: {min_amount_out / 1e7} {token_out.code}")
# 2. Execute the swap
amount_out = execute_swap(
network,
soroban_server,
horizon_server,
keypair,
router_contract_id,
token_in.contract_id(network),
amount_in,
min_amount_out,
swap_path_xdr
)
print("Swap completed successfully!")
print(f"Amount out: {amount_out / 10 ** 7} {token_out.code}")
const StellarSdk = require('@stellar/stellar-sdk');
const {
xdr,
Address,
Asset,
StrKey,
XdrLargeInt,
Networks,
TransactionBuilder,
rpc,
BASE_FEE,
TimeoutInfinite,
Keypair,
Horizon,
} = StellarSdk;
// =========================================
// Configuration & Setup
// =========================================
// TODO: Enter the secret key of the account executing the swap.
// The account must have at least 3 XLM and a trustline to AQUA.
const userSecretKey = 'S.....';
// Input and output tokens
const tokenIn = Asset.native();
const tokenOut = new Asset(
'AQUA',
'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA'
);
// Amount to swap: 1 XLM = 10^7 stroops
const amount = 10_000_000;
// Soroban and Horizon server endpoints
const horizonServerUrl = "https://horizon.stellar.org";
const sorobanServerUrl = 'https://mainnet.sorobanrpc.com';
// AQUA AMM API endpoint
const baseApi = 'https://amm-api.aqua.network/api/external/v1';
// Router contract ID
const routerContractId = "CBQDHNBFBZYE4MKPWBSJOPIYLW4SFSXAXUTSXJN76GNKYVYPCKWC6QUK";
// =========================================
// Utility Function
// =========================================
function u128ToInt(value) {
const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);
if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
return Number(result);
} else {
console.warn("Value exceeds JavaScript's safe integer range");
return null;
}
}
// =========================================
// Functions
// =========================================
async function findSwapPath() {
console.log("Requesting swap path from the AMM API...");
const headers = { 'Content-Type': 'application/json' };
const body = JSON.stringify({
token_in_address: tokenIn.contractId(Networks.PUBLIC),
token_out_address: tokenOut.contractId(Networks.PUBLIC),
amount: amount.toString(),
});
const estimateResponse = await fetch(`${baseApi}/find-path/`, { method: 'POST', body, headers });
const estimateResult = await estimateResponse.json();
if (!estimateResult.success) {
throw new Error('Failed to retrieve swap path from AMM API.');
}
console.log(`Swap path obtained. Estimated output amount: ${estimateResult.amount / 1e7} ${tokenOut.code}`);
return estimateResult;
}
async function executeSwap(estimateResult) {
console.log("Preparing swap transaction...");
const keypair = Keypair.fromSecret(userSecretKey);
const sorobanServer = new rpc.Server(sorobanServerUrl);
const horizonServer = new Horizon.Server(horizonServerUrl);
// Construct the parameters from the estimate result
const swapsChain = xdr.ScVal.fromXDR(estimateResult.swap_chain_xdr, 'base64');
const tokenInScVal = Address.contract(StrKey.decodeContract(tokenIn.contractId(Networks.PUBLIC))).toScVal();
const amountU128 = new XdrLargeInt('u128', amount.toString()).toU128();
// Apply 1% slippage
const amountWithSlippage = estimateResult.amount * 0.99;
const amountWithSlippageU128 = new XdrLargeInt('u128', Math.floor(amountWithSlippage).toString()).toU128();
console.log("Loading account from Soroban server...");
const account = await sorobanServer.getAccount(keypair.publicKey());
console.log("Building transaction...");
const tx = new TransactionBuilder(account, {
fee: BASE_FEE,
networkPassphrase: Networks.PUBLIC,
})
.addOperation(
new StellarSdk.Contract(routerContractId).call(
"swap_chained",
xdr.ScVal.scvAddress(Address.fromString(keypair.publicKey()).toScAddress()),
swapsChain,
tokenInScVal,
amountU128,
amountWithSlippageU128
)
)
.setTimeout(TimeoutInfinite)
.build();
console.log("Preparing transaction on Soroban server...");
const preparedTx = await sorobanServer.prepareTransaction(tx);
console.log("Signing transaction...");
preparedTx.sign(keypair);
console.log("Submitting transaction to Horizon...");
const result = await horizonServer.submitTransaction(preparedTx);
if (!result) {
throw new Error("Transaction submission failed.");
}
console.log("Transaction successful. Extracting results...");
const meta = (await sorobanServer.getTransaction(result.id)).resultMetaXdr;
const returnValue = meta.v3().sorobanMeta().returnValue();
const swapResult = u128ToInt(returnValue.value());
console.log('Swap successful!');
console.log(`Swapped: ${amount / 1e7} ${tokenIn.code} => ${swapResult / 1e7} ${tokenOut.code}`);
}
// =========================================
// Entry Point
// =========================================
findSwapPath()
.then(estimated => executeSwap(estimated))
.catch(err => console.error("Error during swap process:", err));