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.
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.
For simplicity, I'll skip parts that are straightforward; in most cases, following other implementations is sufficient.
-
Create a New File: Under
/ts/src/[Exchange].ts
, inherit the base class, write adescribe
function, and usethis.deepExtend
. -
Define Exchange Properties:
-
Supported Methods: List all available methods under the
has
field. -
API Endpoints: Define API endpoints under
urls
as 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 usingthis.options.MY_CONSTANT
.
-
-
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 frombase/Exchange.ts
, call the API endpoints, format the responses, and handle task logic as needed. -
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.
The limitations in TypeScript for CCXT development can be frustrating. Here are some key constraints:
- Use safe methods instead of directly accessing object properties. (Docs)
- Avoid using JavaScript built-ins like
map()
,filter()
,Number()
,toFixed()
,concat()
,slice()
,Math
, orBuffer
, as they might not be compatible with other languages. - Stick to basic
for
loops (avoidfor...in
). - Avoid fallback values like
const value = object['key'] || other_value;
- For calculations, use
Precise
(Docs). - For precision formatting, use
this.decimalToPrecision
(Docs). - No external dependencies are allowed, except those under
/ts/src/static_dependencies
. Methods added tobase/Exchange.ts
must be implemented across all supported languages.
For further details, consult the CCXT contribution guidelines.
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.
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
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.
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.
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.
Glad it's helpful!