Skip to content

Commit

Permalink
add bingx
Browse files Browse the repository at this point in the history
  • Loading branch information
ivanshukhov committed Dec 20, 2024
1 parent fcf1e9d commit 0b95cb5
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 5 deletions.
4 changes: 2 additions & 2 deletions src/exchange_adapters/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export abstract class BaseExchangeAdapter {
*
*/
protected static readonly standardTokenSymbolMap = new Map<Currency, string>([
[ExternalCurrency.PLQ, 'PLANQ'],
[ExternalCurrency.PLQ, 'PLQ'],
[ExternalCurrency.USD, 'USD'],
[ExternalCurrency.EUR, 'EUR'],
[ExternalCurrency.BRL, 'BRL'],
Expand Down Expand Up @@ -255,7 +255,7 @@ export abstract class BaseExchangeAdapter {
this.standardPairSymbol,
res.status
)
return Promise.reject(new Error(`Bad fetch status code ${res.status}`))
return Promise.reject(new Error(`Bad fetch status code ${res.status} ${await res.text()}`))
}
let jsonRes: JSON
try {
Expand Down
25 changes: 22 additions & 3 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import BigNumber from 'bignumber.js'
import Logger from 'bunyan'
import Web3 from 'web3'
import { min } from 'mathjs'
import { secp256k1 } from 'ethereum-cryptography/secp256k1'
import {keccak256} from "ethers/lib/utils";

export const MS_PER_SECOND = 1000
export const MS_PER_MINUTE = 60 * MS_PER_SECOND
Expand Down Expand Up @@ -506,10 +508,13 @@ export type StrongAddress = `0x${string}`

export const ensureLeading0x = (input: string): StrongAddress =>
input.startsWith('0x') ? (input as StrongAddress) : (`0x${input}` as const)

export const hexToBuffer = (input: string) => Buffer.from(trimLeading0x(input), 'hex')
export const trimLeading0x = (input: string) => (input.startsWith('0x') ? input.slice(2) : input)
export const isValidPrivateKey = (privateKey: string) =>
privateKey.startsWith('0x') && isValidPrivate(hexToBuffer(privateKey))

const privateToAddress = function (privateKey: Buffer): Buffer {
return publicToAddress(privateToPublic(privateKey))
}
export const isValidAddress = (input: string): input is StrongAddress => {
if ('string' !== typeof input) {
return false
Expand All @@ -527,7 +532,21 @@ export const isValidAddress = (input: string): input is StrongAddress => {

return false
}

export const pubToAddress = function (pubKey: Buffer, sanitize: boolean = false): Buffer {
if (sanitize && pubKey.length !== 64) {
pubKey = Buffer.from(secp256k1.ProjectivePoint.fromHex(pubKey).toRawBytes(false).slice(1))
}
if (pubKey.length !== 64) {
throw new Error('Expected pubKey to be of length 64')
}
// Only take the lower 160bits of the hash
return Buffer.from(keccak256(pubKey)).slice(-20)
}
export const publicToAddress = pubToAddress
export const privateToPublic = function (privateKey: Buffer): Buffer {
// skip the type flag and use the X, Y points
return Buffer.from(secp256k1.ProjectivePoint.fromPrivateKey(privateKey).toRawBytes(false).slice(1))
}
export const privateKeyToAddress = (privateKey: string) =>
toChecksumAddress(
ensureLeading0x(privateToAddress(hexToBuffer(privateKey)).toString('hex'))
Expand Down
160 changes: 160 additions & 0 deletions test/exchange_adapters/bingx.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import BigNumber from 'bignumber.js'
import { baseLogger } from '../../src/default_config'
import { ExchangeAdapterConfig } from '../../src/exchange_adapters/base'
import { BingxAdapter } from '../../src/exchange_adapters/bingx'
import { Exchange, ExternalCurrency } from '../../src/utils'

describe('BingxAdapter', () => {
let bingxAdapter: BingxAdapter
const config: ExchangeAdapterConfig = {
baseCurrency: ExternalCurrency.PLQ,
baseLogger,
quoteCurrency: ExternalCurrency.USDT,
}
beforeEach(() => {
bingxAdapter = new BingxAdapter(config)
})
afterEach(() => {
jest.clearAllTimers()
jest.clearAllMocks()
})
describe('parseTicker', () => {
const tickerJson = {timestamp: 1734709128553,data : [{
symbol: 'PLQ-USDT',
priceChange: '-0.000461',
priceChangePercent: '-5.49%',
lastPrice: '0.007937',
bidPrice: '0.007937',
bidQty: '668',
askPrice: '0.007986',
askQty: '1159',
openPrice: '0.008398',
highPrice: '0.008398',
lowPrice: '0.007937',
volume: '32880',
quoteVolume: '267.93',
openTime: 1614604599055,
closeTime: 1614690999055,
firstId: 850037,
lastId: 855106,
count: 5070,
}]}
it('handles a response that matches the documentation', () => {
expect(bingxAdapter.parseTicker(tickerJson)).toEqual({
source: Exchange.BINGX,
symbol: bingxAdapter.pairSymbol,
ask: new BigNumber(0.007986),
baseVolume: new BigNumber(32880),
bid: new BigNumber(0.007937),
high: new BigNumber(0.008398),
lastPrice: new BigNumber(0.007937),
low: new BigNumber(0.007937),
open: new BigNumber(0.008398),
quoteVolume: new BigNumber(267.93),
timestamp: 1734709128553,
})
})
// timestamp, bid, ask, lastPrice, baseVolume
const requiredFields = ['timestamp','data','askPrice', 'bidPrice', 'lastPrice', 'closeTime', 'volume']

for (const field of Object.keys(tickerJson)) {
// @ts-ignore
const { [field]: _removed, ...incompleteTickerJson } = tickerJson
if (requiredFields.includes(field)) {
it(`throws an error if ${field} is missing`, () => {
expect(() => {
bingxAdapter.parseTicker(incompleteTickerJson)
}).toThrowError()
})
} else {
it(`parses a ticker if ${field} is missing`, () => {
expect(() => {
bingxAdapter.parseTicker(incompleteTickerJson)
}).not.toThrowError()
})
}
}
})

describe('isOrderbookLive', () => {
// Note: in the real response, these contain much more info. Only relevant
// fields are included in this test
const mockCeloUsdInfo = {
symbol: 'PLQ-USDT',
status: 1,
apiStateBuy: true,
apiStateSell: false,
}
const mockOtherInfo = {
symbol: 'PLQ-USDT',
status: 1,
apiStateBuy: true,
apiStateSell: true,
}
const mockStatusJson = {
data: {symbols: [mockOtherInfo]},
}

it('returns false if the symbol is not found', async () => {
jest.spyOn(bingxAdapter, 'fetchFromApi').mockReturnValue(
Promise.resolve({
...mockStatusJson,
symbols: [mockOtherInfo, mockOtherInfo, mockOtherInfo],
})
)
expect(await bingxAdapter.isOrderbookLive()).toEqual(false)
})

const otherStatuses = [
'PRE_TRADING',
'POST_TRADING',
'END_OF_DAY',
'HALT',
'AUCTION_MATCH',
'BREAK',
]
for (const status of otherStatuses) {
it(`returns false if the status is ${status}`, async () => {
jest.spyOn(bingxAdapter, 'fetchFromApi').mockReturnValue(
Promise.resolve({
...mockStatusJson,
symbols: [mockOtherInfo],
})
)
expect(await bingxAdapter.isOrderbookLive()).toEqual(false)
})
}

it('returns false if isSpotTradingAllowed is false', async () => {
jest.spyOn(bingxAdapter, 'fetchFromApi').mockReturnValue(
Promise.resolve({
...mockStatusJson,
symbols: [{ ...mockCeloUsdInfo, apiStateBuy: false }, mockOtherInfo],
})
)
expect(await bingxAdapter.isOrderbookLive()).toEqual(false)
})

it('returns false if both LIMIT or MARKET are not present in orderTypes', async () => {
const invalidOrderTypesResponses = [
['LIMIT_MAKER', 'MARKET', 'STOP_LOSS_LIMIT', 'TAKE_PROFIT_LIMIT'],
['LIMIT', 'LIMIT_MAKER', 'STOP_LOSS_LIMIT', 'TAKE_PROFIT_LIMIT'],
['LIMIT_MAKER', 'STOP_LOSS_LIMIT', 'TAKE_PROFIT_LIMIT'],
]
for (const orderTypes of invalidOrderTypesResponses) {
jest.spyOn(bingxAdapter, 'fetchFromApi').mockReturnValue(
Promise.resolve({
...mockStatusJson,
symbols: [{ ...mockCeloUsdInfo, orderTypes }, mockOtherInfo],
})
)
expect(await bingxAdapter.isOrderbookLive()).toEqual(false)
}
})

it('returns true if symbol is found, status === TRADING, isSpotTradingAllowed is true and orderTypes contains both LIMIT and MARKET', async () => {
jest.spyOn(bingxAdapter, 'fetchFromApi').mockReturnValue(Promise.resolve(mockStatusJson))
expect(await bingxAdapter.isOrderbookLive()).toEqual(true)
})
})
})

0 comments on commit 0b95cb5

Please sign in to comment.