Skip to main content

Documentation Index

Fetch the complete documentation index at: https://flashnet-build.mintlify.app/llms.txt

Use this file to discover all available pages before exploring further.

TradingClient wraps ExecutionClient and handles the full DEX path: pulling assets in from Spark, calling the Conductor swap, and sweeping the output back. One signed intent per swap on the EIP-2612 (useAvailableBalance) and Permit2 paths; two on the default exact approval path (one to approve, one to swap).

Setup

import { ExecutionClient, TradingClient } from "@flashnet/sdk";

const execClient = new ExecutionClient(sparkWallet, {
  gatewayUrl,
  rpcUrl,
  chainId,
});
await execClient.authenticate();

const trading = new TradingClient(execClient, {
  conductorAddress: process.env.CONDUCTOR_ADDRESS,
});
TradingConfig accepts conductorAddress (required), and optional permit2Address, approvalMode, swapGasLimit, approveGasLimit. See Approval modes below.

Swap from Spark balance

The most common flow: input asset is sitting in the user’s Spark wallet, output should land back in the same Spark wallet.
const result = await trading.swap({
  assetInAddress: "btc",
  assetOutAddress: "0x...",          // EVM address of the output token (Spark token contract)
  amountIn: "100000",                // 100,000 sats in
  minAmountOut: "950000000000000000",
  fee: 3000,                         // 0.3% Uniswap V3 tier
  withdraw: true,
  useAvailableBalance: true,         // pull amountIn from the Spark wallet
});

console.log(result.intentId);
console.log(result.evmTxHash);
console.log(result.inboundSparkTransferId);
useAvailableBalance: true makes the SDK send the Spark transfer to the gateway and bundle the resulting transferId into the same intent that runs the swap. Nothing is expected to sit on the EVM address before or after. For ERC20 inputs, set assetInSparkTokenId to the Spark-side tokenIdentifier (bech32m form) corresponding to assetInAddress so the SDK can issue the matching Spark transfer. The token contract must implement EIP-2612 Permit (SparkToken does), so no prior on-chain approve is required. The output side never needs a token id — it’s derived from the contract address.

Swap from existing EVM balance

If the assets are already on the Execution side (deposited earlier or held from a previous swap), skip useAvailableBalance:
const result = await trading.swap({
  assetInAddress: "0x...",          // ERC20 contract on Execution
  assetOutAddress: "btc",
  amountIn: "1000000",
  minAmountOut: "95000",            // sats
  fee: 3000,
  withdraw: true,
});
For non-Permit tokens the SDK transparently submits a one-time approval intent before the swap. For Permit tokens the swap is one intent.

Approval modes

The non-useAvailableBalance path needs to authorize the Conductor to pull input tokens. Configure on the TradingClient:
approvalModeBehavior
"exact" (default)Submit a one-off approve(conductor, amountIn) intent before the swap, poll until it lands. Two intents per swap.
"permit2"Sign an EIP-712 PermitTransferFrom and pass it to swap*WithPermit2. One intent per swap. Requires permit2Address on TradingConfig (Uniswap canonical is 0x000000000022D473030F116dDEE9F6B43aC78BA3; localnet deploys its own).
The useAvailableBalance path uses EIP-2612 Permit on the SparkToken directly and bypasses both modes.

Slippage

minAmountOut is yours to compute. The SDK does not fetch quotes. Get a quote off-chain (or simulate via a static call) and apply your slippage tolerance:
const minAmountOut = (quotedOut * 9950n) / 10_000n; // 0.5% slippage
"0" disables slippage protection. Don’t ship that to production.

Fee tiers

Match the tier of the pool you intend to trade against:
TierUse case
500Stablecoin pairs
3000Most pairs
10000Long-tail / volatile pairs
If the pool doesn’t exist at the tier you pass, the swap reverts.

Integrator fees

Pass integratorFeeRateBps and integratorPublicKey to take a basis-point cut of the swap that the Conductor routes to your Spark address.
await trading.swap({
  // ...
  integratorFeeRateBps: 25,        // 0.25%
  integratorPublicKey: "02ab...",
});

What the result tells you

interface SwapResult {
  submissionId: string;          // gateway submission id (track admission)
  intentId: string;              // canonical intent id
  status: IntentStatus;          // "accepted" | "oracle_pending" | "included_pending_finality" | "finalized" | "rejected" | "expired"
  evmTxHash: string;             // keccak of the signed tx, for explorer linking
  inboundSparkTransferId?: string; // set when useAvailableBalance was true
}
When the intent reaches finalized, the output asset has been dispatched back to your Spark wallet. Poll the gateway for status or subscribe to your Spark wallet’s transfer events on the receiving side. See Intents → Status lifecycle for the full lifecycle.

Failure modes

The intent admits but the swap can still revert on-chain. The most common reasons:
  • Slippage exceeded. Output below minAmountOut. Tighten or widen the bound and retry.
  • Pool missing. Wrong fee tier, or the pool hasn’t been created yet.
  • Insufficient input. The deposit didn’t fund what the swap expected. Recheck amountIn and Spark transfer amount.
The gateway returns the revert selector in the intent’s statusMessage. Use decodeRevertReason() from @flashnet/sdk to translate it.

Two-intent fallback

For tokens that don’t implement Permit (and approvalMode: "exact"), the SDK splits the flow into one approval intent followed by the swap intent. You call swap() once and the SDK handles the sequencing. The tradeoff is the time it takes for the approval intent to finalize before the swap fires (typically sub-second on Flashnet sequencer policy, capped at a 30s SDK timeout).

Cross-references