Skip to content

Instantly share code, notes, and snippets.

@patcito
Created March 17, 2026 21:18
Show Gist options
  • Select an option

  • Save patcito/c85844de56c3250ac7fc8714645faedb to your computer and use it in GitHub Desktop.

Select an option

Save patcito/c85844de56c3250ac7fc8714645faedb to your computer and use it in GitHub Desktop.

Swap Collateral — Frontend Integration Guide

Overview

Users can swap one collateral type for another within the FlyingTulip lending protocol. For example, swap USDC collateral to wS collateral (or any supported pair). The frontend signs an EIP-712 order and submits it to the executor API. The executor handles DEX routing and on-chain execution.

User Flow

  1. User has collateral deposited in PositionsManager (e.g., USDC)
  2. User signs an EIP-712 swap order specifying sellToken → buyToken
  3. Frontend submits the signed order to the executor API
  4. Executor gets a DEX quote, executes the swap on-chain via flash fill
  5. User's collateral is swapped atomically (old collateral withdrawn, new collateral deposited)

API Endpoint

POST /leverage/orders
Content-Type: application/json

Request Body

{
  "order": {
    "sellToken": "0x29219dd400f2Bf60E5a23d13Be72B486D4038894",
    "buyToken": "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38",
    "sellAmount": "50000000",
    "buyAmount": "100000000000000",
    "validTo": 1773785334,
    "appData": "0x0000000000000000000000000000000000000000000000000000000000000000",
    "feeAmount": "0",
    "kind": 0,
    "partiallyFillable": false,
    "sellTokenBalance": 0,
    "buyTokenBalance": 0
  },
  "session": {
    "sessionId": "0x...",
    "target": "0xddDfFBb0F870EF1b332019cFCAd8411aD35ebE2D",
    "dataHash": "0x...",
    "nonce": "1",
    "deadline": "1773785334",
    "executor": "0x...",
    "feeAmount": "0"
  },
  "delegateSignature": "0x...",
  "orderType": "swap",
  "user": "0x..."
}

Response

{
  "success": true,
  "orderId": "lev_6f80e46b9de540a42c3bd1897a880827",
  "status": "pending"
}

Step-by-Step Integration (viem/TypeScript)

1. Get Executor Config

const config = await fetch("https://executor.flyingtulip.com/leverage/config").then(r => r.json());
// { chainId: 146, sessionManager: "0x52Ef...", leverageRfqEngine: "0xddDf...", executorAddress: "0xf39F..." }

2. Get Session Nonce

import { createPublicClient, http } from "viem";
import { sonic } from "viem/chains";

const client = createPublicClient({ chain: sonic, transport: http() });
const nonce = await client.readContract({
  address: config.sessionManager,
  abi: [{ name: "nonces", type: "function", inputs: [{ type: "bytes32" }], outputs: [{ type: "uint256" }] }],
  functionName: "nonces",
  args: [sessionId],
});

3. Compute DataHash

import { keccak256, encodeAbiParameters, parseAbiParameters, toHex } from "viem";

const typeHash = keccak256(toHex(
  "SwapCollateralSession(address sellToken,address buyToken,uint256 sellAmount,uint256 buyAmount,uint32 validTo,bytes32 appData,uint256 feeAmount)"
));

const dataHash = keccak256(encodeAbiParameters(
  parseAbiParameters("bytes32, address, address, uint256, uint256, uint32, bytes32, uint256"),
  [typeHash, order.sellToken, order.buyToken, order.sellAmount, order.buyAmount, order.validTo, order.appData, order.feeAmount]
));

4. Sign EIP-712 SessionCall

import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(delegatePrivateKey);

const signature = await account.signTypedData({
  domain: {
    name: "ftUSD SessionManager",
    version: "1",
    chainId: 146,
    verifyingContract: config.sessionManager,
  },
  types: {
    SessionCall: [
      { name: "sessionId", type: "bytes32" },
      { name: "target", type: "address" },
      { name: "dataHash", type: "bytes32" },
      { name: "nonce", type: "uint256" },
      { name: "deadline", type: "uint256" },
      { name: "executor", type: "address" },
      { name: "feeAmount", type: "uint256" },
    ],
  },
  primaryType: "SessionCall",
  message: {
    sessionId,
    target: config.leverageRfqEngine,
    dataHash,
    nonce,
    deadline: BigInt(Math.floor(Date.now() / 1000) + 3600),
    executor: config.executorAddress,
    feeAmount: 0n,
  },
});

5. Submit Order

const response = await fetch("https://executor.flyingtulip.com/leverage/orders", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    order: {
      sellToken: "0x29219dd400f2Bf60E5a23d13Be72B486D4038894",  // USDC
      buyToken: "0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38",   // wS
      sellAmount: "50000000",          // 50 USDC (6 decimals)
      buyAmount: "100000000000000",    // 0.0001 wS minimum (18 decimals)
      validTo: Math.floor(Date.now() / 1000) + 3600,
      appData: "0x" + "0".repeat(64),
      feeAmount: "0",
      kind: 0,
      partiallyFillable: false,
      sellTokenBalance: 0,
      buyTokenBalance: 0,
    },
    session: {
      sessionId,
      target: config.leverageRfqEngine,
      dataHash,
      nonce: nonce.toString(),
      deadline: (Math.floor(Date.now() / 1000) + 3600).toString(),
      executor: config.executorAddress,
      feeAmount: "0",
    },
    delegateSignature: signature,
    orderType: "swap",
    user: userAddress,
  }),
});

const result = await response.json();
console.log(result);
// { success: true, orderId: "lev_...", status: "pending" }

6. Poll Order Status

const poll = async (orderId: string) => {
  while (true) {
    const res = await fetch(`https://executor.flyingtulip.com/leverage/orders/${orderId}`).then(r => r.json());
    if (res.status === "filled") return res;
    if (res.status === "failed") throw new Error(res.errorMessage);
    await new Promise(r => setTimeout(r, 2000));
  }
};

Order Types

orderType Description DataHash Typehash
"open" Open leveraged position (borrow + deposit) OpenLeverageSession(...)
"close" Close leveraged position (repay + withdraw) CloseLeverageSession(...)
"swap" Swap collateral type (withdraw + deposit) SwapCollateralSession(...)

The isOpen field is kept for backwards compatibility (true = open, false = close). When orderType is set, it takes precedence.

Prerequisites for Users

Before submitting a swap order, the user must have:

  1. Collateral deposited in PositionsManager (pm.deposit(sellToken, amount))
  2. Engine debit approved (pm.approveEngine(leverageRfqEngine, sellToken, amount))
  3. Active session on SessionManager with a delegate key
  4. Session asset limits covering the sellToken amount

Token Addresses (Sonic — Chain 146)

Token Address Decimals
USDC 0x29219dd400f2Bf60E5a23d13Be72B486D4038894 6
wS 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38 18

Error Handling

Error Meaning
InvalidSignature() EIP-712 signature doesn't match delegate
SessionNotFound(bytes32) Session doesn't exist or was revoked
SessionExpired(bytes32,uint48) Session past validUntil
InvalidNonce(uint256,uint256) Nonce mismatch — re-read from chain
AssetLimitExceeded(address,uint256,uint256) Session spending limit reached
LeverageBuyAmountTooLow() DEX output less than order.buyAmount
Undercollateralized() Position would be undercollateralized after swap
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment