♒
Aquarius Guide
  • 👋Welcome to Aquarius
  • Developers
    • Integrating with Aquarius
    • Aquarius Soroban Functions
    • Code Examples
      • Prerequisites & Basics
      • Executing Swaps Through Optimal Path
      • Executing Swaps Through Specific Pool
      • Deposit Liquidity
      • Withdraw Liquidity
      • Get Pools Info
      • Claim LP Rewards
      • Add Fees To Swap
        • Deploying a New Fee Collector
        • Executing Swaps with Provider Fees
        • Claiming & Swapping Accumulated Fees
  • Ecosystem Overview
    • 🌐What is Stellar?
      • What are Lumens (XLM)?
      • What are Anchors?
      • What are Trustlines?
      • How much are network fees on Stellar?
      • What are network reserves?
      • Where to trade Stellar assets?
    • 🧮What is Soroban?
  • AQUA tokens
    • ♒What are AQUA tokens?
      • AQUAnomics
      • AQUA Wallets
      • Where can I buy AQUA?
  • ICE
    • 🧊ICE tokens: locking AQUA and getting benefits
    • ICE boosts - how to maximize LP rewards
  • Aquarius AMMs
    • 💱What are Aquarius AMMs?
      • Pools
        • Creating a Pool
        • Deposit & Withdraw Liquidity
      • Swap
      • System limitations
        • Aquarius AMM: Limitations in Support for Fee-on-Transfer, Rebasing, and Deflationary Tokens
        • Aquarius AMM: Token Address Migration Limitations and Mitigation Strategy
  • My Aquarius
    • 👤My Aquarius
      • Main Overview
      • Balances
      • My Liquidity
      • SDEX Rewards
      • Liquidity Votes
      • Governance Votes
      • Airdrop #2
      • ICE Locks
      • Payments History
  • Aquarius AQUA Rewards
    • 🗳️Aquarius voting
      • Aquarius voting: asset Flag Restrictions
    • 🪙SDEX Rewards
    • 🤖Aquarius AMM Rewards
  • Bribes
    • 🎁What are bribes?
      • What are the advantages of protocol level bribes?
  • Aquarius Governance
    • 🧑‍⚖️Aquarius Governance: Community-Led Decision Making
  • Airdrops
    • 1️⃣The Initial Airdrop
      • Am I Eligible For the Initial Airdrop?
      • How can I see if I am eligible?
      • What are Claimable Balances?
      • How is the Initial airdrop distributed?
      • Where can I find more information?
    • 🌠Airdrop #2
      • How could I have been eligible for Airdrop #2?
      • How can I see if I am eligible?
      • When was the Airdrop #2 snapshot?
      • Were there any CEX's taking part?
      • How big was Airdrop #2?
      • How will the airdrop be distributed and for how long?
      • Could I have increased my potential reward?
      • Where can I find more information?
  • Signers Guild
    • 📜What is the signers guild?
      • What percentage of the AQUA supply will be controlled by the Signers Guild?
      • Who will be in the Signers Guild?
      • How does the Signing process work?
      • What will be expected from a guild member?
      • How can I sign up for this position?
      • What are wallets that Guild members will manage?
      • How can I learn more about this?
  • Guides
    • ❔How to use AQUA Locker tool and get ICE tokens
    • ❔How to vote for markets on Aquarius
    • How to create bribes
    • ❔How to use Aquarius Governance
      • How to make a governance vote
      • How to create a proposal
    • ❔How to earn SDEX rewards
    • ❔How to earn AMM rewards
  • Technical Documents
    • 📜Audits
    • 🪲Bug Bounties
    • 🛄Claimable Balances
    • 🗳️The Aquarius Voting Mechanism
    • 🎁SDEX v2 proposal & algorithm
    • ⏩ICE Boost Formula
  • Useful Links
    • Aquarius Home
    • Liquidity Voting
    • Liquidity Rewards
    • Aquarius Bribes
    • ICE locker
    • Aquarius Governance
    • Airdrop #2
Powered by GitBook
On this page
  1. Developers
  2. Code Examples
  3. Add Fees To Swap

Executing Swaps with Provider Fees

PreviousDeploying a New Fee CollectorNextClaiming & Swapping Accumulated Fees

Last updated 26 days ago

After the fee you can start swapping. The interface for swap with fees will be the same as for "free" swaps with one parameter added - fee_fraction .

To charge a fee replace your direct swap_chained call with the wrapper’s version that accepts a fee fraction:

  • Function: swap_chained(e, user: Address, swaps_chain: Vec<…>, token_in: Address, in_amount: u128, out_min: u128, fee_fraction: u32) -> u128

  • Args:

    • fee_fraction: Fee in bps to deduct on this swap.

    • Validated on‑chain: if fee_fraction > max_swap_fee_fraction the call reverts.

  • Returns: Net output amount delivered to user after deducting provider fee.

Note:

  • Keep in mind that fee_fraction must not exceed max_swap_fee_fraction that was set up deploying the fee collector contract.

The example below describes a swap with all the parameters explained.

from decimal import Decimal

import requests
from stellar_sdk import Asset, Keypair, Network, scval, Server, SorobanServer, TransactionBuilder
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 = "SA..........."
keypair = Keypair.from_secret(user_secret_key)

# Input and output tokens
token_in = Asset.native()  # XLM
token_out = Asset("AQUA", "GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA")

# If True, the swap behaves like strict-send: the amount of the sending asset is fixed.
# If False, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
is_send = True
# Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
amount = 1_0000000
slippage = Decimal("0.005")  # 0.5% slippage
provider_fee = Decimal("0.003")  # 0.3% provider fee

# swap provider contract for AMM swaps on mainnet
# IMPORTANT: Replace with your deployed contract ID. This contract address is only for test purposes.
provider_swap_contract_id = "CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS"

# 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: int, is_send: bool) -> (int, str):
    """
    Call the Find Path API to retrieve the swap chain and estimated amount.
    """
    print("Requesting swap path from AMM API...")
    data = {
        'token_in_address': token_in_address,
        'token_out_address': token_out_address,
        'amount': amount,
        'slippage': str(slippage),
        'provider_fee': str(provider_fee),
    }
    endpoint = '/find-path/' if is_send else '/find-path-strict-receive/'
    response = requests.post(f'{base_api}{endpoint}', 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,
          'amount_with_fee': 3627808002,
        }
    """

    if not swap_result.get('success', False):
        raise Exception("Failed to retrieve swap path from the API.")

    print("Swap path retrieved. Estimated amount:", swap_result['amount'])
    print("Estimated amount with fee:", swap_result['amount_with_fee'])
    return int(swap_result['amount_with_fee']), 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: int,
        amount_with_slippage: int,
        swap_path: str,
        provider_fee_bps: int,
        is_send: bool,
) -> 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)

    function_name = 'swap_chained' if is_send else 'swap_chained_strict_receive'

    # 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=function_name,
            parameters=[
                scval.to_address(keypair.public_key),
                SCVal.from_xdr(swap_path),
                scval.to_address(token_in_address),
                scval.to_uint128(amount),
                scval.to_uint128(amount_with_slippage),
                scval.to_uint32(provider_fee_bps),
            ],
        )
        .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 = u128_to_int(return_val.u128)
    print("Swap executed successfully.")
    return final_amount


# =========================================
# Entry Point
# =========================================

print("Starting swap process...")
print(f"Swapping {token_in.code} for {token_out.code}...")

# 1. Find the swap path and estimated output
amount_with_slippage, swap_path_xdr = find_swap_path(
    base_api,
    token_in.contract_id(network),
    token_out.contract_id(network),
    amount,
    is_send,
)

# 2. Execute the swap
amount = execute_swap(
    network,
    soroban_server,
    horizon_server,
    keypair,
    provider_swap_contract_id,
    token_in.contract_id(network),
    amount,
    amount_with_slippage,
    swap_path_xdr,
    int(provider_fee * 10000),  # Convert to basis points
    is_send,
)

print("Swap completed successfully!")

if is_send:
    print(f"Amount out: {amount / 10 ** 7} {token_out.code}")  # If it's strict-send, show output amount
else:
    print(f"Amount in: {amount / 10 ** 7} {token_in.code}")  # If it's strict-receive, show input amount
const {
    Horizon,
    Keypair,
    Asset,
    Networks,
    TransactionBuilder,
    xdr,
    nativeToScVal,
    Address,
    rpc,
    Operation,
} = require('@stellar/stellar-sdk');

// =========================================
// Configuration & Setup
// =========================================
// This account must have at least 3 XLM and a trustline to AQUA.
const userSecretKey = 'SA...........';
const keypair = Keypair.fromSecret(userSecretKey);

// Assets
const tokenIn = Asset.native();
const tokenOut = new Asset('AQUA', 'GBNZILSTVQZ4R7IKQDGHYGY2QXL5QOFJYQMXPKWRRM5PAV7Y4M67AQUA');


// If true, the swap behaves like strict-send: the amount of the sending asset is fixed.
// If false, the swap behaves like strict-receive: the amount of the receiving asset is fixed.
const isSend = true;
// Amount of 1 XLM or 1 AQUA in stroops (depending on is_send)
const amount = 1_0000000;
const slippage = 0.005;   // 0.5%
const providerFee = 0.003; // 0.3%

const providerSwapContractId = 'CDJCVXFIT2UIVNLC22OWCHABTWKXUSYYLPKKBLJW67ISMIWX56YJBGLS';

const sorobanServer = new rpc.Server('https://mainnet.sorobanrpc.com');
const horizonServer = new Horizon.Server('https://horizon.stellar.org');
const networkPassphrase = Networks.TESTNET;

const baseApi = 'https://amm-api.aqua.network/api/external/v1';

// =========================================
// Utilities
// =========================================
function u128ToInt(value) {
    /**
     * Converts UInt128Parts from Stellar's XDR to a JavaScript number.
     *
     * @param {Object} value - UInt128Parts object from Stellar SDK, with `hi` and `lo` properties.
     * @returns {number|null} Corresponding JavaScript number, or null if the number is too large.
     */
    const result = (BigInt(value.hi()._value) << 64n) + BigInt(value.lo()._value);

    // Check if the result is within the safe integer range for JavaScript numbers
    if (result <= BigInt(Number.MAX_SAFE_INTEGER)) {
        return Number(result);
    } else {
        console.warn("Value exceeds JavaScript's safe integer range");
        return null;
    }
}

// =========================================
// API: Find Swap Path
// =========================================
// Call the Find Path API to retrieve the swap chain and estimated amount.
async function findSwapPath(tokenInAddress, tokenOutAddress, amount, isSend) {
    const data = {
        token_in_address: tokenInAddress,
        token_out_address: tokenOutAddress,
        amount: amount,
        slippage: slippage.toString(),
        provider_fee: providerFee.toString()
    };
    const endpoint = isSend ? '/find-path/' : '/find-path-strict-receive/';
    const response = await fetch(`${baseApi}${endpoint}`, {
        method: 'POST',
        body: JSON.stringify(data),
        headers: { 'Content-Type': 'application/json' }
    });

    const swapResult = await response.json();

    if (!swapResult.success) {
        throw new Error('Failed to retrieve swap path from the API.');
    }

    console.log('Swap path retrieved. Estimated amount:', swapResult.amount / 1e7);
    return {
        amountWithFee: parseInt(swapResult.amount_with_fee),
        swapPathXdr: swapResult.swap_chain_xdr
    };
}

// =========================================
// Soroban: Execute Swap Transaction
// =========================================
// Executes the chained swap transaction on Soroban and returns the final amount out.
async function executeSwap({
    keypair,
    routerContractId,
    tokenInAddress,
    amount,
    amountWithSlippage,
    swapPathXdr,
    providerFeeBps,
    isSend
}) {
    const account = await horizonServer.loadAccount(keypair.publicKey());
    const functionName = isSend ? 'swap_chained' : 'swap_chained_strict_receive';

    // Build the transaction to invoke `swap_chained`
    const tx = new TransactionBuilder(account, {
        fee: '10000',
        networkPassphrase
    })
        .setTimeout(300)
        .addOperation(
            Operation.invokeContractFunction({
                contract: routerContractId,
                function: functionName,
                args: [
                    nativeToScVal(new Address(keypair.publicKey())),
                    xdr.ScVal.fromXDR(swapPathXdr, 'base64'),
                    nativeToScVal(new Address(tokenInAddress)),
                    nativeToScVal(BigInt(amount), { type: 'u128' }),
                    nativeToScVal(BigInt(amountWithSlippage), { type: 'u128' }),
                    nativeToScVal(providerFeeBps, { type: 'u32' })
                ],
            })
        )
        .build();

    // Prepare transaction to get Soroban-specific data (footprint, etc.)
    const preparedTx = await sorobanServer.prepareTransaction(tx);
    // Sign the prepared transaction
    preparedTx.sign(keypair);

    // Submit the transaction to Horizon
    const submitResponse = await horizonServer.submitTransaction(preparedTx);

    if (!submitResponse.successful) {
        throw new Error('Transaction failed: ' + JSON.stringify(submitResponse));
    }

    // Get the transaction result from Soroban server to access Soroban metadata
    const txResult = await sorobanServer.getTransaction(submitResponse.id);
    const meta = txResult.resultMetaXdr;

    if (!meta) {
        throw new Error('No metadata returned from transaction.');
    }

    // Extract the result from the Soroban metadata
    const returnVal = meta.value().sorobanMeta().returnValue();
    const u128 = returnVal.value();

    return u128ToInt(u128);
}

// =========================================
// Main
// =========================================
(async () => {
    try {
        console.log(`Swapping ${tokenIn.code} for ${tokenOut.code}...`);

        const tokenInAddress = tokenIn.contractId(networkPassphrase);
        const tokenOutAddress= tokenOut.contractId(networkPassphrase);

        // 1. Get path & expected output with fees/slippage
        const { amountWithFee, swapPathXdr } = await findSwapPath(
            tokenInAddress,
            tokenOutAddress,
            amount,
            isSend
        );

        // 2. Execute Soroban swap
        const providerFeeBps = Math.round(providerFee * 10000);
        const finalAmount = await executeSwap({
            keypair,
            routerContractId: providerSwapContractId,
            tokenInAddress,
            amount,
            amountWithSlippage: amountWithFee,
            swapPathXdr,
            providerFeeBps,
            isSend
        });

        console.log('Swap completed!');
        if (isSend) {
            console.log(`Amount out: ${finalAmount / 1e7} ${tokenOut.code}`);
        } else {
            console.log(`Amount in: ${finalAmount / 1e7} ${tokenIn.code}`);
        }
    } catch (err) {
        console.error('❌ Swap failed:', err.message || err);
    }
})();
collector contract is deployed