Skip to main content
TradingClient provides liquidity to the Uniswap V3 pools on Flashnet Execution. Mint a position, grow or shrink it, collect fees, or reposition, each as one signed intent that funds from your Spark balance and routes through the Conductor.
These are the Uniswap V3 pools the Conductor wraps on Execution, managed with TradingClient. The Spark-native AMM under Markets is a separate system with its own client.

Configure for liquidity

LP needs more contract addresses than swaps. On TradingConfig:
FieldWhen required
positionManagerAddressevery LP method
wbtcAddressBTC-paired pools
permit2Addressfunding from Spark (useAvailableBalance)
lpGasLimit is optional.
const trading = new TradingClient(execClient, {
  conductorAddress: process.env.CONDUCTOR_ADDRESS,
  positionManagerAddress: process.env.POSITION_MANAGER_ADDRESS,
  wbtcAddress: process.env.WBTC_ADDRESS,        // BTC-paired pools
  permit2Address: process.env.PERMIT2_ADDRESS,  // useAvailableBalance funding
});
On staging, new TradingClient(execClient, "regtest") includes all of these, so you can skip the explicit config.

How positions work

A position is a Uniswap V3 concentrated-liquidity NFT held by the NonfungiblePositionManager, identified by a tokenId and bounded by two ticks. Four conventions carry across every method:
  • Pass pair tokens sorted: token0 < token1 by lowercase address.
  • The WBTC leg is denominated in wei, not sats (1 sat = 1e10 wei). Convert with satsToWei and weiToSats from @flashnet/sdk.
  • For a BTC-paired pool, use the WBTC address as the BTC leg’s token. The SDK routes to the Conductor’s BTC entry point and funds msg.value.
  • Ticks are standard Uniswap V3 ticks, where price = 1.0001^tick. V3TickMath from @flashnet/sdk converts between prices and ticks.

Funding

LP methods fund the same two ways as swaps. With useAvailableBalance: true, the SDK pulls each leg from your Spark balance and bundles the funding transfers with the Conductor call in one intent. Supply the Spark token id for each ERC20 leg (token0SparkId, token1SparkId); the BTC leg funds from the WBTC slot. ERC20 legs are pulled via Permit2, so each token needs a standing Permit2 approval from your EVM address first:
await trading.ensurePermit2Approval(tokenAddress);  // requires permit2Address in config
Without useAvailableBalance, the methods spend balances already on the Execution side.

Mint a position

addLiquidity mints a new position NFT.
const result = await trading.addLiquidity({
  token0: "0x...",             // sorted: token0 < token1
  token1: "0x...",
  fee: 3000,
  tickLower: -887220,          // full range for the 0.3% tier
  tickUpper: 887220,
  amount0Desired: "1000000",
  amount1Desired: "1000000",
  amount0Min: "990000",        // slippage floor
  amount1Min: "990000",
  useAvailableBalance: true,
  token0SparkId: "btkn1...",   // required for ERC20 legs funded from Spark
  token1SparkId: "btkn1...",
});
For a BTC-paired pool, set one leg’s token to the WBTC address and its amounts in wei. That leg funds from msg.value and takes no Spark token id.

Grow or shrink

increaseLiquidity adds to a position. decreaseLiquidity removes liquidity and withdraws the recovered tokens to Spark.
await trading.increaseLiquidity({
  tokenId: 123n,
  amount0Desired: "500000",
  amount1Desired: "500000",
  amount0Min: "495000",
  amount1Min: "495000",
  useAvailableBalance: true,
  token0SparkId: "btkn1...",
  token1SparkId: "btkn1...",
});

await trading.decreaseLiquidity({
  tokenId: 123n,
  liquidity: 1_000_000n,       // liquidity units to remove
  amount0Min: "495000",
  amount1Min: "495000",
});
decreaseLiquidity sends the recovered token0 and token1 to your Spark wallet. sparkRecipient defaults to your identity key.

Collect fees

collectFees sweeps accrued fees to Spark without touching principal.
await trading.collectFees({ tokenId: 123n });

Reposition

modifyPosition moves a position to a new tick range. It removes all liquidity from the current position, optionally adds capital, and mints a new NFT at the new range.
await trading.modifyPosition({
  tokenId: 123n,
  newTickLower: -443580,
  newTickUpper: 443580,
  amount0Min: "0",                      // floors on the OLD position's recovery
  amount1Min: "0",
  additionalAmount0Desired: "250000",   // optional extra capital
  additionalAmount1Desired: "250000",
  newAmount0Min: "240000",              // floors on minting the NEW position
  newAmount1Min: "240000",
});

Read positions

getPosition and listPositions read straight from the NonfungiblePositionManager.
const position = await trading.getPosition(123n);
position.liquidity;    // bigint
position.tickLower;    // number
position.tokensOwed0;  // uncollected fees, bigint

const mine = await trading.listPositions();           // your positions
const theirs = await trading.listPositions("0x...");  // another owner's
PositionInfo mirrors the NonfungiblePositionManager’s positions() tuple: token0, token1, fee, tickLower, tickUpper, liquidity, tokensOwed0/tokensOwed1, and the fee-growth checkpoints.

What the result tells you

interface LpWriteResult {
  submissionId: string;
  intentId: string;
  status: string;
  evmTxHash: string;
  inboundSparkTransferIds?: string[];  // set on the useAvailableBalance path
}
The result carries submission handles and the signed-tx hash, not on-chain outputs. The minted tokenId, the liquidity added, amounts used, and fees collected are knowable only after the intent executes. Read them from the transaction receipt or the Conductor LP events (LiquidityAdded, LiquidityIncreased, LiquidityDecreased, FeesCollected, PositionModified) once the intent reaches INCLUDED_PENDING_FINALITY.

Fund recovery

When an addLiquidity, increaseLiquidity, or modifyPosition call with useAvailableBalance fails after its funding transfers commit, the SDK claws the committed transfers back to your Spark balance and throws StrandedFundingError with a clawbackSummary of what was recovered. This matches the swap round-trip’s behavior. decreaseLiquidity and collectFees have no inbound funding, so they carry no clawback.

Cross-references

  • Swaps trade through the same pools.
  • Conductor is the contract these calls route through.