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 has collateral deposited in PositionsManager (e.g., USDC)
- User signs an EIP-712 swap order specifying sellToken → buyToken
- Frontend submits the signed order to the executor API
- Executor gets a DEX quote, executes the swap on-chain via flash fill
- User's collateral is swapped atomically (old collateral withdrawn, new collateral deposited)
POST /leverage/orders
Content-Type: application/json
{
"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..."
}{
"success": true,
"orderId": "lev_6f80e46b9de540a42c3bd1897a880827",
"status": "pending"
}const config = await fetch("https://executor.flyingtulip.com/leverage/config").then(r => r.json());
// { chainId: 146, sessionManager: "0x52Ef...", leverageRfqEngine: "0xddDf...", executorAddress: "0xf39F..." }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],
});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]
));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,
},
});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" }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));
}
};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.
Before submitting a swap order, the user must have:
- Collateral deposited in PositionsManager (
pm.deposit(sellToken, amount)) - Engine debit approved (
pm.approveEngine(leverageRfqEngine, sellToken, amount)) - Active session on SessionManager with a delegate key
- Session asset limits covering the sellToken amount
| Token | Address | Decimals |
|---|---|---|
| USDC | 0x29219dd400f2Bf60E5a23d13Be72B486D4038894 |
6 |
| wS | 0x039e2fB66102314Ce7b64Ce5Ce3E5183bc94aD38 |
18 |
| 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 |