Skip to content

Instantly share code, notes, and snippets.

@zed-wong
Last active November 3, 2024 09:25
Show Gist options
  • Save zed-wong/9edd955e81e526cc413942bbd7288d1e to your computer and use it in GitHub Desktop.
Save zed-wong/9edd955e81e526cc413942bbd7288d1e to your computer and use it in GitHub Desktop.
The guide of adding a new exchange to ccxt, and how did I add the 4swap

Adding 4swap to CCXT

This is the first time we’re integrating an exchange on the Mixin network into CCXT, but the process is actually more straightforward than expected.

Overview

CCXT started as an SDK to simplify connecting to various centralized exchanges. However, with the recent addition of Hyperliquid—a decentralized exchange on an EVM network—CCXT is expanding its reach, paving the way for more DEX integrations across different networks.

To add a new exchange to CCXT, we create a file under /ts/src/ named [exchange-name].ts. This file defines the methods available when initializing an instance of the exchange. It inherits the base class in /ts/src/base/Exchange.ts, which contains all possible methods for an exchange. We implement only the necessary ones for the new exchange in the new file.

CCXT supports multiple languages; we only need to implement the TypeScript version. When we run npm run build, CCXT automatically converts the TypeScript to Python, PHP, and C#. This auto-conversion makes multi-language support efficient but imposes some limitations on TypeScript usage, discussed in What to Keep in Mind.

Steps to Add an Exchange

For simplicity, I'll skip parts that are straightforward; in most cases, following other implementations is sufficient.

  1. Create a New File: Under /ts/src/[Exchange].ts, inherit the base class, write a describe function, and use this.deepExtend.

  2. Define Exchange Properties:

    • Supported Methods: List all available methods under the has field.

    • API Endpoints: Define API endpoints under urlsas shown below:

      'urls': {
        'logo': 'https://mixin-images.zeromesh.net/A2jrSrBJzt0QA4uxeLVlgt67uaXKt8NvBhGzNeLOxxZfwRMz2FjlcMfmM5ZFoXXiynj_6vzsxZiLVloxW478pIdBnLWBJJ8SJu8y=s256',
        'api': {
          'fswapPublic': 'https://safe-swap-api.pando.im/api',
          'fswapPrivate': 'https://safe-swap-api.pando.im/api',
          'mixinPublic': 'https://api.mixin.one',
          'mixinPrivate': 'https://api.mixin.one',
          'ccxtProxy': 'https://4swap-ccxt-proxy-production.up.railway.app',
        },
        'doc': 'https://developers.pando.im/references/4swap/api.html',
      },
      
    • API Paths: Under api, specify paths for each endpoint. Paths usually follow this pattern:

      'api': {
        'fswapPublic': {
          'get': {
            'info': 1,
            'assets': 1,
            'pairs': 1,
            'pairs/{base}/{quote}': 1,
            'cmc/pairs': 1,
            'stats/markets': 1,
            'stats/markets/{base}/{quote}': 1,
            'stats/markets/{base}/{quote}/kline/v2': 1,
            'transactions/{base}/{quote}': 1,
          },
        },
        'fswapPrivate': {
          'get': {
            'orders/{follow_id}': 1,
            'transactions/{base}/{quote}/mine': 1,
          },
        },
      
    • Required Credentials: Map CCXT credentials to Mixin's keystore values:

      'requiredCredentials': {
        'uid': true,        // app_id
        'login': true,      // session_id
        'apiKey': true,     // server_public_key
        'password': true,   // session_private_key
        'privateKey': true, // spend_private_key
        'secret': true,     // oauth_client_secret
      },
      
    • Constants: Store any constants under options and access them using this.options.MY_CONSTANT.

  3. Implement Methods: CCXT provides a set of standard functions, as shown below:

    +-------------------------------------------------------------+
    |                            CCXT                             |
    +------------------------------+------------------------------+
    |            Public            |           Private            |
    +=============================================================+
    │                              .                              |
    │                    The Unified CCXT API                     |
    │                              .                              |
    |       loadMarkets            .           fetchBalance       |
    |       fetchMarkets           .            createOrder       |
    |       fetchCurrencies        .            cancelOrder       |
    |       fetchTicker            .             fetchOrder       |
    |       fetchTickers           .            fetchOrders       |
    |       fetchOrderBook         .        fetchOpenOrders       |
    |       fetchOHLCV             .      fetchClosedOrders       |
    |       fetchStatus            .          fetchMyTrades       |
    |       fetchTrades            .                deposit       |
    |                              .               withdraw       |
    │                              .                              |
    +=============================================================+
    

    Not all of these are required, though loadMarkets is mandatory, as it’s called every time the instance initializes. Copy function names and parameters from base/Exchange.ts, call the API endpoints, format the responses, and handle task logic as needed.

  4. Add Authorization (sign Method): Define a sign method for authorizing requests:

    sign (path, api = 'fswapPublic', method = 'GET', params = {}, headers = undefined, body = undefined)
    

    This function generates the JWT token for 4swap and handles encryption and encoding for API requests.

What to Keep in Mind

The limitations in TypeScript for CCXT development can be frustrating. Here are some key constraints:

  1. Use safe methods instead of directly accessing object properties. (Docs)
  2. Avoid using JavaScript built-ins like map(), filter(), Number(), toFixed(), concat(), slice(), Math, or Buffer, as they might not be compatible with other languages.
  3. Stick to basic for loops (avoid for...in).
  4. Avoid fallback values like const value = object['key'] || other_value;
  5. For calculations, use Precise (Docs).
  6. For precision formatting, use this.decimalToPrecision (Docs).
  7. No external dependencies are allowed, except those under /ts/src/static_dependencies. Methods added to base/Exchange.ts must be implemented across all supported languages.

For further details, consult the CCXT contribution guidelines.

What I have done in fswap.ts

I have set all the static asset id and symbol in `this.options`. And

- [x] fetchMarkets
- [x] fetchCurrencies
- [x] fetchBalance
- [x] createOrder
- [x] fetchDepositAddress
- [x] deposit
- [x] add liqudity
- [x] remove liquidity
- [x] fetchMyTrades
- [x] fetchOrder
- [x] withdraw same chain
- [x] withdraw different chain

All these functions are tested and usable.

How to test

By far the best way of testing I found is, under base folder, run:

node examples/js/cli EXCHANGE_NAME METHOD_NAME PARAMS0 PARAMS1 ... 

e.g. 
node examples/js/cli fswap fetchMarkets 
node examples/js/cli fswap fetchCurrencies 
node examples/js/cli fswap fetchBalance 
node examples/js/cli fswap createOrder 'pUSD/USDT' 'spot' 'buy' '0.01' 
node examples/js/cli fswap createOrder 'pUSD/USDT' 'spot' 'sell' '0.01' 
node examples/js/cli fswap fetchDepositAddress 'BTC' 
node examples/js/cli fswap fetchDepositWithdrawFees '["XRP"]' 
node examples/js/cli fswap createOrder 'pUSD/USDT' 'add_liquidity' 'buy' '0.01' '{"slippage:0.5"}' 
node examples/js/cli fswap createOrder '02d70063-75d1-34dc-a425-1020b8d4a14b' 'remove_liquidity' '' '0.01675134' 
node examples/js/cli fswap fetchMyTrades 'pUSD/USDT' 
node examples/js/cli fswap fetchOrder 'b24dbe25-fe6e-4bd1-b36d-c0c47cbe34b6'
node examples/js/cli fswap withdraw 'EOS' '0.1' 'okbtothemoon' '"6023265"'


Or you can write your own scripts. By importing your forked ccxt library locally:

npm i /home/ccxt
import ccxt from "ccxt"; 
... 

And use it like a real user

How to use the fswap.ts

Just like any other exchanges, import ccxt, initialized the exchange instance, and do anything with it.

import ccxt from "ccxt";

async function main() {
    const keystore = {
        "uid": "REPLACE WITH YOUR APP ID",
        "login": "REPLACE WITH YOUR SESSION ID",
        "apiKey": "REPLACE WITH YOUR SERVER PUBLIC KEY",
        "password": "REPLACE WITH YOUR SESSION PRIVATE KEY",
        "privateKey": "REPLACE WITH YOUR SPEND PRIVATE KEY",
        "secret": "REPLACE WITH YOUR OAUTH SECRET"
    }
    const exchange = new ccxt.fswap(keystore);
    await exchange.loadMarkets();
    console.log(await exchange.fetchMarkets());

    // Swap order
    console.log('createOrder:', await exchange.createOrder('BTC/USDT', 'spot', 'buy', 0.001));
    
    // Add liquidity order
    // Note: When add liqudity, side is buy, then the payment asset would be the quote asset (USDT)
    // The amount of the other asset would be calculated based on the current ratio
    console.log('Add liquidity:', await exchange.createOrder('BTC/USDT', 'buy', 'add_liquidity', 0.001));
    
    // Remove liquidity order
    // Note: When remove liqudiity, the asset id of the lp token should be put in the symbol field.
    // The amount of the other asset would be calculated based on the current ratio
    console.log('Remove liquidity:', await exchange.createOrder('3083fa73-b53f-4024-b9a8-5382022bdd6b', 'buy', 'remove_liquidity', 0.001));
    
    console.log('FetchDepositAddress:', await exchange.fetchDepositAddress('BTC'));
    console.log('Withdraw:', await exchange.withdraw('EOS','0.1','okbtothemoon',"6023265"))
    
    // Note: Different assets might multiple fees options, the cheapest/highest priority one would be selected default
    console.log('fetchDepositWithdrawFees:', await exchange.fetchDepositWithdrawFees(['BTC']));
    
    // Note: Swap order, Add/Remove liquidity orders will be shown
    console.log('fetchMyTrades:', await exchange.fetchMyTrades('pUSD/USDT'));
    
    // Note: You can only fetch your own orders, and can only fetch swap orders
    console.log('fetchOrder:', await exchange.fetchOrder('b24dbe25-fe6e-4bd1-b36d-c0c47cbe34b6'))
}

main();

Functions without params are straight forward, you can just call them directly.

Functions with params are different:

async createOrder (symbol: string, type: OrderType, side: OrderSide, amount: number, price?: Num, params?: {}): Promise<Order> 

- symbol: the trading pair symbol, like pUSD/USDT, both symbol must exist in this.options.AssetMap to be valid. (This paramter is the asset_id when the type of order is 'remove_liquidity')

- type: type of order, could be 'spot', 'add_liquidity', 'remove_liquidity'. 'spot' is swap.

- side: side of order, could be 'buy' or 'sell', when side is 'buy', payment asset is quote asset. (Buy pUSD/USDT, pay USDT). when side is 'sell', payment asset is base asset (Sell pUSD/USDT, pay pUSD). 
When order type is 'add_liqudity', the amount of the other asset will be calculated automatically based on the amount of the payment asset. The calculation is based on the existing liquidity in the pool and the amount of the asset paid. Check out `calculateAddLiquidityBaseAmount` or `calculateAddLiquidityQuoteAmount` in fswap.ts for details.

- amount: the amount of asset being paid. 

- price: useless, not being used.
async fetchDepositAddress (code: string, params = {}): Promise<DepositAddress> 

- code: the symbol of the asset, must exist in this.options.AssetMap to be valid, like 'BTC', 'ETH', 'XRP'.
async fetchDepositWithdrawFees (codes: Strings = undefined, params = {}) 

- codes: an array of the symbol. the symbol must exist in this.options.AssetMap to be valid, like 'BTC', 'ETH', 'XRP'.
async fetchMyTrades (symbol: Str = undefined, since: Int = undefined, limit: Int = undefined, params = {}): Promise<Trade[]> 

- symbol: the trading pair symbol, like pUSD/USDT, both symbol must exist in this.options.AssetMap to be valid. 

- since: useless, not being used. 

- limit: useless, not being used.
async fetchOrder (id: string, symbol: Str = undefined, params = {}): Promise<Order> 

- id: follow id of the fswap order. Can get it when create a spot order. 

- Str: useless, not being used. 

Notice: This function can only fetch 'spot' orders. 'add/remove liquidity' orders won't be found.

How to accomplish the rest part and merge to ccxt main

The forked version is here, and the npm package is here

I have added some methods in base/Exchange.ts that can't be used directly in ts file. They all start with mixin prefix. These methods are from the copied Mixin Js SDK. Basically we just need Mixin Py SDK and Mixin PHP SDK to have these methods. And copy these SDK to static_dependencies, then use these SDK in base/Exchange file.

Right now Mixin Py SDK is completely outdated. Mixin PHP SDK is newer, I'm not sure if it has all these methods.

We may also simplify the base/Exchange, moving directly imported methods out of it and direct import in fswap.ts.

Weird bugs

When you add a new exchange file for the first time, you will need to add this at the first line:

import Exchange from './base/Exchange.js';

And then so you can run to generate the abstract exchange file:

npm run pre-transpile

In this case, abstract/fswap.js would be generated, then you can change the import to the abstract file:

import Exchange from './abstract/fswap.js';

After this, you should be able to get implicit api methods to work, and run npm run tsBuild without errors.

@nguyenhieuec
Copy link

Thanks for the git. I forked it to save the invaluable information for me.

@zed-wong
Copy link
Author

zed-wong commented Nov 3, 2024

Thanks for the git. I forked it to save the invaluable information for me.

Glad it's helpful!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment