├── src ├── globals.d.ts ├── constants │ └── index.ts ├── lib │ ├── zeroex-wrapper │ │ ├── index.ts │ │ ├── __signed_order_utils.ts │ │ ├── _etherToken.ts │ │ ├── __types.ts │ │ ├── helper.ts │ │ ├── __formatters.ts │ │ ├── _token.ts │ │ └── _exchange.ts │ ├── web3-wrapper │ │ └── index.ts │ └── server │ │ ├── _request.ts │ │ └── index.ts ├── types │ ├── pair.ts │ ├── dex.ts │ ├── index.ts │ ├── base.ts │ ├── tokenlon.ts │ └── server.ts ├── utils │ ├── abi.ts │ ├── sign.ts │ ├── math.ts │ ├── gasPriceAdaptor.ts │ ├── helper.ts │ ├── format.ts │ ├── pair.ts │ ├── ethereum.ts │ ├── assert.ts │ └── dex.ts ├── index.ts └── tokenlon.ts ├── .gitignore ├── .npmignore ├── README.md ├── tsconfig.json ├── tests ├── __mock__ │ ├── pair.ts │ ├── config.ts │ ├── simpleOrder.ts │ └── order.ts ├── utils │ ├── abi.test.ts │ ├── sign.test.ts │ ├── gasPriceAdaptor.test.ts │ ├── helper.test.ts │ ├── pair.test.ts │ ├── math.test.ts │ ├── format.test.ts │ ├── ethereum.test.ts │ ├── assert.test.ts │ └── dex.test.ts ├── __utils__ │ ├── wait.ts │ └── helper.ts ├── tokenlon │ ├── weth.test.ts │ ├── balance.test.ts │ ├── allowance.test.ts │ ├── pair.test.ts │ ├── validateOption.test.ts │ ├── orderUtils.test.ts │ ├── orders.test.ts │ ├── cancel.test.ts │ ├── trades.test.ts │ ├── transactionOpts.test.ts │ ├── fillOrder.test.ts │ ├── jwt.test.ts │ └── fillOrdersUpTo.test.ts ├── lib │ └── server.test.ts └── __proxy__ │ └── proxy.ts ├── LICENSE ├── package.json └── tslint.json /src/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.json' { 2 | const value: any 3 | export default value 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | yarn.lock 3 | package-lock.json 4 | /lib/ 5 | /coverage/ 6 | /node_modules/ 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | src 3 | docs 4 | tests 5 | coverage 6 | node_modules 7 | tsconfig.json 8 | tslint.json 9 | yarn.lock 10 | package-lock.json 11 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const TIMEOUT = 3000 2 | 3 | export const REQUEST_TIMEOUT = 5000 4 | 5 | export const ETH_CONTRACT = `0x${'0'.repeat(40)}` 6 | 7 | export const FEE_RECIPIENT = '0x6f7ae872e995f98fcd2a7d3ba17b7ddfb884305f' 8 | 9 | export const ETH_GAS_STATION_URL = 'https://ethgasstation.info/json/ethgasAPI.json' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tokenlon-SDK 2 | 3 | **It's deprecated now (but maybe will upgrade when new tokenlon system supported orderbook mode)! New tokenlon system's market maker tool please see [tokenlon-mmsk](https://github.com/consenlabs/tokenlon-mmsk)** 4 | 5 | [![npm](https://img.shields.io/npm/v/tokenlon-sdk.svg)](https://www.npmjs.com/package/tokenlon-sdk) [![License](https://img.shields.io/npm/l/tokenlon-sdk.svg)](https://www.npmjs.com/package/tokenlon-sdk) 6 | 7 | copyright© imToken PTE. LTD. 8 | -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | import { ZeroEx, ZeroExConfig } from '0x.js' 2 | import web3Wrapper from '../web3-wrapper' 3 | import { coverageToken } from './_token' 4 | import { coverageEtherToken } from './_etherToken' 5 | import { coverageExchange } from './_exchange' 6 | 7 | export const createZeroExWrapper = (config: ZeroExConfig) => { 8 | const instance = new ZeroEx( 9 | web3Wrapper.currentProvider, 10 | config, 11 | ) 12 | 13 | coverageToken(instance.token) 14 | coverageEtherToken(instance.etherToken) 15 | coverageExchange(instance.exchange) 16 | 17 | return instance 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "sourceMap": true, 5 | "outDir": "lib", 6 | "noImplicitThis": true, 7 | "noUnusedParameters": true, 8 | "noUnusedLocals": true, 9 | "declaration": true, 10 | "declarationDir": "lib", 11 | "experimentalDecorators": true, 12 | "allowSyntheticDefaultImports": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "moduleResolution": "node", 15 | "baseUrl": "./", 16 | "pretty": true, 17 | "lib": [ 18 | "es2017" 19 | ] 20 | }, 21 | "include": [ 22 | "./src/**/*" 23 | ], 24 | "exclude": [ 25 | "node_modules" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/types/pair.ts: -------------------------------------------------------------------------------- 1 | export namespace Pair { 2 | export type ExchangePairToken = { 3 | symbol: string 4 | logo: string 5 | contractAddress: string 6 | decimal: number; 7 | } 8 | export type ExchangePair = { 9 | id: number | string 10 | market: string 11 | marketLogo?: string 12 | base: ExchangePairToken 13 | quote: ExchangePairToken 14 | tags: string[] 15 | rate?: number 16 | protocol: string 17 | addedTimestamp?: number 18 | index?: number 19 | infoUrl?: string 20 | price?: number 21 | change?: number 22 | anchored?: boolean 23 | precision: number 24 | rank?: number 25 | quoteMinUnit?: number 26 | marketUrl?: string; 27 | } 28 | } -------------------------------------------------------------------------------- /tests/__mock__/pair.ts: -------------------------------------------------------------------------------- 1 | export const sntWethPairData = { 2 | id: 458, 3 | market: 'Tokenlon', 4 | base: { 5 | symbol: 'SNT', 6 | contractAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 7 | logo: '', 8 | decimal: 18, 9 | }, 10 | quote: { 11 | symbol: 'WETH', 12 | logo: '', 13 | contractAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 14 | decimal: 18, 15 | }, 16 | infoUrl: '', 17 | tags: [ 18 | 'HOT', 19 | ], 20 | relayFee: { 21 | relayRecipient: '', 22 | makerFee: '', 23 | takerFee: '', 24 | }, 25 | instantExEnabled: false, 26 | marketEnabled: true, 27 | tradingEnabled: true, 28 | anchored: false, 29 | protocol: '0x', 30 | rank: 101, 31 | precision: 8, 32 | } -------------------------------------------------------------------------------- /src/utils/abi.ts: -------------------------------------------------------------------------------- 1 | import * as token from '0x.js/lib/src/artifacts/Token.json' 2 | import * as exchange from '0x.js/lib/src/artifacts/Exchange.json' 3 | import * as etherToken from '0x.js/lib/src/artifacts/EtherToken.json' 4 | import { TokenlonError } from '../types' 5 | 6 | import { newError } from './helper' 7 | 8 | const contractStack = { token, exchange, etherToken } 9 | 10 | export const getAbiInputTypes = (contractName: string, method: string) => { 11 | const ct = contractStack[contractName] 12 | if (!ct) throw newError(TokenlonError.InvalidContractName) 13 | 14 | const abiMethod = ct.abi.find(abi => abi.name === method) 15 | if (!abiMethod) throw newError(TokenlonError.InvalidContractMethod) 16 | 17 | return abiMethod.inputs.map(i => i.type) 18 | } -------------------------------------------------------------------------------- /tests/utils/abi.test.ts: -------------------------------------------------------------------------------- 1 | import { getAbiInputTypes } from '../../src/utils/abi' 2 | 3 | describe('getAbiInputTypes', () => { 4 | const etherToken = [ 5 | 'deposit', 6 | 'withdraw', 7 | ] 8 | 9 | const exchange = [ 10 | 'fillOrder', 11 | 'cancelOrder', 12 | 'batchFillOrders', 13 | 'fillOrdersUpTo', 14 | 'fillOrKillOrder', 15 | 'batchFillOrKillOrders', 16 | 'batchCancelOrders', 17 | ] 18 | 19 | etherToken.forEach(method => { 20 | it(`etherToken ${method} must be exists`, () => { 21 | expect(getAbiInputTypes('etherToken', method).length).toBeGreaterThanOrEqual(0) 22 | }) 23 | }) 24 | 25 | exchange.forEach(method => { 26 | it(`exchange ${method} must be exists`, () => { 27 | expect(getAbiInputTypes('exchange', method).length).toBeGreaterThan(0) 28 | }) 29 | }) 30 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 imToken PTE. LTD. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/__utils__/wait.ts: -------------------------------------------------------------------------------- 1 | import Web3 from 'web3' 2 | import { web3ProviderUrl } from '../__mock__/config' 3 | import web3 from '../../src/lib/web3-wrapper' 4 | 5 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 6 | 7 | export const waitSeconds = (seconds) => { 8 | return new Promise(resolve => { 9 | setTimeout(resolve, seconds * 1000) 10 | }) 11 | } 12 | 13 | const getReceiptAsync = (txHash) => { 14 | return new Promise((resolve) => { 15 | web3.eth.getTransactionReceipt(txHash, (err, res) => { 16 | if (!err) { 17 | resolve(res) 18 | } 19 | }) 20 | }) 21 | } 22 | 23 | export const waitMined = async (txHash, seconds) => { 24 | let receipt = await getReceiptAsync(txHash) as any 25 | let timeUsed = 0 26 | 27 | while ((!receipt || receipt.blockNumber <= 0) && timeUsed <= seconds) { 28 | await waitSeconds(2) 29 | receipt = await getReceiptAsync(txHash) 30 | timeUsed += 2 31 | } 32 | 33 | if (receipt && receipt.blockNumber > 0) { 34 | console.log('set seconds', seconds) 35 | console.log('timeUsed', timeUsed) 36 | await waitSeconds(2) 37 | return true 38 | } 39 | return false 40 | } 41 | -------------------------------------------------------------------------------- /tests/tokenlon/weth.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { localConfig } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import { toBN } from '../../src/utils/math' 7 | import { waitMined } from '../__utils__/wait' 8 | 9 | let tokenlon = null as Tokenlon 10 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 11 | 12 | beforeAll(async () => { 13 | tokenlon = await createTokenlon(localConfig) 14 | }) 15 | 16 | describe('test deposit / withdraw', () => { 17 | it('test deposit / withdraw', async () => { 18 | const amount = 0.0000562 19 | const wethBalance1 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 20 | const txHash1 = await tokenlon.withdraw(amount) 21 | await waitMined(txHash1, 30) 22 | 23 | const wethBalance2 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 24 | expect(toBN(wethBalance1).minus(amount).eq(toBN(wethBalance2))).toBe(true) 25 | 26 | const txHash2 = await tokenlon.deposit(amount) 27 | await waitMined(txHash2, 30) 28 | 29 | const wethBalance3 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 30 | expect(wethBalance1).toEqual(wethBalance3) 31 | }) 32 | }) -------------------------------------------------------------------------------- /tests/__utils__/helper.ts: -------------------------------------------------------------------------------- 1 | import { wallet, web3ProviderUrl, placeOrderWalletAddress } from '../__mock__/config' 2 | import { helpCompareStr } from '../../src/utils/helper' 3 | import * as Web3 from 'web3' 4 | import web3 from '../../src/lib/web3-wrapper' 5 | 6 | web3.setProvider(new (Web3 ? Web3 : Web3.default).providers.HttpProvider(web3ProviderUrl)) 7 | 8 | export const filterOrderBook = (orderBooks) => { 9 | return orderBooks.filter(o => { 10 | const singedOrderString = JSON.parse(o.rawOrder) 11 | return !o.isMaker && ( 12 | helpCompareStr(singedOrderString.maker, wallet.address) || 13 | helpCompareStr(singedOrderString.maker, placeOrderWalletAddress) 14 | ) 15 | }) 16 | } 17 | 18 | export const getReceiptAsync = (txHash) => { 19 | return new Promise((resolve) => { 20 | web3.eth.getTransactionReceipt(txHash, (err, res) => { 21 | if (!err) { 22 | resolve(res) 23 | } 24 | }) 25 | }) 26 | } 27 | 28 | export const getTransactionAsync = async (txHash) => { 29 | return web3.eth.getTransaction(txHash) 30 | } 31 | 32 | export const getGasPriceByTransactionAsync = async (txHash) => { 33 | const r = web3.eth.getTransaction(txHash) 34 | return r ? r.gasPrice.toNumber() : null 35 | } 36 | 37 | export const getGasLimitByTransactionAsync = async (txHash) => { 38 | const r = web3.eth.getTransaction(txHash) 39 | return r ? r.gas : null 40 | } -------------------------------------------------------------------------------- /tests/utils/sign.test.ts: -------------------------------------------------------------------------------- 1 | import { ZeroEx } from '0x.js' 2 | import * as _ from 'lodash' 3 | import { personalECSign, personalECSignHex, personalSign } from '../../src/utils/sign' 4 | import { toBN } from '../../src/utils/math' 5 | import { wallet, zeroExConfig } from '../__mock__/config' 6 | import { orders } from '../__mock__/order' 7 | 8 | describe('test sign util', () => { 9 | it('test personalECSignHex', () => { 10 | const order = orders[0] 11 | const hash = ZeroEx.getOrderHashHex({ 12 | exchangeContractAddress: zeroExConfig.exchangeContractAddress, 13 | maker: order.signedOrder.maker, 14 | taker: order.signedOrder.taker, 15 | makerTokenAddress: order.signedOrder.makerTokenAddress, 16 | takerTokenAddress: order.signedOrder.takerTokenAddress, 17 | feeRecipient: order.signedOrder.feeRecipient, 18 | makerTokenAmount: toBN(order.signedOrder.makerTokenAmount), 19 | takerTokenAmount: toBN(order.signedOrder.takerTokenAmount), 20 | makerFee: toBN(order.signedOrder.makerFee), 21 | takerFee: toBN(order.signedOrder.takerFee), 22 | expirationUnixTimestampSec: toBN(order.signedOrder.expirationUnixTimestampSec), 23 | salt: toBN(order.signedOrder.salt), 24 | }) 25 | 26 | const ecSignature = personalECSignHex(wallet.privateKey, hash) 27 | 28 | expect(_.isEqual(ecSignature, order.signedOrder.ecSignature)).toBe(true) 29 | }) 30 | }) -------------------------------------------------------------------------------- /tests/utils/gasPriceAdaptor.test.ts: -------------------------------------------------------------------------------- 1 | import { GasPriceAdaptor } from '../../src/types' 2 | import { getGasPriceByAdaptorAsync } from '../../src/utils/gasPriceAdaptor' 3 | import { waitSeconds } from '../__utils__/wait' 4 | import * as _ from 'lodash' 5 | 6 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 7 | 8 | const testData = ['safeLow', 'average', 'fast'] 9 | 10 | describe('test adaptor', () => { 11 | for (let ad of testData) { 12 | it(`${ad} should larger than or equal with 1Gwei`, async () => { 13 | const gasPriceBefore = await getGasPriceByAdaptorAsync(ad as GasPriceAdaptor) 14 | expect(gasPriceBefore).toBeGreaterThanOrEqual(Math.pow(10, 9)) 15 | }) 16 | 17 | it(`${ad} within 60 seconds should be same`, async () => { 18 | const gasPriceBefore = await getGasPriceByAdaptorAsync(ad as GasPriceAdaptor) 19 | await waitSeconds(40) 20 | const gasPrice25 = await getGasPriceByAdaptorAsync(ad as GasPriceAdaptor) 21 | expect(gasPriceBefore).toEqual(gasPrice25) 22 | }) 23 | } 24 | 25 | it(`fast should larger than average, and average should larger then safeLow`, async () => { 26 | const gasPriceA = await getGasPriceByAdaptorAsync('fast') 27 | const gasPriceB = await getGasPriceByAdaptorAsync('average') 28 | const gasPriceC = await getGasPriceByAdaptorAsync('safeLow') 29 | 30 | expect(gasPriceA).toBeGreaterThanOrEqual(gasPriceB) 31 | expect(gasPriceB).toBeGreaterThanOrEqual(gasPriceC) 32 | }) 33 | }) -------------------------------------------------------------------------------- /src/utils/sign.ts: -------------------------------------------------------------------------------- 1 | import * as ethUtil from 'ethereumjs-util' 2 | import { leftPadWith0 } from './helper' 3 | import { Dex } from '../types' 4 | import { ECSignature } from '0x.js' 5 | 6 | // sig is buffer 7 | export const concatSig = (ecSignatureBuffer: Dex.ECSignatureBuffer): Buffer => { 8 | const { v, r, s } = ecSignatureBuffer 9 | const vSig = ethUtil.bufferToInt(v) 10 | const rSig = ethUtil.fromSigned(r) 11 | const sSig = ethUtil.fromSigned(s) 12 | const rStr = leftPadWith0(ethUtil.toUnsigned(rSig).toString('hex'), 64) 13 | const sStr = leftPadWith0(ethUtil.toUnsigned(sSig).toString('hex'), 64) 14 | const vStr = ethUtil.stripHexPrefix(ethUtil.intToHex(vSig)) 15 | return ethUtil.addHexPrefix(rStr.concat(sStr, vStr)).toString('hex') 16 | } 17 | 18 | export const personalECSign = (privateKey: string, msg: string): Dex.ECSignatureBuffer => { 19 | const message = ethUtil.toBuffer(msg) 20 | const msgHash = ethUtil.hashPersonalMessage(message) 21 | return ethUtil.ecsign(msgHash, new Buffer(privateKey, 'hex')) 22 | } 23 | 24 | export const personalSign = (privateKey: string, msg: string): string => { 25 | const sig = personalECSign(privateKey, msg) 26 | return ethUtil.bufferToHex(concatSig(sig)) 27 | } 28 | 29 | export const personalECSignHex = (privateKey: string, msg: string): ECSignature => { 30 | const { r, s, v } = personalECSign(privateKey, msg) 31 | const ecSignature = { 32 | v, 33 | r: ethUtil.bufferToHex(r), 34 | s: ethUtil.bufferToHex(s), 35 | } 36 | return ecSignature 37 | } -------------------------------------------------------------------------------- /src/utils/math.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber as BN } from '@0xproject/utils' 2 | import * as _ from 'lodash' 3 | 4 | export const isBigNumber = (v: any) => { 5 | return v instanceof BN || 6 | (v && v.isBigNumber === true) || 7 | (v && v._isBigNumber === true) || 8 | false 9 | } 10 | 11 | export const isNumberLike = (n) => { 12 | // if use Number(n), 'toBN(-0xaa) will not pass this condition, will received only 0' 13 | // if you set params with empty string like ' ', it will return false, just like new BigNumber(' ') will throw error 14 | const num = parseFloat(n) 15 | return _.isNumber(num) && _.isFinite(num) && !_.isNaN(num) 16 | } 17 | 18 | export const toBN = (value): BN => { 19 | value = value || 0 20 | if (isBigNumber(value)) { 21 | return value 22 | } 23 | if (!isNumberLike(value)) { 24 | return new BN(0) 25 | } 26 | if (_.isString(value) && ((value).indexOf('0x') === 0 || (value).indexOf('-0x') === 0)) { 27 | return new BN((value).replace('0x', ''), 16) 28 | } 29 | 30 | return new BN((value).toString(10), 10) 31 | } 32 | 33 | /** 34 | * Returns a string representing the value of this BigNumber in normal (fixed-point) notation rounded to dp decimal places using rounding mode rm. 35 | * @param {Number} n 36 | * @param {Number} dp [decimal places, 0 to 1e+9] 37 | * @param {Number} rm [rounding modes 0 to 6, Default value: 1 ROUND_DOWN ] http://mikemcl.github.io/bignumber.js/#round 38 | * @return {String} 39 | */ 40 | export const toFixed = (n, dp = 4, rm = 1): string => { 41 | return toBN(n).toFixed(dp, rm) 42 | } -------------------------------------------------------------------------------- /src/types/dex.ts: -------------------------------------------------------------------------------- 1 | import BigNumber from '@0xproject/utils' 2 | import { ECSignature } from '0x.js' 3 | import { SimpleOrder, DexOrderBNToString, GlobalConfig } from './base' 4 | import { Server } from './server' 5 | import { Pair } from './pair' 6 | 7 | export namespace Dex { 8 | export type ECSignatureBuffer = { 9 | v: number 10 | r: Buffer 11 | s: Buffer; 12 | } 13 | export type GetSimpleOrderParams = { 14 | amountRemaining?: string 15 | order: DexOrderBNToString 16 | pair: Pair.ExchangePair; 17 | } 18 | export type GenerateDexOrderWithoutSaltParams = { 19 | simpleOrder: SimpleOrder 20 | pair: Pair.ExchangePair 21 | config: GlobalConfig; 22 | } 23 | export type DexOrderWithoutSalt = { 24 | exchangeContractAddress: string, 25 | expirationUnixTimestampSec: BigNumber.BigNumber 26 | feeRecipient: string 27 | maker: string 28 | makerFee: BigNumber.BigNumber 29 | makerTokenAddress: string 30 | makerTokenAmount: BigNumber.BigNumber 31 | taker: string 32 | takerFee: BigNumber.BigNumber 33 | takerTokenAddress: string 34 | takerTokenAmount: BigNumber.BigNumber; 35 | } 36 | export interface DexOrder extends DexOrderWithoutSalt { 37 | salt: BigNumber.BigNumber 38 | } 39 | export interface SignedDexOrder extends DexOrder { 40 | ecSignature: ECSignature 41 | } 42 | 43 | export interface TranslateOrderBookToSimpleParams { 44 | orderbookItems: Server.OrderBookItem[] 45 | pair: Pair.ExchangePair 46 | wallet?: { 47 | address: string; 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /tests/tokenlon/balance.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { localConfig, web3ProviderUrl, walletUseToFill } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import Web3 from 'web3' 7 | import web3 from '../../src/lib/web3-wrapper' 8 | import { fromDecimalToUnit } from '../../src/utils/format' 9 | import { getTokenBalance } from '../../src/utils/ethereum' 10 | 11 | let tokenlon = null as Tokenlon 12 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 13 | 14 | beforeAll(async () => { 15 | tokenlon = await createTokenlon(localConfig) 16 | }) 17 | 18 | describe('test getTokenBalance', () => { 19 | it(`test getTokenBalance ${walletUseToFill.address} ETH`, async () => { 20 | const balance1 = await tokenlon.getTokenBalance('ETH', walletUseToFill.address) 21 | const balance2 = fromDecimalToUnit(web3.eth.getBalance(walletUseToFill.address), 18).toNumber() 22 | expect(balance1).toEqual(balance2) 23 | }) 24 | 25 | it(`test getTokenBalance ${localConfig.wallet.address} SNT`, async () => { 26 | const balance1 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 27 | const balance2BN = await getTokenBalance({ 28 | address: localConfig.wallet.address, 29 | contractAddress: sntWethPairData.base.contractAddress, 30 | }) 31 | const balance2 = fromDecimalToUnit(balance2BN, sntWethPairData.base.decimal).toNumber() 32 | 33 | expect(balance1).toEqual(balance2) 34 | }) 35 | }) -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export namespace Web3Wrapper { 2 | export type Web3RequestParams = { 3 | [propName: string]: any; 4 | } 5 | 6 | export type Web3RequestData = { 7 | method: string 8 | data: Web3RequestParams 9 | [propName: string]: any; 10 | } 11 | } 12 | 13 | export namespace Ethereum { 14 | export type SendTransactionParams = { 15 | address: string 16 | privateKey: string 17 | gasLimit?: number 18 | gasPrice: number 19 | to: string 20 | data?: string 21 | value: number; 22 | } 23 | } 24 | 25 | export enum TokenlonError { 26 | InvalidOrders = 'INVALID_ORDERS', 27 | UnsupportedPair = 'UNSUPPORTED_PAIR', 28 | UnsupportedToken = 'UNSUPPORTED_TOKEN', 29 | WalletDoseNotExist = 'WALLET_DOSE_NOT_EXIST', 30 | InvalidContractName = 'INVALID_CONTRACT_NAME', 31 | InvalidContractMethod = 'INVALID_CONTRACT_METHOD', 32 | InvalidSideWithOrder = 'INVALID_SIDE_WITH_ORDER', 33 | InvalidWalletPrivateKey = 'INVALID_WALLET_PRIVATE_KEY', 34 | InvalidGasPriceAdaptor = 'INVALID_GAS_PRICE_ADAPTOR', 35 | EthDoseNotHaveApprovedMethod = 'ETH_DOSE_NOT_HAVE_APPROVED_METHOD', 36 | InvalidPriceWithToBeFilledOrder = 'INVALID_PRICE_WITH_TO_BE_FILLED_ORDER', 37 | OrdersMustBeSamePairAndSameSideWithFillOrdersUpTo = 'ORDERS_MUST_BE_SAME_PAIR_AND_SAME_SIDE_WITH_FILLORDERSUPTO', 38 | } 39 | 40 | export { GasPriceAdaptor, Side, Wallet, GlobalConfig, SimpleOrder, DexOrderBNToString } from './base' 41 | export { Dex } from './dex' 42 | export { Pair } from './pair' 43 | export { Server } from './server' 44 | export { Tokenlon } from './tokenlon' -------------------------------------------------------------------------------- /tests/__mock__/config.ts: -------------------------------------------------------------------------------- 1 | import { GasPriceAdaptor } from '../../src/types' 2 | 3 | export const wallet = { 4 | address: '0x20F0C6e79A763E1Fe83DE1Fbf08279Aa3953FB5f', 5 | privateKey: '3f992df8720a778e68a82d27a47d91155ce69ea9954b46ef85afaf19c75bd192', 6 | } 7 | 8 | export const placeOrderWalletAddress = '0xd7a0D7889577ef77C11Ab5CC00817D1c9adE6B36' 9 | 10 | export const walletUseToFill = { 11 | address: '0x17bf552da0ec40b1614660a773d371909dbe3eaa', 12 | privateKey: '80af1fcd21e974802c664a65f9c19a45c6388606a8784adb5266efd72eb1096d', 13 | } 14 | 15 | export const localUrl = 'http://localhost' 16 | export const localPort = 5620 17 | export const localServerUrl = `${localUrl}:${localPort}` 18 | 19 | export const web3ProviderUrl = 'https://kovan.infura.io' 20 | 21 | export const zeroExConfig = { 22 | networkId: 42, 23 | gasLimit: 150000, 24 | etherTokenContractAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 25 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 26 | tokenTransferProxyContractAddress: '0x087Eed4Bc1ee3DE49BeFbd66C662B434B15d49d4', 27 | } 28 | 29 | export const localConfig = { 30 | wallet, 31 | server: { 32 | url: localServerUrl, 33 | }, 34 | web3: { 35 | providerUrl: web3ProviderUrl, 36 | }, 37 | zeroEx: zeroExConfig, 38 | gasPriceAdaptor: 'average' as GasPriceAdaptor, 39 | } 40 | 41 | export const localConfigUseToFill = { 42 | wallet: walletUseToFill, 43 | server: { 44 | url: localServerUrl, 45 | }, 46 | web3: { 47 | providerUrl: web3ProviderUrl, 48 | }, 49 | zeroEx: zeroExConfig, 50 | gasPriceAdaptor: 'safeLow' as GasPriceAdaptor, 51 | } -------------------------------------------------------------------------------- /src/types/base.ts: -------------------------------------------------------------------------------- 1 | import { ECSignature } from '0x.js' 2 | 3 | export type Side = 'BUY' | 'SELL' 4 | 5 | export type Wallet = { 6 | address: string 7 | privateKey: string; 8 | } 9 | 10 | export type GasPriceAdaptor = 'safeLow' | 'average' | 'fast' 11 | 12 | export type GlobalConfig = { 13 | server: { 14 | url: string; 15 | } 16 | web3: { 17 | providerUrl: string; 18 | } 19 | wallet: Wallet 20 | onChainValidate?: boolean 21 | gasPriceAdaptor: GasPriceAdaptor 22 | zeroEx: { 23 | gasLimit: number 24 | networkId: number 25 | exchangeContractAddress: undefined | string 26 | etherTokenContractAddress: string 27 | tokenTransferProxyContractAddress: undefined | string 28 | zrxContractAddress?: undefined | string 29 | tokenRegistryContractAddress?: undefined | string 30 | orderWatcherConfig?: { 31 | cleanupJobIntervalMs: undefined | number 32 | eventPollingIntervalMs: undefined | number 33 | expirationMarginMs: undefined | number 34 | orderExpirationCheckingIntervalMs: undefined | number; 35 | }; 36 | }; 37 | } 38 | 39 | export type SimpleOrder = { 40 | side: Side 41 | price: number 42 | amount: number 43 | expirationUnixTimestampSec?: number; 44 | } 45 | 46 | export type DexOrderBNToString = { 47 | maker: string 48 | taker: string 49 | makerTokenAddress: string 50 | takerTokenAddress: string 51 | exchangeContractAddress: string 52 | expirationUnixTimestampSec: string 53 | feeRecipient: string 54 | makerFee: string 55 | makerTokenAmount: string 56 | takerFee: string 57 | takerTokenAmount: string 58 | salt: string 59 | ecSignature: ECSignature; 60 | } -------------------------------------------------------------------------------- /tests/tokenlon/allowance.test.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '0x.js/lib/src/utils/constants' 2 | import { localConfig, localConfigUseToFill, web3ProviderUrl } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import Web3 from 'web3' 7 | import web3 from '../../src/lib/web3-wrapper' 8 | import { fromDecimalToUnit } from '../../src/utils/format' 9 | import { getTokenBalance } from '../../src/utils/ethereum' 10 | import { toBN } from '../../src/utils/math' 11 | import { waitMined } from '../__utils__/wait' 12 | 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 14 | 15 | let tokenlon = null as Tokenlon 16 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 17 | 18 | beforeAll(async () => { 19 | tokenlon = await createTokenlon(localConfigUseToFill) 20 | }) 21 | 22 | describe('test setAllowance / setAllowance / getAllowance', () => { 23 | it(`test setAllowance / setAllowance / getAllowance`, async () => { 24 | const amount = 562.562562 25 | const tokenName = sntWethPairData.base.symbol 26 | const txHash1 = await tokenlon.setAllowance(tokenName, amount) 27 | await waitMined(txHash1, 30) 28 | const allowance1 = await tokenlon.getAllowance(tokenName) 29 | expect(allowance1).toEqual(amount) 30 | 31 | const txHash2 = await tokenlon.setUnlimitedAllowance(tokenName) 32 | await waitMined(txHash2, 30) 33 | const allowance2 = await tokenlon.getAllowance(tokenName) 34 | expect(allowance2).toEqual(fromDecimalToUnit(constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, sntWethPairData.base.decimal).toNumber()) 35 | }) 36 | }) -------------------------------------------------------------------------------- /src/utils/gasPriceAdaptor.ts: -------------------------------------------------------------------------------- 1 | import { GasPriceAdaptor } from '../types' 2 | import axios from 'axios' 3 | import { ETH_GAS_STATION_URL } from '../constants' 4 | import { getTimestamp, newError } from './helper' 5 | 6 | const getGasPriceByAdaptorHelper = async (adaptor: GasPriceAdaptor): Promise => { 7 | return axios(ETH_GAS_STATION_URL).then(res => { 8 | return res.data[adaptor] * 0.1 9 | }).catch(e => { 10 | throw newError(`${ETH_GAS_STATION_URL} server error ${e && e.message}`) 11 | }) 12 | } 13 | 14 | const stack = {} 15 | 16 | export const getGasPriceByAdaptorAsync = async (adaptor: GasPriceAdaptor): Promise => { 17 | // Use variable cache data within 5mins 18 | if (stack[adaptor] && stack[adaptor].timestamp + 300 > getTimestamp()) { 19 | return stack[adaptor].gasPrice 20 | } else if (stack[adaptor] && stack[adaptor].requesting) { 21 | 22 | // only try to recursive call 15 times 23 | if (stack[adaptor].tried > 15) { 24 | newError(`${ETH_GAS_STATION_URL} server request timeout`) 25 | 26 | } else { 27 | await new Promise((resolve) => { 28 | setTimeout(resolve, 1000) 29 | }) 30 | stack[adaptor].tried = stack[adaptor].tried ? stack[adaptor].tried + 1 : 1 31 | return getGasPriceByAdaptorAsync(adaptor) 32 | } 33 | } else { 34 | stack[adaptor] = { requesting: true } 35 | const gasPrice = await getGasPriceByAdaptorHelper(adaptor) 36 | const gasPriceInGwei = gasPrice * Math.pow(10, 9) // gwei process 37 | stack[adaptor] = { 38 | gasPrice: gasPriceInGwei, 39 | timestamp: getTimestamp(), 40 | requesting: false, 41 | } 42 | return gasPriceInGwei 43 | } 44 | } -------------------------------------------------------------------------------- /tests/tokenlon/pair.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { localConfig } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | 7 | let tokenlon = null as Tokenlon 8 | 9 | beforeAll(async () => { 10 | tokenlon = await createTokenlon(localConfig) 11 | }) 12 | 13 | describe('test getPairs', () => { 14 | it('test getPairs', async () => { 15 | const pairs = await tokenlon.getPairs() 16 | const found = pairs.find(p => { 17 | return p.base.contractAddress === sntWethPairData.base.contractAddress && 18 | p.quote.contractAddress === sntWethPairData.quote.contractAddress 19 | }) 20 | const compared = _.isEqual(found.base.contractAddress, sntWethPairData.base.contractAddress) && 21 | _.isEqual(found.quote.contractAddress, sntWethPairData.quote.contractAddress) 22 | expect(compared).toBe(true) 23 | }) 24 | }) 25 | 26 | describe('test getPairInfo', () => { 27 | it('test getPairInfo', async () => { 28 | const pair = await tokenlon.getPairInfo({ 29 | base: sntWethPairData.base.symbol, 30 | quote: sntWethPairData.quote.symbol, 31 | }) 32 | const compared = _.isEqual(pair.base.contractAddress, sntWethPairData.base.contractAddress) && 33 | _.isEqual(pair.quote.contractAddress, sntWethPairData.quote.contractAddress) 34 | expect(compared).toBe(true) 35 | }) 36 | }) 37 | 38 | describe('test getTokenInfo', () => { 39 | it('test getTokenInfo', async () => { 40 | const token = await tokenlon.getTokenInfo(sntWethPairData.base.symbol) 41 | const compared = _.isEqual(token.contractAddress, sntWethPairData.base.contractAddress) 42 | expect(compared).toBe(true) 43 | }) 44 | }) -------------------------------------------------------------------------------- /tests/__mock__/simpleOrder.ts: -------------------------------------------------------------------------------- 1 | import { getTimestamp } from '../../src/utils/helper' 2 | import { Side } from '../../src/types' 3 | export const validSimpleOrder = [ 4 | { 5 | side: 'BUY', 6 | price: 0.00002, 7 | amount: 100, 8 | }, 9 | { 10 | side: 'SELL', 11 | price: 0.01, 12 | amount: 100, 13 | expirationUnixTimestampSec: getTimestamp() + 60 * 60, 14 | }, 15 | ] 16 | 17 | export const invalidSimpleOrder = [ 18 | { 19 | side: 'BUY', 20 | price: '0.00002', 21 | amount: 0.01, 22 | }, 23 | { 24 | side: 'sell', 25 | price: 0.01, 26 | amount: 100, 27 | expirationUnixTimestampSec: getTimestamp() + 60 * 60, 28 | }, 29 | { 30 | side: 'SELL', 31 | price: '0.01', 32 | amount: 100, 33 | expirationUnixTimestampSec: getTimestamp() + 60 * 60, 34 | }, 35 | { 36 | side: 'SELL', 37 | price: 0.01, 38 | amount: 100, 39 | expirationUnixTimestampSec: getTimestamp() - 10, 40 | }, 41 | ] 42 | 43 | export const simpleOrders = [ 44 | { 45 | price: 0.00019999, 46 | amount: 200.2787342, 47 | side: 'BUY' as Side, 48 | expirationUnixTimestampSec: getTimestamp() + 10 * 60, 49 | }, 50 | { 51 | price: 0.00020256, 52 | amount: 666.562562, 53 | side: 'BUY' as Side, 54 | expirationUnixTimestampSec: getTimestamp() + 10 * 60, 55 | }, 56 | { 57 | price: 0.00020257, 58 | amount: 908.24535291, 59 | side: 'SELL' as Side, 60 | expirationUnixTimestampSec: getTimestamp() + 10 * 60, 61 | }, 62 | { 63 | price: 0.00020257, 64 | amount: 562.56256256, 65 | side: 'SELL' as Side, 66 | expirationUnixTimestampSec: getTimestamp() + 10 * 60, 67 | }, 68 | { 69 | price: 0.0003333, 70 | amount: 562.56256256, 71 | side: 'SELL' as Side, 72 | expirationUnixTimestampSec: getTimestamp() + 20 * 60, 73 | }, 74 | ] -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { toBN } from './math' 3 | import { Tokenlon } from '../types' 4 | import { TransactionOpts } from '0x.js' 5 | 6 | export const newError = (msg: string): Error => new Error(msg) 7 | 8 | export const lowerCase = (str: string): string => str.toLowerCase() 9 | 10 | export const getTimestamp = (): number => Math.ceil(Date.now() / 1000) 11 | 12 | export const helpCompareStr = (a: string, b: string): boolean => lowerCase(a) === lowerCase(b) 13 | 14 | export const convertTrades = (trades => { 15 | return trades.map(item => { 16 | const { tradeType, payload } = item 17 | const rawOrder = JSON.stringify(payload) 18 | delete item.payload 19 | return { 20 | ...item, 21 | rawOrder, 22 | side: tradeType === 'ask' ? 'SELL' : (tradeType === 'bid' ? 'BUY' : ''), 23 | } 24 | }) 25 | }) 26 | 27 | // Only support object 28 | export const lowerCaseObj0xValue = (obj: any): any => { 29 | const keys = _.keys(obj) 30 | const conf = {} 31 | 32 | keys.forEach(k => { 33 | const v = obj[k] 34 | 35 | if (_.isPlainObject(v)) { 36 | conf[k] = lowerCaseObj0xValue(v) 37 | 38 | } else if (_.isString(v) && v.toLowerCase().startsWith('0x')) { 39 | conf[k] = v.toLowerCase() 40 | 41 | } else { 42 | conf[k] = v 43 | } 44 | }) 45 | 46 | return conf 47 | } 48 | 49 | export const leftPadWith0 = (str, len) => { 50 | str = str + '' 51 | len = len - str.length 52 | if (len <= 0) return str 53 | return '0'.repeat(len) + str 54 | } 55 | 56 | export const convertTokenlonTxOptsTo0xOpts = (opts: Tokenlon.TxOpts): TransactionOpts => { 57 | if (opts) { 58 | const { gasLimit, gasPrice } = opts 59 | return { 60 | gasLimit, 61 | gasPrice: gasPrice ? toBN(gasPrice) : gasPrice, 62 | } as TransactionOpts 63 | } 64 | return opts as any 65 | } -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/__signed_order_utils.ts: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | // waiting 0x.js update to latest version 3 | 4 | import { SignedOrder } from '0x.js' 5 | import { BigNumber } from '@0xproject/utils' 6 | 7 | export const signedOrderUtils = { 8 | createFill: ( 9 | signedOrder: SignedOrder, 10 | shouldThrowOnInsufficientBalanceOrAllowance?: boolean, 11 | fillTakerTokenAmount?: BigNumber, 12 | ) => { 13 | const fill = { 14 | ...signedOrderUtils.getOrderAddressesAndValues(signedOrder), 15 | fillTakerTokenAmount: fillTakerTokenAmount || signedOrder.takerTokenAmount, 16 | shouldThrowOnInsufficientBalanceOrAllowance: !!shouldThrowOnInsufficientBalanceOrAllowance, 17 | ...signedOrder.ecSignature, 18 | } 19 | return fill 20 | }, 21 | createCancel(signedOrder: SignedOrder, cancelTakerTokenAmount?: BigNumber) { 22 | const cancel = { 23 | ...signedOrderUtils.getOrderAddressesAndValues(signedOrder), 24 | cancelTakerTokenAmount: cancelTakerTokenAmount || signedOrder.takerTokenAmount, 25 | } 26 | return cancel 27 | }, 28 | getOrderAddressesAndValues(signedOrder: SignedOrder) { 29 | return { 30 | orderAddresses: [ 31 | signedOrder.maker, 32 | signedOrder.taker, 33 | signedOrder.makerTokenAddress, 34 | signedOrder.takerTokenAddress, 35 | signedOrder.feeRecipient, 36 | ], 37 | orderValues: [ 38 | signedOrder.makerTokenAmount, 39 | signedOrder.takerTokenAmount, 40 | signedOrder.makerFee, 41 | signedOrder.takerFee, 42 | signedOrder.expirationUnixTimestampSec, 43 | signedOrder.salt, 44 | ], 45 | } 46 | }, 47 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tokenlon-sdk", 3 | "version": "0.3.1", 4 | "description": "imToken Tokenlon API for node", 5 | "keywords": [ 6 | "imToken", 7 | "tokenlon", 8 | "0x.js", 9 | "zeroex" 10 | ], 11 | "main": "lib/index.js", 12 | "types": "src/globals.d.ts", 13 | "author": "imToken PTE. LTD.", 14 | "license": "MIT", 15 | "scripts": { 16 | "watch": "tsc -w", 17 | "clean": "rm -rf ./lib", 18 | "build:commonjs": "tsc", 19 | "build": "run-s clean build:commonjs", 20 | "lint": "tslint --project . 'src/**/*.ts'", 21 | "proxy-server": "ts-node ./tests/__proxy__/proxy.ts", 22 | "test": "yarn lint && yarn jest -- --runInBand --colors --coverage", 23 | "prepublish": "npm run build", 24 | "coveralls": "cat ./coverage/lcov.info | coveralls" 25 | }, 26 | "jest": { 27 | "transform": { 28 | "^.+\\.tsx?$": "ts-jest" 29 | }, 30 | "testRegex": "(/tests/[^_]*\\.test)\\.ts$", 31 | "moduleFileExtensions": [ 32 | "ts", 33 | "tsx", 34 | "js", 35 | "jsx", 36 | "json", 37 | "node" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@types/jest": "^22.2.0", 42 | "@types/node": "^9.4.6", 43 | "coveralls": "^3.0.0", 44 | "jest": "^22.4.3", 45 | "koa": "^2.5.0", 46 | "koa-body": "^2.5.0", 47 | "koa-router": "^7.4.0", 48 | "npm-run-all": "^4.1.2", 49 | "nyc": "^11.4.1", 50 | "ts-jest": "^22.4.1", 51 | "ts-node": "^5.0.1", 52 | "tslint": "^5.9.1", 53 | "typescript": "^2.7.2" 54 | }, 55 | "dependencies": { 56 | "0x.js": "^0.33.0", 57 | "@0xproject/utils": "^0.4.0", 58 | "axios": "^0.18.0", 59 | "ethereumjs-abi": "^0.6.5", 60 | "ethereumjs-tx": "^1.3.3", 61 | "ethereumjs-util": "^5.1.5", 62 | "lodash": "^4.17.5", 63 | "web3": "git+https://github.com/consenlabs/web3.js.git" 64 | }, 65 | "resolutions": { 66 | "**/istanbul-lib-instrument": "1.9.2" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tests/lib/server.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { Server } from '../../src/lib/server' 3 | import { localServerUrl, wallet } from '../__mock__/config' 4 | import { sntWethPairData } from '../__mock__/pair' 5 | 6 | let pairs = [] 7 | let server = new Server(localServerUrl, wallet) 8 | 9 | beforeAll(async () => { 10 | pairs = await server.getPairList() 11 | return pairs 12 | }) 13 | 14 | describe('getPairList', () => { 15 | it('getPairList contains snt-weth pair', () => { 16 | expect(pairs.some(p => { 17 | return _.isEqual(p.base.contractAddress, sntWethPairData.base.contractAddress) && 18 | _.isEqual(p.quote.contractAddress, sntWethPairData.quote.contractAddress) 19 | })).toBe(true) 20 | }) 21 | }) 22 | 23 | describe('getOrderBook', () => { 24 | it('getOrderBook api response asks and bids', async () => { 25 | const orderbook = await server.getOrderBook({ 26 | baseTokenAddress: sntWethPairData.base.contractAddress, 27 | quoteTokenAddress: sntWethPairData.quote.contractAddress, 28 | }) 29 | 30 | expect(!!orderbook.asks && !!orderbook.bids).toBe(true) 31 | expect(orderbook.asks.length).toBeGreaterThanOrEqual(0) 32 | expect(orderbook.bids.length).toBeGreaterThanOrEqual(0) 33 | 34 | // server data not sorted 35 | if (orderbook.bids.length > 1) { 36 | expect(orderbook.bids[0].rate).toBeGreaterThanOrEqual(orderbook.bids[1].rate) 37 | } 38 | 39 | if (orderbook.asks.length > 1) { 40 | expect(orderbook.asks[0].rate).toBeLessThanOrEqual(orderbook.asks[1].rate) 41 | } 42 | }) 43 | }) 44 | 45 | describe('getOrders', () => { 46 | it('get orders from server', async () => { 47 | const orders = await server.getOrders({ 48 | maker: wallet.address, 49 | tokenPair: [sntWethPairData.base.contractAddress, sntWethPairData.quote.contractAddress], 50 | }) 51 | expect(!!orders).toBe(true) 52 | expect(orders.length).toBeGreaterThanOrEqual(0) 53 | }) 54 | }) -------------------------------------------------------------------------------- /tests/tokenlon/validateOption.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { ZeroEx } from '0x.js' 3 | import { localConfig, web3ProviderUrl, walletUseToFill } from '../__mock__/config' 4 | import { sntWethPairData } from '../__mock__/pair' 5 | import { createTokenlon } from '../../src/index' 6 | import { orderStringToBN } from '../../src/utils/dex' 7 | import Tokenlon from '../../src/tokenlon' 8 | import Web3 from 'web3' 9 | import web3 from '../../src/lib/web3-wrapper' 10 | import { orders } from '../__mock__/order' 11 | import { getTimestamp } from '../../src/utils/helper' 12 | 13 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 16 | 17 | const simpleOrder = { 18 | base: 'SNT', 19 | quote: 'WETH', 20 | price: 0.0002, 21 | amount: 100000000, 22 | side: 'BUY', 23 | expirationUnixTimestampSec: getTimestamp() + 3 * 60, 24 | } 25 | 26 | describe('test validateOption with getSignedOrderBySimpleOrderAsync', () => { 27 | it('should validate and thorw error when validate option is true', async () => { 28 | const tokenlon = await createTokenlon(localConfig) 29 | let errorMsg = null 30 | try { 31 | await tokenlon.utils.getSignedOrderBySimpleOrderAsync(simpleOrder) 32 | } catch (e) { 33 | errorMsg = e && e.message 34 | } 35 | 36 | expect(errorMsg).toBeTruthy() 37 | }) 38 | 39 | it('should not do validate and got a signedOrder when validate option is false', async () => { 40 | const tokenlon = await createTokenlon({ 41 | ...localConfig, 42 | onChainValidate: false, 43 | }) 44 | const signedOrder = await tokenlon.utils.getSignedOrderBySimpleOrderAsync(simpleOrder) 45 | const convertedSimpleOrder = tokenlon.utils.getSimpleOrderWithBaseQuoteBySignedOrder(signedOrder) 46 | 47 | for (let prop of ['base', 'quote', 'side', 'price', 'amount', 'expirationUnixTimestampSec']) { 48 | expect(convertedSimpleOrder[prop]).toEqual(simpleOrder[prop]) 49 | } 50 | }) 51 | }) -------------------------------------------------------------------------------- /src/types/tokenlon.ts: -------------------------------------------------------------------------------- 1 | import { SimpleOrder, Side } from './base' 2 | import { Server } from './server' 3 | 4 | export namespace Tokenlon { 5 | export type makerTaker = { 6 | maker: string 7 | taker: string; 8 | } 9 | 10 | export type BaseQuote = { 11 | base: string 12 | quote: string; 13 | } 14 | 15 | export interface GetOrdersParams extends BaseQuote { 16 | page?: number 17 | perpage?: number 18 | } 19 | 20 | export interface OrderBookItem extends SimpleOrder { 21 | amountTotal: number 22 | rawOrder: string 23 | isMaker: boolean 24 | } 25 | 26 | export interface OrderBookResult { 27 | asks: OrderBookItem[] 28 | bids: OrderBookItem[] 29 | } 30 | 31 | export interface SimpleOrderWithBaseQuote extends SimpleOrder { 32 | base: string 33 | quote: string 34 | } 35 | 36 | export interface FillOrderParams extends SimpleOrderWithBaseQuote { 37 | rawOrder: string 38 | [propName: string]: any 39 | } 40 | 41 | export interface TradesParams extends BaseQuote { 42 | page: number 43 | perpage: number 44 | timeRange?: [number, number] 45 | } 46 | 47 | export interface MakerTradesItem { 48 | tradeType: Server.tradeType 49 | trades: Server.MakerTradesDetailItem[] 50 | amountRemaining: number 51 | expirationUnixTimestampSec: string 52 | 53 | side: Side 54 | rawOrder: string 55 | } 56 | 57 | export interface TakerTradesItem { 58 | tradeType: Server.tradeType 59 | id: number 60 | price: number 61 | amount: number 62 | timestamp: number 63 | txHash: string 64 | 65 | side: Side 66 | rawOrder: string 67 | } 68 | 69 | export interface OrderDetail extends OrderBookItem { 70 | trades: Server.MakerTradesDetailItem[] 71 | } 72 | 73 | export interface FillOrdersUpTo { 74 | base: string 75 | quote: string 76 | side: string 77 | amount: number 78 | rawOrders: string[] 79 | } 80 | 81 | export interface TxOpts { 82 | gasPrice?: number 83 | gasLimit?: number 84 | } 85 | } -------------------------------------------------------------------------------- /tests/tokenlon/orderUtils.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { ZeroEx } from '0x.js' 3 | import { localConfig, web3ProviderUrl, walletUseToFill } from '../__mock__/config' 4 | import { sntWethPairData } from '../__mock__/pair' 5 | import { createTokenlon } from '../../src/index' 6 | import { orderStringToBN } from '../../src/utils/dex' 7 | import Tokenlon from '../../src/tokenlon' 8 | import Web3 from 'web3' 9 | import web3 from '../../src/lib/web3-wrapper' 10 | import { orders } from '../__mock__/order' 11 | 12 | let tokenlon = null as Tokenlon 13 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 16 | 17 | beforeAll(async () => { 18 | tokenlon = await createTokenlon(localConfig) 19 | }) 20 | 21 | const testData = orders[0] 22 | const simpleOrderData = testData.simpleOrder 23 | const signedOrderData = testData.signedOrder 24 | 25 | describe('test getSimpleOrderWithBaseQuoteBySignedOrder', () => { 26 | it(`should get a simple order`, () => { 27 | const simpleOrder = tokenlon.utils.getSimpleOrderWithBaseQuoteBySignedOrder(signedOrderData) 28 | expect(_.isEqual(simpleOrder, simpleOrderData)).toBe(true) 29 | }) 30 | }) 31 | 32 | describe('test getSignedOrderBySimpleOrderAsync', () => { 33 | it(`should get a signed order`, async () => { 34 | const signedOrder = await tokenlon.utils.getSignedOrderBySimpleOrderAsync(simpleOrderData); 35 | [ 36 | 'exchangeContractAddress', 37 | 'maker', 38 | 'taker', 39 | 'makerTokenAddress', 40 | 'takerTokenAddress', 41 | 'feeRecipient', 42 | 'makerTokenAmount', 43 | 'takerTokenAmount', 44 | 'makerFee', 45 | 'takerFee', 46 | 'expirationUnixTimestampSec', 47 | ].forEach(key => { 48 | expect(signedOrderData[key]).toEqual(signedOrder[key]) 49 | }) 50 | 51 | // check signature 52 | expect( 53 | ZeroEx.isValidSignature(ZeroEx.getOrderHashHex(orderStringToBN(signedOrder)), signedOrder.ecSignature, signedOrder.maker.toLowerCase()), 54 | ).toBe(true) 55 | }) 56 | }) -------------------------------------------------------------------------------- /src/utils/format.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { toBN, toFixed } from './math' 3 | import { BigNumber } from '@0xproject/utils' 4 | 5 | const toLocaleStringSupportsLocales = () => { 6 | const num = 0 7 | 8 | try { 9 | num.toLocaleString('i') 10 | } catch (e) { 11 | return e.name === 'RangeError' 12 | } 13 | return false 14 | } 15 | 16 | export const thousandCommas = (num, min = 4, max = 8) => { 17 | if (min > max) { 18 | throw new Error('maximumFractionDigits value is out of range') 19 | } 20 | 21 | if (!toLocaleStringSupportsLocales()) { 22 | const n = Number(num).toFixed(max) // 限制小数位长度 3.14159000265359 => 3.14159000 23 | const parts = Number(n).toString().split('.') // 小数位去零 3.14159000 => ["3", "14159"] 24 | parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',') 25 | return parts.join('.') 26 | } 27 | return Number(num).toLocaleString('en-US', { minimumFractionDigits: min, maximumFractionDigits: max }) 28 | } 29 | 30 | export const decimal = (num, place = 4) => { 31 | if (!num || Number(num) === 0) return '0' 32 | return toFixed(num, place) 33 | } 34 | 35 | export const formatMoney = (value, place = 4) => { 36 | return +value > 0.0001 ? decimal(value, place) : thousandCommas(value, +place, 8) 37 | } 38 | 39 | const fillHelper = (v, fill) => { 40 | if (_.isUndefined(fill) || fill) { 41 | return v 42 | } 43 | const result = toBN(v).toString() 44 | if (result.indexOf('.') !== -1) { 45 | return result 46 | } 47 | return toBN(v).toFixed(2) 48 | } 49 | 50 | // 统一小数点后位数量 fill 传递 false 表示如果后面都是 0,会移除 51 | export const formatNumHelper = (place) => { 52 | return (value, fill) => fillHelper(formatMoney(value, place || 8), fill) 53 | } 54 | 55 | export const fromUnitToDecimalBN = (balance, decimal): BigNumber => { 56 | const amountBN = toBN(balance || 0) 57 | const decimalBN = toBN(10).toPower(decimal) 58 | return amountBN.times(decimalBN) 59 | } 60 | 61 | export const fromDecimalToUnit = (balance, decimal) => { 62 | return toBN(balance).dividedBy(Math.pow(10, decimal)) 63 | } 64 | 65 | export const fromUnitToDecimal = (balance, decimal, base) => { 66 | return fromUnitToDecimalBN(balance, decimal).toString(base) 67 | } -------------------------------------------------------------------------------- /src/utils/pair.ts: -------------------------------------------------------------------------------- 1 | import { helpCompareStr, newError } from './helper' 2 | import { Pair, Tokenlon, DexOrderBNToString, TokenlonError } from '../types' 3 | import { assert } from './assert' 4 | 5 | export const getTokenByName = (tokenName: string, pairs: Pair.ExchangePair[]): Pair.ExchangePairToken => { 6 | assert.isValidTokenName(tokenName, pairs) 7 | 8 | let token = null as Pair.ExchangePairToken 9 | pairs.some(p => { 10 | if (helpCompareStr(p.base.symbol, tokenName)) { 11 | token = p.base 12 | return true 13 | } else if (helpCompareStr(p.quote.symbol, tokenName)) { 14 | token = p.quote 15 | return true 16 | } 17 | }) 18 | if (token) return token 19 | } 20 | 21 | const getPairHelper = (baseQuote: Tokenlon.BaseQuote, pairs: Pair.ExchangePair[], tokenPropName: string): Pair.ExchangePair => { 22 | const { base, quote } = baseQuote 23 | const pair = pairs.find(p => helpCompareStr(p.base[tokenPropName], base) && helpCompareStr(p.quote[tokenPropName], quote)) 24 | if (!pair) throw newError(TokenlonError.UnsupportedPair) 25 | return pair 26 | } 27 | 28 | export const getPairBySymbol = (baseQuote: Tokenlon.BaseQuote, pairs: Pair.ExchangePair[]): Pair.ExchangePair => { 29 | assert.isValidBaseQuote(baseQuote, pairs) 30 | return getPairHelper(baseQuote, pairs, 'symbol') 31 | } 32 | 33 | export const getPairByContractAddress = (baseQuote: Tokenlon.BaseQuote, pairs: Pair.ExchangePair[]): Pair.ExchangePair => { 34 | return getPairHelper(baseQuote, pairs, 'contractAddress') 35 | } 36 | 37 | export const getPairBySignedOrder = (order: DexOrderBNToString, pairs: Pair.ExchangePair[]) => { 38 | let pair = null 39 | const baseQuotes = [ 40 | { 41 | base: order.makerTokenAddress, 42 | quote: order.takerTokenAddress, 43 | }, { 44 | base: order.takerTokenAddress, 45 | quote: order.makerTokenAddress, 46 | }, 47 | ] 48 | 49 | baseQuotes.some((baseQuote, index) => { 50 | try { 51 | const result = getPairByContractAddress(baseQuote, pairs) 52 | pair = result 53 | return true 54 | } catch (e) { 55 | if (index === baseQuotes.length) { 56 | throw e 57 | } 58 | } 59 | }) 60 | return pair 61 | } -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/_etherToken.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import BigNumber from '@0xproject/utils' 3 | import { TransactionOpts, ZeroExError } from '0x.js' 4 | import { assert } from '@0xproject/assert' 5 | import helper from './helper' 6 | 7 | export const coverageEtherToken = (obj) => { 8 | _.extend(obj, { 9 | async depositAsync( 10 | etherTokenAddress: string, 11 | amountInWei: BigNumber.BigNumber, 12 | depositor: string, 13 | txOpts: TransactionOpts = {}, 14 | ) { 15 | assert.isETHAddressHex('etherTokenAddress', etherTokenAddress) 16 | assert.isValidBaseUnitAmount('amountInWei', amountInWei) 17 | // remove ownerAddress check, because we use privateKey to send tx, not metamask etc. 18 | // await assert.isSenderAddressAsync('depositor', depositor, this._web3Wrapper) 19 | // const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase() 20 | const normalizedDepositorAddress = depositor.toLowerCase() 21 | 22 | const ethBalanceInWei = await this._web3Wrapper.getBalanceInWeiAsync(normalizedDepositorAddress) 23 | assert.assert(ethBalanceInWei.gte(amountInWei), ZeroExError.InsufficientEthBalanceForDeposit) 24 | 25 | const txHash = helper.etherTokenTransaction('deposit', amountInWei, txOpts ? { 26 | gasLimit: txOpts.gasLimit, 27 | gasPrice: txOpts.gasPrice, 28 | } : txOpts) 29 | return txHash 30 | }, 31 | async withdrawAsync ( 32 | etherTokenAddress: string, 33 | amountInWei: BigNumber.BigNumber, 34 | withdrawer: string, 35 | txOpts: TransactionOpts = {}, 36 | ) { 37 | assert.isValidBaseUnitAmount('amountInWei', amountInWei) 38 | assert.isETHAddressHex('etherTokenAddress', etherTokenAddress) 39 | // remove ownerAddress check, because we use privateKey to send tx, not metamask etc. 40 | // await assert.isSenderAddressAsync('withdrawer', withdrawer, this._web3Wrapper) 41 | const normalizedEtherTokenAddress = etherTokenAddress.toLowerCase() 42 | const normalizedWithdrawerAddress = withdrawer.toLowerCase() 43 | 44 | const WETHBalanceInBaseUnits = await this._tokenWrapper.getBalanceAsync( 45 | normalizedEtherTokenAddress, 46 | normalizedWithdrawerAddress, 47 | ) 48 | assert.assert(WETHBalanceInBaseUnits.gte(amountInWei), ZeroExError.InsufficientWEthBalanceForWithdrawal) 49 | 50 | const txHash = helper.etherTokenTransaction('withdraw', amountInWei, txOpts ? { 51 | gasLimit: txOpts.gasLimit, 52 | gasPrice: txOpts.gasPrice, 53 | } : txOpts) 54 | return txHash 55 | }, 56 | }) 57 | } -------------------------------------------------------------------------------- /tests/__proxy__/proxy.ts: -------------------------------------------------------------------------------- 1 | import * as Koa from 'koa' 2 | import * as Router from 'koa-router' 3 | import * as koaBody from 'koa-body' 4 | import { jsonrpc } from '../../src/lib/server/_request' 5 | import { localPort } from '../__mock__/config' 6 | 7 | const app = new Koa() 8 | const router = Router() 9 | 10 | if (!process.env.serverUrl) { 11 | throw new Error('Need to set serverUrl') 12 | } 13 | 14 | const serverUrl = process.env.serverUrl 15 | 16 | // Add headers 17 | app.use(async (ctx, next) => { 18 | 19 | // Website you wish to allow to connect 20 | ctx.set('Access-Control-Allow-Origin', '*') 21 | 22 | // Request methods you wish to allow 23 | ctx.set('Access-Control-Allow-Methods', '*') 24 | 25 | // Request headers you wish to allow 26 | ctx.set('Access-Control-Allow-Headers', 'Authorization,X-DEVICE-TOKEN,X-CLIENT-VERSION,X-LOCALE,X-CURRENCY,X-ACCESS-TOKEN,X-DEVICE-LOCALE,ACCESS-TOKEN') 27 | 28 | // Set to true if you need the website to include cookies in the requests sent 29 | // to the API (e.g. in case you use sessions) 30 | ctx.set('Access-Control-Allow-Credentials', true) 31 | 32 | await next() 33 | }) 34 | 35 | router 36 | .get('/', 37 | async (ctx, next) => { 38 | await new Promise(resolve => { 39 | setTimeout(() => { 40 | resolve() 41 | }, 1000) 42 | }) 43 | ctx.body = 'OK GET' 44 | }, 45 | ) 46 | .options('/', ctx => { 47 | ctx.body = 'OK OPTIONS' 48 | }) 49 | .post( 50 | '/', 51 | koaBody(), 52 | async (ctx, next) => { 53 | const reqHeader = ctx.request.header 54 | const reqBody = ctx.request.body 55 | const useHeader = {}; 56 | 57 | ['content-type', 'user-agent', 'access-token'].forEach(key => { 58 | if (reqHeader[key]) { 59 | useHeader[key] = reqHeader[key] 60 | } 61 | }) 62 | 63 | let result = null 64 | let error = null 65 | try { 66 | result = await jsonrpc.get(serverUrl, useHeader, reqBody.method, reqBody.params) 67 | } catch (e) { 68 | console.log(e) 69 | error = { 70 | code: -32000, 71 | message: e.toString(), 72 | data: null, 73 | } 74 | } 75 | 76 | ctx.type = 'text/plain; charset=utf-8' 77 | ctx.set('Content-Type', 'application/json') 78 | ctx.status = 200 79 | ctx.body = error ? { 80 | jsonrpc: '2.0', 81 | error, 82 | } : { 83 | jsonrpc: '2.0', 84 | result, 85 | } 86 | }, 87 | ) 88 | 89 | app.use(router.routes()) 90 | 91 | console.log(`proxy listen on ${localPort}`) 92 | app.listen(localPort) 93 | -------------------------------------------------------------------------------- /tests/utils/helper.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { newError, lowerCase, getTimestamp, helpCompareStr, lowerCaseObj0xValue, leftPadWith0 } from '../../src/utils/helper' 3 | 4 | describe('newError', () => { 5 | const msg = 'foo' 6 | it(`Should get an error with a message ${msg}`, () => { 7 | const err = newError(msg) 8 | expect(err instanceof Error).toBe(true) 9 | expect(err.toString()).toBe(`Error: ${msg}`) 10 | }) 11 | }) 12 | 13 | describe('lowerCase', () => { 14 | it('lowerCase', () => { 15 | const x = 'xxXX' 16 | expect(lowerCase(x)).toBe(x.toLowerCase()) 17 | }) 18 | }) 19 | 20 | describe('getTimestamp', () => { 21 | it('getTimestamp', () => { 22 | const nowWithDecimalPoint = Date.now() / 1000 23 | const nowTimestamp = getTimestamp() 24 | expect(nowTimestamp).toBeLessThanOrEqual(Math.ceil(nowWithDecimalPoint)) 25 | expect(nowTimestamp).toBeGreaterThanOrEqual(Math.floor(nowWithDecimalPoint)) 26 | }) 27 | }) 28 | 29 | describe('helpCompareStr', () => { 30 | it('helpCompareStr', () => { 31 | const x = 'xxXX' 32 | expect(helpCompareStr(x, x.toUpperCase())).toBe(true) 33 | expect(helpCompareStr(x, x.toLowerCase())).toBe(true) 34 | }) 35 | }) 36 | 37 | describe('lowerCaseObj0xValue - 0x string', () => { 38 | const x = { 39 | a: { 40 | b: { 41 | c: { 42 | d: '0X', 43 | }, 44 | }, 45 | }, 46 | } 47 | const xTobeLowerCase = lowerCaseObj0xValue(x) 48 | it('lowerCaseObj0xValue', () => { 49 | expect(x.a.b.c.d.toLowerCase()).toBe(xTobeLowerCase.a.b.c.d) 50 | }) 51 | }) 52 | 53 | describe('lowerCaseObj0xValue - not 0x string', () => { 54 | const x = { 55 | a: { 56 | b: { 57 | c: { 58 | d: 'X', 59 | }, 60 | }, 61 | }, 62 | } 63 | const converted = lowerCaseObj0xValue(x) 64 | it('lowerCaseObj0xValue', () => { 65 | expect(x.a.b.c.d).toBe(converted.a.b.c.d) 66 | expect(x.a.b.c.d.toLowerCase()).toBe(converted.a.b.c.d.toLowerCase()) 67 | }) 68 | }) 69 | 70 | describe('leftPadWith0', () => { 71 | [ 72 | { 73 | str: '1', 74 | len: 54, 75 | result: '0'.repeat(53) + '1', 76 | }, 77 | { 78 | str: 'aa', 79 | len: 54, 80 | result: '0'.repeat(52) + 'aa', 81 | }, 82 | { 83 | str: '1', 84 | len: -1, 85 | result: '1', 86 | }, 87 | { 88 | str: '1', 89 | len: 1, 90 | result: '1', 91 | }, 92 | { 93 | str: '1', 94 | len: 3, 95 | result: '001', 96 | }, 97 | ].forEach(item => { 98 | it(`leftPadWith0(${item.str}, ${item.len}) should be ${item.result}`, () => { 99 | expect(leftPadWith0(item.str, item.len)).toBe(item.result) 100 | }) 101 | }) 102 | }) -------------------------------------------------------------------------------- /src/lib/web3-wrapper/index.ts: -------------------------------------------------------------------------------- 1 | import * as Web3Export from 'web3' 2 | import * as RequestManagerExport from 'web3/lib/web3/requestmanager' 3 | import * as HttpProviderExport from 'web3/lib/web3/httpprovider' 4 | import * as JsonrpcExport from 'web3/lib/web3/jsonrpc' 5 | import * as errorsExport from 'web3/lib/web3/errors' 6 | import { TIMEOUT } from '../../constants' 7 | import { Web3Wrapper } from '../../types' 8 | 9 | const Web3 = Web3Export.default ? Web3Export.default : Web3Export 10 | const RequestManager = RequestManagerExport.default ? RequestManagerExport.default : RequestManagerExport 11 | const HttpProvider = HttpProviderExport.default ? HttpProviderExport.default : HttpProviderExport 12 | const Jsonrpc = JsonrpcExport.default ? JsonrpcExport.default : JsonrpcExport 13 | const errors = errorsExport.default ? errorsExport.default : errorsExport 14 | 15 | const wrapperCallback = (_host, _data: Web3Wrapper.Web3RequestData, callback) => { 16 | // const method = data.method 17 | return (err, result?: any) => { 18 | if (err) { 19 | // console.log(`🍅 web3 fetch ${host} %c${method}`, `background: #E47361;color:#fff`, data, err) 20 | } else { 21 | // console.log(`🍅 web3 fetch ${host} %c${method}`, `background: #60B47A;color:#fff`, data, result) 22 | } 23 | if (callback) callback(err, result) 24 | } 25 | } 26 | 27 | RequestManager.prototype.sendAsync = function (data: Web3Wrapper.Web3RequestData, cb) { 28 | const host = this.provider && this.provider.host 29 | const callback = wrapperCallback(host, data, cb) 30 | 31 | if (!this.provider) { 32 | return callback(errors.InvalidProvider()) 33 | } 34 | 35 | const payload = Jsonrpc.toPayload(data.method, data.params) 36 | this.provider.sendAsync(payload, (err, result) => { 37 | if (err) { 38 | return callback(err) 39 | } 40 | 41 | if (!Jsonrpc.isValidResponse(result)) { 42 | return callback(errors.InvalidResponse(result)) 43 | } 44 | 45 | callback(null, result.result) 46 | }) 47 | } 48 | 49 | HttpProvider.prototype.sendAsync = function (payload, callback) { 50 | const request = this.prepareRequest(true) 51 | 52 | request.onreadystatechange = () => { 53 | if (request.readyState === 4) { 54 | let result = request.responseText 55 | let error = null 56 | 57 | try { 58 | result = JSON.parse(result) 59 | 60 | } catch (e) { 61 | error = errors.InvalidResponse(request.responseText) 62 | } 63 | 64 | callback(error, result) 65 | } 66 | } 67 | 68 | try { 69 | // TODO remove but using JWT and server apiKey, apiSecret to send tx 70 | request.timeout = TIMEOUT 71 | request.setRequestHeader('agent', 'ios:2.0.1:0') 72 | request.setRequestHeader('deviceToken', 'foobar') 73 | request.send(JSON.stringify(payload)) 74 | } catch (error) { 75 | callback(errors.InvalidConnection(this.host)) 76 | } 77 | } 78 | 79 | const web3 = new Web3() 80 | 81 | export default web3 82 | -------------------------------------------------------------------------------- /tests/tokenlon/orders.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { localConfigUseToFill } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { simpleOrders } from '../__mock__/simpleOrder' 5 | import { createTokenlon } from '../../src/index' 6 | import Tokenlon from '../../src/tokenlon' 7 | import { toBN } from '../../src/utils/math' 8 | import { getTimestamp } from '../../src/utils/helper' 9 | import { waitSeconds } from '../__utils__/wait' 10 | import { formatNumHelper } from '../../src/utils/format' 11 | 12 | let tokenlon = null as Tokenlon 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 14 | 15 | beforeAll(async () => { 16 | tokenlon = await createTokenlon(localConfigUseToFill) 17 | }) 18 | 19 | const containSimpleOrder = (orders, simpleOrder) => { 20 | return orders.some(o => { 21 | let expirationUnixTimestampSecCheck = true 22 | if (simpleOrder.expirationUnixTimestampSec) { 23 | expirationUnixTimestampSecCheck = _.isEqual(+o.expirationUnixTimestampSec, simpleOrder.expirationUnixTimestampSec) 24 | } 25 | 26 | return _.isEqual(o.side, simpleOrder.side) && 27 | _.isEqual(o.price, simpleOrder.price) && 28 | _.isEqual(o.amount, simpleOrder.amount) && 29 | expirationUnixTimestampSecCheck 30 | }) 31 | } 32 | 33 | describe('test placeOrder amount less then quoteMinUnit should throw error', () => { 34 | const baseQuote = { 35 | base: sntWethPairData.base.symbol, 36 | quote: sntWethPairData.quote.symbol, 37 | } 38 | simpleOrders.forEach((simpleOrder) => { 39 | const amount = +formatNumHelper(6)(simpleOrder.amount / 10000, false) 40 | it(`${simpleOrder.side} - ${amount} - ${simpleOrder.price} test placeOrder should throw error`, async () => { 41 | let errorMsg = '' 42 | try { 43 | await tokenlon.placeOrder({ 44 | ...baseQuote, 45 | ...simpleOrder, 46 | amount, 47 | }) 48 | } catch (e) { 49 | errorMsg = e.message 50 | } 51 | 52 | expect(errorMsg).toBeTruthy() 53 | }) 54 | }) 55 | }) 56 | 57 | describe('test placeOrder / getOrderBook / getOrders', () => { 58 | const baseQuote = { 59 | base: sntWethPairData.base.symbol, 60 | quote: sntWethPairData.quote.symbol, 61 | } 62 | simpleOrders.forEach((simpleOrder) => { 63 | it(`${simpleOrder.side} - ${simpleOrder.amount} - ${simpleOrder.price} test placeOrder / getOrderBook / getOrders`, async () => { 64 | await tokenlon.placeOrder({ 65 | ...baseQuote, 66 | ...simpleOrder, 67 | }) 68 | await waitSeconds(3) 69 | const orderBook = await tokenlon.getOrderBook(baseQuote) 70 | const orderBookOrders = orderBook[simpleOrder.side === 'BUY' ? 'bids' : 'asks'] 71 | expect(containSimpleOrder(orderBookOrders, simpleOrder)).toBe(true) 72 | 73 | const myOrders = await tokenlon.getOrders(baseQuote) 74 | expect(containSimpleOrder(myOrders, simpleOrder)).toBe(true) 75 | }) 76 | }) 77 | }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import web3Wrapper from './lib/web3-wrapper' 3 | import { createZeroExWrapper } from './lib/zeroex-wrapper' 4 | import zeroExWrapperHelper from './lib/zeroex-wrapper/helper' 5 | import { assert as assertUtils } from '0x.js/lib/src/utils/assert' 6 | import { Server } from './lib/server' 7 | import Tokenlon from './tokenlon' 8 | import { toBN } from './utils/math' 9 | import { lowerCaseObj0xValue } from './utils/helper' 10 | import { assert, rewriteAssertUtils } from './utils/assert' 11 | import { getGasPriceByAdaptorAsync } from './utils/gasPriceAdaptor' 12 | import { GlobalConfig, DexOrderBNToString, Tokenlon as TokenlonInterface } from './types' 13 | import { getPairBySymbol } from './utils/pair' 14 | import { getSimpleOrderWithBaseQuoteBySignedOrder, getSignedOrder, generateDexOrderWithoutSalt, orderBNToString, orderStringToBN } from './utils/dex' 15 | 16 | export const createTokenlon = async (options: GlobalConfig): Promise => { 17 | const config = lowerCaseObj0xValue(options) 18 | // default onChainValidate config is true 19 | config.onChainValidate = config.onChainValidate === false ? config.onChainValidate : true 20 | assert.isValidConfig(config) 21 | 22 | const server = new Server(config.server.url, config.wallet) 23 | const pairList = await server.getPairList() 24 | const tokenlon = new Tokenlon() 25 | const pairs = pairList.filter(p => p.protocol === '0x') 26 | const gasPrice = await getGasPriceByAdaptorAsync(config.gasPriceAdaptor) 27 | 28 | // need to set privider fitst 29 | await web3Wrapper.setProvider(new web3Wrapper.providers.HttpProvider(config.web3.providerUrl)) 30 | 31 | const zeroExConfig = config.zeroEx 32 | zeroExWrapperHelper.setConfig(config) 33 | // Notice: prevent that isSenderAddressAsync get web3.eth.accounts that made assert error 34 | rewriteAssertUtils(assertUtils) 35 | const zeroExWrapper = createZeroExWrapper({ 36 | ...zeroExConfig, 37 | gasPrice: toBN(gasPrice), 38 | }) 39 | 40 | return _.extend(tokenlon, { 41 | _pairs: pairs, 42 | _config: config, 43 | server, 44 | web3Wrapper, 45 | zeroExWrapper, 46 | utils: { 47 | orderBNToString, 48 | orderStringToBN, 49 | getSimpleOrderWithBaseQuoteBySignedOrder(order: DexOrderBNToString) { 50 | return getSimpleOrderWithBaseQuoteBySignedOrder(order, pairs) 51 | }, 52 | async getSignedOrderBySimpleOrderAsync(order: TokenlonInterface.SimpleOrderWithBaseQuote) { 53 | const pair = getPairBySymbol(order, pairs) 54 | const orderWithoutSalt = generateDexOrderWithoutSalt({ 55 | pair, 56 | config, 57 | simpleOrder: order, 58 | }) 59 | const signedOrder = getSignedOrder(orderWithoutSalt, config) 60 | if (config.onChainValidate) { 61 | await zeroExWrapper.exchange.validateOrderFillableOrThrowAsync(signedOrder) 62 | } 63 | return orderBNToString(signedOrder) 64 | }, 65 | }, 66 | }) 67 | } -------------------------------------------------------------------------------- /tests/utils/pair.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { Server } from '../../src/lib/server' 3 | import { getPairBySymbol, getPairByContractAddress, getTokenByName, getPairBySignedOrder } from '../../src/utils/pair' 4 | import { localServerUrl, wallet } from '../__mock__/config' 5 | import { sntWethPairData } from '../__mock__/pair' 6 | import { orders } from '../__mock__/order' 7 | 8 | let pairs = [] 9 | const server = new Server(localServerUrl, wallet) 10 | 11 | beforeAll(async () => { 12 | pairs = await server.getPairList() 13 | return pairs 14 | }) 15 | 16 | describe('getPairBySymbol', () => { 17 | it('SNT-WETH should be found', () => { 18 | const found = getPairBySymbol({ 19 | base: sntWethPairData.base.symbol, 20 | quote: sntWethPairData.quote.symbol, 21 | }, pairs) 22 | expect(_.isEqual(found.base.contractAddress, sntWethPairData.base.contractAddress) && 23 | _.isEqual(found.quote.contractAddress, sntWethPairData.quote.contractAddress)).toBe(true) 24 | }) 25 | 26 | it('snt-WETH should thorw error', () => { 27 | expect(() => { 28 | getPairBySymbol({ 29 | base: sntWethPairData.base.symbol.toLowerCase(), 30 | quote: sntWethPairData.quote.symbol, 31 | }, pairs) 32 | }).toThrow() 33 | }) 34 | 35 | it('WETH-SNT should thorw error', () => { 36 | expect(() => { 37 | getPairBySymbol({ 38 | base: sntWethPairData.quote.symbol, 39 | quote: sntWethPairData.base.symbol, 40 | }, pairs) 41 | }).toThrow() 42 | }) 43 | }) 44 | 45 | describe('getPairByContractAddress', () => { 46 | it('SNT-WETH should be found', () => { 47 | const found = getPairByContractAddress({ 48 | base: sntWethPairData.base.contractAddress, 49 | quote: sntWethPairData.quote.contractAddress, 50 | }, pairs) 51 | expect(_.isEqual(found.base.contractAddress, sntWethPairData.base.contractAddress) && 52 | _.isEqual(found.quote.contractAddress, sntWethPairData.quote.contractAddress)).toBe(true) 53 | }) 54 | 55 | it('WETH-SNT should thorw error', () => { 56 | expect(() => { 57 | getPairByContractAddress({ 58 | base: sntWethPairData.quote.contractAddress, 59 | quote: sntWethPairData.base.contractAddress, 60 | }, pairs) 61 | }).toThrow() 62 | }) 63 | }) 64 | 65 | describe('getTokenByName', () => { 66 | it('SNT should be found', () => { 67 | const found = getTokenByName(sntWethPairData.base.symbol.toUpperCase(), pairs) 68 | expect(_.isEqual(found.contractAddress, sntWethPairData.base.contractAddress)).toBe(true) 69 | }) 70 | 71 | it('snt should thorw error', () => { 72 | expect(() => { 73 | getTokenByName(sntWethPairData.base.symbol.toLowerCase(), pairs) 74 | }).toThrow() 75 | }) 76 | 77 | it('CREDO should thorw error', () => { 78 | expect(() => { 79 | getTokenByName('CREDO', pairs) 80 | }).toThrow() 81 | }) 82 | }) 83 | 84 | describe('getPairBySignedOrder', () => { 85 | it('SNT-WETH pair should be found', () => { 86 | const pair = getPairBySignedOrder(orders[0].signedOrder, pairs) 87 | expect(_.isEqual(pair.base.contractAddress, sntWethPairData.base.contractAddress) && 88 | _.isEqual(pair.quote.contractAddress, sntWethPairData.quote.contractAddress)).toBe(true) 89 | }) 90 | }) -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/__types.ts: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | // waiting 0x.js update to latest version 3 | 4 | import { BigNumber } from '@0xproject/utils' 5 | import * as Web3 from 'web3' 6 | 7 | export interface BalancesByOwner { 8 | [ownerAddress: string]: { 9 | [tokenAddress: string]: BigNumber; 10 | } 11 | } 12 | 13 | export interface SubmissionContractEventArgs { 14 | transactionId: BigNumber 15 | } 16 | 17 | export interface BatchFillOrders { 18 | orderAddresses: string[][] 19 | orderValues: BigNumber[][] 20 | fillTakerTokenAmounts: BigNumber[] 21 | shouldThrowOnInsufficientBalanceOrAllowance: boolean 22 | v: number[] 23 | r: string[] 24 | s: string[] 25 | } 26 | 27 | export interface FillOrdersUpTo { 28 | orderAddresses: string[][] 29 | orderValues: BigNumber[][] 30 | fillTakerTokenAmount: BigNumber 31 | shouldThrowOnInsufficientBalanceOrAllowance: boolean 32 | v: number[] 33 | r: string[] 34 | s: string[] 35 | } 36 | 37 | export interface BatchCancelOrders { 38 | orderAddresses: string[][] 39 | orderValues: BigNumber[][] 40 | cancelTakerTokenAmounts: BigNumber[] 41 | } 42 | 43 | export interface DefaultOrderParams { 44 | exchangeContractAddress: string 45 | maker: string 46 | feeRecipient: string 47 | makerTokenAddress: string 48 | takerTokenAddress: string 49 | makerTokenAmount: BigNumber 50 | takerTokenAmount: BigNumber 51 | makerFee: BigNumber 52 | takerFee: BigNumber 53 | } 54 | 55 | export interface TransactionDataParams { 56 | name: string 57 | abi: Web3.AbiDefinition[] 58 | args: any[] 59 | } 60 | 61 | export interface MultiSigConfig { 62 | owners: string[] 63 | confirmationsRequired: number 64 | secondsRequired: number 65 | } 66 | 67 | export interface MultiSigConfigByNetwork { 68 | [networkName: string]: MultiSigConfig 69 | } 70 | 71 | export interface Token { 72 | address?: string 73 | name: string 74 | symbol: string 75 | decimals: number 76 | ipfsHash: string 77 | swarmHash: string 78 | } 79 | 80 | export interface TokenInfoByNetwork { 81 | development: Token[] 82 | live: Token[] 83 | } 84 | 85 | export enum ExchangeContractErrs { 86 | ERROR_ORDER_EXPIRED, 87 | ERROR_ORDER_FULLY_FILLED_OR_CANCELLED, 88 | ERROR_ROUNDING_ERROR_TOO_LARGE, 89 | ERROR_INSUFFICIENT_BALANCE_OR_ALLOWANCE, 90 | } 91 | 92 | export enum ContractName { 93 | TokenTransferProxy = 'TokenTransferProxy', 94 | TokenRegistry = 'TokenRegistry', 95 | MultiSigWalletWithTimeLock = 'MultiSigWalletWithTimeLock', 96 | Exchange = 'Exchange', 97 | ZRXToken = 'ZRXToken', 98 | DummyToken = 'DummyToken', 99 | EtherToken = 'WETH9', 100 | MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress = 'MultiSigWalletWithTimeLockExceptRemoveAuthorizedAddress', 101 | MaliciousToken = 'MaliciousToken', 102 | } 103 | 104 | export interface Artifact { 105 | contract_name: ContractName 106 | networks: { 107 | [networkId: number]: { 108 | abi: Web3.ContractAbi 109 | solc_version: string 110 | keccak256: string 111 | optimizer_enabled: number 112 | unlinked_binary: string 113 | updated_at: number 114 | address: string 115 | constructor_args: string; 116 | }; 117 | } 118 | } -------------------------------------------------------------------------------- /tests/tokenlon/cancel.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { localConfigUseToFill } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { simpleOrders as testSimpleOrders } from '../__mock__/simpleOrder' 5 | import { createTokenlon } from '../../src/index' 6 | import Tokenlon from '../../src/tokenlon' 7 | import { toBN } from '../../src/utils/math' 8 | import { getTimestamp } from '../../src/utils/helper' 9 | import { waitSeconds } from '../__utils__/wait' 10 | 11 | let tokenlon = null as Tokenlon 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 13 | 14 | // only use 3 test case 15 | const simpleOrders = testSimpleOrders.slice(0, 3) 16 | 17 | beforeAll(async () => { 18 | tokenlon = await createTokenlon(localConfigUseToFill) 19 | }) 20 | 21 | const containSimpleOrder = (orders, simpleOrder) => { 22 | return orders.some(o => { 23 | let expirationUnixTimestampSecCheck = true 24 | if (simpleOrder.expirationUnixTimestampSec) { 25 | expirationUnixTimestampSecCheck = _.isEqual(+o.expirationUnixTimestampSec, simpleOrder.expirationUnixTimestampSec) 26 | } 27 | return _.isEqual(o.side, simpleOrder.side) && 28 | _.isEqual(o.price, simpleOrder.price) && 29 | _.isEqual(o.amount, simpleOrder.amount) && 30 | _.isEqual(JSON.parse(o.rawOrder), JSON.parse(simpleOrder.rawOrder)) && 31 | expirationUnixTimestampSecCheck 32 | }) 33 | } 34 | 35 | describe('test cancelOrder / cancelOrders', () => { 36 | const baseQuote = { 37 | base: sntWethPairData.base.symbol, 38 | quote: sntWethPairData.quote.symbol, 39 | } 40 | it('test cancelOrder / cancelOrders', async () => { 41 | const placedOrders = [] 42 | for (let simpleOrder of simpleOrders) { 43 | const placed = await tokenlon.placeOrder({ 44 | ...baseQuote, 45 | ...simpleOrder, 46 | }) 47 | expect(JSON.parse(placed.rawOrder).exchangeContractAddress).toEqual(localConfigUseToFill.zeroEx.exchangeContractAddress) 48 | expect(placed.isMaker).toBe(true) 49 | placedOrders.push(placed) 50 | await waitSeconds(2) 51 | } 52 | 53 | const orderBook1 = await tokenlon.getOrderBook(baseQuote) 54 | await waitSeconds(5) 55 | const myOrders1 = await tokenlon.getOrders(baseQuote) 56 | 57 | // check placed 58 | for (let placed of placedOrders) { 59 | const orderBookOrders = orderBook1[placed.side === 'BUY' ? 'bids' : 'asks'] 60 | expect(containSimpleOrder(orderBookOrders, placed)).toBe(true) 61 | expect(containSimpleOrder(myOrders1, placed)).toBe(true) 62 | } 63 | 64 | await tokenlon.cancelOrder(placedOrders[0].rawOrder) 65 | await waitSeconds(2) 66 | await tokenlon.batchCancelOrders(placedOrders.slice(1).map(x => x.rawOrder)) 67 | await waitSeconds(2) 68 | 69 | const orderBook2 = await tokenlon.getOrderBook(baseQuote) 70 | await waitSeconds(2) 71 | const myOrders2 = await tokenlon.getOrders(baseQuote) 72 | 73 | // check cancelled 74 | for (let placed of placedOrders) { 75 | const orderBookOrders = orderBook2[placed.side === 'BUY' ? 'bids' : 'asks'] 76 | expect(containSimpleOrder(orderBookOrders, placed)).toBe(false) 77 | expect(containSimpleOrder(myOrders2, placed)).toBe(false) 78 | } 79 | }) 80 | 81 | // TODO onchain 82 | // check order fillable 83 | }) -------------------------------------------------------------------------------- /tests/utils/math.test.ts: -------------------------------------------------------------------------------- 1 | import { isBigNumber, isNumberLike, toBN, toFixed } from '../../src/utils/math' 2 | import { BigNumber } from '@0xproject/utils' 3 | import * as _ from 'lodash' 4 | 5 | describe('test isNumberLike', () => { 6 | const testData = [{ 7 | value: 1, 8 | result: true, 9 | }, { 10 | value: Infinity, 11 | result: false, 12 | }, { 13 | value: {}, 14 | result: false, 15 | }, { 16 | value: ' ', 17 | result: false, 18 | }, { 19 | value: undefined, 20 | result: false, 21 | }, { 22 | value: null, 23 | result: false, 24 | }, { 25 | value: NaN, 26 | result: false, 27 | }, { 28 | value: 'a', 29 | result: false, 30 | }] 31 | 32 | testData.forEach(data => { 33 | it(`isNumberLike(${data.value}) => ${data.result}`, () => { 34 | expect(isNumberLike(data.value)).toEqual(data.result) 35 | }) 36 | }) 37 | }) 38 | 39 | describe('test isBigNumber', () => { 40 | const testData = [{ 41 | value: 1, 42 | result: false, 43 | }, { 44 | value: Infinity, 45 | result: false, 46 | }, { 47 | value: {}, 48 | result: false, 49 | }, { 50 | value: ' ', 51 | result: false, 52 | }, { 53 | value: undefined, 54 | result: false, 55 | }, { 56 | value: null, 57 | result: false, 58 | }, { 59 | value: NaN, 60 | result: false, 61 | }, { 62 | value: 'a', 63 | result: false, 64 | }] 65 | 66 | testData.forEach(data => { 67 | it(`isBigNumber(${data.value}) => ${data.result}`, () => { 68 | expect(isBigNumber(data.value)).toEqual(data.result) 69 | }) 70 | }) 71 | 72 | testData.filter(item => (_.isString(item.value) && +item.value) || _.isNumber(item.value)).forEach(data => { 73 | const v = new BigNumber(data.value as string | number) 74 | const r = !data.result 75 | it(`isBigNumber(${v}) => ${r}`, () => { 76 | expect(isBigNumber(v)).toEqual(r) 77 | }) 78 | }) 79 | }) 80 | 81 | describe('test toBN', () => { 82 | const testData = [{ 83 | value: 10.012312313, 84 | result: '10.012312313', 85 | }, { 86 | value: Infinity, 87 | result: '0', 88 | }, { 89 | value: '-10.012312313', 90 | result: '-10.012312313', 91 | }, { 92 | value: '0xaa', 93 | result: '170', 94 | }, { 95 | value: '-0xaa', 96 | result: '-170', 97 | }] 98 | 99 | testData.forEach(data => { 100 | it(`toBN(${data.value}).toString() => ${data.result}`, () => { 101 | expect(toBN(data.value).toString()).toEqual(data.result) 102 | }) 103 | }) 104 | }) 105 | 106 | // TODO more test case for toFixed rm params 107 | describe('test toFixed', () => { 108 | const testData = [{ 109 | value: 10.012312313, 110 | dp: 4, 111 | rm: 4, 112 | result: '10.0123', 113 | }, { 114 | value: Infinity, 115 | dp: 4, 116 | result: '0.0000', 117 | }, { 118 | value: 10.012312313, 119 | dp: 4, 120 | result: '10.0123', 121 | }, { 122 | value: '-0xaa', 123 | dp: 8, 124 | result: '-170.00000000', 125 | }] 126 | 127 | testData.forEach(data => { 128 | it(`toFixed(${data.value}) => ${data.result}`, () => { 129 | expect(toFixed(data.value, data.dp, data.rm)).toEqual(data.result) 130 | }) 131 | }) 132 | }) -------------------------------------------------------------------------------- /src/types/server.ts: -------------------------------------------------------------------------------- 1 | import { DexOrderBNToString } from './base' 2 | 3 | export namespace Server { 4 | export type Transformer = { 5 | (data: any): any; 6 | } 7 | 8 | export type RequestParams = { 9 | [propName: string]: any; 10 | } 11 | 12 | export type RequestConfig = { 13 | url: string 14 | method: string 15 | baseURL?: string 16 | transformRequest?: Transformer | Transformer[] 17 | transformResponse?: Transformer | Transformer[] 18 | headers?: any 19 | params?: any 20 | paramsSerializer?: (params: any) => string 21 | data?: any 22 | timeout?: number 23 | withCredentials?: boolean 24 | responseType?: string 25 | xsrfCookieName?: string 26 | xsrfHeaderName?: string 27 | onUploadProgress?: (progressEvent: any) => void 28 | onDownloadProgress?: (progressEvent: any) => void 29 | maxContentLength?: number 30 | validateStatus?: (status: number) => boolean; 31 | } 32 | 33 | export type tradeType = 'ask' | 'bid' 34 | 35 | export type GetTokenParams = { 36 | timestamp: number 37 | signature: string; 38 | } 39 | 40 | export type GetOrderBookParams = { 41 | baseTokenAddress: string 42 | quoteTokenAddress: string; 43 | } 44 | 45 | export type OrderBookItem = { 46 | rate: number 47 | tradeType?: tradeType 48 | amountRemaining: string 49 | payload: DexOrderBNToString; 50 | } 51 | 52 | export type OrderBookResult = { 53 | bids: OrderBookItem[] 54 | asks: OrderBookItem[]; 55 | } 56 | 57 | export type CancelOrderItem = { 58 | orderHash: string 59 | txHash: string; 60 | } 61 | 62 | export type FillOrderItem = { 63 | order: DexOrderBNToString 64 | amount: string; 65 | } 66 | export interface FillOrderParams extends FillOrderItem { 67 | txHash: string 68 | } 69 | 70 | export type BatchFillOrdersParams = { 71 | txHash: string 72 | orders: FillOrderItem[]; 73 | } 74 | 75 | export type GetOrdersParams = { 76 | maker: string 77 | page?: number 78 | perpage?: number 79 | tokenPair?: string[]; 80 | } 81 | 82 | export type GetTradesParams = { 83 | timeRange: number[] 84 | baseTokenAddress: string 85 | quoteTokenAddress: string 86 | page: number 87 | perpage: number; 88 | } 89 | 90 | export interface MakerTradesParams extends GetTradesParams { 91 | maker: string 92 | } 93 | 94 | export interface TakerTradesParams extends GetTradesParams { 95 | taker: string 96 | } 97 | 98 | export type TradesDetailItem = { 99 | id: number 100 | price: number 101 | amount: number 102 | timestamp: number; 103 | } 104 | 105 | export interface MakerTradesDetailItem extends TradesDetailItem { 106 | txHash: string 107 | } 108 | 109 | export interface MakerTradesItem extends TradesDetailItem { 110 | tradeType: tradeType 111 | amountRemaining: number 112 | expirationUnixTimestampSec: string 113 | payload: DexOrderBNToString 114 | trades: MakerTradesDetailItem[] 115 | } 116 | 117 | export interface TakerTradesItem extends TradesDetailItem { 118 | tradeType: tradeType 119 | payload: DexOrderBNToString 120 | txHash: string 121 | } 122 | 123 | export interface OrderDetail extends OrderBookItem { 124 | trades: MakerTradesDetailItem[] 125 | } 126 | } -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "member-access": false, 4 | "no-any": false, 5 | "no-inferrable-types": [ 6 | false 7 | ], 8 | "no-internal-module": true, 9 | "no-var-requires": true, 10 | "typedef": [ 11 | false 12 | ], 13 | "typedef-whitespace": [ 14 | true, 15 | { 16 | "call-signature": "nospace", 17 | "index-signature": "nospace", 18 | "parameter": "nospace", 19 | "property-declaration": "nospace", 20 | "variable-declaration": "nospace" 21 | }, 22 | { 23 | "call-signature": "space", 24 | "index-signature": "space", 25 | "parameter": "space", 26 | "property-declaration": "space", 27 | "variable-declaration": "space" 28 | } 29 | ], 30 | "ban": false, 31 | "curly": false, 32 | "forin": false, 33 | "label-position": true, 34 | "no-arg": true, 35 | "no-bitwise": true, 36 | "no-conditional-assignment": true, 37 | "no-console": [ 38 | true, 39 | "debug", 40 | "info", 41 | "time", 42 | "timeEnd", 43 | "trace" 44 | ], 45 | "no-construct": true, 46 | "no-debugger": true, 47 | "no-duplicate-variable": true, 48 | "no-empty": false, 49 | "no-eval": true, 50 | "no-null-keyword": false, 51 | "no-shadowed-variable": false, 52 | "no-string-literal": true, 53 | "no-switch-case-fall-through": true, 54 | "no-unused-expression": [ 55 | true, 56 | "allow-fast-null-checks" 57 | ], 58 | "no-use-before-declare": true, 59 | "no-var-keyword": true, 60 | "radix": true, 61 | "switch-default": true, 62 | "triple-equals": [ 63 | true, 64 | "allow-undefined-check" 65 | ], 66 | "eofline": false, 67 | "indent": [ 68 | true, 69 | "spaces" 70 | ], 71 | "max-line-length": [ 72 | false, 73 | 150 74 | ], 75 | "no-require-imports": false, 76 | "no-trailing-whitespace": true, 77 | "object-literal-sort-keys": false, 78 | "trailing-comma": [ 79 | true, 80 | { 81 | "multiline": "always", 82 | "singleline": "never" 83 | } 84 | ], 85 | "align": [ 86 | true 87 | ], 88 | "class-name": true, 89 | "comment-format": [ 90 | true, 91 | "check-space" 92 | ], 93 | "interface-name": [ 94 | false 95 | ], 96 | "jsdoc-format": false, 97 | "no-consecutive-blank-lines": [ 98 | true 99 | ], 100 | "no-parameter-properties": false, 101 | "one-line": [ 102 | true, 103 | "check-open-brace", 104 | "check-catch", 105 | "check-else", 106 | "check-finally", 107 | "check-whitespace" 108 | ], 109 | "quotemark": [ 110 | true, 111 | "single", 112 | "jsx-double", 113 | "avoid-escape" 114 | ], 115 | "semicolon": [ 116 | true, 117 | "never" 118 | ], 119 | "variable-name": [ 120 | true, 121 | "check-format", 122 | "allow-pascal-case", 123 | "allow-leading-underscore", 124 | "ban-keywords" 125 | ], 126 | "whitespace": [ 127 | true, 128 | "check-branch", 129 | "check-decl", 130 | "check-operator", 131 | "check-separator", 132 | "check-type" 133 | ] 134 | } 135 | } -------------------------------------------------------------------------------- /src/lib/server/_request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import * as _ from 'lodash' 3 | import { REQUEST_TIMEOUT } from '../../constants' 4 | import { Server } from '../../types' 5 | 6 | // `validateStatus` defines whether to resolve or reject the promise for a given 7 | // HTTP response status code. If `validateStatus` returns `true` (or is set to `null` 8 | // or `undefined`), the promise will be resolved; otherwise, the promise will be rejected. 9 | const validateStatus = function (status: number): boolean { 10 | return status >= 200 && status < 300 // default 11 | } 12 | 13 | const getHeaders = () => { 14 | return { 15 | 'Content-Type': 'application/json', 16 | } 17 | } 18 | 19 | const newError = (message, url: string) => { 20 | if (_.isObject(message) && message.message) { 21 | const error = message 22 | if (_.isObject(error.response) && _.isObject(error.response.data)) { 23 | if (error.response.data.error) { 24 | message = error.response.data.error.message 25 | } 26 | } else { 27 | message = `${url}: ${message.message}` 28 | } 29 | } else { 30 | message = `${url}: ${message}` 31 | } 32 | const error = new Error(message) 33 | error.message = message 34 | error.toString = () => message 35 | return error 36 | } 37 | 38 | // TODO do something with request error 39 | const handleError = function (error) { 40 | if (error.response) { 41 | // The request was made and the server responded with a status code 42 | // that falls out of the range of 2xx 43 | // console.log(error.response.data) 44 | // console.log(error.response.status) 45 | // console.log(error.response.headers) 46 | } else if (error.request) { 47 | // The request was made but no response was received 48 | // `error.request` is an instance of XMLHttpRequest in the browser and an instance of 49 | // http.ClientRequest in node.js 50 | // console.log(error.request) 51 | } else { 52 | // Something happened in setting up the request that triggered an Error 53 | // console.log('Error', error.message) 54 | } 55 | } 56 | 57 | // TODO add debounceTime 58 | const sendRequest = (config: Server.RequestConfig) => { 59 | const rConfig = { validateStatus, timeout: REQUEST_TIMEOUT, ...config } 60 | return new Promise((resolve, reject) => { 61 | axios(rConfig).then(res => { 62 | if (res.data) { 63 | resolve(res.data) 64 | } else { 65 | reject(newError('null response', config.url)) 66 | } 67 | }).catch(error => { 68 | handleError(error) 69 | reject(newError(error, config.url)) 70 | }) 71 | }) as Promise<{ error: object, result: any }> 72 | } 73 | 74 | export const jsonrpc = { 75 | get(url, header = {}, method, params, timeout: number = REQUEST_TIMEOUT) { 76 | const headers = { 77 | ...getHeaders(), 78 | ...header, 79 | } 80 | const data = { 81 | jsonrpc: '2.0', 82 | id: 1, 83 | method, 84 | params, 85 | } 86 | return sendRequest({ method: 'post', url, data, timeout, headers }).then(data => { 87 | if (data.error) { 88 | throw newError(data.error, url) 89 | } 90 | 91 | if (_.isUndefined(data.result)) { 92 | throw newError('server result is undefined', url) 93 | } 94 | return data.result 95 | }).catch(err => { 96 | throw err 97 | }) 98 | }, 99 | } 100 | -------------------------------------------------------------------------------- /tests/tokenlon/trades.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { ZeroEx } from '0x.js' 3 | import { localConfig, web3ProviderUrl, walletUseToFill } from '../__mock__/config' 4 | import { sntWethPairData } from '../__mock__/pair' 5 | import { createTokenlon } from '../../src/index' 6 | import { orderStringToBN } from '../../src/utils/dex' 7 | import Tokenlon from '../../src/tokenlon' 8 | import Web3 from 'web3' 9 | import web3 from '../../src/lib/web3-wrapper' 10 | import { orders } from '../__mock__/order' 11 | import { simpleOrders } from '../__mock__/simpleOrder' 12 | import { waitSeconds, waitMined } from '../__utils__/wait' 13 | import { getTimestamp } from '../../src/utils/helper' 14 | 15 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 200000 16 | 17 | let tokenlon = null as Tokenlon 18 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 19 | 20 | beforeAll(async () => { 21 | tokenlon = await createTokenlon(localConfig) 22 | }) 23 | 24 | const isSameOrder = (ob1, ob2) => { 25 | return ['price', 'amount', 'expirationUnixTimestampSec', 'amount', 'amountTotal'].every(key => { 26 | return ob1[key] === ob2[key] 27 | }) && _.isEqual(JSON.parse(ob1.rawOrder), JSON.parse(ob2.rawOrder)) 28 | } 29 | 30 | describe('test placeOrder / getOrders / getMakerTrades / getTakerTrades / getOrder', () => { 31 | const baseQuote = { 32 | base: sntWethPairData.base.symbol, 33 | quote: sntWethPairData.quote.symbol, 34 | } 35 | const simpleOrder = simpleOrders[0] 36 | 37 | it(`${simpleOrder.side} - ${simpleOrder.amount} - ${simpleOrder.price} test placeOrder / getOrderBook / getOrders`, async () => { 38 | const obItem = await tokenlon.placeOrder({ 39 | ...baseQuote, 40 | ...simpleOrder, 41 | expirationUnixTimestampSec: getTimestamp() + 10 * 60, 42 | }) 43 | await waitSeconds(3) 44 | const myOrders = await tokenlon.getOrders(baseQuote) 45 | 46 | expect(myOrders.some(o => { 47 | return isSameOrder(o, obItem) 48 | })).toBe(true) 49 | 50 | await waitSeconds(3) 51 | const orderDetail = await tokenlon.getOrder(obItem.rawOrder) 52 | expect(isSameOrder(orderDetail, obItem)) 53 | expect(orderDetail.amountTotal).toEqual(orderDetail.amount) 54 | 55 | const toFillHalfBaseAmount = orderDetail.amount / 2 56 | 57 | // fill this order and check maker / taker / getOrder trades 58 | const txHash = await tokenlon.fillOrder({ 59 | ...baseQuote, 60 | ...obItem, 61 | side: obItem.side === 'BUY' ? 'SELL' : 'BUY', 62 | amount: toFillHalfBaseAmount, 63 | }) 64 | 65 | await waitMined(txHash, 60) 66 | await waitSeconds(60) 67 | 68 | const orderDetailAfterFill = await tokenlon.getOrder(obItem.rawOrder) 69 | 70 | // test amountTotal 71 | expect(orderDetailAfterFill.amount + toFillHalfBaseAmount).toEqual(obItem.amountTotal) 72 | 73 | const makerTrades = await tokenlon.getMakerTrades({ ...baseQuote, page: 1, perpage: 30 }) 74 | const makerTrade = makerTrades.find(t => t.rawOrder === orderDetailAfterFill.rawOrder) 75 | expect(makerTrade).toBeTruthy() 76 | 77 | const mts = makerTrade.trades 78 | // test maker trades txHash 79 | expect(mts[0].txHash).toEqual(txHash) 80 | 81 | const takerTrades = await tokenlon.getTakerTrades({ ...baseQuote, page: 1, perpage: 30 }) 82 | const takerTrade = takerTrades.find(t => t.rawOrder === orderDetailAfterFill.rawOrder) 83 | // test taker trades txHash 84 | expect(takerTrade.txHash).toEqual(txHash); 85 | 86 | // maker / taker trades detail 87 | ['id', 'price', 'amount', 'timestamp'].forEach(key => { 88 | expect(mts[0][key]).toEqual(takerTrade[key]) 89 | }) 90 | 91 | // test trade item amount 92 | expect(+takerTrade.amount).toEqual(toFillHalfBaseAmount) 93 | }) 94 | }) -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/helper.ts: -------------------------------------------------------------------------------- 1 | import { encodeData, sendTransaction } from '../../utils/ethereum' 2 | import { OrderTransactionOpts } from '0x.js' 3 | import { BigNumber } from '@0xproject/utils' 4 | import { GlobalConfig, Tokenlon } from '../../types' 5 | import { TransactionOpts } from '0x.js' 6 | import { getGasPriceByAdaptorAsync } from '../../utils/gasPriceAdaptor' 7 | 8 | export default { 9 | _config: {} as GlobalConfig, 10 | setConfig(config: GlobalConfig) { 11 | this._config = config 12 | }, 13 | async _getGasLimitAndGasPriceAsync(opts?: TransactionOpts): Promise { 14 | const { zeroEx, gasPriceAdaptor } = this._config 15 | const { gasLimit } = zeroEx 16 | let gasP = null 17 | 18 | if (opts && opts.gasPrice) { 19 | gasP = opts.gasPrice.toNumber() 20 | } else { 21 | gasP = await getGasPriceByAdaptorAsync(gasPriceAdaptor) 22 | } 23 | 24 | return { 25 | gasPrice: gasP, 26 | gasLimit: opts && opts.gasLimit ? opts.gasLimit : gasLimit, 27 | } 28 | }, 29 | async exchangeSendTransaction(method: string, args: any[], orderTransactionOpts?: OrderTransactionOpts) { 30 | const { wallet, zeroEx } = this._config 31 | const { address, privateKey } = wallet 32 | const { exchangeContractAddress } = zeroEx 33 | const { gasPrice, gasLimit } = await this._getGasLimitAndGasPriceAsync(orderTransactionOpts) 34 | 35 | return sendTransaction({ 36 | address, 37 | privateKey, 38 | gasLimit, 39 | gasPrice, 40 | to: exchangeContractAddress, 41 | value: 0, 42 | data: encodeData('exchange', method, args), 43 | }) 44 | }, 45 | async _tokenTransaction(to: string, method: string, args: any[], opts?: TransactionOpts) { 46 | const { wallet } = this._config 47 | const { address, privateKey } = wallet 48 | const { gasPrice, gasLimit } = await this._getGasLimitAndGasPriceAsync(opts) 49 | 50 | return sendTransaction({ 51 | address, 52 | privateKey, 53 | gasLimit, 54 | gasPrice, 55 | to, 56 | value: 0, 57 | data: encodeData('token', method, args), 58 | }) 59 | }, 60 | async etherTokenTransaction(method: string, amountInBaseUnits: BigNumber, opts?: TransactionOpts) { 61 | const { wallet, zeroEx } = this._config 62 | const { address, privateKey } = wallet 63 | const { etherTokenContractAddress } = zeroEx 64 | const { gasPrice, gasLimit } = await this._getGasLimitAndGasPriceAsync(opts) 65 | 66 | return sendTransaction({ 67 | address, 68 | privateKey, 69 | gasLimit, 70 | gasPrice, 71 | to: etherTokenContractAddress, 72 | value: method === 'deposit' ? amountInBaseUnits.toNumber() : 0, 73 | data: encodeData('etherToken', method, [amountInBaseUnits.toString()]), 74 | }) 75 | }, 76 | tokenApproveTransaction(normalizedTokenAddress, normalizedSpenderAddress, amountInBaseUnits, opts?: TransactionOpts) { 77 | return this._tokenTransaction(normalizedTokenAddress, 'approve', [ 78 | normalizedSpenderAddress, 79 | amountInBaseUnits, 80 | ], opts) 81 | }, 82 | tokenTransferTransaction(normalizedTokenAddress, normalizedSpenderAddress, amountInBaseUnits, opts?: TransactionOpts) { 83 | return this._tokenTransaction(normalizedTokenAddress, 'transfer', [ 84 | normalizedSpenderAddress, 85 | amountInBaseUnits, 86 | ], opts) 87 | }, 88 | tokenTransferFromTransaction( 89 | normalizedTokenAddress, 90 | normalizedFromAddress, 91 | normalizedToAddress, 92 | amountInBaseUnits, 93 | opts?: TransactionOpts, 94 | ) { 95 | return this._tokenTransaction(normalizedTokenAddress, 'transferFrom', [ 96 | normalizedFromAddress, 97 | normalizedToAddress, 98 | amountInBaseUnits, 99 | ], opts) 100 | }, 101 | } -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/__formatters.ts: -------------------------------------------------------------------------------- 1 | // TODO remove 2 | // waiting 0x.js update to latest version 3 | // then we can directly use https://github.com/0xProject/0x.js/blob/7aa070f9eaef734274df6e6eaa4590fe30d52899/packages/contracts/util/formatters.ts 4 | 5 | import { SignedOrder } from '0x.js' 6 | import { BigNumber } from '@0xproject/utils' 7 | import * as _ from 'lodash' 8 | 9 | import { BatchCancelOrders, BatchFillOrders, FillOrdersUpTo } from './__types' 10 | 11 | export const formatters = { 12 | createBatchFill( 13 | signedOrders: SignedOrder[], 14 | shouldThrowOnInsufficientBalanceOrAllowance: boolean, 15 | fillTakerTokenAmounts: BigNumber[] = [], 16 | ) { 17 | const batchFill: BatchFillOrders = { 18 | orderAddresses: [], 19 | orderValues: [], 20 | fillTakerTokenAmounts, 21 | shouldThrowOnInsufficientBalanceOrAllowance, 22 | v: [], 23 | r: [], 24 | s: [], 25 | } 26 | _.forEach(signedOrders, signedOrder => { 27 | batchFill.orderAddresses.push([ 28 | signedOrder.maker, 29 | signedOrder.taker, 30 | signedOrder.makerTokenAddress, 31 | signedOrder.takerTokenAddress, 32 | signedOrder.feeRecipient, 33 | ]) 34 | batchFill.orderValues.push([ 35 | signedOrder.makerTokenAmount, 36 | signedOrder.takerTokenAmount, 37 | signedOrder.makerFee, 38 | signedOrder.takerFee, 39 | signedOrder.expirationUnixTimestampSec, 40 | signedOrder.salt, 41 | ]) 42 | batchFill.v.push(signedOrder.ecSignature.v) 43 | batchFill.r.push(signedOrder.ecSignature.r) 44 | batchFill.s.push(signedOrder.ecSignature.s) 45 | if (fillTakerTokenAmounts.length < signedOrders.length) { 46 | batchFill.fillTakerTokenAmounts.push(signedOrder.takerTokenAmount) 47 | } 48 | }) 49 | return batchFill 50 | }, 51 | createFillUpTo( 52 | signedOrders: SignedOrder[], 53 | shouldThrowOnInsufficientBalanceOrAllowance: boolean, 54 | fillTakerTokenAmount: BigNumber, 55 | ) { 56 | const fillUpTo: FillOrdersUpTo = { 57 | orderAddresses: [], 58 | orderValues: [], 59 | fillTakerTokenAmount, 60 | shouldThrowOnInsufficientBalanceOrAllowance, 61 | v: [], 62 | r: [], 63 | s: [], 64 | } 65 | signedOrders.forEach(signedOrder => { 66 | fillUpTo.orderAddresses.push([ 67 | signedOrder.maker, 68 | signedOrder.taker, 69 | signedOrder.makerTokenAddress, 70 | signedOrder.takerTokenAddress, 71 | signedOrder.feeRecipient, 72 | ]) 73 | fillUpTo.orderValues.push([ 74 | signedOrder.makerTokenAmount, 75 | signedOrder.takerTokenAmount, 76 | signedOrder.makerFee, 77 | signedOrder.takerFee, 78 | signedOrder.expirationUnixTimestampSec, 79 | signedOrder.salt, 80 | ]) 81 | fillUpTo.v.push(signedOrder.ecSignature.v) 82 | fillUpTo.r.push(signedOrder.ecSignature.r) 83 | fillUpTo.s.push(signedOrder.ecSignature.s) 84 | }) 85 | return fillUpTo 86 | }, 87 | createBatchCancel(signedOrders: SignedOrder[], cancelTakerTokenAmounts: BigNumber[] = []) { 88 | const batchCancel: BatchCancelOrders = { 89 | orderAddresses: [], 90 | orderValues: [], 91 | cancelTakerTokenAmounts, 92 | } 93 | signedOrders.forEach(signedOrder => { 94 | batchCancel.orderAddresses.push([ 95 | signedOrder.maker, 96 | signedOrder.taker, 97 | signedOrder.makerTokenAddress, 98 | signedOrder.takerTokenAddress, 99 | signedOrder.feeRecipient, 100 | ]) 101 | batchCancel.orderValues.push([ 102 | signedOrder.makerTokenAmount, 103 | signedOrder.takerTokenAmount, 104 | signedOrder.makerFee, 105 | signedOrder.takerFee, 106 | signedOrder.expirationUnixTimestampSec, 107 | signedOrder.salt, 108 | ]) 109 | if (cancelTakerTokenAmounts.length < signedOrders.length) { 110 | batchCancel.cancelTakerTokenAmounts.push(signedOrder.takerTokenAmount) 111 | } 112 | }) 113 | return batchCancel 114 | }, 115 | } -------------------------------------------------------------------------------- /src/utils/ethereum.ts: -------------------------------------------------------------------------------- 1 | import * as ethAbi from 'ethereumjs-abi' 2 | import * as ethUtil from 'ethereumjs-util' 3 | import * as Tx from 'ethereumjs-tx' 4 | import * as _ from 'lodash' 5 | import web3 from '../lib/web3-wrapper' 6 | import { getAbiInputTypes } from './abi' 7 | import { isBigNumber, toBN } from './math' 8 | import { Ethereum } from '../types' 9 | import { BigNumber } from '@0xproject/utils' 10 | 11 | // Executes a message call or transaction, which is directly executed in the VM of the node, 12 | // but never mined into the blockchain and returns the amount of the gas used. 13 | export const getEstimateGas = (tx): Promise => { 14 | return new Promise((resolve, reject) => { 15 | // console.log(`[web3 req] estimateGas params: ${JSON.stringify(tx)}`) 16 | web3.eth.estimateGas(tx, (err, gas) => { 17 | if (!err) { 18 | resolve(gas) 19 | } else { 20 | reject(err) 21 | } 22 | }) 23 | }) 24 | } 25 | 26 | // Get the numbers of transactions sent from this address. 27 | export const getNonce = (address) => { 28 | return new Promise((resolve, reject) => { 29 | // console.log(`[web3 req] getTransactionCount params: ${address}`) 30 | web3.eth.getTransactionCount(address, (err, nonce) => { 31 | if (!err) { 32 | // console.log(`[web3 res] getTransactionCount: ${nonce}`) 33 | resolve(nonce) 34 | } else { 35 | reject(err) 36 | } 37 | }) 38 | }) 39 | } 40 | 41 | const formatArgs = (args: any[]) => { 42 | return args.map(item => { 43 | if (_.isArray(item)) return formatArgs(item) 44 | if (isBigNumber(item)) return item.toString() 45 | return item 46 | }) 47 | } 48 | 49 | /** 50 | * @params.contractName 51 | * @params.method contract method name 52 | * @params.args contract method arguments 53 | * @description 54 | * generate data 的 三种实现方式 55 | * 1. 两年前的 https://github.com/ethereum/solidity.js (未实验) 56 | * 2. web3.js lib/solidity/coder (拆不出来) 57 | * https://github.com/ethereum/web3.js/blob/6d3e61a010501011a107a79574cc7516900fa9e4/lib/solidity/coder.js 58 | * https://github.com/ethereum/web3.js/blob/db6efd5f2309f9aeab6283383b3f4a3d1dcb7177/lib/web3/function.js#L92 59 | * https://github.com/ethereum/web3.js/blob/db6efd5f2309f9aeab6283383b3f4a3d1dcb7177/lib/web3.js#L107 60 | * 3. ethereumjs-abi https://github.com/ethereumjs/ethereumjs-abi/blob/master/lib/index.js 61 | */ 62 | export const encodeData = (contractName: string, method: string, args: any[]): string => { 63 | const types = getAbiInputTypes(contractName, method) 64 | const signatureBuffer = ethAbi.methodID(method, types) 65 | const mainBuffer = ethAbi.rawEncode(types, formatArgs(args)) 66 | const data = ethUtil.bufferToHex(Buffer.concat([signatureBuffer, mainBuffer])) 67 | return data 68 | } 69 | 70 | export const getTokenBalance = ({ address, contractAddress }): Promise => { 71 | return new Promise((resolve, reject) => { 72 | web3.eth.call({ 73 | to: contractAddress, 74 | data: encodeData('token', 'balanceOf', [address]), 75 | }, (err, res) => { 76 | if (err) { 77 | return reject(err) 78 | } 79 | resolve(toBN(res)) 80 | }) 81 | }) 82 | } 83 | 84 | // use ethereumjs-tx and web3.eth.sendRawTransaction to send transaction by privateKey 85 | // value must be a decimal processed number 86 | export const sendTransaction = async (params: Ethereum.SendTransactionParams): Promise => { 87 | const { address, privateKey, gasPrice, to, value, gasLimit, data } = params 88 | const nonce = await getNonce(address) 89 | let estimateGas = 0 90 | try { 91 | estimateGas = await getEstimateGas({ 92 | from: address, 93 | to, 94 | value: web3.toHex(value), 95 | data, 96 | }) 97 | } catch (_e) {} 98 | 99 | const privateKeyBuffer = new Buffer(privateKey, 'hex') 100 | const gas = estimateGas && gasLimit ? Math.max(estimateGas, gasLimit) : (estimateGas || gasLimit) 101 | const rawTx = { 102 | to, 103 | data: data || '', 104 | nonce: web3.toHex(nonce), 105 | gasPrice: web3.toHex(gasPrice), 106 | gasLimit: web3.toHex(gas), 107 | value: web3.toHex(value), 108 | } 109 | const tx = new (Tx.default ? Tx.default : Tx)(rawTx) 110 | tx.sign(privateKeyBuffer) 111 | const serializedTx = tx.serialize() 112 | 113 | return new Promise((resolve, reject) => { 114 | web3.eth.sendRawTransaction('0x' + serializedTx.toString('hex'), (err, txHash: string) => { 115 | if (!err) { 116 | resolve(txHash) 117 | } else { 118 | reject(err) 119 | } 120 | }) 121 | }) 122 | } -------------------------------------------------------------------------------- /src/utils/assert.ts: -------------------------------------------------------------------------------- 1 | import * as ethUtil from 'ethereumjs-util' 2 | import { assert as sharedAssert } from '@0xproject/assert' 3 | import { toBN } from './math' 4 | import * as _ from 'lodash' 5 | import { SimpleOrder, Pair, Tokenlon, GlobalConfig, TokenlonError } from '../types' 6 | import { Web3Wrapper } from '@0xproject/web3-wrapper' 7 | import { helpCompareStr, newError, getTimestamp } from './helper' 8 | 9 | export const assert = { 10 | isValidSide(value: string) { 11 | sharedAssert.assert(value === 'BUY' || value === 'SELL', `side ${value} must be one of BUY or SELL`) 12 | }, 13 | isValidPrecision(variableName: string, value: number, precision: number) { 14 | const formatedNum = toBN(value).toString() 15 | const pre = formatedNum.split('.')[1] 16 | precision = precision || 8 17 | sharedAssert.assert(_.isUndefined(pre) || pre.length <= precision, `${variableName} ${value} must match precision ${precision}`) 18 | }, 19 | isValidExpirationUnixTimestampSec(value?: number) { 20 | sharedAssert.assert(_.isUndefined(value) || (getTimestamp() < +value), `expirationUnixTimestampSec ${value} must after the current time`) 21 | }, 22 | isValidAmount(order: SimpleOrder, quoteMinUnit: number | string) { 23 | const { price, amount } = order 24 | sharedAssert.assert(price * amount >= (+quoteMinUnit ? +quoteMinUnit : 0.0001), `Total amount must larger then or be equal with quoteMinUnit ${quoteMinUnit}`) 25 | }, 26 | isValidSimpleOrder(order: SimpleOrder, precision: number) { 27 | const { side, expirationUnixTimestampSec } = order 28 | this.isValidSide(side) 29 | this.isValidExpirationUnixTimestampSec(expirationUnixTimestampSec); 30 | ['amount', 'price'].forEach(key => { 31 | sharedAssert.isNumber(key, order[key]) 32 | this.isValidPrecision(key, order[key], precision) 33 | }) 34 | }, 35 | isValidrawOrder(rawOrder: string) { 36 | try { 37 | const o = JSON.parse(rawOrder) 38 | if (!_.isPlainObject(o)) { 39 | throw newError(`rawOrder ${rawOrder} must be a JSON Object`) 40 | } 41 | } catch (e) { 42 | throw newError(`rawOrder ${rawOrder} must be a JSON Object`) 43 | } 44 | }, 45 | isValidTokenNameString(variableName: string, value: string) { 46 | sharedAssert.assert(_.isString(value), `${variableName} ${value} must be a string`) 47 | sharedAssert.assert(!!value.trim(), `${variableName} ${value} must not be a empty string`) 48 | sharedAssert.assert(value.trim() === value, `${variableName} ${value} must be trimed`) 49 | sharedAssert.assert(value.toUpperCase() === value, `${variableName} ${value} must be upper case`) 50 | }, 51 | isValidTokenName(tokenName: string, pairs: Pair.ExchangePair[]) { 52 | this.isValidTokenNameString('Token name', tokenName) 53 | sharedAssert.assert( 54 | pairs.some(p => helpCompareStr(p.base.symbol, tokenName) || helpCompareStr(p.quote.symbol, tokenName)), 55 | TokenlonError.UnsupportedToken) 56 | }, 57 | isValidBaseQuote(baseQuote: Tokenlon.BaseQuote, pairs: Pair.ExchangePair[]) { 58 | const { base, quote } = baseQuote; 59 | ['base', 'quote'].forEach(key => this.isValidTokenNameString(key, baseQuote[key])) 60 | sharedAssert.assert( 61 | pairs.some(p => p.base.symbol === base && p.quote.symbol === quote), 62 | TokenlonError.UnsupportedPair, 63 | ) 64 | }, 65 | isValidWallet(wallet) { 66 | if (!wallet) throw newError(TokenlonError.WalletDoseNotExist) 67 | const { address, privateKey } = wallet 68 | sharedAssert.isETHAddressHex('wallet.address', address) 69 | const addr = ethUtil.privateToAddress(new Buffer(privateKey, 'hex')) 70 | sharedAssert.assert(helpCompareStr(`0x${addr.toString('hex')}`, address), TokenlonError.InvalidWalletPrivateKey) 71 | }, 72 | isValidGasPriceAdaptor(adaptor) { 73 | sharedAssert.assert(['safeLow', 'average', 'fast'].includes(adaptor), TokenlonError.InvalidGasPriceAdaptor) 74 | }, 75 | isValidConfig(config: GlobalConfig) { 76 | const { wallet, web3, server, gasPriceAdaptor } = config 77 | sharedAssert.isUri('web3.providerUrl', web3.providerUrl) 78 | sharedAssert.isUri('server.url', server.url) 79 | this.isValidWallet(wallet) 80 | this.isValidGasPriceAdaptor(gasPriceAdaptor) 81 | }, 82 | } 83 | 84 | export const rewriteAssertUtils = (assert: any) => { 85 | assert.isSenderAddressAsync = async function ( 86 | variableName: string, 87 | senderAddressHex: string, 88 | _web3Wrapper: Web3Wrapper, 89 | ): Promise { 90 | sharedAssert.isETHAddressHex(variableName, senderAddressHex) 91 | // Overwrite 92 | // const isSenderAddressAvailable = await web3Wrapper.isSenderAddressAvailableAsync(senderAddressHex) 93 | // sharedAssert.assert( 94 | // isSenderAddressAvailable, 95 | // `Specified ${variableName} ${senderAddressHex} isn't available through the supplied web3 provider`, 96 | // ) 97 | } 98 | } -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/_token.ts: -------------------------------------------------------------------------------- 1 | import { BigNumber } from '@0xproject/utils' 2 | import { assert } from '@0xproject/assert' 3 | import * as _ from 'lodash' 4 | 5 | import { TransactionOpts, ZeroExError } from '0x.js/lib/src/types' 6 | import helper from './helper' 7 | 8 | // only need to cover setAllowanceAsync, transferAsync, transferFromAsync 9 | // other functions just use these below functions to send transaction 10 | export const coverageToken = (obj) => { 11 | // use _.extends to shallow extend 12 | return _.extend(obj, { 13 | async setAllowanceAsync( 14 | tokenAddress: string, 15 | _ownerAddress: string, 16 | spenderAddress: string, 17 | amountInBaseUnits: BigNumber, 18 | txOpts: TransactionOpts = {}, 19 | ) { 20 | assert.isETHAddressHex('spenderAddress', spenderAddress) 21 | assert.isETHAddressHex('tokenAddress', tokenAddress) 22 | // remove ownerAddress check, because we use privateKey to send tx, not metamask etc. 23 | // await assert.isSenderAddressAsync('ownerAddress', ownerAddress, this._web3Wrapper) 24 | const normalizedTokenAddress = tokenAddress.toLowerCase() 25 | const normalizedSpenderAddress = spenderAddress.toLowerCase() 26 | assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits) 27 | return helper.tokenApproveTransaction( 28 | normalizedTokenAddress, 29 | normalizedSpenderAddress, 30 | amountInBaseUnits, 31 | { 32 | gasLimit: txOpts.gasLimit, 33 | gasPrice: txOpts.gasPrice, 34 | }, 35 | ) 36 | }, 37 | 38 | async transferAsync( 39 | tokenAddress: string, 40 | fromAddress: string, 41 | toAddress: string, 42 | amountInBaseUnits: BigNumber, 43 | txOpts: TransactionOpts = {}, 44 | ) { 45 | assert.isETHAddressHex('tokenAddress', tokenAddress) 46 | assert.isETHAddressHex('toAddress', toAddress) 47 | // remove ownerAddress check, because we use privateKey to send tx, not metamask etc. 48 | // await assert.isSenderAddressAsync('fromAddress', fromAddress, this._web3Wrapper) 49 | const normalizedTokenAddress = tokenAddress.toLowerCase() 50 | const normalizedFromAddress = fromAddress.toLowerCase() 51 | const normalizedToAddress = toAddress.toLowerCase() 52 | assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits) 53 | 54 | const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress) 55 | if (fromAddressBalance.lessThan(amountInBaseUnits)) { 56 | throw new Error(ZeroExError.InsufficientBalanceForTransfer) 57 | } 58 | return helper.tokenTransferTransaction( 59 | normalizedTokenAddress, 60 | normalizedToAddress, 61 | amountInBaseUnits, 62 | { 63 | gasLimit: txOpts.gasLimit, 64 | gasPrice: txOpts.gasPrice, 65 | }, 66 | ) 67 | }, 68 | 69 | async transferFromAsync( 70 | tokenAddress: string, 71 | fromAddress: string, 72 | toAddress: string, 73 | senderAddress: string, 74 | amountInBaseUnits: BigNumber, 75 | txOpts: TransactionOpts = {}, 76 | ): Promise { 77 | assert.isETHAddressHex('toAddress', toAddress) 78 | assert.isETHAddressHex('fromAddress', fromAddress) 79 | assert.isETHAddressHex('tokenAddress', tokenAddress) 80 | // remove ownerAddress check, because we use privateKey to send tx, not metamask etc. 81 | // await assert.isSenderAddressAsync('senderAddress', senderAddress, this._web3Wrapper) 82 | const normalizedToAddress = toAddress.toLowerCase() 83 | const normalizedFromAddress = fromAddress.toLowerCase() 84 | const normalizedTokenAddress = tokenAddress.toLowerCase() 85 | const normalizedSenderAddress = senderAddress.toLowerCase() 86 | assert.isValidBaseUnitAmount('amountInBaseUnits', amountInBaseUnits) 87 | 88 | const fromAddressAllowance = await this.getAllowanceAsync( 89 | normalizedTokenAddress, 90 | normalizedFromAddress, 91 | normalizedSenderAddress, 92 | ) 93 | if (fromAddressAllowance.lessThan(amountInBaseUnits)) { 94 | throw new Error(ZeroExError.InsufficientAllowanceForTransfer) 95 | } 96 | 97 | const fromAddressBalance = await this.getBalanceAsync(normalizedTokenAddress, normalizedFromAddress) 98 | if (fromAddressBalance.lessThan(amountInBaseUnits)) { 99 | throw new Error(ZeroExError.InsufficientBalanceForTransfer) 100 | } 101 | 102 | return helper.tokenTransferFromTransaction( 103 | normalizedTokenAddress, 104 | normalizedFromAddress, 105 | normalizedToAddress, 106 | amountInBaseUnits, 107 | { 108 | gasLimit: txOpts.gasLimit, 109 | gasPrice: txOpts.gasPrice, 110 | }, 111 | ) 112 | }, 113 | }) 114 | } -------------------------------------------------------------------------------- /src/lib/server/index.ts: -------------------------------------------------------------------------------- 1 | import { jsonrpc } from './_request' 2 | import { DexOrderBNToString, Pair, Server as ServerInterface } from '../../types' 3 | import { Wallet } from '../../types' 4 | import { getTimestamp } from '../../utils/helper' 5 | import { personalSign } from '../../utils/sign' 6 | export class Server { 7 | private _url: string 8 | private _wallet: Wallet 9 | private _tokenObj = { timestamp: 0, token: '' } 10 | private _tokenRequesting = false 11 | 12 | constructor(url: string, wallet: Wallet) { 13 | this._url = url 14 | this._wallet = wallet 15 | } 16 | 17 | private async getToken(params: ServerInterface.GetTokenParams): Promise { 18 | return jsonrpc.get(this._url, {}, 'auth.getToken', [params]).then(data => { 19 | return data.token 20 | }) 21 | } 22 | 23 | private async _getHeader() { 24 | const timestamp = getTimestamp() 25 | 26 | // 因每一个 Tokenlon instantce 都有各自的 JWT Token 27 | // 并且 SDK 用于自动化交易,过程中没有切换节点需要更新JWT Token的情况 28 | // 因此使用定期提前更新 JWT Token 的方式来避免 JWT Token 的过期 29 | if (!this._tokenRequesting && (!this._tokenObj.timestamp || this._tokenObj.timestamp < timestamp - 3600)) { 30 | const signature = personalSign(this._wallet.privateKey, timestamp.toString()) 31 | this._tokenRequesting = true 32 | try { 33 | const token = await this.getToken({ timestamp, signature }) 34 | this._tokenObj = { timestamp, token } 35 | } catch (e) { 36 | } 37 | this._tokenRequesting = false 38 | } 39 | 40 | return { 'access-token': this._tokenObj.token } 41 | } 42 | 43 | async getPairList(): Promise { 44 | const header = await this._getHeader() 45 | return jsonrpc.get(this._url, header, 'dex.getPairList', [{ market: 'Tokenlon' }]).then(data => { 46 | const res = data || [] 47 | return res.filter(p => p.tradingEnabled) 48 | }) 49 | } 50 | 51 | async getOrderBook(params: ServerInterface.GetOrderBookParams): Promise { 52 | const header = await this._getHeader() 53 | return jsonrpc.get(this._url, header, 'dex.getOrderBook', [params]).then(res => { 54 | const result = { 55 | bids: [], 56 | asks: [], 57 | } as ServerInterface.OrderBookResult 58 | if (res.bids && res.bids.length) { 59 | result.bids = res.bids.sort((s, l) => l.rate - s.rate) 60 | } 61 | if (res.asks && res.asks.length > 1) { 62 | result.asks = res.asks.sort((s, l) => s.rate - l.rate) 63 | } 64 | return result 65 | }) 66 | } 67 | 68 | async placeOrder(order: DexOrderBNToString): Promise { 69 | const header = await this._getHeader() 70 | return jsonrpc.get(this._url, header, 'dex.placeOrder', [{ 71 | protocol: '0x', 72 | order, 73 | }]) 74 | } 75 | 76 | async fillOrder(params: ServerInterface.FillOrderParams): Promise { 77 | const header = await this._getHeader() 78 | return jsonrpc.get(this._url, header, 'dex.fillOrder', [{ 79 | protocol: '0x', 80 | ...params, 81 | }]) 82 | } 83 | 84 | async batchFillOrders(params: ServerInterface.BatchFillOrdersParams): Promise { 85 | const header = await this._getHeader() 86 | return jsonrpc.get(this._url, header, 'dex.batchFillOrders', [{ 87 | protocol: '0x', 88 | ...params, 89 | }]) 90 | } 91 | 92 | async cancelOrders(params: string[]): Promise { 93 | const header = await this._getHeader() 94 | return jsonrpc.get(this._url, header, 'dex.cancelOrders', params) 95 | } 96 | 97 | async cancelOrdersWithHash(params: ServerInterface.CancelOrderItem[]): Promise { 98 | const header = await this._getHeader() 99 | return jsonrpc.get(this._url, header, 'dex.cancelOrdersWithHash', params) 100 | } 101 | 102 | async getOrders(params: ServerInterface.GetOrdersParams): Promise { 103 | const header = await this._getHeader() 104 | return jsonrpc.get(this._url, header, 'dex.getOrders', [params]).then(data => data || []) 105 | } 106 | 107 | async getOrder(orderHash: string): Promise { 108 | const header = await this._getHeader() 109 | return jsonrpc.get(this._url, header, 'dex.getOrder', [{ orderHash }]).then(data => data || []) 110 | } 111 | 112 | async getMakerTrades(params: ServerInterface.MakerTradesParams): Promise { 113 | const header = await this._getHeader() 114 | return jsonrpc.get(this._url, header, 'dex.getMakerTrades', [params]).then(data => data || []) 115 | } 116 | 117 | async getTakerTrades(params: ServerInterface.TakerTradesParams): Promise { 118 | const header = await this._getHeader() 119 | return jsonrpc.get(this._url, header, 'dex.getTakerTrades', [params]).then(data => data || []) 120 | } 121 | } -------------------------------------------------------------------------------- /tests/utils/format.test.ts: -------------------------------------------------------------------------------- 1 | import { thousandCommas, decimal, formatMoney, formatNumHelper, fromUnitToDecimalBN, fromDecimalToUnit, fromUnitToDecimal } from '../../src/utils/format' 2 | import { BigNumber } from '@0xproject/utils' 3 | 4 | describe('test thousandCommas', () => { 5 | const testData = [{ 6 | num: 1, 7 | min: undefined, 8 | max: undefined, 9 | result: '1.0000', 10 | }, { 11 | num: 1, 12 | min: undefined, 13 | max: 4, 14 | result: '1.0000', 15 | }, { 16 | num: 1, 17 | min: 4, 18 | max: undefined, 19 | result: '1.0000', 20 | }, { 21 | num: '123456789', 22 | min: 4, 23 | max: 8, 24 | result: '123,456,789.0000', 25 | }] 26 | 27 | const testInvalidData = [{ 28 | num: 1, 29 | min: 2, 30 | max: 1, 31 | result: 'maximumFractionDigits value is out of range', 32 | }, { 33 | num: 1, 34 | min: undefined, 35 | max: 1, 36 | result: 'maximumFractionDigits value is out of range', 37 | }, { 38 | num: 1, 39 | min: 9, 40 | max: undefined, 41 | result: 'maximumFractionDigits value is out of range', 42 | }] 43 | 44 | testData.forEach(data => { 45 | it(`thousandCommas(${data.num}, ${data.min}, ${data.max}) => ${data.result}`, () => { 46 | expect(thousandCommas(data.num, data.min, data.max)).toBe(data.result) 47 | }) 48 | }) 49 | 50 | testInvalidData.forEach(data => { 51 | it(`thousandCommas(${data.num}, ${data.min}, ${data.max}) => ${data.result}`, () => { 52 | expect(() => thousandCommas(data.num, data.min, data.max)).toThrow(data.result) 53 | }) 54 | }) 55 | }) 56 | 57 | describe('test decimal', () => { 58 | const testData = [{ 59 | num: '', 60 | place: undefined, 61 | result: '0', 62 | }, { 63 | num: 0, 64 | place: undefined, 65 | result: '0', 66 | }, { 67 | num: 0, 68 | place: 4, 69 | result: '0', 70 | }, { 71 | num: '1', 72 | place: 4, 73 | result: '1.0000', 74 | }] 75 | 76 | testData.forEach(data => { 77 | it(`decimal(${data.num}, ${data.place}) => ${data.result}`, () => { 78 | expect(decimal(data.num, data.place)).toBe(data.result) 79 | }) 80 | }) 81 | }) 82 | 83 | describe('test formatMoney', () => { 84 | const testData = [{ 85 | num: '1', 86 | place: undefined, 87 | result: '1.0000', 88 | }, { 89 | num: 1, 90 | place: undefined, 91 | result: '1.0000', 92 | }, { 93 | num: 1, 94 | place: 4, 95 | result: '1.0000', 96 | }, { 97 | num: '0', 98 | place: 4, 99 | result: '0.0000', 100 | }, { 101 | num: 0, 102 | place: 4, 103 | result: '0.0000', 104 | }, { 105 | num: '-1', 106 | place: 4, 107 | result: '-1.0000', 108 | }] 109 | 110 | testData.forEach(data => { 111 | it(`formatMoney(${data.num}, ${data.place}) => ${data.result}`, () => { 112 | expect(formatMoney(data.num, data.place)).toBe(data.result) 113 | }) 114 | }) 115 | }) 116 | 117 | describe('test formatNumHelper', () => { 118 | const testData = [{ 119 | num: '1', 120 | place: undefined, 121 | fill: undefined, 122 | result: '1.00000000', 123 | }, { 124 | num: 1, 125 | place: undefined, 126 | fill: undefined, 127 | result: '1.00000000', 128 | }, { 129 | num: 1, 130 | place: 4, 131 | fill: false, 132 | result: '1.00', 133 | }, { 134 | num: '0', 135 | place: 4, 136 | fill: false, 137 | result: '0.00', 138 | }, { 139 | num: 0, 140 | place: 4, 141 | fill: true, 142 | result: '0.0000', 143 | }, { 144 | num: '-1', 145 | place: 4, 146 | fill: true, 147 | result: '-1.0000', 148 | }] 149 | 150 | testData.forEach(data => { 151 | it(`formatNumHelper(${data.place})(${data.num}, ${data.fill}) => ${data.result}`, () => { 152 | expect(formatNumHelper(data.place)(data.num, data.fill)).toBe(data.result) 153 | }) 154 | }) 155 | }) 156 | 157 | describe('decimal unit test', () => { 158 | const testData = [{ 159 | balance: '259806613708406784', 160 | decimal: 18, 161 | unit: '0.259806613708406784', 162 | }, { 163 | balance: '1000000000000000000000000000', 164 | decimal: 18, 165 | unit: '1000000000', 166 | }] 167 | 168 | // fromDecimalToUnit 169 | testData.forEach(asset => { 170 | it(`${asset.balance} from decimal ${asset.decimal} to unit is ${asset.unit}`, () => { 171 | expect(fromDecimalToUnit(asset.balance, asset.decimal).toString()).toBe(asset.unit) 172 | }) 173 | }) 174 | 175 | // fromUnitToDecimal 176 | testData.forEach(asset => { 177 | it(`${asset.unit} from decimal ${asset.decimal} to unit is ${asset.balance}`, () => { 178 | const decimalBN = fromUnitToDecimalBN(asset.balance, asset.decimal) 179 | expect(decimalBN instanceof BigNumber).toBe(true) 180 | // fromUnitToDecimalBN 181 | expect(fromUnitToDecimalBN(asset.unit, asset.decimal).toString()).toBe(asset.balance) 182 | // fromUnitToDecimal 183 | expect(fromUnitToDecimal(asset.unit, asset.decimal, 10)).toBe(asset.balance) 184 | }) 185 | }) 186 | }) -------------------------------------------------------------------------------- /tests/tokenlon/transactionOpts.test.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '0x.js/lib/src/utils/constants' 2 | import { localConfig, localConfigUseToFill, web3ProviderUrl } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import Web3 from 'web3' 7 | import web3 from '../../src/lib/web3-wrapper' 8 | import { fromDecimalToUnit } from '../../src/utils/format' 9 | import { getTokenBalance, getEstimateGas } from '../../src/utils/ethereum' 10 | import { toBN } from '../../src/utils/math' 11 | import { waitMined } from '../__utils__/wait' 12 | import { getTimestamp } from '../../src/utils/helper' 13 | import { getGasPriceByAdaptorAsync } from '../../src/utils/gasPriceAdaptor' 14 | import { getGasLimitByTransactionAsync, getGasPriceByTransactionAsync } from '../__utils__/helper' 15 | 16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 3000000 17 | 18 | let tokenlon = null as Tokenlon 19 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 20 | 21 | beforeAll(async () => { 22 | tokenlon = await createTokenlon(localConfigUseToFill) 23 | }) 24 | 25 | const getPlacedOrderAsync = async () => { 26 | return tokenlon.placeOrder({ 27 | base: 'SNT', 28 | quote: 'WETH', 29 | price: 0.001, 30 | amount: 10, 31 | side: 'BUY', 32 | expirationUnixTimestampSec: getTimestamp() + 60 * 10, 33 | }) 34 | } 35 | 36 | const testDatas = [ 37 | { 38 | method: 'deposit', 39 | params: [0.0001], 40 | }, 41 | { 42 | method: 'withdraw', 43 | params: [0.0001], 44 | }, 45 | { 46 | method: 'setAllowance', 47 | params: ['SNT', 1], 48 | }, 49 | { 50 | method: 'setUnlimitedAllowance', 51 | params: ['SNT'], 52 | }, 53 | { 54 | getOrder: getPlacedOrderAsync, 55 | method: 'fillOrder', 56 | params: [], 57 | }, 58 | { 59 | getOrder: getPlacedOrderAsync, 60 | method: 'fillOrKillOrder', 61 | params: [], 62 | }, 63 | { 64 | getOrder: getPlacedOrderAsync, 65 | method: 'batchFillOrders', 66 | params: [], 67 | }, 68 | { 69 | getOrder: getPlacedOrderAsync, 70 | method: 'batchFillOrKill', 71 | params: [], 72 | }, 73 | { 74 | getOrder: getPlacedOrderAsync, 75 | method: 'fillOrdersUpTo', 76 | params: [], 77 | }, 78 | { 79 | getOrder: getPlacedOrderAsync, 80 | method: 'cancelOrder', 81 | params: [], 82 | }, 83 | { 84 | getOrder: getPlacedOrderAsync, 85 | method: 'batchCancelOrders', 86 | params: [], 87 | }, 88 | ] 89 | 90 | describe('test transaction', () => { 91 | it('test transaction', async () => { 92 | for (let type of ['coverage', 'default']) { 93 | const opts = type === 'default' ? undefined : { gasLimit: 216666, gasPrice: 5260000000 } 94 | 95 | for (let item of testDatas) { 96 | let params = [] 97 | 98 | if (item.getOrder) { 99 | const order = await item.getOrder() 100 | if (['fillOrKillOrder', 'fillOrder'].includes(item.method)) { 101 | params = [{ 102 | base: 'SNT', 103 | quote: 'WETH', 104 | ...order, 105 | side: order.side === 'BUY' ? 'SELL' : 'BUY', 106 | }] 107 | 108 | } else if (['batchFillOrders', 'batchFillOrKill'].includes(item.method)) { 109 | params = [[{ 110 | base: 'SNT', 111 | quote: 'WETH', 112 | ...order, 113 | side: order.side === 'BUY' ? 'SELL' : 'BUY', 114 | }]] 115 | 116 | } else if (item.method === 'fillOrdersUpTo') { 117 | params = [{ 118 | base: 'SNT', 119 | quote: 'WETH', 120 | ...order, 121 | side: order.side === 'BUY' ? 'SELL' : 'BUY', 122 | rawOrders: [order.rawOrder], 123 | } as any] 124 | 125 | } else if (item.method === 'cancelOrder') { 126 | params = [order.rawOrder, true] 127 | 128 | } else if (item.method === 'batchCancelOrders') { 129 | params = [[order.rawOrder], true] 130 | 131 | } 132 | } else { 133 | params = item.params 134 | } 135 | 136 | let expectGasPrice = 0 137 | let expectGasLimit = 0 138 | 139 | if (type === 'default') { 140 | expectGasPrice = await getGasPriceByAdaptorAsync(localConfigUseToFill.gasPriceAdaptor) 141 | expectGasLimit = localConfigUseToFill.zeroEx.gasLimit 142 | 143 | } else { 144 | expectGasPrice = opts.gasPrice 145 | expectGasLimit = opts.gasLimit 146 | params.push(opts) 147 | } 148 | 149 | console.log('type', type) 150 | console.log('item.method', item.method) 151 | console.log('params', params) 152 | const txHash = await tokenlon[item.method].apply(tokenlon, params) 153 | 154 | await waitMined(txHash, 60) 155 | 156 | const resultGasLimit = await getGasLimitByTransactionAsync(txHash) 157 | const resultGasPrice = await getGasPriceByTransactionAsync(txHash) 158 | 159 | expect(resultGasLimit).toEqual(expectGasLimit) 160 | expect(resultGasPrice).toEqual(expectGasPrice) 161 | } 162 | } 163 | }) 164 | }) -------------------------------------------------------------------------------- /src/lib/zeroex-wrapper/_exchange.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SignedOrder, 3 | OrderFillRequest, 4 | OrderTransactionOpts, 5 | OrderCancellationRequest, 6 | } from '0x.js' 7 | import { BigNumber } from '@0xproject/utils' 8 | import helper from './helper' 9 | import * as _ from 'lodash' 10 | 11 | import { signedOrderUtils } from './__signed_order_utils' 12 | import { formatters } from './__formatters' 13 | 14 | // using signedOrderUtils and formatters from 15 | // https://github.com/0xProject/0x.js/blob/7aa070f9eaef734274df6e6eaa4590fe30d52899/packages/contracts/util/exchange_wrapper.ts 16 | // to cover zeroEx.exchange's methods with transaction 17 | const fillOrderAsync = ( 18 | signedOrder: SignedOrder, 19 | fillTakerTokenAmount: BigNumber, 20 | shouldThrowOnInsufficientBalanceOrAllowance: boolean, 21 | _takerAddress: string, 22 | orderTransactionOpts: OrderTransactionOpts = {}, 23 | ) => { 24 | const params = signedOrderUtils.createFill( 25 | signedOrder, 26 | shouldThrowOnInsufficientBalanceOrAllowance, 27 | fillTakerTokenAmount, 28 | ) 29 | return helper.exchangeSendTransaction('fillOrder', [ 30 | params.orderAddresses, 31 | params.orderValues, 32 | params.fillTakerTokenAmount, 33 | params.shouldThrowOnInsufficientBalanceOrAllowance, 34 | params.v, 35 | params.r, 36 | params.s, 37 | ], orderTransactionOpts) 38 | } 39 | 40 | const cancelOrderAsync = ( 41 | signedOrder: SignedOrder, 42 | cancelTakerTokenAmount: BigNumber, 43 | orderTransactionOpts: OrderTransactionOpts = {}, 44 | ) => { 45 | const params = signedOrderUtils.createCancel(signedOrder, cancelTakerTokenAmount) 46 | return helper.exchangeSendTransaction('cancelOrder', [ 47 | params.orderAddresses, 48 | params.orderValues, 49 | params.cancelTakerTokenAmount, 50 | ], orderTransactionOpts) 51 | } 52 | 53 | const fillOrKillOrderAsync = ( 54 | signedOrder: SignedOrder, 55 | fillTakerTokenAmount: BigNumber, 56 | _takerAddress: string, 57 | orderTransactionOpts: OrderTransactionOpts = {}, 58 | ) => { 59 | const shouldThrowOnInsufficientBalanceOrAllowance = true 60 | const params = signedOrderUtils.createFill( 61 | signedOrder, 62 | shouldThrowOnInsufficientBalanceOrAllowance, 63 | fillTakerTokenAmount, 64 | ) 65 | return helper.exchangeSendTransaction('fillOrKillOrder', [ 66 | params.orderAddresses, 67 | params.orderValues, 68 | params.fillTakerTokenAmount, 69 | params.v, 70 | params.r, 71 | params.s, 72 | ], orderTransactionOpts) 73 | } 74 | 75 | const batchFillOrdersAsync = ( 76 | orderFillRequests: OrderFillRequest[], 77 | shouldThrowOnInsufficientBalanceOrAllowance: boolean, 78 | _takerAddress: string, 79 | orderTransactionOpts: OrderTransactionOpts = {}, 80 | ) => { 81 | const params = formatters.createBatchFill( 82 | orderFillRequests.map(r => r.signedOrder), 83 | shouldThrowOnInsufficientBalanceOrAllowance, 84 | orderFillRequests.map(r => r.takerTokenFillAmount), 85 | ) 86 | return helper.exchangeSendTransaction('batchFillOrders', [ 87 | params.orderAddresses, 88 | params.orderValues, 89 | params.fillTakerTokenAmounts, 90 | params.shouldThrowOnInsufficientBalanceOrAllowance, 91 | params.v, 92 | params.r, 93 | params.s, 94 | ], orderTransactionOpts) 95 | } 96 | 97 | const batchFillOrKillAsync = ( 98 | orderFillRequests: OrderFillRequest[], 99 | _takerAddress: string, 100 | orderTransactionOpts: OrderTransactionOpts = {}, 101 | ) => { 102 | const params = formatters.createBatchFill( 103 | orderFillRequests.map(r => r.signedOrder), 104 | false, 105 | orderFillRequests.map(r => r.takerTokenFillAmount), 106 | ) 107 | return helper.exchangeSendTransaction('batchFillOrKillOrders', [ 108 | params.orderAddresses, 109 | params.orderValues, 110 | params.fillTakerTokenAmounts, 111 | params.v, 112 | params.r, 113 | params.s, 114 | ], orderTransactionOpts) 115 | } 116 | 117 | const fillOrdersUpToAsync = ( 118 | signedOrders: SignedOrder[], 119 | fillTakerTokenAmount: BigNumber, 120 | shouldThrowOnInsufficientBalanceOrAllowance: boolean, 121 | _takerAddress: string, 122 | orderTransactionOpts: OrderTransactionOpts = {}, 123 | ) => { 124 | const params = formatters.createFillUpTo( 125 | signedOrders, 126 | shouldThrowOnInsufficientBalanceOrAllowance, 127 | fillTakerTokenAmount, 128 | ) 129 | return helper.exchangeSendTransaction('fillOrdersUpTo', [ 130 | params.orderAddresses, 131 | params.orderValues, 132 | params.fillTakerTokenAmount, 133 | params.shouldThrowOnInsufficientBalanceOrAllowance, 134 | params.v, 135 | params.r, 136 | params.s, 137 | ], orderTransactionOpts) 138 | } 139 | 140 | const batchCancelOrdersAsync = ( 141 | orderCancellationRequests: OrderCancellationRequest[], 142 | orderTransactionOpts: OrderTransactionOpts = {}, 143 | ) => { 144 | const orders = orderCancellationRequests.map(r => r.order) as SignedOrder[] 145 | const cancelTakerTokenAmounts = orderCancellationRequests.map(r => r.takerTokenCancelAmount) 146 | const params = formatters.createBatchCancel(orders, cancelTakerTokenAmounts) 147 | return helper.exchangeSendTransaction('batchCancelOrders', [ 148 | params.orderAddresses, 149 | params.orderValues, 150 | params.cancelTakerTokenAmounts, 151 | ], orderTransactionOpts) 152 | } 153 | 154 | export const coverageExchange = (exchange) => { 155 | // use _.extends to shallow extend 156 | return _.extend(exchange, { 157 | fillOrderAsync, 158 | cancelOrderAsync, 159 | fillOrKillOrderAsync, 160 | batchFillOrdersAsync, 161 | batchFillOrKillAsync, 162 | fillOrdersUpToAsync, 163 | batchCancelOrdersAsync, 164 | }) 165 | } -------------------------------------------------------------------------------- /tests/utils/ethereum.test.ts: -------------------------------------------------------------------------------- 1 | import { fromUnitToDecimalBN } from '../../src/utils/format' 2 | import { encodeData, getEstimateGas, getNonce, sendTransaction, getTokenBalance } from '../../src/utils/ethereum' 3 | import Web3 from 'web3' 4 | import web3 from '../../src/lib/web3-wrapper' 5 | import * as _ from 'lodash' 6 | import { toBN } from '../../src/utils/math' 7 | import { sntWethPairData } from '../__mock__/pair' 8 | import { wallet, web3ProviderUrl } from '../__mock__/config' 9 | import { BigNumber } from '@0xproject/utils' 10 | import { orders } from '../__mock__/order' 11 | import { waitSeconds } from '../__utils__/wait' 12 | import { getReceiptAsync } from '../__utils__/helper' 13 | 14 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 15 | 16 | web3.setProvider(new Web3.providers.HttpProvider(web3ProviderUrl)) 17 | 18 | const waitMined = async (txHash, seconds) => { 19 | let receipt = await getReceiptAsync(txHash) 20 | let timeUsed = 0 21 | if (timeUsed <= seconds) { 22 | while (!receipt || !timeUsed) { 23 | await waitSeconds(2) 24 | receipt = await getReceiptAsync(txHash) 25 | timeUsed += 2 26 | } 27 | console.log('set seconds', seconds) 28 | console.log('timeUsed', timeUsed) 29 | return true 30 | } else { 31 | return false 32 | } 33 | } 34 | 35 | describe('test getNonce', () => { 36 | it('test getNonce', async () => { 37 | const n1 = await getNonce(wallet.address) 38 | const n2 = await new Promise((resolve, reject) => { 39 | web3.eth.getTransactionCount(wallet.address, (err, nonce) => { 40 | if (!err) { 41 | resolve(nonce) 42 | } else { 43 | reject(err) 44 | } 45 | }) 46 | }) 47 | 48 | expect(_.isNumber(n1)).toBe(true) 49 | expect(n1).toEqual(n2) 50 | }) 51 | }) 52 | 53 | describe('test encodeData', () => { 54 | const order = orders[0] 55 | const testData = [ 56 | { 57 | contractName: 'etherToken', 58 | method: 'deposit', 59 | args: [fromUnitToDecimalBN(1, sntWethPairData.quote.decimal)], 60 | encodedData: '0xd0e30db0', 61 | }, 62 | { 63 | contractName: 'etherToken', 64 | method: 'withdraw', 65 | args: [fromUnitToDecimalBN(1, sntWethPairData.quote.decimal)], 66 | encodedData: '0x2e1a7d4d0000000000000000000000000000000000000000000000000de0b6b3a7640000', 67 | }, 68 | { 69 | contractName: 'exchange', 70 | method: 'fillOrder', 71 | args: [ 72 | [ 73 | order.signedOrder.maker, 74 | order.signedOrder.taker, 75 | order.signedOrder.makerTokenAddress, 76 | order.signedOrder.takerTokenAddress, 77 | order.signedOrder.feeRecipient, 78 | ], 79 | [ 80 | order.signedOrder.makerTokenAmount, 81 | order.signedOrder.takerTokenAmount, 82 | order.signedOrder.makerFee, 83 | order.signedOrder.takerFee, 84 | order.signedOrder.expirationUnixTimestampSec, 85 | order.signedOrder.salt, 86 | ].map(n => toBN(n)), 87 | toBN(order.fillTakerTokenAmount), 88 | order.shouldThrowOnInsufficientBalanceOrAllowance, 89 | toBN(order.signedOrder.ecSignature.v), 90 | order.signedOrder.ecSignature.r, 91 | order.signedOrder.ecSignature.s, 92 | ], 93 | encodedData: order.encodedData, 94 | }, 95 | ] 96 | 97 | testData.forEach((item) => { 98 | it(`test ${item.contractName} ${item.method}`, () => { 99 | expect(encodeData(item.contractName, item.method, item.args)).toEqual(item.encodedData) 100 | }) 101 | }) 102 | }) 103 | 104 | describe('test getEstimateGas', () => { 105 | it('test getEstimateGas', async () => { 106 | const tx = { 107 | from: wallet.address, 108 | to: sntWethPairData.quote.contractAddress, 109 | data: encodeData('etherToken', 'withdraw', [fromUnitToDecimalBN(0.00001, sntWethPairData.quote.decimal)]), 110 | } 111 | const g1 = await getEstimateGas(tx) 112 | const g2 = await new Promise((resolve, reject) => { 113 | web3.eth.estimateGas(tx, (err, gasLimit) => { 114 | if (!err) { 115 | resolve(gasLimit) 116 | } else { 117 | reject(err) 118 | } 119 | }) 120 | }) 121 | 122 | expect(_.isNumber(g1)).toBe(true) 123 | expect(g1).toEqual(g2) 124 | }) 125 | }) 126 | 127 | describe('test getTokenBalance and sendTransaction', () => { 128 | it('test getTokenBalance and sendTransaction', async () => { 129 | const amount = 0.00001 130 | const decimalAmountBN = fromUnitToDecimalBN(amount, sntWethPairData.quote.decimal) 131 | const wethAddr = sntWethPairData.quote.contractAddress 132 | const defaultParams = { 133 | ...wallet, 134 | gasPrice: 20000000000, 135 | to: wethAddr, 136 | } 137 | const withdrawParams = { 138 | ...defaultParams, 139 | value: 0, 140 | data: encodeData('etherToken', 'withdraw', [decimalAmountBN]), 141 | } 142 | const depositParams = { 143 | ...defaultParams, 144 | value: decimalAmountBN.toNumber(), 145 | data: encodeData('etherToken', 'deposit', []), 146 | } 147 | 148 | const wethBalance1 = await getTokenBalance({ 149 | address: wallet.address, 150 | contractAddress: wethAddr, 151 | }) 152 | const txHash1 = await sendTransaction(withdrawParams) 153 | await waitMined(txHash1, 20) 154 | const wethBalance2 = await getTokenBalance({ 155 | address: wallet.address, 156 | contractAddress: wethAddr, 157 | }) 158 | 159 | expect(wethBalance1.minus(decimalAmountBN).toString()).toEqual(wethBalance2.toString()) 160 | 161 | const txHash2 = await sendTransaction(depositParams) 162 | await waitMined(txHash2, 20) 163 | 164 | const wethBalance3 = await getTokenBalance({ 165 | address: wallet.address, 166 | contractAddress: wethAddr, 167 | }) 168 | expect(wethBalance1.toString()).toEqual(wethBalance3.toString()) 169 | }) 170 | }) -------------------------------------------------------------------------------- /tests/tokenlon/fillOrder.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { wallet, web3ProviderUrl, localConfigUseToFill, placeOrderWalletAddress } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import { toBN } from '../../src/utils/math' 7 | import { helpCompareStr } from '../../src/utils/helper' 8 | import { waitSeconds, waitMined } from '../__utils__/wait' 9 | import { filterOrderBook } from '../__utils__/helper' 10 | 11 | let tokenlon = null as Tokenlon 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 600000 13 | 14 | const baseQuote = { 15 | base: sntWethPairData.base.symbol, 16 | quote: sntWethPairData.quote.symbol, 17 | } 18 | 19 | beforeAll(async () => { 20 | tokenlon = await createTokenlon(localConfigUseToFill) 21 | }) 22 | 23 | describe('test fillOrder / batchFillOrders / fillOrKillOrder / batchfillOrKill', () => { 24 | it('test fillOrder / batchFillOrders / fillOrKillOrder / batchfillOrKill', async () => { 25 | 26 | for (let singleFillMethod of ['fillOrKillOrder', 'fillOrder']) { 27 | const isKill = singleFillMethod === 'fillOrKillOrder' 28 | const batchFillOMethod = isKill ? 'batchFillOrKill' : 'batchFillOrders' 29 | 30 | const orderBook = await tokenlon.getOrderBook(baseQuote) 31 | const processedOrderBook = { 32 | asks: filterOrderBook(orderBook.asks), 33 | bids: filterOrderBook(orderBook.bids), 34 | } 35 | 36 | // small amount 37 | for (let side of ['BUY', 'SELL']) { 38 | const baseTokenBalance1 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 39 | const quoteTokenBalance1 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 40 | const isBuy = side === 'BUY' 41 | const simpleOrder = processedOrderBook[isBuy ? 'asks' : 'bids'][0] 42 | if (!simpleOrder) { 43 | continue 44 | } 45 | // change fillTakerTokenAmount to test 46 | const baseAmount = simpleOrder.amount > 1 ? 1 : simpleOrder.amount 47 | console.log(`test ${singleFillMethod} ${side} ${baseAmount}`) 48 | const txHash = await tokenlon[singleFillMethod]({ 49 | ...baseQuote, 50 | ...simpleOrder, 51 | amount: baseAmount, 52 | side, 53 | }) 54 | await waitMined(txHash, 60) 55 | const baseTokenBalance2 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 56 | const quoteTokenBalance2 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 57 | expect(toBN(baseTokenBalance1)[isBuy ? 'plus' : 'minus'](toBN(baseAmount)).toFixed(10)).toEqual(toBN(baseTokenBalance2).toFixed(10)) 58 | expect(toBN(quoteTokenBalance1)[isBuy ? 'minus' : 'plus'](toBN(baseAmount).times(toBN(simpleOrder.price))).toFixed(10)).toEqual(toBN(quoteTokenBalance2).toFixed(10)) 59 | } 60 | 61 | // large amount 62 | for (let side of ['BUY', 'SELL']) { 63 | const baseTokenBalance3 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 64 | const quoteTokenBalance3 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 65 | const isBuy = side === 'BUY' 66 | const simpleOrder = processedOrderBook[isBuy ? 'asks' : 'bids'][1] 67 | if (!simpleOrder) { 68 | continue 69 | } 70 | // change fillTakerTokenAmount to test 71 | // if amount can not be filled, fillOrKill will throw error 72 | // but fillOrder will still to fill the order's can be filled amount 73 | const baseAmount = 100000000 74 | console.log(`test ${singleFillMethod} ${side} ${baseAmount}`) 75 | if (isKill) { 76 | try { 77 | const txHash = await tokenlon[singleFillMethod]({ 78 | ...baseQuote, 79 | ...simpleOrder, 80 | amount: baseAmount, 81 | side, 82 | }) 83 | } catch (e) { 84 | expect(e.toString()).toMatch(/INSUFFICIENT_REMAINING_FILL_AMOUNT/) 85 | } 86 | } else { 87 | const txHash = await tokenlon[singleFillMethod]({ 88 | ...baseQuote, 89 | ...simpleOrder, 90 | amount: baseAmount, 91 | side, 92 | }) 93 | await waitMined(txHash, 60) 94 | const baseTokenBalance4 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 95 | const quoteTokenBalance4 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 96 | expect(toBN(baseTokenBalance3)[isBuy ? 'plus' : 'minus'](toBN(simpleOrder.amount)).toFixed(10)).toEqual(toBN(baseTokenBalance4).toFixed(10)) 97 | expect(toBN(quoteTokenBalance3)[isBuy ? 'minus' : 'plus'](toBN(simpleOrder.amount).times(toBN(simpleOrder.price))).toFixed(10)).toEqual(toBN(quoteTokenBalance4).toFixed(10)) 98 | } 99 | } 100 | 101 | const orderFillReqs = [...processedOrderBook.bids.slice(2, 3), ...processedOrderBook.asks.slice(2, 3)].map(o => { 102 | return { 103 | ...baseQuote, 104 | ...o, 105 | side: o.side === 'BUY' ? 'SELL' : 'BUY', 106 | } 107 | }) 108 | 109 | if (!orderFillReqs.length) { 110 | return 111 | } 112 | 113 | let baseTokenBalance5 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 114 | let quoteTokenBalance5 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 115 | console.log(`test ${batchFillOMethod}`) 116 | const txHash = await tokenlon[batchFillOMethod](orderFillReqs) 117 | await waitMined(txHash, 90) 118 | 119 | orderFillReqs.forEach(req => { 120 | const { side, price, amount } = req 121 | const isBuy = side === 'BUY' 122 | baseTokenBalance5 = toBN(baseTokenBalance5)[isBuy ? 'plus' : 'minus'](toBN(amount)).toNumber() 123 | quoteTokenBalance5 = toBN(quoteTokenBalance5)[isBuy ? 'minus' : 'plus'](toBN(amount).times(toBN(price))).toNumber() 124 | }) 125 | 126 | const baseTokenBalance6 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 127 | const quoteTokenBalance6 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 128 | 129 | expect(toBN(baseTokenBalance5).toFixed(10)).toEqual(toBN(baseTokenBalance6).toFixed(10)) 130 | expect(toBN(quoteTokenBalance5).toFixed(10)).toEqual(toBN(quoteTokenBalance6).toFixed(10)) 131 | } 132 | }) 133 | }) 134 | 135 | // TODO batchFill special fillTakerAmount -------------------------------------------------------------------------------- /tests/tokenlon/jwt.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import * as ethUtil from 'ethereumjs-util' 3 | import { Wallet } from '../../src/types' 4 | import { localConfig, walletUseToFill } from '../__mock__/config' 5 | import { sntWethPairData } from '../__mock__/pair' 6 | import { Server } from '../../src/lib/server' 7 | 8 | import { jsonrpc } from '../../src/lib/server/_request' 9 | import { personalSign } from '../../src/utils/sign' 10 | import { getTimestamp } from '../../src/utils/helper' 11 | 12 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 13 | 14 | const server = new Server(localConfig.server.url, localConfig.wallet) 15 | 16 | const getToken = async (timestamp, wallet: Wallet) => { 17 | const signature = personalSign(wallet.privateKey, timestamp.toString()) 18 | return jsonrpc.get(localConfig.server.url, {}, 'auth.getToken', [{ 19 | timestamp, 20 | signature, 21 | }]).then(data => { 22 | return data.token 23 | }) 24 | } 25 | 26 | describe('test JWT signature', () => { 27 | it('test signature', () => { 28 | const timestamp = getTimestamp() 29 | const signature = personalSign(localConfig.wallet.privateKey, timestamp.toString()) 30 | 31 | // Same data as before 32 | const message = ethUtil.toBuffer(timestamp.toString()) 33 | const msgHash = ethUtil.hashPersonalMessage(message) 34 | const signatureBuffer = ethUtil.toBuffer(signature) 35 | const sigParams = ethUtil.fromRpcSig(signatureBuffer) 36 | const publicKey = ethUtil.ecrecover(msgHash, sigParams.v, sigParams.r, sigParams.s) 37 | const sender = ethUtil.publicToAddress(publicKey) 38 | const addr = ethUtil.bufferToHex(sender) 39 | 40 | expect(addr).toEqual(localConfig.wallet.address.toLowerCase()) 41 | }) 42 | }) 43 | 44 | describe('test getToken', () => { 45 | const testItems = [ 46 | { 47 | testMsg: 'get token success with now timestamp', 48 | timestamp: getTimestamp(), 49 | wallet: localConfig.wallet, 50 | result: true, 51 | }, 52 | { 53 | testMsg: 'get token success with timestamp 1 hour before', 54 | timestamp: getTimestamp() - 3599, 55 | wallet: localConfig.wallet, 56 | result: true, 57 | }, 58 | { 59 | testMsg: 'get token success with timestamp 1 hour after', 60 | timestamp: getTimestamp() + 3599, 61 | wallet: localConfig.wallet, 62 | result: true, 63 | }, 64 | { 65 | testMsg: 'should faild when getting token failed with timestamp more then 1 hour before', 66 | timestamp: getTimestamp() - 3602, 67 | wallet: localConfig.wallet, 68 | errorMsg: 'timestamp', 69 | result: false, 70 | }, 71 | { 72 | testMsg: 'should faild when getting token failed with timestamp more then 1 hour after', 73 | timestamp: getTimestamp() + 3660, 74 | wallet: localConfig.wallet, 75 | errorMsg: 'timestamp', 76 | result: false, 77 | }, 78 | { 79 | testMsg: 'should faild when getting token failed with address not in whitelist', 80 | timestamp: getTimestamp(), 81 | wallet: { 82 | address: '0xfb2a16a6c94268a0d5bccc89a78ab769896a84b2', 83 | privateKey: '1c8805ba17a35372391fd8f76f2a321dde0e63c4527f3da648c4febb9283dc99', 84 | }, 85 | errorMsg: 'address', 86 | result: false, 87 | }, 88 | ] 89 | testItems.forEach(item => { 90 | it(item.testMsg, async () => { 91 | if (item.result) { 92 | const token = await getToken(item.timestamp, item.wallet) 93 | expect(token).toBeTruthy() 94 | } else { 95 | let errorMsg = null 96 | try { 97 | await getToken(item.timestamp, item.wallet) 98 | } catch (e) { 99 | errorMsg = e.message 100 | } 101 | if (item.errorMsg) { 102 | expect(errorMsg).toMatch(item.errorMsg) 103 | } else { 104 | expect(errorMsg).toBeTruthy() 105 | } 106 | } 107 | }) 108 | }) 109 | }) 110 | 111 | describe('test send JWT request', () => { 112 | const testItems = [ 113 | { 114 | testMsg: 'send a JWT request with now timestamp', 115 | timestamp: getTimestamp(), 116 | wallet: localConfig.wallet, 117 | result: true, 118 | }, 119 | { 120 | testMsg: 'send a request with a invalid signature token', 121 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMHgxN2JmNTUyZGEwZWM0MGIxNjE0NjYwYTc3M2QzNzE5MDlkYmUzZWFhIiwiZXhwaXJlZEF0IjoiMTUyNTgzNDk5MCJ9.cOQz9gs1lH222EQKUppK49r-asd9ydBPWjn7S78mwZg', 122 | errorMsg: 'signature', 123 | result: false, 124 | }, 125 | { 126 | testMsg: 'send a request with a invalid signature token', 127 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMHgxN2JmNTUyZGEwZWM0MGIxNjE0NjYwYTc3M2QzNzE5MDlkYmUzZWFhIiwiZXhwaXJlZEF0IjoiMTUyNTgzNTAyNCJ9.Qgg2yk0lz2DQ4ClTTmlI7jPgpKXKO_YEGVp0AbR_MfI', 128 | errorMsg: 'signature', 129 | result: false, 130 | }, 131 | { 132 | testMsg: 'send a request with a invalid signature token', 133 | token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMHgxN2JmNTUyZGEwZWM0MGIxNjE0NjYwYTc3M2QzNzE5MDlkYmUzZWFhIiwiZXhwaXJlZEF0IjoiMTUyNTgzNTA0MyJ9.gGshnK7gqIHz2043-RfFNGunP0C1YwYI1STjUo_g4po', 134 | errorMsg: 'signature', 135 | result: false, 136 | }, 137 | { 138 | testMsg: 'send a request without access-token', 139 | result: false, 140 | }, 141 | ] 142 | 143 | testItems.forEach((item) => { 144 | it(item.testMsg, async () => { 145 | let header = {} 146 | 147 | if (item.timestamp) { 148 | const token = await getToken(item.timestamp, item.wallet) 149 | header = { 'access-token': token } 150 | } else if (item.token) { 151 | header = { 'access-token': item.token } 152 | } 153 | 154 | if (item.result) { 155 | const pairs = await jsonrpc.get(localConfig.server.url, header, 'dex.getPairList', [{ market: 'Tokenlon' }]) 156 | expect(pairs.length).toBeGreaterThan(0) 157 | expect(pairs.some(p => { 158 | return _.isEqual(p.base.contractAddress, sntWethPairData.base.contractAddress) && 159 | _.isEqual(p.quote.contractAddress, sntWethPairData.quote.contractAddress) 160 | })).toBe(true) 161 | 162 | } else { 163 | let errorMsg = '' 164 | try { 165 | await jsonrpc.get(localConfig.server.url, header, 'dex.getPairList', [{ market: 'Tokenlon' }]) 166 | } catch (e) { 167 | errorMsg = e.message 168 | } 169 | if (item.errorMsg) { 170 | expect(errorMsg).toMatch(item.errorMsg) 171 | } else { 172 | expect(errorMsg).toBeTruthy() 173 | } 174 | } 175 | }) 176 | }) 177 | }) -------------------------------------------------------------------------------- /tests/tokenlon/fillOrdersUpTo.test.ts: -------------------------------------------------------------------------------- 1 | import { constants } from '0x.js/lib/src/utils/constants' 2 | import { localConfig, localConfigUseToFill, web3ProviderUrl } from '../__mock__/config' 3 | import { sntWethPairData } from '../__mock__/pair' 4 | import { createTokenlon } from '../../src/index' 5 | import Tokenlon from '../../src/tokenlon' 6 | import { fromDecimalToUnit } from '../../src/utils/format' 7 | import { getTokenBalance } from '../../src/utils/ethereum' 8 | import { toBN } from '../../src/utils/math' 9 | import { waitMined, waitSeconds } from '../__utils__/wait' 10 | import { filterOrderBook } from '../__utils__/helper' 11 | import { TokenlonError } from '../../src/types' 12 | 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 300000 14 | 15 | let tokenlon = null as Tokenlon 16 | const baseQuote = { 17 | base: sntWethPairData.base.symbol, 18 | quote: sntWethPairData.quote.symbol, 19 | } 20 | 21 | beforeAll(async () => { 22 | tokenlon = await createTokenlon(localConfigUseToFill) 23 | }) 24 | 25 | describe('test fillOrdersUpTo', () => { 26 | it(`should thorw error because these orders not same pair`, async () => { 27 | const orderBook1 = await tokenlon.getOrderBook(baseQuote) 28 | const orderBook2 = await tokenlon.getOrderBook({ 29 | base: 'KNC', 30 | quote: 'WETH', 31 | }) 32 | const processedOrderBook1 = { 33 | asks: filterOrderBook(orderBook1.asks), 34 | bids: filterOrderBook(orderBook1.bids), 35 | } 36 | const processedOrderBook2 = { 37 | asks: filterOrderBook(orderBook2.asks), 38 | bids: filterOrderBook(orderBook2.bids), 39 | } 40 | 41 | try { 42 | await tokenlon.fillOrdersUpTo({ 43 | ...baseQuote, 44 | side: 'SELL', 45 | amount: 1000000, 46 | rawOrders: [processedOrderBook1.bids[0].rawOrder, processedOrderBook2.bids[0].rawOrder], 47 | }) 48 | } catch (e) { 49 | expect(e.toString()).toMatch(TokenlonError.OrdersMustBeSamePairAndSameSideWithFillOrdersUpTo) 50 | } 51 | }) 52 | 53 | it(`should thorw error because these orders not same side`, async () => { 54 | const orderBook = await tokenlon.getOrderBook(baseQuote) 55 | const processedOrderBook = { 56 | asks: filterOrderBook(orderBook.asks), 57 | bids: filterOrderBook(orderBook.bids), 58 | } 59 | 60 | try { 61 | await tokenlon.fillOrdersUpTo({ 62 | ...baseQuote, 63 | side: 'BUY', 64 | amount: 1000000, 65 | rawOrders: [processedOrderBook.asks[0].rawOrder, processedOrderBook.bids[0].rawOrder], 66 | }) 67 | } catch (e) { 68 | expect(e.toString()).toMatch(TokenlonError.OrdersMustBeSamePairAndSameSideWithFillOrdersUpTo) 69 | } 70 | }) 71 | 72 | it(`should thorw error because side not match`, async () => { 73 | const orderBook = await tokenlon.getOrderBook(baseQuote) 74 | const processedOrderBook = { 75 | asks: filterOrderBook(orderBook.asks), 76 | bids: filterOrderBook(orderBook.bids), 77 | } 78 | 79 | try { 80 | await tokenlon.fillOrdersUpTo({ 81 | ...baseQuote, 82 | side: 'SELL', 83 | amount: 1000000, 84 | rawOrders: [processedOrderBook.asks[0].rawOrder, processedOrderBook.asks[1].rawOrder], 85 | }) 86 | } catch (e) { 87 | expect(e.toString()).toMatch(TokenlonError.InvalidSideWithOrder) 88 | } 89 | }) 90 | }) 91 | 92 | describe(`test fillOrdersUpTo flow`, () => { 93 | it(`test fillOrdersUpTo flow`, async () => { 94 | for (let amountType of ['small', 'large']) { 95 | let i = 0 96 | const isSmall = amountType === 'small' 97 | const baseAmount = isSmall ? 10 : 10000000 98 | 99 | for (let side of ['SELL', 'BUY']) { 100 | const isBuy = side === 'BUY' 101 | const operateOrderBookType = isBuy ? 'asks' : 'bids' 102 | const baseTokenBalance1 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 103 | const quoteTokenBalance1 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 104 | const orderBook = await tokenlon.getOrderBook(baseQuote) 105 | const processedOrderBook = { 106 | asks: filterOrderBook(orderBook.asks), 107 | bids: filterOrderBook(orderBook.bids), 108 | } 109 | // price sorted 110 | // use different order because of server pulling not update immediately 111 | const order1 = processedOrderBook[operateOrderBookType][i] 112 | const order2 = processedOrderBook[operateOrderBookType][i + 7] 113 | 114 | // use to test sort 115 | const operateOrderBookOrders = [order2, order1] 116 | const txHash = await tokenlon.fillOrdersUpTo({ 117 | ...baseQuote, 118 | side, 119 | amount: baseAmount, 120 | rawOrders: operateOrderBookOrders.map(o => o.rawOrder), 121 | }) 122 | 123 | console.log('txHash', txHash) 124 | await waitMined(txHash, 60) 125 | const baseTokenBalance2 = await tokenlon.getTokenBalance(sntWethPairData.base.symbol) 126 | const quoteTokenBalance2 = await tokenlon.getTokenBalance(sntWethPairData.quote.symbol) 127 | 128 | let baseUnitBN = toBN(0) 129 | let quoteUnitBN = toBN(0) 130 | 131 | if (isSmall) { 132 | baseUnitBN = toBN(baseAmount) 133 | // use order1 134 | quoteUnitBN = toBN(order1.price).times(baseUnitBN) 135 | } else { 136 | operateOrderBookOrders.forEach(o => { 137 | baseUnitBN = baseUnitBN.plus(o.amount) 138 | quoteUnitBN = quoteUnitBN.plus(toBN(o.price).times(o.amount)) 139 | }) 140 | } 141 | 142 | await new Promise((reosolve) => { 143 | setTimeout(() => { 144 | console.log(side) 145 | console.log('baseTokenBalance1', toBN(baseTokenBalance1).toString()) 146 | console.log('quoteTokenBalance1', toBN(quoteTokenBalance1).toString()) 147 | 148 | console.log('baseUnitBN', baseUnitBN.toString()) 149 | console.log('quoteUnitBN', quoteUnitBN.toString()) 150 | 151 | console.log('cal baseTokenBalance2', toBN(baseTokenBalance1)[isBuy ? 'plus' : 'minus'](baseUnitBN).toString()) 152 | console.log('baseTokenBalance2', toBN(baseTokenBalance2).toString()) 153 | 154 | console.log('cal quoteTokenBalance2', toBN(quoteTokenBalance1)[isBuy ? 'minus' : 'plus'](quoteUnitBN).toString()) 155 | console.log('quoteTokenBalance2', toBN(quoteTokenBalance2).toString()) 156 | 157 | reosolve() 158 | }, 10000) 159 | }) 160 | 161 | // change orders 162 | i += 2 163 | 164 | expect(toBN(baseTokenBalance1)[isBuy ? 'plus' : 'minus'](baseUnitBN).toFixed(6)).toEqual(baseTokenBalance2.toFixed(6)) 165 | expect(toBN(quoteTokenBalance1)[isBuy ? 'minus' : 'plus'](quoteUnitBN).toFixed(6)).toEqual(quoteTokenBalance2.toFixed(6)) 166 | } 167 | } 168 | }) 169 | }) -------------------------------------------------------------------------------- /tests/__mock__/order.ts: -------------------------------------------------------------------------------- 1 | import { Side, Server } from '../../src/types' 2 | import { zeroExConfig } from './config' 3 | import { FEE_RECIPIENT } from '../../src/constants' 4 | export const orders = [ 5 | { 6 | signedOrder: { 7 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 8 | maker: '0x20f0c6e79a763e1fe83de1fbf08279aa3953fb5f', 9 | taker: '0x0000000000000000000000000000000000000000', 10 | makerTokenAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 11 | takerTokenAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 12 | feeRecipient: FEE_RECIPIENT, 13 | makerTokenAmount: '10000000000000000', 14 | takerTokenAmount: '1940000000000', 15 | makerFee: '0', 16 | takerFee: '0', 17 | expirationUnixTimestampSec: '1549361646', 18 | salt: '42214642756128894000000000000000000000000000000000000000000000000000000000000', 19 | ecSignature: { 20 | v: 27, 21 | r: '0x9ac217c58690e67290e60e5d0588226588da3a049d468ade9200e7b0cf008859', 22 | s: '0x4bfcbe1baedc2be9b07dfea1d2b9eb1159121db93397093bc66c47bdc1e83282', 23 | }, 24 | }, 25 | simpleOrder: { 26 | side: 'SELL' as Side, 27 | base: 'SNT', 28 | quote: 'WETH', 29 | amount: 0.01, 30 | price: 0.000194, 31 | expirationUnixTimestampSec: 1549361646, 32 | }, 33 | fillTakerTokenAmount: 1940000000000, 34 | shouldThrowOnInsufficientBalanceOrAllowance: false, 35 | encodedData: '0xbc61394a00000000000000000000000020f0c6e79a763e1fe83de1fbf08279aa3953fb5f0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f26085682797370769bbb4391a0ed05510d9029d000000000000000000000000d0a1e359811322d97991e03f863a0c30c2cf029c0000000000000000000000006f7ae872e995f98fcd2a7d3ba17b7ddfb884305f000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000000001c3b102c80000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000005c5961ee5d54a41d99debc3a2dbb9898bb066541059be337488023d5e000000000000000000000000000000000000000000000000000000000000000000001c3b102c8000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001b9ac217c58690e67290e60e5d0588226588da3a049d468ade9200e7b0cf0088594bfcbe1baedc2be9b07dfea1d2b9eb1159121db93397093bc66c47bdc1e83282', 36 | }, 37 | ] 38 | 39 | export const orderBook = { 40 | bids: [ 41 | { 42 | orderId: 359828, 43 | protocol: '0x', 44 | rate: 0.000255, 45 | amountRemaining: '0.00000000473965', 46 | tradeType: 'bid' as Server.tradeType, 47 | payload: { 48 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 49 | maker: '0x75eb52e0265b80d5eac78e714b85ea9e199013aa', 50 | taker: '0x0000000000000000000000000000000000000000', 51 | makerTokenAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 52 | takerTokenAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 53 | feeRecipient: '0x0000000000000000000000000000000000000000', 54 | makerTokenAmount: '25500000000000000', 55 | takerTokenAmount: '100000000000000000000', 56 | makerFee: '0', 57 | takerFee: '0', 58 | expirationUnixTimestampSec: '1551342565', 59 | salt: '76669967321507220000000000000000000000000000000000000000000000000000000000000', 60 | ecSignature: { 61 | v: 27, 62 | r: '0x8afe8a8e4f7d18b65ae8e28ded6dcea22a80b7997b73d75d93b4b04a4dcca3e9', 63 | s: '0x5abe9f4928b0b9ad1e46083c406adbfd9441897e39ab2eb82869653a39e2d5ab', 64 | }, 65 | }, 66 | }, 67 | { 68 | orderId: 373059, 69 | protocol: '0x', 70 | rate: 0.00019, 71 | amountRemaining: '1.27437233', 72 | tradeType: 'bid' as Server.tradeType, 73 | payload: { 74 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 75 | maker: '0xa8050208f9b869820fcdcc5dbe0505bfa4913e21', 76 | taker: '0x0000000000000000000000000000000000000000', 77 | makerTokenAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 78 | takerTokenAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 79 | feeRecipient: '0x0000000000000000000000000000000000000000', 80 | makerTokenAmount: '242130000000000', 81 | takerTokenAmount: '1274372330000000000', 82 | makerFee: '0', 83 | takerFee: '0', 84 | expirationUnixTimestampSec: '1553005325', 85 | salt: '86700055187767680000000000000000000000000000000000000000000000000000000000000', 86 | ecSignature: { 87 | v: 28, 88 | r: '0x63febf6d9442770990ba0c9fcfc7ca59895ee696eb686e70ab9e3fc91e0dd553', 89 | s: '0x5887b1bdc687da14f5b391ae6f81e26976f2172b5ef766b95bdb2f5a7e733f42', 90 | }, 91 | }, 92 | }, 93 | { 94 | orderId: 380463, 95 | protocol: '0x', 96 | rate: 0.00020908, 97 | amountRemaining: '30', 98 | tradeType: 'bid' as Server.tradeType, 99 | payload: { 100 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 101 | maker: '0xcd986f1b0942b25537585b79f4a53b109064334f', 102 | taker: '0x0000000000000000000000000000000000000000', 103 | makerTokenAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 104 | takerTokenAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 105 | feeRecipient: '0x0000000000000000000000000000000000000000', 106 | makerTokenAmount: '6272400000000000', 107 | takerTokenAmount: '30000000000000000000', 108 | makerFee: '0', 109 | takerFee: '0', 110 | expirationUnixTimestampSec: '1553068696', 111 | salt: '41919574519780110000000000000000000000000000000000000000000000000000000000000', 112 | ecSignature: { 113 | v: 28, 114 | r: '0x0e50e54c752cb64e554e66b634cd19b236fda524850040947b6a678ea50dc5e9', 115 | s: '0x488c9f4ee7f3dae3edfcd327e28395d446b106efe3466ba99e2fe7ab82705167', 116 | }, 117 | }, 118 | }, 119 | ], 120 | asks: [ 121 | { 122 | orderId: 453020, 123 | protocol: '0x', 124 | rate: 0.00025575, 125 | amountRemaining: '74.57846494', 126 | tradeType: 'ask' as Server.tradeType, 127 | payload: { 128 | exchangeContractAddress: '0x90fe2af704b34e0224bf2299c838e04d4dcf1364', 129 | maker: '0x20f0c6e79a763e1fe83de1fbf08279aa3953fb5f', 130 | taker: '0x0000000000000000000000000000000000000000', 131 | makerTokenAddress: '0xf26085682797370769bbb4391a0ed05510d9029d', 132 | takerTokenAddress: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 133 | feeRecipient: '0x0000000000000000000000000000000000000000', 134 | makerTokenAmount: '74578464940000000000', 135 | takerTokenAmount: '19073440000000000', 136 | makerFee: '0', 137 | takerFee: '0', 138 | expirationUnixTimestampSec: '1553600026', 139 | salt: '32147202111500004000000000000000000000000000000000000000000000000000000000000', 140 | ecSignature: { 141 | v: 27, 142 | r: '0xd02e3bb746a9af70eaf0472685520d8a973ffcce15d74c22b9acc4de4984c1b6', 143 | s: '0x141096b647077ce6d3be4cc26ed1089e7b2f375a57639380b7c0128e81833c65', 144 | }, 145 | }, 146 | }, 147 | ], 148 | } -------------------------------------------------------------------------------- /tests/utils/assert.test.ts: -------------------------------------------------------------------------------- 1 | import { getTimestamp } from '../../src/utils/helper' 2 | import { assert as assertUtils } from '0x.js/lib/src/utils/assert' 3 | import { assert, rewriteAssertUtils } from '../../src/utils/assert' 4 | import Web3 from 'web3' 5 | import { Web3Wrapper } from '@0xproject/web3-wrapper' 6 | import { SimpleOrder } from '../../src/types' 7 | import { Server } from '../../src/lib/server' 8 | 9 | import { validSimpleOrder, invalidSimpleOrder } from '../__mock__/simpleOrder' 10 | import { wallet, localServerUrl, web3ProviderUrl, localConfig } from '../__mock__/config' 11 | 12 | let pairs = [] 13 | const server = new Server(localServerUrl, wallet) 14 | const web3 = new Web3Wrapper(new Web3.providers.HttpProvider(web3ProviderUrl)) 15 | 16 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000 17 | 18 | beforeAll(async () => { 19 | pairs = await server.getPairList() 20 | return pairs 21 | }) 22 | 23 | describe('our assert utils', () => { 24 | describe('assert', () => { 25 | describe('assert.isValidSide', () => { 26 | ['BUY', 'SELL'].forEach(side => { 27 | it(`${side} is valid`, () => { 28 | expect(assert.isValidSide(side)).toBeUndefined() 29 | }) 30 | }); 31 | 32 | ['', 'buy', 'sell'].forEach(side => { 33 | it(`${side} is invalid`, () => { 34 | expect(() => { 35 | assert.isValidSide(side) 36 | }).toThrow() 37 | }) 38 | }) 39 | }) 40 | 41 | describe('assert.isValidPrecision', () => { 42 | const arr = [0.001, 23123.2223] 43 | arr.forEach((amount) => { 44 | it(`amount ${amount} is valid`, () => { 45 | expect(assert.isValidPrecision('amount', amount, 4)).toBeUndefined() 46 | }) 47 | }) 48 | arr.forEach((amount) => { 49 | it(`amount ${amount} is invalid`, () => { 50 | expect(() => { 51 | assert.isValidPrecision('amount', amount, 2) 52 | }).toThrow() 53 | }) 54 | }) 55 | }) 56 | 57 | describe('assert.isValidExpirationUnixTimestampSec', () => { 58 | const now = getTimestamp() 59 | it(`undefined is valid`, () => { 60 | expect(assert.isValidExpirationUnixTimestampSec()).toBeUndefined() 61 | }) 62 | it(`now + 10s is valid`, () => { 63 | expect(assert.isValidExpirationUnixTimestampSec(now + 10)).toBeUndefined() 64 | }) 65 | it(`now - 10s is invalid`, () => { 66 | expect(() => { 67 | assert.isValidExpirationUnixTimestampSec(now - 10) 68 | }).toThrow() 69 | }) 70 | }) 71 | 72 | describe('assert.isValidAmount', () => { 73 | const datas = [ 74 | { 75 | params: [{ 76 | price: 1, 77 | amount: 1, 78 | }, 1], 79 | result: true, 80 | }, 81 | { 82 | params: [{ 83 | price: 1, 84 | amount: 1, 85 | }, 0.2], 86 | result: true, 87 | }, 88 | { 89 | params: [{ 90 | price: 0.1, 91 | amount: 1, 92 | }, 0.2], 93 | result: false, 94 | }, 95 | ] 96 | 97 | datas.forEach(data => { 98 | if (data.result) { 99 | it(`${JSON.stringify(data.params)}is valid`, () => { 100 | expect(assert.isValidAmount.apply(assert.isValidAmount, data.params)).toBeUndefined() 101 | }) 102 | } else { 103 | it(`${JSON.stringify(data.params)}is invalid`, () => { 104 | expect(() => { 105 | assert.isValidAmount.apply(assert.isValidAmount, data.params) 106 | }).toThrow() 107 | }) 108 | } 109 | }) 110 | }) 111 | 112 | describe('assert.isValidSimpleOrder', () => { 113 | it('these simple orders are valid', () => { 114 | validSimpleOrder.forEach(o => expect(assert.isValidSimpleOrder(o as SimpleOrder, 8)).toBeUndefined()) 115 | }) 116 | 117 | it('these simple orders are invalid', () => { 118 | invalidSimpleOrder.forEach(o => { 119 | expect(() => { 120 | assert.isValidSimpleOrder(o as SimpleOrder, 8) 121 | }).toThrow() 122 | }) 123 | }) 124 | }) 125 | 126 | describe('assert.isValidrawOrder', () => { 127 | it('string {} is valid rawOrder', () => { 128 | expect(assert.isValidrawOrder('{}')).toBeUndefined() 129 | }); 130 | 131 | ['', null, undefined, '[]'].forEach(item => { 132 | it(`${item} is invalid`, () => { 133 | expect(() => { 134 | assert.isValidrawOrder(item as string) 135 | }).toThrow() 136 | }) 137 | }) 138 | }) 139 | 140 | describe('assert.isValidTokenNameString', () => { 141 | const arr = ['', 'SNT ', 'snt'] 142 | arr 143 | .filter(item => item.trim()) 144 | .map(item => item.trim().toUpperCase()) 145 | .forEach(item => { 146 | it(`${item} is valid`, () => { 147 | expect(assert.isValidTokenNameString('token name', item)).toBeUndefined() 148 | }) 149 | }) 150 | 151 | arr.forEach(item => { 152 | it(`${item} is invalid`, () => { 153 | expect(() => { 154 | assert.isValidTokenNameString('token name', item) 155 | }).toThrow() 156 | }) 157 | }) 158 | }) 159 | 160 | describe('assert.isValidGasPriceAdaptor', () => { 161 | const arr = ['safeLow', 'average', 'fast'] 162 | arr.forEach(item => { 163 | it(`${item} is valid`, () => { 164 | expect(assert.isValidGasPriceAdaptor(item)).toBeUndefined() 165 | }) 166 | }) 167 | 168 | arr.map(x => ' ' + x).forEach(item => { 169 | it(`${item} is invalid`, () => { 170 | expect(() => { 171 | assert.isValidGasPriceAdaptor(item) 172 | }).toThrow() 173 | }) 174 | }) 175 | }) 176 | 177 | describe('assert.isValidWallet', () => { 178 | it('wallet is valid', () => { 179 | expect(assert.isValidWallet(wallet)).toBeUndefined() 180 | }) 181 | 182 | it('wallet is invalid', () => { 183 | expect(() => { 184 | assert.isValidWallet({ 185 | ...wallet, 186 | address: wallet.address.slice(1), 187 | }) 188 | }).toThrow() 189 | }) 190 | }) 191 | 192 | describe('assert.isValidConfig', () => { 193 | it('config is valid', () => { 194 | expect(assert.isValidConfig(localConfig)).toBeUndefined() 195 | }) 196 | 197 | it('config is invalid', () => { 198 | expect(() => { 199 | assert.isValidConfig({ 200 | ...localConfig, 201 | web3: { 202 | providerUrl: '', 203 | }, 204 | }) 205 | }).toThrow() 206 | }) 207 | 208 | it('config is invalid', () => { 209 | expect(() => { 210 | assert.isValidConfig({ 211 | ...localConfig, 212 | server: { 213 | url: '', 214 | }, 215 | }) 216 | }).toThrow() 217 | }) 218 | }) 219 | 220 | describe('assert.isValidTokenName', () => { 221 | const arr = ['snt', 'weth'] 222 | 223 | arr 224 | .map(t => t.trim().toUpperCase()) 225 | .forEach(t => { 226 | it(`${t} is valid`, () => { 227 | expect(assert.isValidTokenName(t, pairs)).toBeUndefined() 228 | }) 229 | }) 230 | 231 | arr 232 | .concat('CREDO') 233 | .forEach(t => { 234 | it(`${t} is invalid`, () => { 235 | expect(() => { 236 | assert.isValidTokenName(t, pairs) 237 | }).toThrow() 238 | }) 239 | }) 240 | }) 241 | 242 | describe('assert.isValidBaseQuote', () => { 243 | it(`SNT-WETH pair is valid`, () => { 244 | expect(assert.isValidBaseQuote({ 245 | base: 'SNT', 246 | quote: 'WETH', 247 | }, pairs)).toBeUndefined() 248 | }) 249 | 250 | it(`SNT-WETH pair is valid`, () => { 251 | expect(() => { 252 | assert.isValidBaseQuote({ 253 | base: 'CREDO', 254 | quote: 'SNT', 255 | }, pairs) 256 | }).toThrow() 257 | }) 258 | }) 259 | }) 260 | }) 261 | 262 | describe('rewrite 0x.js AssertUtils', () => { 263 | it('original 0x.js asset utils isSenderAddressAsync should thorw error', async () => { 264 | try { 265 | await assertUtils.isSenderAddressAsync('address', wallet.address.toLowerCase(), web3) 266 | } catch (e) { 267 | expect(e.toString()).toMatch(/Error/) 268 | } 269 | }) 270 | 271 | it('overwrited 0x.js asset utils isSenderAddressAsync should skip check sender address', async () => { 272 | rewriteAssertUtils(assertUtils) 273 | const res = await assertUtils.isSenderAddressAsync('address', wallet.address, web3) 274 | expect(res).toBeUndefined() 275 | }) 276 | }) -------------------------------------------------------------------------------- /src/utils/dex.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { ETH_CONTRACT, FEE_RECIPIENT } from '../constants' 3 | import { lowerCase, getTimestamp, helpCompareStr, newError } from './helper' 4 | import { toBN, isBigNumber } from './math' 5 | import { fromUnitToDecimalBN, formatNumHelper, fromDecimalToUnit } from './format' 6 | import { Dex, DexOrderBNToString, SimpleOrder, Pair, Tokenlon, GlobalConfig, TokenlonError } from '../types' 7 | import { ZeroEx, OrderFillRequest } from '0x.js' 8 | import { personalECSignHex } from './sign' 9 | import { assert } from './assert' 10 | import { getPairByContractAddress, getPairBySignedOrder } from './pair' 11 | import { BigNumber } from '@0xproject/utils' 12 | 13 | // generate a dex order without salt by simple order 14 | export const generateDexOrderWithoutSalt = (params: Dex.GenerateDexOrderWithoutSaltParams): Dex.DexOrderWithoutSalt => { 15 | const { simpleOrder, pair, config } = params 16 | const { base, quote } = pair 17 | const { 18 | side, 19 | price, 20 | amount, 21 | expirationUnixTimestampSec, 22 | } = simpleOrder 23 | const isBuy = side === 'BUY' 24 | const baseTokenAmountUnit = amount 25 | const quoteTokenAmountUnit = toBN(price).times(amount) 26 | 27 | return { 28 | maker: lowerCase(config.wallet.address), 29 | taker: lowerCase(ETH_CONTRACT), 30 | makerTokenAmount: isBuy ? fromUnitToDecimalBN(quoteTokenAmountUnit, quote.decimal) : fromUnitToDecimalBN(baseTokenAmountUnit, base.decimal), 31 | takerTokenAmount: isBuy ? fromUnitToDecimalBN(baseTokenAmountUnit, base.decimal) : fromUnitToDecimalBN(quoteTokenAmountUnit, quote.decimal), 32 | makerTokenAddress: lowerCase(isBuy ? quote.contractAddress : base.contractAddress), 33 | takerTokenAddress: lowerCase(isBuy ? base.contractAddress : quote.contractAddress), 34 | 35 | // 智能合约上的判断 因此 expirationUnixTimestampSec 为必填项 未设置情况下设置到明年今日 36 | // if(block.timestamp >= order.expirationTimestampInSec) { 37 | // LogError(uint8(Errors.ORDER_EXPIRED), order.orderHash); 38 | // return 0; 39 | // } 40 | expirationUnixTimestampSec: toBN(expirationUnixTimestampSec || getTimestamp() + 86400 * 365), 41 | exchangeContractAddress: lowerCase(config.zeroEx.exchangeContractAddress), 42 | feeRecipient: FEE_RECIPIENT, 43 | // TODO setting fees 44 | makerFee: toBN(0), 45 | takerFee: toBN(0), 46 | } 47 | } 48 | 49 | // use 0x.js and privateKey's personal sign, to generate a dex order 50 | export const getSignedOrder = (orderWithoutSalt: Dex.DexOrderWithoutSalt, config: GlobalConfig): Dex.SignedDexOrder => { 51 | const order = { 52 | ...orderWithoutSalt, 53 | salt: ZeroEx.generatePseudoRandomSalt(), 54 | } as Dex.DexOrder 55 | const hash = ZeroEx.getOrderHashHex(order) 56 | 57 | return { 58 | ...order, 59 | ecSignature: personalECSignHex(config.wallet.privateKey, hash), 60 | } 61 | } 62 | 63 | const translateValueHelper = (obj: object, check: (v) => boolean, operate: (v) => any): any => { 64 | let result = {} 65 | _.keys(obj).forEach((key) => { 66 | const v = obj[key] 67 | result[key] = check(v) ? operate(v) : v 68 | }) 69 | return result 70 | } 71 | 72 | // translate a dex order with bigNumber to string 73 | export const orderBNToString = (order: Dex.SignedDexOrder): DexOrderBNToString => { 74 | let result = {} as DexOrderBNToString 75 | result = translateValueHelper(order, isBigNumber, (v) => v.toString()) 76 | return result 77 | } 78 | 79 | export const orderStringToBN = (order: DexOrderBNToString | Dex.DexOrderWithoutSalt): Dex.SignedDexOrder => { 80 | let result = {} as Dex.SignedDexOrder 81 | const check = (v) => _.isString(v) && !v.startsWith('0x') 82 | result = translateValueHelper(order, check, toBN) 83 | return result 84 | } 85 | 86 | export const getAmountTotal = (params: Dex.GetSimpleOrderParams): number => { 87 | const { order, pair } = params 88 | const { base, quote, precision } = pair 89 | const formatPrice = formatNumHelper(precision) 90 | const { makerTokenAddress, makerTokenAmount, takerTokenAddress, takerTokenAmount } = order 91 | const isBuy = helpCompareStr(base.contractAddress, takerTokenAddress) && helpCompareStr(quote.contractAddress, makerTokenAddress) 92 | const baseTokenAmountBN = isBuy ? fromDecimalToUnit(takerTokenAmount, base.decimal) : fromDecimalToUnit(makerTokenAmount, base.decimal) 93 | const amountTotal = toBN(formatPrice(baseTokenAmountBN.toString(), false)).toNumber() 94 | 95 | return amountTotal 96 | } 97 | 98 | // translate dex order to simple order, for us to check which order we want to fill 99 | export const getSimpleOrder = (params: Dex.GetSimpleOrderParams): SimpleOrder => { 100 | const { order, pair, amountRemaining } = params 101 | const { base, quote, precision } = pair 102 | const formatPrice = formatNumHelper(precision) 103 | const { 104 | expirationUnixTimestampSec, 105 | makerTokenAddress, 106 | makerTokenAmount, 107 | takerTokenAddress, 108 | takerTokenAmount, 109 | } = order 110 | const isBuy = helpCompareStr(base.contractAddress, takerTokenAddress) && helpCompareStr(quote.contractAddress, makerTokenAddress) 111 | const side = isBuy ? 'BUY' : 'SELL' 112 | const baseTokenAmountBN = isBuy ? fromDecimalToUnit(takerTokenAmount, base.decimal) : fromDecimalToUnit(makerTokenAmount, base.decimal) 113 | const quoteTokenAmountBN = isBuy ? fromDecimalToUnit(makerTokenAmount, quote.decimal) : fromDecimalToUnit(takerTokenAmount, quote.decimal) 114 | const amountRemainingBN = amountRemaining && toBN(amountRemaining).lt(baseTokenAmountBN) ? toBN(amountRemaining) : baseTokenAmountBN 115 | const price = toBN(formatPrice(quoteTokenAmountBN.dividedBy(baseTokenAmountBN).toString(), false)).toNumber() 116 | const amount = toBN(formatPrice(amountRemainingBN.toString(), false)).toNumber() 117 | 118 | return { 119 | side, 120 | price, 121 | amount, 122 | expirationUnixTimestampSec: toBN(expirationUnixTimestampSec).toNumber(), 123 | } 124 | } 125 | 126 | export const getSimpleOrderWithBaseQuoteBySignedOrder = (order: DexOrderBNToString, pairs: Pair.ExchangePair[]): Tokenlon.SimpleOrderWithBaseQuote => { 127 | const pair = getPairBySignedOrder(order, pairs) 128 | const simpleOrder = getSimpleOrder({ 129 | order, 130 | pair, 131 | }) 132 | return { 133 | base: pair.base.symbol.toUpperCase(), 134 | quote: pair.quote.symbol.toUpperCase(), 135 | ...simpleOrder, 136 | } 137 | } 138 | 139 | export const translateOrderBookToSimple = (params: Dex.TranslateOrderBookToSimpleParams): Tokenlon.OrderBookItem[] => { 140 | const { orderbookItems, pair, wallet } = params 141 | return orderbookItems.map(item => { 142 | const { amountRemaining, payload } = item 143 | return { 144 | ...getSimpleOrder({ 145 | pair, 146 | order: payload, 147 | amountRemaining, 148 | }), 149 | amountTotal: getAmountTotal({ pair, order: payload }), 150 | isMaker: !!wallet && helpCompareStr(wallet.address, payload.maker), 151 | rawOrder: JSON.stringify(payload), 152 | } 153 | }) 154 | } 155 | 156 | // fill order need to change side 157 | // if is buy, then maker token is weth, so we use quote decimal 158 | export const getFillTakerTokenAmountBN = (side, amount, price, pair): BigNumber => { 159 | return side === 'SELL' ? fromUnitToDecimalBN(amount, pair.base.decimal) : fromUnitToDecimalBN(toBN(amount).times(price).toNumber(), pair.quote.decimal) 160 | } 161 | 162 | export const getFillTakerTokenAmountBNByUpToOrders = (side, amount, simpleOrders, pair) => { 163 | if (side === 'SELL') { 164 | return fromUnitToDecimalBN(amount, pair.base.decimal) 165 | } 166 | let remainedAmountBN = toBN(amount) 167 | let takerTokenAmountBN = toBN(0) 168 | simpleOrders.some(so => { 169 | const orderPrice = so.price 170 | const orderAmount = so.amount 171 | // if base amount is too large, then set takerTokenAmount as orders quote amounts 172 | if (remainedAmountBN.gt(toBN(orderAmount))) { 173 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', orderAmount, orderPrice, pair)) 174 | remainedAmountBN = remainedAmountBN.minus(toBN(orderAmount)) 175 | // // if orders quote amount larger then base amount, then set takerTokenAmount as calculated amount 176 | } else { 177 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', remainedAmountBN, orderPrice, pair)) 178 | return true 179 | } 180 | }) 181 | 182 | return takerTokenAmountBN 183 | } 184 | 185 | export const getOrderFillRequest = (params: Tokenlon.FillOrderParams, pairs: Pair.ExchangePair[]): OrderFillRequest => { 186 | const { rawOrder, price, amount, side, base, quote } = params 187 | assert.isValidrawOrder(rawOrder) 188 | assert.isValidBaseQuote(params, pairs) 189 | 190 | const order = JSON.parse(rawOrder) 191 | const isBuy = side === 'BUY' 192 | const pair = getPairByContractAddress(isBuy ? { 193 | base: order.makerTokenAddress, 194 | quote: order.takerTokenAddress, 195 | } : { 196 | base: order.takerTokenAddress, 197 | quote: order.makerTokenAddress, 198 | }, pairs) 199 | 200 | if (!helpCompareStr(pair.base.symbol, base) || !helpCompareStr(pair.quote.symbol, quote)) { 201 | throw newError(TokenlonError.UnsupportedPair) 202 | } 203 | 204 | assert.isValidSimpleOrder(params, pair.precision) 205 | 206 | const simpleOrder = getSimpleOrder({ 207 | order, 208 | pair, 209 | }) 210 | const formatPrice = formatNumHelper(pair.precision) 211 | if (formatPrice(simpleOrder.price, false) !== formatPrice(price, false)) { 212 | throw newError(TokenlonError.InvalidPriceWithToBeFilledOrder) 213 | } 214 | const takerTokenAmountBN = getFillTakerTokenAmountBN(side, amount, price, pair) 215 | return { 216 | signedOrder: orderStringToBN(order), 217 | takerTokenFillAmount: takerTokenAmountBN, 218 | } 219 | } -------------------------------------------------------------------------------- /tests/utils/dex.test.ts: -------------------------------------------------------------------------------- 1 | import * as _ from 'lodash' 2 | import { simpleOrders } from '../__mock__/simpleOrder' 3 | import { getPairBySymbol } from '../../src/utils/pair' 4 | import { Server } from '../../src/lib/server' 5 | import { helpCompareStr, getTimestamp } from '../../src/utils/helper' 6 | import { toBN, isBigNumber } from '../../src/utils/math' 7 | import { Side, Pair } from '../../src/types' 8 | import { 9 | generateDexOrderWithoutSalt, 10 | getSignedOrder, 11 | orderBNToString, 12 | orderStringToBN, 13 | getSimpleOrder, 14 | translateOrderBookToSimple, 15 | getFillTakerTokenAmountBN, 16 | getOrderFillRequest, 17 | getFillTakerTokenAmountBNByUpToOrders, 18 | } from '../../src/utils/dex' 19 | import { personalECSignHex, personalECSign } from '../../src/utils/sign' 20 | import { fromUnitToDecimalBN, fromUnitToDecimal } from '../../src/utils/format' 21 | import { orders, orderBook } from '../__mock__/order' 22 | import { sntWethPairData } from '../__mock__/pair' 23 | import { localServerUrl, wallet, zeroExConfig, localConfig } from '../__mock__/config' 24 | import { signatureUtils } from '0x.js/lib/src/utils/signature_utils' 25 | import { ZeroEx } from '0x.js' 26 | import { FEE_RECIPIENT } from '../../src/constants' 27 | 28 | let pairs = [] 29 | let pair = null 30 | const server = new Server(localServerUrl, wallet) 31 | 32 | beforeAll(async () => { 33 | pairs = await server.getPairList() 34 | pair = getPairBySymbol({ 35 | base: 'SNT', 36 | quote: 'WETH', 37 | }, pairs) 38 | return pairs 39 | }) 40 | 41 | describe('test dex simple utils', () => { 42 | it('test getSimpleOrder', () => { 43 | const order = orders[0] 44 | const signedOrder = order.signedOrder 45 | const simpleOrder = getSimpleOrder({ 46 | pair, 47 | order: { 48 | ...signedOrder, 49 | exchangeContractAddress: zeroExConfig.exchangeContractAddress, 50 | makerTokenAmount: toBN(signedOrder.makerTokenAmount).toString(), 51 | takerTokenAmount: toBN(signedOrder.takerTokenAmount).toString(), 52 | makerFee: toBN(signedOrder.makerFee).toString(), 53 | takerFee: toBN(signedOrder.takerFee).toString(), 54 | expirationUnixTimestampSec: toBN(signedOrder.expirationUnixTimestampSec).toString(), 55 | salt: toBN(signedOrder.salt).toString(), 56 | }, 57 | }) 58 | 59 | expect(simpleOrder.price).toEqual(order.simpleOrder.price) 60 | expect(simpleOrder.amount).toEqual(order.simpleOrder.amount) 61 | expect(simpleOrder.expirationUnixTimestampSec).toEqual(order.simpleOrder.expirationUnixTimestampSec) 62 | }) 63 | 64 | describe('test translateOrderBookToSimple', () => { 65 | orderBook.bids.concat(orderBook.asks).forEach(o => { 66 | it(`test order orderId: ${o.orderId} - ${o.tradeType} - ${o.rate} - ${o.amountRemaining}`, () => { 67 | const simpleOrders = translateOrderBookToSimple({ 68 | pair, 69 | wallet, 70 | orderbookItems: [o], 71 | }) 72 | const simpleOrder = simpleOrders[0] 73 | expect(simpleOrder.side).toEqual(o.tradeType === 'bid' ? 'BUY' : 'SELL') 74 | expect(simpleOrder.isMaker).toEqual(helpCompareStr(wallet.address, o.payload.maker)) 75 | expect(simpleOrder.expirationUnixTimestampSec).toEqual(+o.payload.expirationUnixTimestampSec) 76 | expect(simpleOrder.rawOrder).toEqual(JSON.stringify(o.payload)) 77 | // TODO price / amount process 78 | // maybe server calculate wrong 79 | // expect(simpleOrder.price).toEqual(o.rate) 80 | // expect(toBN(simpleOrder.amount).toString()).toEqual(o.amountRemaining) 81 | }) 82 | }) 83 | }) 84 | 85 | describe('test getFillTakerTokenAmountBN', () => { 86 | orderBook.bids.concat(orderBook.asks).forEach(o => { 87 | it(`test order orderId: ${o.orderId} - ${o.tradeType === 'bid' ? 'SELL' : 'BUY'} - ${o.rate} - ${o.amountRemaining}`, () => { 88 | const simpleOrders = translateOrderBookToSimple({ 89 | pair, 90 | wallet, 91 | orderbookItems: [o], 92 | }) 93 | const simpleOrder = simpleOrders[0] 94 | const fillTakerTokenAmountBN = getFillTakerTokenAmountBN(simpleOrder.side === 'BUY' ? 'SELL' : 'BUY', simpleOrder.amount, simpleOrder.price, pair) 95 | expect(fillTakerTokenAmountBN.toString()).toEqual( 96 | ( 97 | simpleOrder.side === 'BUY' ? 98 | fromUnitToDecimalBN(simpleOrder.amount, pair.base.decimal) : 99 | fromUnitToDecimalBN(toBN(simpleOrder.amount).times(simpleOrder.price).toNumber(), pair.quote.decimal) 100 | ).toString(), 101 | ) 102 | }) 103 | }) 104 | }) 105 | 106 | describe('test getFillTakerTokenAmountBNByUpToOrders', () => { 107 | it('check SELL', () => { 108 | const baseAmount = 10000 109 | const orders = simpleOrders.filter(o => o.side === 'BUY') 110 | const fillTakerTokenAmountBN = getFillTakerTokenAmountBNByUpToOrders('SELL', baseAmount, orders, sntWethPairData) 111 | 112 | expect(isBigNumber(fillTakerTokenAmountBN)).toEqual(true) 113 | expect(fillTakerTokenAmountBN.toString()).toEqual(fromUnitToDecimal(baseAmount, sntWethPairData.base.decimal, 10)) 114 | }) 115 | 116 | it('should get listed orders takerTokenAmount when set baseAmount 1000000000 and is buy sell orders', () => { 117 | const baseAmount = 1000000000 118 | const orders = simpleOrders.filter(o => o.side === 'SELL') 119 | const fillTakerTokenAmountBN = getFillTakerTokenAmountBNByUpToOrders('BUY', baseAmount, orders, sntWethPairData) 120 | 121 | let remainedAmountBN = toBN(baseAmount) 122 | let takerTokenAmountBN = toBN(0) 123 | let quoteAmountBN = toBN(0) 124 | 125 | orders.some(so => { 126 | const orderPrice = so.price 127 | const orderAmount = so.amount 128 | quoteAmountBN = quoteAmountBN.plus(getFillTakerTokenAmountBN('BUY', orderAmount, orderPrice, sntWethPairData)) 129 | if (remainedAmountBN.gt(toBN(orderAmount))) { 130 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', orderAmount, orderPrice, sntWethPairData)) 131 | remainedAmountBN = remainedAmountBN.minus(toBN(orderAmount)) 132 | } else { 133 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', remainedAmountBN, orderPrice, sntWethPairData)) 134 | return true 135 | } 136 | }) 137 | 138 | expect(quoteAmountBN.toString()).toEqual(takerTokenAmountBN.toString()) 139 | expect(fillTakerTokenAmountBN.toString()).toEqual(takerTokenAmountBN.toString()) 140 | }) 141 | 142 | it('should get orders[0] quoteAmount as takerTokenAmount when set baseAmount 0.1 and is buy sell orders', () => { 143 | const baseAmount = 0.1 144 | const orders = simpleOrders.filter(o => o.side === 'SELL') 145 | const fillTakerTokenAmountBN = getFillTakerTokenAmountBNByUpToOrders('BUY', baseAmount, orders, sntWethPairData) 146 | 147 | let remainedAmountBN = toBN(baseAmount) 148 | let takerTokenAmountBN = toBN(0) 149 | let quoteAmountBN = getFillTakerTokenAmountBN('BUY', baseAmount, orders[0].price, sntWethPairData) 150 | 151 | orders.some(so => { 152 | const orderPrice = so.price 153 | const orderAmount = so.amount 154 | if (remainedAmountBN.gt(toBN(orderAmount))) { 155 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', orderAmount, orderPrice, sntWethPairData)) 156 | remainedAmountBN = remainedAmountBN.minus(toBN(orderAmount)) 157 | } else { 158 | takerTokenAmountBN = takerTokenAmountBN.plus(getFillTakerTokenAmountBN('BUY', remainedAmountBN, orderPrice, sntWethPairData)) 159 | return true 160 | } 161 | }) 162 | 163 | expect(quoteAmountBN.toString()).toEqual(takerTokenAmountBN.toString()) 164 | expect(fillTakerTokenAmountBN.toString()).toEqual(takerTokenAmountBN.toString()) 165 | }) 166 | }) 167 | }) 168 | 169 | describe('test dex flow by dex utils', () => { 170 | const baseQuote = { 171 | base: 'SNT', 172 | quote: 'WETH', 173 | } 174 | const testData = simpleOrders 175 | 176 | const pair = sntWethPairData as Pair.ExchangePair 177 | 178 | testData.map(simple => { 179 | return { 180 | ...baseQuote, 181 | ...simple, 182 | } 183 | }).forEach(simpleOrder => { 184 | // simpleOrder 185 | // generateDexOrderWithoutSalt 186 | // getSignedOrder 187 | // orderBNToString 188 | // orderStringToBN 189 | // getSimpleOrder 190 | // string order to orderbook item 191 | // translateOrderBookToSimple 192 | // getFillTakerTokenAmountBN 193 | // getOrderFillRequest 194 | describe(`test item ${simpleOrder.side} - ${simpleOrder.amount} - ${simpleOrder.price}`, () => { 195 | const orderWithoutSalt = generateDexOrderWithoutSalt({ 196 | config: localConfig, 197 | simpleOrder, 198 | pair: sntWethPairData, 199 | }) 200 | 201 | it('test generateDexOrderWithoutSalt', () => { 202 | const { side, amount, price } = simpleOrder 203 | const isBuy = side === 'BUY' 204 | const { maker, takerTokenAddress, makerTokenAddress, makerTokenAmount, takerTokenAmount, expirationUnixTimestampSec, exchangeContractAddress, feeRecipient } = orderWithoutSalt 205 | const makerAmountBN = isBuy ? fromUnitToDecimalBN(toBN(amount).times(price), pair.quote.decimal) : fromUnitToDecimalBN(amount, pair.base.decimal) 206 | const takerAmountBN = isBuy ? fromUnitToDecimalBN(amount, pair.base.decimal) : fromUnitToDecimalBN(toBN(amount).times(price), pair.quote.decimal) 207 | 208 | expect(feeRecipient).toBe(FEE_RECIPIENT) 209 | expect(localConfig.wallet.address.toLowerCase()).toEqual(maker) 210 | expect(takerTokenAddress).toEqual((isBuy ? pair.base.contractAddress : pair.quote.contractAddress).toLowerCase()) 211 | expect(makerTokenAddress).toEqual((isBuy ? pair.quote.contractAddress : pair.base.contractAddress).toLowerCase()) 212 | if (simpleOrder.expirationUnixTimestampSec) { 213 | expect(+expirationUnixTimestampSec).toEqual(simpleOrder.expirationUnixTimestampSec) 214 | } else { 215 | expect(+expirationUnixTimestampSec).toBeLessThanOrEqual(getTimestamp() + 86400 * 365) 216 | expect(+expirationUnixTimestampSec).toBeGreaterThanOrEqual(getTimestamp() + 86400 * 365 - 2) 217 | } 218 | 219 | expect(exchangeContractAddress).toEqual(localConfig.zeroEx.exchangeContractAddress.toLowerCase()) 220 | 221 | expect(makerAmountBN.eq(makerTokenAmount)).toBe(true) 222 | expect(takerAmountBN.eq(takerTokenAmount)).toBe(true) 223 | }) 224 | 225 | const signedOrder = getSignedOrder(orderWithoutSalt, localConfig) 226 | it('test getSignedOrder', () => { 227 | const orderHash = ZeroEx.getOrderHashHex(signedOrder) 228 | expect(signatureUtils.isValidSignature(orderHash, signedOrder.ecSignature, localConfig.wallet.address.toLowerCase())).toBe(true) 229 | }) 230 | 231 | const orderString = orderBNToString(signedOrder) 232 | it('test orderBNToString', () => { 233 | [ 234 | 'maker', 235 | 'taker', 236 | 'makerTokenAmount', 237 | 'takerTokenAmount', 238 | 'makerTokenAddress', 239 | 'takerTokenAddress', 240 | 'expirationUnixTimestampSec', 241 | 'exchangeContractAddress', 242 | 'feeRecipient', 243 | 'makerFee', 244 | 'takerFee', 245 | 'salt', 246 | ].forEach((key) => { 247 | expect(orderString[key]).toEqual(signedOrder[key].toString()) 248 | }) 249 | expect(_.isEqual(signedOrder.ecSignature, orderString.ecSignature)).toBe(true) 250 | }) 251 | 252 | const orderBN = orderStringToBN(orderString) 253 | it('test orderStringToBN', () => { 254 | [ 255 | 'maker', 256 | 'taker', 257 | 'makerTokenAddress', 258 | 'takerTokenAddress', 259 | 'exchangeContractAddress', 260 | 'feeRecipient', 261 | ].forEach((key) => { 262 | expect(_.isString(orderBN[key])).toBe(true) 263 | expect(orderBN[key]).toEqual(signedOrder[key]) 264 | }); 265 | 266 | [ 267 | 'expirationUnixTimestampSec', 268 | 'makerTokenAmount', 269 | 'takerTokenAmount', 270 | 'makerFee', 271 | 'takerFee', 272 | 'salt', 273 | ].forEach((key) => { 274 | expect(isBigNumber(orderBN[key])).toBe(true) 275 | expect(orderBN[key].toString()).toBe(signedOrder[key].toString()) 276 | }) 277 | 278 | expect(_.isEqual(signedOrder.ecSignature, orderString.ecSignature)).toBe(true) 279 | }) 280 | 281 | const gotSimpleOrder = getSimpleOrder({ 282 | order: orderString, 283 | pair, 284 | }) 285 | it('test getSimpleOrder', () => { 286 | ['price', 'amount', 'side', 'expirationUnixTimestampSec'].forEach(key => { 287 | if (key !== 'expirationUnixTimestampSec' || simpleOrder.expirationUnixTimestampSec) { 288 | expect(gotSimpleOrder[key]).toEqual(simpleOrder[key]) 289 | } else { 290 | expect(gotSimpleOrder[key]).toBeLessThanOrEqual(getTimestamp() + 86400 * 365) 291 | expect(gotSimpleOrder[key]).toBeGreaterThan(getTimestamp() + 86400 * 365 - 5) 292 | } 293 | }) 294 | }) 295 | 296 | const orderBookItem = { 297 | rate: simpleOrder.price, 298 | tradeType: simpleOrder.side === 'BUY' ? 'bid' : 'ask', 299 | amountRemaining: simpleOrder.amount.toString(), 300 | payload: orderString, 301 | } 302 | const translatedSimpleOrderFromOrderBook = translateOrderBookToSimple({ 303 | orderbookItems: [orderBookItem], 304 | pair, 305 | wallet, 306 | })[0] 307 | it('test translateOrderBookToSimple', () => { 308 | ['price', 'amount', 'side', 'expirationUnixTimestampSec'].forEach(key => { 309 | if (key !== 'expirationUnixTimestampSec' || simpleOrder.expirationUnixTimestampSec) { 310 | expect(translatedSimpleOrderFromOrderBook[key]).toEqual(simpleOrder[key]) 311 | } else { 312 | expect(translatedSimpleOrderFromOrderBook[key]).toBeLessThanOrEqual(getTimestamp() + 86400 * 365) 313 | expect(translatedSimpleOrderFromOrderBook[key]).toBeGreaterThan(getTimestamp() + 86400 * 365 - 5) 314 | } 315 | }) 316 | expect(translatedSimpleOrderFromOrderBook.rawOrder).toEqual(JSON.stringify(orderString)) 317 | }) 318 | 319 | const fillTakerTokenAmountBN = getFillTakerTokenAmountBN( 320 | translatedSimpleOrderFromOrderBook.side === 'BUY' ? 'SELL' : 'BUY', 321 | translatedSimpleOrderFromOrderBook.amount, 322 | translatedSimpleOrderFromOrderBook.price, 323 | pair, 324 | ) 325 | it('test getFillTakerTokenAmountBN', () => { 326 | expect(isBigNumber(fillTakerTokenAmountBN)).toBe(true) 327 | expect(fillTakerTokenAmountBN.toNumber()).toEqual(signedOrder.takerTokenAmount.toNumber()) 328 | expect(fillTakerTokenAmountBN.eq(signedOrder.takerTokenAmount)).toBe(true) 329 | }) 330 | 331 | it('test getOrderFillRequest', () => { 332 | const orderFillReqest = getOrderFillRequest({ 333 | ...baseQuote, 334 | side: translatedSimpleOrderFromOrderBook.side === 'BUY' ? 'SELL' : 'BUY', 335 | price: translatedSimpleOrderFromOrderBook.price, 336 | amount: translatedSimpleOrderFromOrderBook.amount, 337 | rawOrder: translatedSimpleOrderFromOrderBook.rawOrder, 338 | }, pairs) 339 | const { signedOrder, takerTokenFillAmount } = orderFillReqest 340 | expect(_.isEqual(orderBNToString(signedOrder), orderString)).toBe(true) 341 | 342 | expect(isBigNumber(takerTokenFillAmount)).toBe(true) 343 | expect(takerTokenFillAmount.toNumber()).toEqual(signedOrder.takerTokenAmount.toNumber()) 344 | expect(takerTokenFillAmount.eq(signedOrder.takerTokenAmount)).toBe(true) 345 | }) 346 | }) 347 | }) 348 | }) -------------------------------------------------------------------------------- /src/tokenlon.ts: -------------------------------------------------------------------------------- 1 | import { ZeroEx } from '0x.js' 2 | import * as Web3 from 'web3' 3 | import { constants } from '0x.js/lib/src/utils/constants' 4 | import { fromDecimalToUnit, fromUnitToDecimalBN } from './utils/format' 5 | import { assert as zeroExAssertUtils } from '@0xproject/assert' 6 | import { assert } from './utils/assert' 7 | import { Server } from './lib/server' 8 | import { getPairBySymbol, getTokenByName, getPairBySignedOrder } from './utils/pair' 9 | import { 10 | orderStringToBN, 11 | translateOrderBookToSimple, 12 | getOrderFillRequest, 13 | getSimpleOrder, 14 | getFillTakerTokenAmountBNByUpToOrders, 15 | orderBNToString, 16 | } from './utils/dex' 17 | import { Pair, Tokenlon as TokenlonInterface, GlobalConfig, TokenlonError } from './types' 18 | import { helpCompareStr, newError, convertTrades, convertTokenlonTxOptsTo0xOpts } from './utils/helper' 19 | import { BigNumber } from '@0xproject/utils' 20 | 21 | export default class Tokenlon { 22 | constructor() {} 23 | 24 | private _config: GlobalConfig 25 | private _pairs: Pair.ExchangePair[] 26 | 27 | server: Server 28 | web3Wrapper: Web3 29 | zeroExWrapper: ZeroEx 30 | utils: { 31 | getSimpleOrderWithBaseQuoteBySignedOrder: any, 32 | getSignedOrderBySimpleOrderAsync: any, 33 | orderStringToBN: any, 34 | orderBNToString: any, 35 | } 36 | 37 | async getPairs(): Promise { 38 | return Promise.resolve(this._pairs) 39 | } 40 | 41 | async getPairInfo(baseQuote: TokenlonInterface.BaseQuote): Promise { 42 | const pair = getPairBySymbol(baseQuote, this._pairs) 43 | return Promise.resolve(pair) 44 | } 45 | 46 | async getTokenInfo(tokenName: string): Promise { 47 | const token = getTokenByName(tokenName, this._pairs) 48 | return Promise.resolve(token) 49 | } 50 | 51 | async getOrderBook(params: TokenlonInterface.BaseQuote): Promise { 52 | const pair = getPairBySymbol(params, this._pairs) 53 | const baseTokenAddress = pair.base.contractAddress 54 | const quoteTokenAddress = pair.quote.contractAddress 55 | const { wallet } = this._config 56 | const orderBook = await this.server.getOrderBook({ baseTokenAddress, quoteTokenAddress }) 57 | return { 58 | asks: translateOrderBookToSimple({ 59 | orderbookItems: orderBook.asks, 60 | pair, 61 | wallet, 62 | }).sort((a, b) => a.price - b.price), 63 | bids: translateOrderBookToSimple({ 64 | orderbookItems: orderBook.bids, 65 | pair, 66 | wallet, 67 | }).sort((a, b) => b.price - a.price), 68 | } 69 | } 70 | 71 | async getOrders(params: TokenlonInterface.GetOrdersParams): Promise { 72 | const pair = getPairBySymbol(params, this._pairs) 73 | const baseTokenAddress = pair.base.contractAddress 74 | const quoteTokenAddress = pair.quote.contractAddress 75 | const { page, perpage } = params 76 | const { wallet } = this._config 77 | const myOrders = await this.server.getOrders({ 78 | maker: wallet.address, 79 | page, 80 | perpage, 81 | tokenPair: [baseTokenAddress, quoteTokenAddress], 82 | }) 83 | return translateOrderBookToSimple({ 84 | orderbookItems: myOrders, 85 | pair, 86 | }) 87 | } 88 | 89 | async getOrder(rawOrder: string): Promise { 90 | const signedOrder = JSON.parse(rawOrder) 91 | const orderHash = ZeroEx.getOrderHashHex(signedOrder) 92 | const pair = getPairBySignedOrder(signedOrder, this._pairs) 93 | const order = await this.server.getOrder(orderHash) 94 | const ob = translateOrderBookToSimple({ 95 | orderbookItems: [order], 96 | pair, 97 | })[0] 98 | return { 99 | ...ob, 100 | trades: order.trades, 101 | } 102 | } 103 | 104 | async getMakerTrades(params: TokenlonInterface.TradesParams): Promise { 105 | const pair = getPairBySymbol(params, this._pairs) 106 | const baseTokenAddress = pair.base.contractAddress 107 | const quoteTokenAddress = pair.quote.contractAddress 108 | const { timeRange, page, perpage } = params 109 | const { wallet } = this._config 110 | 111 | const trades = await this.server.getMakerTrades({ 112 | page, 113 | perpage, 114 | timeRange, 115 | baseTokenAddress, 116 | quoteTokenAddress, 117 | maker: wallet.address, 118 | }) 119 | 120 | return convertTrades(trades) 121 | } 122 | 123 | async getTakerTrades(params: TokenlonInterface.TradesParams): Promise { 124 | const pair = getPairBySymbol(params, this._pairs) 125 | const baseTokenAddress = pair.base.contractAddress 126 | const quoteTokenAddress = pair.quote.contractAddress 127 | const { timeRange, page, perpage } = params 128 | const { wallet } = this._config 129 | 130 | const trades = await this.server.getTakerTrades({ 131 | page, 132 | perpage, 133 | timeRange, 134 | baseTokenAddress, 135 | quoteTokenAddress, 136 | taker: wallet.address, 137 | }) 138 | return convertTrades(trades) 139 | } 140 | 141 | async placeOrder(params: TokenlonInterface.SimpleOrderWithBaseQuote): Promise { 142 | const pairs = this._pairs 143 | assert.isValidBaseQuote(params, pairs) 144 | const pair = getPairBySymbol(params, pairs) 145 | const { precision, quoteMinUnit } = pair 146 | assert.isValidSimpleOrder(params, precision) 147 | assert.isValidAmount(params, quoteMinUnit) 148 | const toBePlacedOrder = await this.utils.getSignedOrderBySimpleOrderAsync(params) 149 | await this.server.placeOrder(toBePlacedOrder) 150 | return { 151 | isMaker: true, 152 | side: params.side, 153 | price: params.price, 154 | amount: params.amount, 155 | amountTotal: params.amount, 156 | expirationUnixTimestampSec: params.expirationUnixTimestampSec || +toBePlacedOrder.expirationUnixTimestampSec, 157 | // for key sequence to be same with server order rawOrder 158 | rawOrder: JSON.stringify({ 159 | exchangeContractAddress: toBePlacedOrder.exchangeContractAddress, 160 | maker: toBePlacedOrder.maker, 161 | taker: toBePlacedOrder.taker, 162 | makerTokenAddress: toBePlacedOrder.makerTokenAddress, 163 | takerTokenAddress: toBePlacedOrder.takerTokenAddress, 164 | feeRecipient: toBePlacedOrder.feeRecipient, 165 | makerTokenAmount: toBePlacedOrder.makerTokenAmount, 166 | takerTokenAmount: toBePlacedOrder.takerTokenAmount, 167 | makerFee: toBePlacedOrder.makerFee, 168 | takerFee: toBePlacedOrder.takerFee, 169 | expirationUnixTimestampSec: toBePlacedOrder.expirationUnixTimestampSec, 170 | salt: toBePlacedOrder.salt, 171 | ecSignature: toBePlacedOrder.ecSignature, 172 | }), 173 | } 174 | } 175 | 176 | async deposit(amount: number, opts?: TokenlonInterface.TxOpts) { 177 | const { wallet, zeroEx } = this._config 178 | zeroExAssertUtils.isNumber('amount', amount) 179 | return this.zeroExWrapper.etherToken.depositAsync( 180 | zeroEx.etherTokenContractAddress, 181 | fromUnitToDecimalBN(amount, 18), 182 | wallet.address, 183 | convertTokenlonTxOptsTo0xOpts(opts), 184 | ) 185 | } 186 | 187 | async withdraw(amount: number, opts?: TokenlonInterface.TxOpts) { 188 | const { wallet, zeroEx } = this._config 189 | zeroExAssertUtils.isNumber('amount', amount) 190 | return this.zeroExWrapper.etherToken.withdrawAsync( 191 | zeroEx.etherTokenContractAddress, 192 | fromUnitToDecimalBN(amount, 18), 193 | wallet.address, 194 | convertTokenlonTxOptsTo0xOpts(opts), 195 | ) 196 | } 197 | 198 | async getTokenBalance(tokenName: string, address?: string): Promise { 199 | if (address) { 200 | zeroExAssertUtils.isETHAddressHex('address', address) 201 | } 202 | const { wallet } = this._config 203 | let balanceDecimalBN: BigNumber 204 | let decimal: number 205 | if (tokenName === 'ETH') { 206 | balanceDecimalBN = this.web3Wrapper.eth.getBalance(address || wallet.address) 207 | decimal = 18 208 | } else { 209 | const token = getTokenByName(tokenName, this._pairs) 210 | balanceDecimalBN = await this.zeroExWrapper.token.getBalanceAsync(token.contractAddress, address || wallet.address) 211 | decimal = token.decimal 212 | } 213 | return fromDecimalToUnit(balanceDecimalBN, decimal).toNumber() 214 | } 215 | 216 | async getAllowance(tokenName: string, address?: string) { 217 | if (helpCompareStr(tokenName, 'ETH')) throw newError(TokenlonError.EthDoseNotHaveApprovedMethod) 218 | if (address) { 219 | zeroExAssertUtils.isETHAddressHex('address', address) 220 | } 221 | const { wallet, zeroEx } = this._config 222 | const token = getTokenByName(tokenName, this._pairs) 223 | const allowanceBN = await this.zeroExWrapper.token.getAllowanceAsync(token.contractAddress, (address || wallet.address).toLowerCase(), zeroEx.tokenTransferProxyContractAddress) 224 | return fromDecimalToUnit(allowanceBN, token.decimal).toNumber() 225 | } 226 | 227 | async setAllowance(tokenName: string, amount: number, opts?: TokenlonInterface.TxOpts) { 228 | if (helpCompareStr(tokenName, 'ETH')) throw newError(TokenlonError.EthDoseNotHaveApprovedMethod) 229 | const { wallet, zeroEx } = this._config 230 | zeroExAssertUtils.isNumber('amount', amount) 231 | const token = getTokenByName(tokenName, this._pairs) 232 | const amountDecimalBN = fromUnitToDecimalBN(amount, token.decimal) 233 | 234 | return this.zeroExWrapper.token.setAllowanceAsync( 235 | token.contractAddress, 236 | wallet.address, 237 | zeroEx.tokenTransferProxyContractAddress, 238 | amountDecimalBN, 239 | convertTokenlonTxOptsTo0xOpts(opts), 240 | ) 241 | } 242 | 243 | async setUnlimitedAllowance(tokenName, opts?: TokenlonInterface.TxOpts) { 244 | if (helpCompareStr(tokenName, 'ETH')) throw newError(TokenlonError.EthDoseNotHaveApprovedMethod) 245 | const { wallet, zeroEx } = this._config 246 | const token = getTokenByName(tokenName, this._pairs) 247 | return this.zeroExWrapper.token.setAllowanceAsync( 248 | token.contractAddress, 249 | wallet.address, 250 | zeroEx.tokenTransferProxyContractAddress, 251 | constants.UNLIMITED_ALLOWANCE_IN_BASE_UNITS, 252 | convertTokenlonTxOptsTo0xOpts(opts), 253 | ) 254 | } 255 | 256 | private async fillOrderHelper({ params, fill, validate }) { 257 | const { wallet, onChainValidate } = this._config 258 | const { rawOrder } = params 259 | const orderFillRequest = getOrderFillRequest(params, this._pairs) 260 | const { signedOrder, takerTokenFillAmount } = orderFillRequest 261 | let txHash = '' 262 | if (onChainValidate) { 263 | await validate(signedOrder, takerTokenFillAmount, wallet.address) 264 | } 265 | txHash = await fill(signedOrder, takerTokenFillAmount, wallet.address) 266 | 267 | await this.server.fillOrder({ 268 | txHash, 269 | order: JSON.parse(rawOrder), 270 | amount: takerTokenFillAmount.toString(), 271 | }) 272 | return txHash 273 | } 274 | 275 | async fillOrder(params: TokenlonInterface.FillOrderParams, opts?: TokenlonInterface.TxOpts) { 276 | return this.fillOrderHelper({ 277 | fill: (signedOrder, takerTokenFillAmount, address) => { 278 | return this.zeroExWrapper.exchange.fillOrderAsync( 279 | signedOrder, 280 | takerTokenFillAmount, 281 | false, 282 | address, 283 | convertTokenlonTxOptsTo0xOpts(opts), 284 | ) 285 | }, 286 | validate: (signedOrder, takerTokenFillAmount, address) => { 287 | return this.zeroExWrapper.exchange.validateFillOrderThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, address) 288 | }, 289 | params, 290 | }) 291 | } 292 | 293 | async fillOrKillOrder(params: TokenlonInterface.FillOrderParams, opts?: TokenlonInterface.TxOpts) { 294 | return this.fillOrderHelper({ 295 | fill: (signedOrder, takerTokenFillAmount, address) => { 296 | return this.zeroExWrapper.exchange.fillOrKillOrderAsync( 297 | signedOrder, 298 | takerTokenFillAmount, 299 | address, 300 | convertTokenlonTxOptsTo0xOpts(opts), 301 | ) 302 | }, 303 | validate: (signedOrder, takerTokenFillAmount, address) => { 304 | return this.zeroExWrapper.exchange.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, address) 305 | }, 306 | params, 307 | }) 308 | } 309 | 310 | private async batchFillOrdersHelper({ batchFill, validate, orderFillReqs }) { 311 | const { wallet, onChainValidate } = this._config 312 | const orderFillRequests = orderFillReqs.map(req => getOrderFillRequest(req, this._pairs)) 313 | const errors = [] 314 | let errorMsg = '' 315 | if (onChainValidate) { 316 | for (let r of orderFillRequests) { 317 | const { signedOrder, takerTokenFillAmount } = r 318 | try { 319 | await validate(signedOrder, takerTokenFillAmount, wallet.address) 320 | } catch (e) { 321 | errors.push(e && e.message && e.message.toString()) 322 | } 323 | } 324 | if (errors.length) { 325 | errorMsg = `These orders are invalid ${JSON.stringify(errors)}` 326 | } 327 | } 328 | 329 | if (errors.length !== orderFillRequests.length) { 330 | if (errorMsg) { 331 | console.log(errorMsg) 332 | } 333 | // !! Using orderFillRequests, even though there has some orders invalid 334 | const txHash = await batchFill(orderFillRequests, wallet.address) 335 | await this.server.batchFillOrders({ 336 | txHash, 337 | orders: orderFillReqs.map(({ rawOrder }, index) => { 338 | return { 339 | order: JSON.parse(rawOrder), 340 | amount: orderFillRequests[index].takerTokenFillAmount.toString(), 341 | } 342 | }), 343 | }) 344 | return txHash 345 | } else { 346 | console.log(errorMsg) 347 | throw newError(TokenlonError.InvalidOrders) 348 | } 349 | } 350 | 351 | async batchFillOrders(orderFillReqs: TokenlonInterface.FillOrderParams[], opts?: TokenlonInterface.TxOpts) { 352 | return this.batchFillOrdersHelper({ 353 | batchFill: (orderFillRequests, address) => { 354 | return this.zeroExWrapper.exchange.batchFillOrdersAsync( 355 | orderFillRequests, 356 | false, 357 | address, 358 | convertTokenlonTxOptsTo0xOpts(opts), 359 | ) 360 | }, 361 | validate: (signedOrder, takerTokenFillAmount, address) => { 362 | return this.zeroExWrapper.exchange.validateFillOrderThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, address) 363 | }, 364 | orderFillReqs, 365 | }) 366 | } 367 | 368 | async batchFillOrKill(orderFillReqs: TokenlonInterface.FillOrderParams[], opts?: TokenlonInterface.TxOpts) { 369 | return this.batchFillOrdersHelper({ 370 | batchFill: (orderFillRequests, address) => { 371 | return this.zeroExWrapper.exchange.batchFillOrKillAsync( 372 | orderFillRequests, 373 | address, 374 | convertTokenlonTxOptsTo0xOpts(opts), 375 | ) 376 | }, 377 | validate: (signedOrder, takerTokenFillAmount, address) => { 378 | return this.zeroExWrapper.exchange.validateFillOrKillOrderThrowIfInvalidAsync(signedOrder, takerTokenFillAmount, address) 379 | }, 380 | orderFillReqs, 381 | }) 382 | } 383 | 384 | async fillOrdersUpTo(params: TokenlonInterface.FillOrdersUpTo, opts?: TokenlonInterface.TxOpts) { 385 | const { wallet } = this._config 386 | const { side, rawOrders, amount } = params 387 | const signedOrders = rawOrders.map(s => JSON.parse(s)) 388 | const isBuy = side === 'BUY' 389 | let makerTaker = {} as any 390 | const checkSamePair = signedOrders.every(o => { 391 | const { maker, taker } = makerTaker 392 | const { makerTokenAddress, takerTokenAddress } = o 393 | if (maker && taker) { 394 | return helpCompareStr(maker, makerTokenAddress) && helpCompareStr(taker, takerTokenAddress) 395 | } else if (!maker && !taker) { 396 | makerTaker = { maker: makerTokenAddress, taker: takerTokenAddress } 397 | return true 398 | } else { 399 | return false 400 | } 401 | }) 402 | 403 | if (!checkSamePair) { 404 | throw newError(TokenlonError.OrdersMustBeSamePairAndSameSideWithFillOrdersUpTo) 405 | } 406 | 407 | const pair = getPairBySymbol(params, this._pairs) 408 | const { maker, taker } = makerTaker 409 | 410 | // to filled order is another side 411 | if ( 412 | (isBuy && (pair.base.contractAddress !== maker || pair.quote.contractAddress !== taker)) || 413 | (!isBuy && (pair.base.contractAddress !== taker || pair.quote.contractAddress !== maker)) 414 | ) { 415 | throw newError(TokenlonError.InvalidSideWithOrder) 416 | } 417 | 418 | signedOrders.sort((s1, s2) => { 419 | const simple1 = getSimpleOrder({ order: s1, pair }) 420 | 421 | const simple2 = getSimpleOrder({ order: s2, pair }) 422 | 423 | if (side === 'BUY') { 424 | return simple1.price - simple2.price 425 | } else { 426 | return simple2.price - simple1.price 427 | } 428 | }) 429 | 430 | const simpleOrders = signedOrders.map(order => getSimpleOrder({ order, pair })) 431 | const takerTokenAmountBN = getFillTakerTokenAmountBNByUpToOrders(side, amount, simpleOrders, pair) 432 | const txHash = await this.zeroExWrapper.exchange.fillOrdersUpToAsync( 433 | signedOrders.map(orderStringToBN), 434 | takerTokenAmountBN, 435 | false, 436 | wallet.address, 437 | convertTokenlonTxOptsTo0xOpts(opts), 438 | ) 439 | 440 | await this.server.batchFillOrders({ 441 | txHash, 442 | orders: signedOrders.map(signedOrder => { 443 | return { 444 | order: orderBNToString(signedOrder), 445 | // TODO use signedOrder takerTokenAmount temporality, the server dosen't solve this amount param 446 | amount: signedOrder.takerTokenAmount.toString(), 447 | } 448 | }), 449 | }) 450 | return txHash 451 | } 452 | 453 | async cancelOrder(rawOrder: string, onChain?: boolean, opts?: TokenlonInterface.TxOpts) { 454 | const { onChainValidate } = this._config 455 | const order = JSON.parse(rawOrder) 456 | const bnOrder = orderStringToBN(order) 457 | const orderHash = ZeroEx.getOrderHashHex(bnOrder) 458 | 459 | if (onChain) { 460 | if (onChainValidate) { 461 | await this.zeroExWrapper.exchange.validateCancelOrderThrowIfInvalidAsync(bnOrder, bnOrder.takerTokenAmount) 462 | } 463 | const txHash = await this.zeroExWrapper.exchange.cancelOrderAsync( 464 | bnOrder, 465 | bnOrder.takerTokenAmount, 466 | convertTokenlonTxOptsTo0xOpts(opts), 467 | ) 468 | await this.server.cancelOrdersWithHash([{ orderHash, txHash }]) 469 | return txHash 470 | } else { 471 | return this.server.cancelOrders([orderHash]) 472 | } 473 | } 474 | 475 | async batchCancelOrders(rawOrders: string[], onChain?: boolean, opts?: TokenlonInterface.TxOpts) { 476 | const { onChainValidate } = this._config 477 | const bnOrders = rawOrders.map(rawOrder => orderStringToBN(JSON.parse(rawOrder))) 478 | const orderHashs = bnOrders.map(ZeroEx.getOrderHashHex) 479 | 480 | if (onChain) { 481 | const errors = [] 482 | let errorMsg = '' 483 | for (let bnOrder of bnOrders) { 484 | try { 485 | if (onChainValidate) { 486 | await this.zeroExWrapper.exchange.validateCancelOrderThrowIfInvalidAsync(bnOrder, bnOrder.takerTokenAmount) 487 | } 488 | } catch (e) { 489 | errors.push(e && e.message && e.message.toString()) 490 | } 491 | } 492 | if (errors.length) { 493 | errorMsg = `These orders are invalid ${JSON.stringify(errors)}` 494 | console.log(errorMsg) 495 | } 496 | 497 | if (errors.length !== bnOrders.length) { 498 | const orderCancellationRequests = bnOrders.map(bnOrder => { 499 | return { 500 | order: bnOrder, 501 | takerTokenCancelAmount: bnOrder.takerTokenAmount, 502 | } 503 | }) 504 | // !! Using orderCancellationRequests, even though there has some orders invalid 505 | const txHash = await this.zeroExWrapper.exchange.batchCancelOrdersAsync(orderCancellationRequests, convertTokenlonTxOptsTo0xOpts(opts)) 506 | await this.server.cancelOrdersWithHash(orderHashs.map(orderHash => ({ orderHash, txHash }))) 507 | return txHash 508 | } else { 509 | throw newError(errorMsg) 510 | } 511 | } else { 512 | return this.server.cancelOrders(orderHashs) 513 | } 514 | } 515 | } --------------------------------------------------------------------------------