gateway icon indicating copy to clipboard operation
gateway copied to clipboard

Feat/orca connector

Open mlguys opened this issue 1 month ago • 3 comments

Before submitting this PR, please make sure:

  • [x] Your code builds clean without any errors or warnings
  • [x] You are using approved title ("feat/", "fix/", "docs/", "refactor/")

A description of the changes proposed in the pull request:

  • Add Orca connector to gateway

Tests performed by the developer:

  • [x] Use all clmm routes of Orca to manage clmm positions
  • [x] Merge with latest changes from development
  • [x] Add unit tests to meet test coverage

Tips for QA testing:

  • Use dev mode on http://localhost:15888/docs to manage Orca CLMM positions

mlguys avatar Nov 02 '25 17:11 mlguys

Also, there's a Pancakeswap Solana build error, possibly related to library changes

The issue would be the return of getAnchorProgram() is missing a param, I added the missing param as follow

https://github.com/hummingbot/gateway/blob/192ad822501b42c604778242250331a0fc36c909/src/connectors/pancakeswap-sol/pancakeswap-sol.ts#L68

mlguys avatar Nov 22 '25 18:11 mlguys

See comments.

Also, there's now Solana wrap and unwrap functionality on the development branch, which I'm using for Pancakeswap Solana. Please revise this PR to use it.

I have integrated "unwrap" functionality to my orca routes since it was missing before, for "wrap", I already have it in my built transactions so it is not needed.

mlguys avatar Nov 22 '25 18:11 mlguys

Commit f8b24733f2b0210cc6ff44b8ff158fea4993b9c9

  • Setup with hummingbot + gateway ok, however orca is not displayed on the list of available connectors ❌ image
    • Clean install on both linux and macOs had same results
  • /pools and /trading/clmm does not support orca ❌
    • /pools/{tradingPair}
      Unsupported connector: orca. Supported connectors: jupiter, meteora, raydium, uniswap, 0x, pancakeswap, pancakeswap-sol
      
    • /trading/clmm/pool-info ❌
      curl -X 'GET' \
        'http://localhost:15888/trading/clmm/pool-info?connector=oraca&chainNetwork=solana-mainnet-beta&poolAddress=A2J7vmG9xAdWUzYscN7oQssxZBFihwD3UonkWB8Kod1A' \
        -H 'accept: application/json' | jq
        % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                       Dload  Upload   Total   Spent    Left  Speed
      100   373  100   373    0     0   244k      0 --:--:-- --:--:-- --:--:--  364k
      {
        "statusCode": 400,
        "error": "Validation Error",
        "message": "querystring/connector must be equal to one of the allowed values",
        "validation": [
          {
            "instancePath": "/connector",
            "schemaPath": "#/properties/connector/enum",
            "keyword": "enum",
            "params": {
              "allowedValues": [
                "raydium",
                "meteora",
                "pancakeswap-sol",
                "uniswap",
                "pancakeswap"
              ]
            },
            "message": "must be equal to one of the allowed values"
          }
        ]
      }
      

Test orca gateway endpoints:

  • GET /pool-info ok
  • GET /quote-position ok
  • GET /quote-swap ok
  • POST /execute-swap ok
    • Sell https://solscan.io/tx/35cT2a4uDoZmaQtcuuKy78ZQatZFi5N2nPqNiV36wmtmuBRdDKvnZYZSgsxDCtTdQst6cB3bvJGZ2tW5sE8FF4bx
    • Buy https://solscan.io/tx/66k5ZkBbDnacBgVhtwHoMFZZGMFwQ2MS8xGmVRVaLziL586xKBvwnGkn7ABZsz3wHrNdf3Q8KvTdR4oAjbPuZogx
  • POST /open-position
    • GoZUhD6x2Ju6gBGZga17KrvaDicncDVWajpt9oxj66ij ok
    • FZAkREUSxr4p3nYL7VfjFcgEFzPo1BFqTdvTNQQaVomT ok
    • ACVYm5sUqCmz2BRLPBFudDtAdbHVshxzMmpu6esGqiU3 ok
  • POST /add-liquidity ok
  • GET /position-info ❌ image
    • The response from position-info does not match whats on the orca's position detail
    • Tried 2/2 attempts got same behavior
  • GET /position-owned ❌
    • Same with earlier /position-info endpoint, it is not matching the Orca’s position details image
  • POST /remove-liquidity ok
  • POST /collect-fees ok
  • POST /close-position ok

See attached gateway and endpoint logs for reference: 11242025.zip


There are instances of endpoints failing due to Transaction simulation failed. This usually means the swap parameters are invalid or market conditions changed. Try again

curl -sS -X POST "http://localhost:15888/connectors/orca/clmm/remove-liquidity" \
  -H "Content-Type: application/json" \
  -d '{"network":"mainnet-beta","walletAddress":"GiooMWkHziPuXhfRar2ChBxU6VhQSrAh7xJTG2zHcxWV","positionAddress":"ACVYm5sUqCmz2BRLPBFudDtAdbHVshxzMmpu6esGqiU3","liquidityPct":50,"slippagePct":1}' | jq
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "Transaction simulation failed. This usually means the swap parameters are invalid or market conditions changed. Try again."
}
  • Tested on helius and paid nodeURL
  • Tested with adjusting slippage to 1% up to 3%

rapcmia avatar Nov 24 '25 12:11 rapcmia

I believe there might be some misunderstanding on the usage of WhirlpoolClient. To give you a concrete example, here's what the addLiquidity.ts could look like with proper usage of the WhirlpoolClient:

async function addLiquidity(
  fastify: FastifyInstance,
  network: string,
  address: string,
  positionAddress: string,
  baseTokenAmount: number,
  quoteTokenAmount: number,
  slippagePct: number,
): Promise<AddLiquidityResponseType> {
  // Validate at least one amount is provided
  if ((!baseTokenAmount || baseTokenAmount <= 0) && (!quoteTokenAmount || quoteTokenAmount <= 0)) {
    throw fastify.httpErrors.badRequest('At least one token amount must be provided and greater than 0');
  }

  const solana = await Solana.getInstance(network);
  const orca = await Orca.getInstance(network);
  const wallet = await solana.getWallet(address);
  const client = await orca.getWhirlpoolClientForWallet(address);
  const positionPubkey = new PublicKey(positionAddress);

  // Fetch position data
  const position = await client.getPosition(positionPubkey);
  if (!position) {
    throw fastify.httpErrors.notFound(`Position not found: ${positionAddress}`);
  }
  const positionData = position.getData()

  const positionMint = positionData.positionMint;
  if (!positionMint) {
    throw fastify.httpErrors.notFound(`Position mint not found: ${position.positionMint.toString()}`);
  }

  // Fetch whirlpool data
  const whirlpoolPubkey = positionData.whirlpool;
  const whirlpool = await client.getPool(whirlpoolPubkey);
  if (!whirlpool) {
    throw fastify.httpErrors.notFound(`Whirlpool not found: ${whirlpoolPubkey.toString()}`);
  }

  // Fetch token mint info
  const mintA = await whirlpool.getTokenAInfo();
  const mintB = await whirlpool.getTokenBInfo();
  if (!mintA || !mintB) {
    throw fastify.httpErrors.notFound('Token mint not found');
  }

  // Determine which token to use as input (prefer base if both provided)
  const useBaseToken = baseTokenAmount > 0;
  const inputTokenAmount = useBaseToken ? baseTokenAmount : quoteTokenAmount;
  const inputTokenMint = useBaseToken ? whirlpool.tokenMintA : whirlpool.tokenMintB;
  const inputTokenDecimals = useBaseToken ? mintA.decimals : mintB.decimals;

  // Convert input amount to BN
  const amount = new BN(Math.floor(inputTokenAmount * Math.pow(10, inputTokenDecimals)));

  // Get increase liquidity quote
  const quote = increaseLiquidityQuoteByInputTokenWithParams({
    inputTokenAmount: amount,
    inputTokenMint,
    sqrtPrice: whirlpool.sqrtPrice,
    tickCurrentIndex: whirlpool.tickCurrentIndex,
    tickLowerIndex: position.tickLowerIndex,
    tickUpperIndex: position.tickUpperIndex,
    tokenExtensionCtx: await TokenExtensionUtil.buildTokenExtensionContext(ctx.fetcher, whirlpool),
    tokenMintA: whirlpool.tokenMintA,
    tokenMintB: whirlpool.tokenMintB,
    slippageTolerance: Percentage.fromDecimal(new Decimal(slippagePct)),
  });

  logger.info(
    `Adding liquidity: ${(Number(quote.tokenEstA) / Math.pow(10, mintA.decimals)).toFixed(6)} tokenA, ${(Number(quote.tokenEstB) / Math.pow(10, mintB.decimals)).toFixed(6)} tokenB`,
  );

  // Build transaction
  const txBuilder = position.increaseLiquidity(quote);

  const txPayload = await txBuilder.build();
  await solana.simulateWithErrorHandling(txPayload.transaction, fastify);
  const { signature, fee } = await solana.sendAndConfirmTransaction(txPayload.transaction, [wallet]);

  // Extract added amounts from balance changes
  const tokenA = await solana.getToken(whirlpool.tokenMintA.toString());
  const tokenB = await solana.getToken(whirlpool.tokenMintB.toString());
  if (!tokenA || !tokenB) {
    throw fastify.httpErrors.notFound('Tokens not found for balance extraction');
  }

  const { balanceChanges } = await solana.extractBalanceChangesAndFee(signature, ctx.wallet.publicKey.toString(), [
    tokenA.address,
    tokenB.address,
  ]);

  logger.info(
    `Liquidity added: ${Math.abs(balanceChanges[0]).toFixed(6)} ${tokenA.symbol}, ${Math.abs(balanceChanges[1]).toFixed(6)} ${tokenB.symbol}`,
  );

  return {
    signature,
    status: 1, // CONFIRMED
    data: {
      fee,
      baseTokenAmountAdded: Math.abs(balanceChanges[0]),
      quoteTokenAmountAdded: Math.abs(balanceChanges[1]),
    },
  };
}

// rest of the code

calintje avatar Nov 28 '25 13:11 calintje

Commit 19fb06395e1a43caf012c93fa9bfd0a0401c3e2b

  • Prev issue on gateway list now works: image
  • Tested with paid nodeURL
  • Tested trading/clmm routes
    • /trading/clmm/pool-info ✅
    • /trading/clmm/open ✅
      • 9SXudnWFRXDp5tJ5EwLZBcqEMeYBDWWXXBUEzCUkNrpV
      • DGUdvqDmyfUKbGsNdXcK6JtcexmKKNAzZAudA2L3H4PS
    • /trading/clmm/positions-owned ✅
    • /trading/clmm/position-info ✅
    • /trading/clmm/quote-position ✅
    • /trading/clmm/add ✅
      • Checked positions-owned and position-info ✅
    • /trading/clmm/collect-fees ✅
    • /trading/clmm/remove ✅
    • /trading/clmm/close ✅
  • Test all orca/clmm routes all completed ok
    • Compare results with trading/clmm ok
    • Prev reported issue for position-owned and position-info match position details

Added log file of gateway and endpoint tests here: 11292025a.zip


The /pools/find/:address currently fails for Orca with has no connector/type mapping from GeckoTerminal (dex: orca)

  • orca/clmm/fetch-pools successfully returns the pair (jup/usdc) pools i tested.
  • orca/clmm/pool-info returns the detailed pool data when provided a pool address

rapcmia avatar Nov 28 '25 17:11 rapcmia

Commit 407702bf06f82ff40a54f9a113a1a8e9352c446c

  • Run endpoint and coingecko initalized successfully then returned data of the pool ✅
    GET /pools/find/:address (orca RAY/USDC 0.3% pool)
    curl -X GET "http://localhost:15888/pools/find/96UbjyFmQY1JLpTvRujrkABxm1ft5hQvSVnv4JbagTMZ?chainNetwork=solana-mainnet-beta" | jq
    {
      "type": "clmm",
      "network": "mainnet-beta",
      "baseSymbol": "RAY",
      "quoteSymbol": "USDC",
      "baseTokenAddress": "4k3Dyjzvzp8eMZWUXbBCjEvwSkkk59S5iCNLY3QrkX6R",
      "quoteTokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
      "feePct": 0.3,
      "address": "96UbjyFmQY1JLpTvRujrkABxm1ft5hQvSVnv4JbagTMZ",
      "geckoData": {
        "volumeUsd24h": "17.8785914691",
        "liquidityUsd": "295.0312",
        "priceNative": "0.9995275862",
        "priceUsd": "0.997363087973868241619602475858721998584694651",
        "buys24h": 14,
        "sells24h": 58,
        "timestamp": 1764583454439,
        "apr": 6.635588933870215
      }
    }
    

rapcmia avatar Dec 01 '25 10:12 rapcmia

For now the transactions built from builder from Orca SDK are not working, they do pass the transaction simulation but did not get included in new block and keep failing.

Th reason why these transactions keep failing is still unknown, but if resolved, the refactor to use these transaction builders from Orca would be minimal.

mlguys avatar Dec 02 '25 01:12 mlguys

Th reason why these transactions keep failing is still unknown, but if resolved, the refactor to use these transaction builders from Orca would be minimal.

Sounds good - merging this PR into development

fengtality avatar Dec 02 '25 03:12 fengtality