♒
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 5 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