diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 499291ab..6894f6d6 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: - node-version: '10' + node-version: '14' - run: yarn install --frozen-lockfile - run: yarn test:lint - run: yarn test:unit diff --git a/.github/workflows/deploy-gh-pages.yml b/.github/workflows/deploy-gh-pages.yml index caff6e01..f37824ad 100644 --- a/.github/workflows/deploy-gh-pages.yml +++ b/.github/workflows/deploy-gh-pages.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v2-beta with: - node-version: '10' + node-version: '14' - run: yarn install --frozen-lockfile - run: yarn docs - name: Deploy docs 🚀 diff --git a/CHANGELOG.md b/CHANGELOG.md index 45a48feb..bd552dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [4.0.7](https://github.com/nash-io/nash-protocol/compare/v4.0.1...v4.0.7) (2021-01-26) + +### [4.0.5](https://github.com/nash-io/nash-protocol/compare/v4.0.3...v4.0.5) (2021-01-21) + +### [4.0.3](https://github.com/nash-io/nash-protocol/compare/v4.0.1...v4.0.3) (2021-01-21) + +### [4.0.1](https://github.com/nash-io/nash-protocol/compare/v3.3.24...v4.0.1) (2020-12-15) + +### [3.3.24](https://github.com/nash-io/nash-protocol/compare/v3.3.14...v3.3.24) (2020-12-15) + +### [3.3.14](https://github.com/nash-io/nash-protocol/compare/v3.3.11...v3.3.14) (2020-11-24) + ### [3.3.21](https://github.com/nash-io/nash-protocol/compare/v3.3.11...v3.3.21) (2020-12-16) ### [3.3.11](https://github.com/nash-io/nash-protocol/compare/v3.4.7...v3.3.11) (2020-08-11) diff --git a/package.json b/package.json index a770fc8e..621f8cc7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neon-exchange/nash-protocol", - "version": "3.3.21", + "version": "4.0.7", "description": "TypeScript implementation of Nash crypto routines", "main": "build/main/index.js", "typings": "build/main/index.d.ts", @@ -10,7 +10,7 @@ "license": "MIT", "keywords": [], "scripts": { - "copy:nativewasm": "cp -r src/wasm build/main/wasm && cp src/native/index_osx.node build/main/native && cp src/native/index_linux.node build/main/native && cp -r wasm-webpack build/module/wasm && rm -r build/module/mpc-lib && cp -r wasm-webpack build/module/mpc-lib", + "copy:nativewasm": "cp -r src/wasm build/main/wasm && cp src/native/index_osx.node build/main/native && cp src/native/index_linux.node build/main/native && cp src/native/index_win.node build/main/native && cp -r wasm-webpack build/module/wasm && rm -r build/module/mpc-lib && cp -r wasm-webpack build/module/mpc-lib", "copy:wasm": "cp -r src/wasm build/main/wasm && cp -r wasm-webpack build/module/wasm", "build": "yarn clean && yarn build:main && yarn build:module && yarn copy:nativewasm", "build:main": "tsc -p tsconfig.json", @@ -37,7 +37,7 @@ "preinstall": "node -e \"if(process.env.npm_execpath.indexOf('yarn') === -1) throw new Error('nash-protocol must be installed with Yarn: https://yarnpkg.com/')\"" }, "engines": { - "node": ">=8.9" + "node": ">=14.3" }, "dependencies": { "bignumber.js": "8.1.1", @@ -66,7 +66,7 @@ "@types/elliptic": "6.4.5", "@types/jest": "23.3.9", "@types/lodash": "4.14.122", - "@types/node": "13.7.0", + "@types/node": "^14.14.14", "@types/randombytes": "2.0.0", "jest": "23.6.0", "nyc": "13.1.0", diff --git a/src/__tests__/blockchain_config.json b/src/__tests__/blockchain_config.json index e60b385a..6a82d69d 100644 --- a/src/__tests__/blockchain_config.json +++ b/src/__tests__/blockchain_config.json @@ -24,6 +24,11 @@ "blockchain": "eth", "hash": "aed88173df2578fad078c48a33f0040364989fa8", "precision": 18 + }, + "bat": { + "blockchain": "eth", + "hash": "abcd", + "precision": 18 } }, "marketData": { @@ -98,6 +103,12 @@ "minTradeSize": 4, "minTradeIncrement": 4, "minTradeIncrementB": 4 + }, + "eth_bat": { + "minTickSize": 2, + "minTradeSize": 5, + "minTradeIncrement": 8, + "minTradeIncrementB": 5 } }, "payloadSigningKey": { diff --git a/src/__tests__/config.json b/src/__tests__/config.json index 5ee92b2d..848efbc5 100644 --- a/src/__tests__/config.json +++ b/src/__tests__/config.json @@ -45,6 +45,12 @@ "minTradeIncrement": 5, "minTradeIncrementB": 5 }, + "eth_bat": { + "minTickSize": 2, + "minTradeSize": 5, + "minTradeIncrement": 5, + "minTradeIncrementB": 5 + }, "gas_eth": { "minTickSize": 8, "minTradeSize": 4, diff --git a/src/__tests__/signatureVectors.json b/src/__tests__/signatureVectors.json index a3141858..75bb572f 100644 --- a/src/__tests__/signatureVectors.json +++ b/src/__tests__/signatureVectors.json @@ -65,10 +65,10 @@ "allowTaker": false, "amount": { "value": "10.000000", - "currency": "gas" + "currency": "neo" }, "buyOrSell": "SELL", - "marketName": "gas_neo", + "marketName": "neo_gas", "cancellationPolicy": "immediate_or_cancel", "limitPrice": { "value": "17.000", diff --git a/src/constants/rates.ts b/src/constants/rates.ts index 564bdac9..b24ed415 100644 --- a/src/constants/rates.ts +++ b/src/constants/rates.ts @@ -1,3 +1,5 @@ -export const minOrderRate = 0 -export const maxOrderRate = 'ffffffffffffffff' // 18446744073709551615 -export const maxFeeRate = 250000 +export const MIN_ORDER_RATE = 0 +export const MAX_ORDER_RATE = 'ffffffffffffffff' // 18446744073709551615 +export const MAX_ORDER_AMOUNT = MAX_ORDER_RATE +export const MAX_FEE_RATE = 0.0025 +export const BLOCKCHAIN_PRECISION = 8 diff --git a/src/index.ts b/src/index.ts index 459b25bf..032c5e5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,7 +7,7 @@ export { default as getSecretKey } from './getSecretKey' export { default as mnemonicToMasterSeed } from './mnemonicToMasterSeed' export { default as mnemonicToSecretKey } from './mnemonicToSecretKey' export { computePresig } from './mpc/computePresig' -export { fillRPool, configurePoolSettings } from './mpc/fillRPool' +export { fillRPool, configurePoolSettings, getDhPoolSize } from './mpc/fillRPool' export { fillRPoolIfNeeded } from './mpc/fillRPool' export { generateAPIKeys } from './mpc/generateAPIKeys' export { createAPIKey } from './mpc/createAPIKey' diff --git a/src/mpc/computePresig.ts b/src/mpc/computePresig.ts index 336d247a..58fd6f51 100644 --- a/src/mpc/computePresig.ts +++ b/src/mpc/computePresig.ts @@ -10,11 +10,7 @@ export async function computePresig(params: ComputePresigParams): Promise MIN_RPOOL_SIZE = Math.max(BLOCK_RPOOL_SIZE, minPoolSize || poolSize / 2) } -async function getDhPoolSize(fillPoolParams: FillRPoolParams): Promise { +export async function getDhPoolSize(fillPoolParams: FillRPoolParams): Promise { const MPCWallet = await import('../mpc-lib') - const curveStr = JSON.stringify(BlockchainCurve[fillPoolParams.blockchain]) + const curveStr = BlockchainCurve[fillPoolParams.blockchain] const [getRPoolSizeSuccess, msgOrSize] = JSON.parse(MPCWallet.get_rpool_size(curveStr)) as [boolean, number | string] if (getRPoolSizeSuccess === false) { throw new Error('Error querying rpool size. ' + (msgOrSize as string)) @@ -31,7 +31,7 @@ const _FILL_JOB: Record | null> = { async function _fill(fillPoolParams: FillRPoolParams): Promise { const { fillPoolFn, blockchain, paillierPkStr } = fillPoolParams const MPCWallet = await import('../mpc-lib') - const curveStr = JSON.stringify(BlockchainCurve[blockchain]) + const curveStr = BlockchainCurve[fillPoolParams.blockchain] const [initDHSuccess, clientDHSecrets, clientDHPublics] = JSON.parse(MPCWallet.dh_init(RPOOL_SIZE, curveStr)) as [ boolean, string[], diff --git a/src/native/index.ts b/src/native/index.ts index 96933c52..0ad0b807 100644 --- a/src/native/index.ts +++ b/src/native/index.ts @@ -17,9 +17,10 @@ const loadNodeFile = (): NodeFileInterface => { // case 'freebsd': // case 'openbsd': // case 'sunos': - // case 'win32': - // case 'cygwin': // case 'netbsd': + case 'cygwin': + case 'win32': + return require('./index_win.node') case 'linux': return require('./index_linux.node') case 'darwin': @@ -35,25 +36,25 @@ export function dh_init(size: number, curve: string): string { if (wasm === MpcWallet) { return wasm.dh_init(size, curve) } - return MpcWallet.dh_init(size, JSON.parse(curve)) + return MpcWallet.dh_init(size, curve) } export function fill_rpool(clientDHSecrets: string, serverDHPublics: string, curve: string, pkstr: string): string { if (wasm === MpcWallet) { return wasm.fill_rpool(clientDHSecrets, serverDHPublics, curve, pkstr) } - return MpcWallet.fill_rpool(clientDHSecrets, serverDHPublics, JSON.parse(curve), pkstr) + return MpcWallet.fill_rpool(clientDHSecrets, serverDHPublics, pkstr, curve) } export function get_rpool_size(curve: string): string { if (wasm === MpcWallet) { return wasm.get_rpool_size(curve) } - return MpcWallet.get_rpool_size(JSON.parse(curve)) + return MpcWallet.get_rpool_size(curve) } export function compute_presig(apiKeyStr: string, msgHashStr: string, curve: string): string { if (wasm === MpcWallet) { return wasm.compute_presig(apiKeyStr, msgHashStr, curve) } - return MpcWallet.compute_presig(apiKeyStr, msgHashStr, JSON.parse(curve)) + return MpcWallet.compute_presig(apiKeyStr, msgHashStr, curve) } export const init_api_childkey_creator = wasm.init_api_childkey_creator diff --git a/src/native/index_linux.node b/src/native/index_linux.node index b1498877..c2341b08 100644 Binary files a/src/native/index_linux.node and b/src/native/index_linux.node differ diff --git a/src/native/index_osx.node b/src/native/index_osx.node old mode 100644 new mode 100755 index 63001506..62f0c246 Binary files a/src/native/index_osx.node and b/src/native/index_osx.node differ diff --git a/src/native/index_win.node b/src/native/index_win.node new file mode 100755 index 00000000..5f1d1f6e Binary files /dev/null and b/src/native/index_win.node differ diff --git a/src/signETHBlockchainData/signETHBlockchainData.spec.ts b/src/signETHBlockchainData/signETHBlockchainData.spec.ts index 5510444d..fa8d2de0 100644 --- a/src/signETHBlockchainData/signETHBlockchainData.spec.ts +++ b/src/signETHBlockchainData/signETHBlockchainData.spec.ts @@ -3,11 +3,19 @@ import { buildETHOrderSignatureData, signETHBlockchainData } from './signETHBlockchainData' -import { SigningPayloadID, MovementTypeDeposit, MovementTypeWithdrawal, createAddMovementParams } from '../payload' +import { + SigningPayloadID, + MovementTypeDeposit, + MovementTypeWithdrawal, + createAddMovementParams, + BuyOrSellBuy, + BuyOrSellSell +} from '../payload' import config from '../__tests__/blockchain_config.json' -import signPayload from '../signPayload' +import signPayload, { determineSignatureNonceTuplesNeeded } from '../signPayload' import sigTestVectors from '../__tests__/signatureVectors.json' -import { buildNEOOrderSignatureData, signNEOBlockchainData } from '../signNEOBlockchainData' +import { buildOrderSignatureData, inferBlockchainData } from '../utils/blockchain' +// import { buildNEOOrderSignatureData, signNEOBlockchainData } from '../signNEOBlockchainData' test('eth sig generation', async () => { const payload = @@ -86,136 +94,306 @@ test('sign usdc withdraw movement', async () => { }) }) -test('sign ETH blockchain market order data', async () => { - const data = sigTestVectors.marketOrders.eth_usdc +// these test vectors taken from MatchingEngine test vectors + +test('sign ETH/BAT market buy order', async () => { const payload = { - amount: { amount: data.amount.value, currency: data.amount.currency }, - buyOrSell: data.buyOrSell, - marketName: data.marketName, - nonceOrder: data.nonceOrder, - noncesFrom: [data.nonceFrom], - noncesTo: [data.nonceTo], - timestamp: data.timestamp + amount: { amount: 10, currency: 'ETH' }, + buyOrSell: BuyOrSellBuy, + marketName: 'eth_bat', + nonceOrder: 42, + noncesFrom: [23], + noncesTo: [5], + timestamp: 1234 } const signingPayload = { kind: SigningPayloadID.placeMarketOrderPayload, payload } - const rawData = buildETHOrderSignatureData(config.wallets.eth.address, config.marketData, signingPayload, { - chain: 'eth', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - }) - expect(rawData).toBe(data.raw.eth) - - const sig = await signETHBlockchainData(config.wallets.eth.privateKey, rawData) - expect(sig.blockchain).toBe('ETH') - expect(sig.signature).toBe(data.blockchainSignatures.eth) - - const payloadRes = signPayload(Buffer.from(config.payloadSigningKey.privateKey, 'hex'), signingPayload, config) - const canonicalExpected = - 'place_market_order,{"amount":{"amount":"10.000000","currency":"eth"},"buy_or_sell":"sell","market_name":"eth_usdc","nonce_from":4294967295,"nonce_order":5432876,"nonce_to":4294967295,"timestamp":12345648}' - - expect(payloadRes.canonicalString).toBe(canonicalExpected) + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + '9BAE2051097DC5DDF68D3C01D5FA5CCC7833109D', + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '019BAE2051097DC5DDF68D3C01D5FA5CCC7833109D000100000000001700000005FFFFFFFFFFFFFFFF0000000000000000FFFFFFFFFFFFFFFF00000000000000000000002A' + ) }) -test('sign ETH/NEO blockchain market order data', async () => { - const data = sigTestVectors.marketOrders.eth_neo +test('sign ETH/BAT market sell order', async () => { const payload = { - amount: { amount: data.amount.value, currency: data.amount.currency }, - buyOrSell: data.buyOrSell, - marketName: data.marketName, - nonceOrder: data.nonceOrder, - noncesFrom: [data.nonceFrom], - noncesTo: [data.nonceTo], - timestamp: data.timestamp + amount: { amount: '10', currency: 'ETH' }, + buyOrSell: BuyOrSellSell, + marketName: 'eth_bat', + nonceOrder: 42, + noncesFrom: [5], + noncesTo: [23], + timestamp: 1234 } const signingPayload = { kind: SigningPayloadID.placeMarketOrderPayload, payload } - const rawDataEth = buildETHOrderSignatureData(config.wallets.eth.address, config.marketData, signingPayload, { - chain: 'eth', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - }).toUpperCase() - const rawDataNeo = buildNEOOrderSignatureData( - config.wallets.neo.address, - config.wallets.neo.publicKey, - config.assetData, - config.marketData, + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + '927BEFA25F0CE45C5948DF9583C84234704C8DBA', signingPayload, - { - chain: 'neo', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - } - ).toUpperCase() - expect(rawDataEth).toBe(data.raw.eth) - expect(rawDataNeo).toBe(data.raw.neo) - - const sigEth = signETHBlockchainData(config.wallets.eth.privateKey, rawDataEth) - const sigNeo = signNEOBlockchainData(config.wallets.neo.privateKey, rawDataNeo) + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '01927BEFA25F0CE45C5948DF9583C84234704C8DBA000000010000000500000017000000003B9ACA000000000000000000FFFFFFFFFFFFFFFF00000000000000000000002A' + ) +}) - expect(sigNeo.signature).toBe(data.blockchainSignatures.neo) - expect(sigEth.signature).toBe(data.blockchainSignatures.eth) +test('sign ETH/BAT limit buy order', async () => { + const payload = { + amount: { amount: '10', currency: 'ETH' }, + buyOrSell: BuyOrSellBuy, + limitPrice: { + amount: '2', + currency_a: 'eth', + currency_b: 'bat' + }, + marketName: 'eth_bat', + nonceOrder: 42, + noncesFrom: [23], + noncesTo: [5], + timestamp: 1234 + } - const canonicalExpected = - 'place_market_order,{"amount":{"amount":"10.00000000","currency":"eth"},"buy_or_sell":"sell","market_name":"eth_neo","nonce_from":4294967295,"nonce_order":5432876,"nonce_to":4294967295,"timestamp":12345648}' + const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + // console.info(orderData) + const rawData = buildETHOrderSignatureData( + '795B8C76F6C532FBE4B57EE6F9494BFA7C66B9EF', + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '01795B8C76F6C532FBE4B57EE6F9494BFA7C66B9EF000100000000001700000005000000001DCD6500000000000BE420E0FFFFFFFFFFFFFFFF00000000000000000000002A' + ) +}) - const payloadRes = signPayload(Buffer.from(config.payloadSigningKey.privateKey, 'hex'), signingPayload, config) - expect(payloadRes.canonicalString).toBe(canonicalExpected) +test('sign ETH/BAT limit sell order', async () => { + const payload = { + amount: { amount: '10', currency: 'ETH' }, + buyOrSell: BuyOrSellSell, + limitPrice: { + amount: '2', + currency_a: 'eth', + currency_b: 'bat' + }, + marketName: 'eth_bat', + nonceOrder: 42, + noncesFrom: [5], + noncesTo: [23], + timestamp: 1234 + } - expect(payloadRes.signature).toBe(data.signature) + const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + '84FAB1D4EB2BC381A7710C84EBE1649EC045C3D9', + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '0184FAB1D4EB2BC381A7710C84EBE1649EC045C3D9000000010000000500000017000000003B9ACA000000000002F90838FFFFFFFFFFFFFFFF00000000000000000000002A' + ) }) -test('sign ETH_GAS blockchain limit order data', async () => { - const data = sigTestVectors.limitOrders.b +test('sign ETH/USDC rate rounding', async () => { const payload = { - allowTaker: data.allowTaker, - amount: { amount: data.amount.value, currency: data.amount.currency }, - buyOrSell: data.buyOrSell, - cancellationPolicy: data.cancellationPolicy, + amount: { amount: '1', currency: 'ETH' }, + buyOrSell: BuyOrSellBuy, limitPrice: { - amount: data.limitPrice.value, - currency_a: data.limitPrice.currency_a, - currency_b: data.limitPrice.currency_b + amount: '150', + currency_a: 'usdc', + currency_b: 'eth' }, - marketName: data.marketName, - nonceOrder: data.nonceOrder, - noncesFrom: [data.nonceFrom], - noncesTo: [data.nonceTo], - timestamp: data.timestamp + marketName: 'eth_usdc', + nonceOrder: 42, + noncesFrom: [23], + noncesTo: [5], + timestamp: 1234 } const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } - - const rawDataEth = buildETHOrderSignatureData(config.wallets.eth.address, config.marketData, signingPayload, { - chain: 'eth', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - }).toUpperCase() - const rawDataNeo = buildNEOOrderSignatureData( - config.wallets.neo.address, - config.wallets.neo.publicKey, - config.assetData, - config.marketData, + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + 'BE90B183D8FB2E50E821AEF2415522AB7D6CCB05', signingPayload, - { - chain: 'neo', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - } - ).toUpperCase() - expect(rawDataEth).toBe(data.raw.eth) - expect(rawDataNeo).toBe(data.raw.neo) + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '01BE90B183D8FB2E50E821AEF2415522AB7D6CCB05000300000000001700000005000000037E11D60000000000000A25A8FFFFFFFFFFFFFFFF00000000000000000000002A' + ) +}) - const sigEth = signETHBlockchainData(config.wallets.eth.privateKey, rawDataEth) - const sigNeo = signNEOBlockchainData(config.wallets.neo.privateKey, rawDataNeo) +test('limit eth_usdc sell 1.0 eth', async () => { + const payload = { + amount: { amount: '1', currency: 'ETH' }, + buyOrSell: BuyOrSellSell, + limitPrice: { + amount: '800', + currency_a: 'usdc', + currency_b: 'eth' + }, + marketName: 'eth_usdc', + nonceOrder: 42, + noncesFrom: [5], + noncesTo: [23], + timestamp: 1234 + } - expect(sigNeo.signature).toBe(data.blockchainSignatures.neo) - expect(sigEth.signature).toBe(data.blockchainSignatures.eth) + const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + '7BB0E95708FDA1AFEB83F567E8E47595A41904DC', + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '017BB0E95708FDA1AFEB83F567E8E47595A41904DC0000000300000005000000170000000005F5E1000000001294735E00FFFFFFFFFFFFFFFF00000000000000000000002A' + ) +}) - const payloadRes = signPayload(Buffer.from(config.payloadSigningKey.privateKey, 'hex'), signingPayload, config) - expect(payloadRes.payload.blockchainSignatures).toHaveLength(2) +test('limit eth_usdc sell 1.0 eth', async () => { + const payload = { + amount: { amount: '1', currency: 'ETH' }, + buyOrSell: BuyOrSellSell, + limitPrice: { + amount: '800', + currency_a: 'usdc', + currency_b: 'eth' + }, + marketName: 'eth_usdc', + nonceOrder: 42, + noncesFrom: [5], + noncesTo: [23], + timestamp: 1234 + } - const expectedCanonicalString = - 'place_limit_order,{"allow_taker":true,"amount":{"amount":"10.00000000","currency":"eth"},"buy_or_sell":"sell","cancellation_policy":"immediate_or_cancel","limit_price":{"amount":"0.0024","currency_a":"gas","currency_b":"eth"},"market_name":"eth_gas","nonce_from":4294967295,"nonce_order":5432876,"nonce_to":4294967295,"timestamp":1565361133707}' - expect(payloadRes.canonicalString).toBe(expectedCanonicalString) + const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) + const rawData = buildETHOrderSignatureData( + '7BB0E95708FDA1AFEB83F567E8E47595A41904DC', + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '017BB0E95708FDA1AFEB83F567E8E47595A41904DC0000000300000005000000170000000005F5E1000000001294735E00FFFFFFFFFFFFFFFF00000000000000000000002A' + ) }) + +// # WARNING: The following 3 tests an order that the matching engine won't accept +// # "wrong amount unit for target market", + +// test('limit eth_bat buy 10.0 bat', async () => { +// const payload = { +// amount: { amount: '10', currency: 'BAT' }, +// buyOrSell: BuyOrSellBuy, +// limitPrice: { +// amount: '2', +// currency_a: 'eth', +// currency_b: 'bat' +// }, +// marketName: 'eth_bat', +// nonceOrder: 42, +// noncesFrom: [5], +// noncesTo: [23], +// timestamp: 1234 +// } + +// const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } +// const blockchainData = inferBlockchainData(signingPayload) +// const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) +// const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) +// const rawData = buildETHOrderSignatureData( +// 'A24002F03F2AD04D2409AF47BB43E76A00C3EF01', +// signingPayload, +// chainNoncePair[0], +// orderData +// ) +// expect(rawData).toBe( +// '01A24002F03F2AD04D2409AF47BB43E76A00C3EF0100000001000000050000001700000000773594000000000002F90838FFFFFFFFFFFFFFFF00000000000000000000002A' +// ) +// }) +// test('limit eth_bat sell 10.0 bat', async () => { +// const payload = { +// amount: { amount: '10', currency: 'BAT' }, +// buyOrSell: BuyOrSellSell, +// limitPrice: { +// amount: '2', +// currency_a: 'eth', +// currency_b: 'bat' +// }, +// marketName: 'eth_bat', +// nonceOrder: 42, +// noncesFrom: [23], +// noncesTo: [5], +// timestamp: 1234 +// } + +// const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } +// const blockchainData = inferBlockchainData(signingPayload) +// const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) +// const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) +// const rawData = buildETHOrderSignatureData( +// '1E9FAD205D8D02C9CF347E2AA61A1E922AF0D0EA', +// signingPayload, +// chainNoncePair[0], +// orderData +// ) +// expect(rawData).toBe( +// '011E9FAD205D8D02C9CF347E2AA61A1E922AF0D0EA000100000000001700000005000000003B9ACA00000000000BE420E0FFFFFFFFFFFFFFFF00000000000000000000002A' +// ) +// }) +// test('limit eth_bat buy 10.0 bat, test price inversion', async () => { +// const payload = { +// amount: { amount: '10', currency: 'BAT' }, +// buyOrSell: BuyOrSellBuy, +// limitPrice: { +// amount: '.5', +// currency_a: 'bat', +// currency_b: 'eth' +// }, +// marketName: 'eth_bat', +// nonceOrder: 42, +// noncesFrom: [5], +// noncesTo: [23], +// timestamp: 1234 +// } + +// const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } +// const blockchainData = inferBlockchainData(signingPayload) +// const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) +// const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) +// const rawData = buildETHOrderSignatureData( +// '229470549E8978659247DB9E1D73E61316B9BC70', +// signingPayload, +// chainNoncePair[0], +// orderData +// ) +// expect(rawData).toBe( +// '01229470549E8978659247DB9E1D73E61316B9BC7000000001000000050000001700000000773594000000000002F90838FFFFFFFFFFFFFFFF00000000000000000000002A' +// ) +// }) diff --git a/src/signETHBlockchainData/signETHBlockchainData.ts b/src/signETHBlockchainData/signETHBlockchainData.ts index 20aee54a..d253108c 100644 --- a/src/signETHBlockchainData/signETHBlockchainData.ts +++ b/src/signETHBlockchainData/signETHBlockchainData.ts @@ -1,11 +1,21 @@ import { SmartBuffer } from 'smart-buffer' -import { inferBlockchainData, getUnitPairs, convertEthNonce, getETHAssetID, ellipticContext } from '../utils/blockchain' +import { + inferBlockchainData, + getUnitPairs, + convertEthNonce, + getETHAssetID, + ellipticContext, + OrderSignatureData, + bigNumberFormat, + rateWithFees +} from '../utils/blockchain' import { toBigEndianHex, normalizeAmount } from '../utils/currency' -import { isLimitOrderPayload, kindToOrderPrefix, PayloadAndKind, BuyOrSellBuy } from '../payload' +import { isLimitOrderPayload, kindToOrderPrefix, PayloadAndKind, SigningPayloadID, BuyOrSellBuy } from '../payload' import { computePresig } from '../mpc/computePresig' -import { minOrderRate, maxOrderRate, maxFeeRate } from '../constants' -import { Config, Blockchain, BIP44, BlockchainSignature, APIKey, PresignConfig, ChainNoncePair } from '../types' +import { BLOCKCHAIN_PRECISION, MIN_ORDER_RATE, MAX_FEE_RATE, MAX_ORDER_RATE, MAX_ORDER_AMOUNT } from '../constants' +import { Blockchain, BIP44, BlockchainSignature, APIKey, PresignConfig, ChainNoncePair } from '../types' import createKeccakHash from 'keccak' +import BigNumber from 'bignumber.js' const createHashedMessage = (data: string): Buffer => { const initialHash = createKeccakHash('keccak256') @@ -67,45 +77,38 @@ export async function presignETHBlockchainData( export function buildETHOrderSignatureData( address: string, - marketData: Config['marketData'], payloadAndKind: PayloadAndKind, - chainNoncePair: ChainNoncePair + chainNoncePair: ChainNoncePair, + orderData: OrderSignatureData ): string { const { kind } = payloadAndKind const blockchainData = inferBlockchainData(payloadAndKind) - const { unitA, unitB } = getUnitPairs(blockchainData.marketName) - - let assetFrom = unitA - let assetTo = unitB - const amountPrecision = marketData[blockchainData.marketName].minTradeIncrement - - if (blockchainData.buyOrSell === BuyOrSellBuy) { - assetFrom = unitB - assetTo = unitA - } const buffer = new SmartBuffer() buffer.writeString(kindToOrderPrefix(kind)) buffer.writeString(address) - buffer.writeString(getETHAssetID(assetFrom)) - buffer.writeString(getETHAssetID(assetTo)) - + buffer.writeString(getETHAssetID(orderData.source.symbol)) + buffer.writeString(getETHAssetID(orderData.destination.symbol)) buffer.writeString(convertEthNonce(chainNoncePair.nonceFrom)) buffer.writeString(convertEthNonce(chainNoncePair.nonceTo)) - // normalize + to big endian - const amount = normalizeAmount(blockchainData.amount, amountPrecision) - buffer.writeString(toBigEndianHex(amount)) - - let orderRate: string = maxOrderRate + // market buy orders are not supported, but if they were we would set the amount to max value + if (payloadAndKind.kind === SigningPayloadID.placeMarketOrderPayload && blockchainData.buyOrSell === BuyOrSellBuy) { + buffer.writeString(MAX_ORDER_AMOUNT) + } else { + const amount = normalizeAmount(orderData.source.amount, BLOCKCHAIN_PRECISION) + buffer.writeString(toBigEndianHex(amount)) + } + let orderRate: number = MIN_ORDER_RATE if (isLimitOrderPayload(kind)) { - orderRate = toBigEndianHex(normalizeAmount(blockchainData.limitPrice as string, 8)) + const rate = rateWithFees(orderData.rate) + orderRate = normalizeAmount(rate.toFormat(8, BigNumber.ROUND_DOWN, bigNumberFormat), BLOCKCHAIN_PRECISION) } - buffer.writeString(toBigEndianHex(minOrderRate)) - buffer.writeString(orderRate) - buffer.writeString(toBigEndianHex(maxFeeRate)) + buffer.writeString(toBigEndianHex(orderRate)) + buffer.writeString(MAX_ORDER_RATE) + buffer.writeString(toBigEndianHex(MAX_FEE_RATE)) buffer.writeString(convertEthNonce(blockchainData.nonceOrder)) diff --git a/src/signNEOBlockchainData/signNEOBlockchainData.spec.ts b/src/signNEOBlockchainData/signNEOBlockchainData.spec.ts index 99131eb5..5a7d822a 100644 --- a/src/signNEOBlockchainData/signNEOBlockchainData.spec.ts +++ b/src/signNEOBlockchainData/signNEOBlockchainData.spec.ts @@ -1,13 +1,15 @@ // import { signNEOBlockchainData, buildNEOBlockchainSignatureData } from './signNEOBlockchainData' -import signPayload from '../signPayload' -import { SigningPayloadID, MovementTypeWithdrawal, MovementTypeDeposit } from '../payload' +import signPayload, { determineSignatureNonceTuplesNeeded } from '../signPayload' +import { SigningPayloadID, MovementTypeWithdrawal, MovementTypeDeposit, createPlaceLimitOrderParams } from '../payload' import config from '../__tests__/blockchain_config.json' import sigTestVectors from '../__tests__/signatureVectors.json' import { - buildNEOOrderSignatureData, + // buildNEOOrderSignatureData, signNEOBlockchainData, - buildNEOMovementSignatureData + buildNEOMovementSignatureData, + buildNEOOrderSignatureData } from './signNEOBlockchainData' +import { inferBlockchainData, buildOrderSignatureData } from '../utils/blockchain' test('sign NEO deposit movement', async () => { const data = sigTestVectors.movements.a @@ -104,69 +106,53 @@ test('sign NEO_GAS blockchain market order data', async () => { } const signingPayload = { kind: SigningPayloadID.placeMarketOrderPayload, payload } + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) const rawData = buildNEOOrderSignatureData( config.wallets.neo.address, config.wallets.neo.publicKey, - config.assetData, - config.marketData, signingPayload, - { - chain: 'neo', - nonceFrom: data.nonceFrom, - nonceTo: data.nonceTo - } - ).toUpperCase() - expect(rawData).toBe(data.raw.neo) - const sig = signNEOBlockchainData(config.wallets.neo.privateKey, rawData) - expect(sig.blockchain).toBe('NEO') - expect(sig.signature).toBe(data.blockchainSignatures.neo) - - const payloadRes = signPayload(Buffer.from(config.payloadSigningKey.privateKey, 'hex'), signingPayload, config) + chainNoncePair[0], + orderData + ) - const expectedCanonicalString = - 'place_market_order,{"amount":{"amount":"10.000","currency":"neo"},"buy_or_sell":"sell","market_name":"neo_gas","nonce_from":4294967295,"nonce_order":5432876,"nonce_to":4294967295,"timestamp":12345648}' + expect(rawData).toBe( + '016f6f85bfffb412967af3dd0d71a5e2f8a759006c9b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c602ce65200000000002ce652000000000000ca9a3b000000000000000000000000ffffffffffffffff00000000000000002ce65200000000000292cbf3790801cef47c5cdc9abf4b010ec50aad117f595350d77ecd385d286e63' + ) +}) - expect(payloadRes.payload.blockchainSignatures).toHaveLength(1) +test('sign NEO_GAS blockchain limit order data', async () => { + const data = sigTestVectors.limitOrders.a + const signingPayload = createPlaceLimitOrderParams( + data.allowTaker, + { amount: data.amount.value, currency: data.amount.currency }, + data.buyOrSell, + data.cancellationPolicy, + { + amount: data.limitPrice.value, + currency_a: data.limitPrice.currency_a, + currency_b: data.limitPrice.currency_b + }, + data.marketName, + [data.nonceFrom], + [data.nonceTo], + data.nonceOrder + ) + + const blockchainData = inferBlockchainData(signingPayload) + const orderData = buildOrderSignatureData(config.marketData, config.assetData, signingPayload) + const chainNoncePair = determineSignatureNonceTuplesNeeded(orderData, blockchainData) - expect(payloadRes.canonicalString).toBe(expectedCanonicalString) - expect(payloadRes.blockchainRaw[0].raw.toUpperCase()).toBe(data.raw.neo) + const rawData = buildNEOOrderSignatureData( + config.wallets.neo.address, + config.wallets.neo.publicKey, + signingPayload, + chainNoncePair[0], + orderData + ) + expect(rawData).toBe( + '016f6f85bfffb412967af3dd0d71a5e2f8a759006c9b7cffdaa674beae0f930ebe6085af9093e5fe56b34a5c220ccdcf6efc336fc5e72d286979ee6cb1b7e65dfddfb2e384100b8d148e7758de42e4168b71792c602ce65200000000002ce652000000000000ca9a3b000000007f88590000000000ffffffffffffffff00000000000000002ce65200000000000292cbf3790801cef47c5cdc9abf4b010ec50aad117f595350d77ecd385d286e63' + ) }) - -// test('sign NEO_GAS blockchain limit order data', async () => { -// const data = sigTestVectors.limitOrders.a -// const signingPayload = createPlaceLimitOrderParams( -// data.allowTaker, -// { amount: data.amount.value, currency: data.amount.currency }, -// data.buyOrSell, -// data.cancellationPolicy, -// { -// amount: data.limitPrice.value, -// currency_a: data.limitPrice.currency_a, -// currency_b: data.limitPrice.currency_b -// }, -// data.marketName, -// [data.nonceFrom], -// [data.nonceTo], -// data.nonceOrder -// ) - -// // const signingPayload = { kind: SigningPayloadID.placeLimitOrderPayload, payload } - -// // const rawData = buildNEOOrderSignatureData(config, signingPayload, { -// // chain: 'neo', -// // nonceFrom: data.nonceFrom, -// // nonceTo: data.nonceTo -// // }).toUpperCase() -// // expect(rawData).toBe(data.raw.neo) -// // const sig = signNEOBlockchainData(config.wallets.neo.privateKey, rawData) -// // expect(sig.blockchain).toBe('NEO') -// // expect(sig.signature).toBe(data.blockchainSignatures.neo) - -// const payloadRes = signPayload(Buffer.from(config.payloadSigningKey.privateKey, 'hex'), signingPayload, config) - -// // const expectedCanonicalString = -// // 'place_limit_order,{"allow_taker":false,"amount":{"amount":"10.000000","currency":"gas"},"buy_or_sell":"sell","cancellation_policy":"immediate_or_cancel","limit_price":{"amount":"17.000","currency_a":"neo","currency_b":"gas"},"market_name":"gas_neo","nonce_from":4294967295,"nonce_order":5432876,"nonce_to":4294967295,"timestamp":12345648}' -// // expect(payloadRes.canonicalString).toBe(expectedCanonicalString) - -// }) diff --git a/src/signNEOBlockchainData/signNEOBlockchainData.ts b/src/signNEOBlockchainData/signNEOBlockchainData.ts index 815219a4..8d045b70 100644 --- a/src/signNEOBlockchainData/signNEOBlockchainData.ts +++ b/src/signNEOBlockchainData/signNEOBlockchainData.ts @@ -3,13 +3,21 @@ import { SmartBuffer } from 'smart-buffer' import crypto from 'crypto' import { normalizeAmount, toLittleEndianHex } from '../utils/currency' -import { getUnitPairs, inferBlockchainData, getNEOAssetHash } from '../utils/blockchain' +import { + getUnitPairs, + inferBlockchainData, + getNEOAssetHash, + OrderSignatureData, + bigNumberFormat, + rateWithFees +} from '../utils/blockchain' import reverseHexString from '../utils/reverseHexString' -import { maxOrderRate, maxFeeRate, minOrderRate } from '../constants' -import { isLimitOrderPayload, kindToOrderPrefix, PayloadAndKind, BuyOrSellBuy } from '../payload' +import { BLOCKCHAIN_PRECISION, MIN_ORDER_RATE, MAX_FEE_RATE, MAX_ORDER_RATE, MAX_ORDER_AMOUNT } from '../constants' +import { isLimitOrderPayload, kindToOrderPrefix, PayloadAndKind, SigningPayloadID, BuyOrSellBuy } from '../payload' import { BlockchainSignature, Blockchain, APIKey, PresignConfig, BIP44, Config, ChainNoncePair } from '../types' import getNEOScriptHash from '../utils/getNEOScriptHash' import { computePresig } from '../mpc/computePresig' +import { BigNumber } from 'bignumber.js' const curve = new EC('p256') const sha256 = (msg: string): string => { @@ -57,43 +65,41 @@ export async function presignNEOBlockchainData( export function buildNEOOrderSignatureData( address: string, publicKey: string, - assetData: Config['assetData'], - marketData: Config['marketData'], payloadAndKind: PayloadAndKind, - chainNoncePair: ChainNoncePair + chainNoncePair: ChainNoncePair, + orderData: OrderSignatureData ): string { const { kind } = payloadAndKind const blockchainData = inferBlockchainData(payloadAndKind) - const { unitA, unitB } = getUnitPairs(blockchainData.marketName) - - let assetFrom = unitA - let assetTo = unitB - const amountPrecision = marketData[blockchainData.marketName].minTradeIncrement - if (blockchainData.buyOrSell === BuyOrSellBuy) { - assetFrom = unitB - assetTo = unitA - } + // let minOrderRate = orderData.rate const buffer = new SmartBuffer() buffer.writeString(kindToOrderPrefix(kind)) // prefix buffer.writeString(reverseHexString(getNEOScriptHash(address))) - buffer.writeString(getNEOAssetHash(assetData[assetFrom])) - buffer.writeString(getNEOAssetHash(assetData[assetTo])) + buffer.writeString(getNEOAssetHash(orderData.source.asset)) + buffer.writeString(getNEOAssetHash(orderData.destination.asset)) buffer.writeString(toLittleEndianHex(chainNoncePair.nonceFrom)) buffer.writeString(toLittleEndianHex(chainNoncePair.nonceTo)) - const amount = normalizeAmount(blockchainData.amount, amountPrecision) - buffer.writeString(toLittleEndianHex(amount)) - let orderRate: string = maxOrderRate + // market buy orders are not supported, but if they were we would set the amount to max value + if (payloadAndKind.kind === SigningPayloadID.placeMarketOrderPayload && blockchainData.buyOrSell === BuyOrSellBuy) { + buffer.writeString(MAX_ORDER_AMOUNT) + } else { + const amount = normalizeAmount(orderData.source.amount, BLOCKCHAIN_PRECISION) + buffer.writeString(toLittleEndianHex(amount)) + } + + let orderRate: number = MIN_ORDER_RATE if (isLimitOrderPayload(kind)) { - orderRate = toLittleEndianHex(normalizeAmount(blockchainData.limitPrice as string, 8)) + const rate = rateWithFees(orderData.rate) + orderRate = normalizeAmount(rate.toFormat(8, BigNumber.ROUND_DOWN, bigNumberFormat), BLOCKCHAIN_PRECISION) } - buffer.writeString(toLittleEndianHex(minOrderRate)) - buffer.writeString(orderRate) - buffer.writeString(toLittleEndianHex(maxFeeRate)) + buffer.writeString(toLittleEndianHex(orderRate)) + buffer.writeString(MAX_ORDER_RATE) + buffer.writeString(toLittleEndianHex(MAX_FEE_RATE)) // use the nonceOrder field when we need to sign any order payload. buffer.writeString(toLittleEndianHex(blockchainData.nonceOrder)) diff --git a/src/signPayload/signPayload.spec.ts b/src/signPayload/signPayload.spec.ts index f4342205..cd5e8dc6 100644 --- a/src/signPayload/signPayload.spec.ts +++ b/src/signPayload/signPayload.spec.ts @@ -8,7 +8,7 @@ import config from '../__tests__/blockchain_config.json' import state_signing_config from '../__tests__/state_signing_config.json' import sigTestVectors from '../__tests__/signatureVectors.json' import _ from 'lodash' -import { inferBlockchainData } from '../utils/blockchain' +import { inferBlockchainData, OrderSignatureData, buildOrderSignatureData } from '../utils/blockchain' import { determineSignatureNonceTuplesNeeded } from './signPayload' const privateKeyHex = '2304cae8deb223fbc6774964af6bc4fcda6ba6cff8276cb2c0f49fb0c8a51d57' @@ -222,7 +222,12 @@ test('signing orders with multiple nonces', async () => { } let blockchainData = inferBlockchainData({ kind: SigningPayloadID.placeMarketOrderPayload, payload }) - let result = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + let orderData: OrderSignatureData = buildOrderSignatureData(config.marketData, config.assetData, { + kind: SigningPayloadID.placeMarketOrderPayload, + payload + }) + + let result = determineSignatureNonceTuplesNeeded(orderData, blockchainData) expect(result).toEqual([ { chain: 'eth', nonceFrom: 1, nonceTo: ORDER_NONCE_IGNORE }, { chain: 'neo', nonceFrom: ORDER_NONCE_IGNORE, nonceTo: 1 }, @@ -240,7 +245,12 @@ test('signing orders with multiple nonces', async () => { payload.noncesTo = [1] blockchainData = inferBlockchainData({ kind: SigningPayloadID.placeMarketOrderPayload, payload }) - result = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + orderData = buildOrderSignatureData(config.marketData, config.assetData, { + kind: SigningPayloadID.placeMarketOrderPayload, + payload + }) + + result = determineSignatureNonceTuplesNeeded(orderData, blockchainData) expect(result).toEqual([ { chain: 'eth', nonceFrom: 1, nonceTo: ORDER_NONCE_IGNORE }, { chain: 'eth', nonceFrom: 2, nonceTo: ORDER_NONCE_IGNORE }, @@ -251,7 +261,12 @@ test('signing orders with multiple nonces', async () => { payload.noncesTo = [7, 8] blockchainData = inferBlockchainData({ kind: SigningPayloadID.placeMarketOrderPayload, payload }) - result = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + orderData = buildOrderSignatureData(config.marketData, config.assetData, { + kind: SigningPayloadID.placeMarketOrderPayload, + payload + }) + + result = determineSignatureNonceTuplesNeeded(orderData, blockchainData) expect(result).toEqual([ { chain: 'eth', nonceFrom: 1, nonceTo: ORDER_NONCE_IGNORE }, { chain: 'eth', nonceFrom: 2, nonceTo: ORDER_NONCE_IGNORE }, @@ -263,7 +278,12 @@ test('signing orders with multiple nonces', async () => { payload.noncesTo = [17, 18] payload.marketName = 'neo_gas' blockchainData = inferBlockchainData({ kind: SigningPayloadID.placeMarketOrderPayload, payload }) - result = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + orderData = buildOrderSignatureData(config.marketData, config.assetData, { + kind: SigningPayloadID.placeMarketOrderPayload, + payload + }) + + result = determineSignatureNonceTuplesNeeded(orderData, blockchainData) expect(result).toEqual([ { chain: 'neo', nonceFrom: 11, nonceTo: 17 }, { chain: 'neo', nonceFrom: 11, nonceTo: 18 }, diff --git a/src/signPayload/signPayload.ts b/src/signPayload/signPayload.ts index 40d38fd2..698814e0 100644 --- a/src/signPayload/signPayload.ts +++ b/src/signPayload/signPayload.ts @@ -33,10 +33,14 @@ import { ClientSignedState, SignStatesRequestPayload, AddMovementPayload, - BuyOrSellBuy, TransactionDigest } from '../payload' -import { inferBlockchainData, getUnitPairs, getBlockchainMovement } from '../utils/blockchain' +import { + inferBlockchainData, + getBlockchainMovement, + OrderSignatureData, + buildOrderSignatureData +} from '../utils/blockchain' import { buildNEOOrderSignatureData, buildNEOMovementSignatureData, @@ -338,7 +342,9 @@ export async function presignBlockchainData( } } const blockchainData = inferBlockchainData(payloadAndKind) - const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + const orderData: OrderSignatureData = buildOrderSignatureData(config.marketData, config.assetData, payloadAndKind) + + const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(orderData, blockchainData) const sigs: BlockchainSignature[] = await Promise.all( signatureNeeded.map(async chainNoncePair => { switch (chainNoncePair.chain) { @@ -346,10 +352,9 @@ export async function presignBlockchainData( const neoData = buildNEOOrderSignatureData( apiKey.child_keys[BIP44.NEO].address, apiKey.child_keys[BIP44.NEO].public_key, - config.assetData, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) const neoSignature = await presignNEOBlockchainData(apiKey, config, neoData) return { @@ -362,9 +367,9 @@ export async function presignBlockchainData( case 'eth': const ethData = buildETHOrderSignatureData( apiKey.child_keys[BIP44.ETH].address, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) const ethSignature = await presignETHBlockchainData(apiKey, config, ethData) return { @@ -426,7 +431,9 @@ export function signBlockchainData(config: Config, payloadAndKind: PayloadAndKin // if this is an order then its a bit more complicated const blockchainData = inferBlockchainData(payloadAndKind) - const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + const orderData: OrderSignatureData = buildOrderSignatureData(config.marketData, config.assetData, payloadAndKind) + + const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(orderData, blockchainData) const sigs = signatureNeeded.map(chainNoncePair => { switch (chainNoncePair.chain) { @@ -434,10 +441,9 @@ export function signBlockchainData(config: Config, payloadAndKind: PayloadAndKin const neoData = buildNEOOrderSignatureData( config.wallets.neo.address, config.wallets.neo.publicKey, - config.assetData, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) const neoSignature = signNEOBlockchainData(config.wallets.neo.privateKey, neoData) return { @@ -449,9 +455,9 @@ export function signBlockchainData(config: Config, payloadAndKind: PayloadAndKin case 'eth': const ethData = buildETHOrderSignatureData( config.wallets.eth.address, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) const ethSignature = signETHBlockchainData(config.wallets.eth.privateKey, ethData) return { @@ -481,21 +487,11 @@ export function signBlockchainData(config: Config, payloadAndKind: PayloadAndKin * @TODO Add documentation. */ export function determineSignatureNonceTuplesNeeded( - assetData: Config['assetData'], + orderData: OrderSignatureData, blockchainData: BlockchainData ): ChainNoncePair[] { - const { unitA, unitB } = getUnitPairs(blockchainData.marketName) - - let assetFrom = unitA - let assetTo = unitB - - if (blockchainData.buyOrSell === BuyOrSellBuy) { - assetFrom = unitB - assetTo = unitA - } - - const blockchainFrom = assetData[assetFrom].blockchain - const blockchainTo = assetData[assetTo].blockchain + const blockchainFrom = orderData.source.asset.blockchain + const blockchainTo = orderData.destination.asset.blockchain const blockchains = _.uniq([blockchainFrom, blockchainTo]) const needed: ChainNoncePair[] = [] @@ -516,7 +512,8 @@ export function determineSignatureNonceTuplesNeeded( */ export function addRawBlockchainOrderData(config: Config, payloadAndKind: PayloadAndKind): object { const blockchainData = inferBlockchainData(payloadAndKind) - const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + const orderData: OrderSignatureData = buildOrderSignatureData(config.marketData, config.assetData, payloadAndKind) + const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(orderData, blockchainData) const rawData = signatureNeeded.map(chainNoncePair => { switch (chainNoncePair.chain) { @@ -526,16 +523,15 @@ export function addRawBlockchainOrderData(config: Config, payloadAndKind: Payloa raw: buildNEOOrderSignatureData( config.wallets.neo.address, config.wallets.neo.publicKey, - config.assetData, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) } case 'eth': return { payload: payloadAndKind.payload, - raw: buildETHOrderSignatureData(config.wallets.eth.address, config.marketData, payloadAndKind, chainNoncePair) + raw: buildETHOrderSignatureData(config.wallets.eth.address, payloadAndKind, chainNoncePair, orderData) } case 'btc': return { @@ -556,7 +552,8 @@ export function addRawPresignBlockchainOrderData( payloadAndKind: PayloadAndKind ): object { const blockchainData = inferBlockchainData(payloadAndKind) - const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(config.assetData, blockchainData) + const orderData: OrderSignatureData = buildOrderSignatureData(config.marketData, config.assetData, payloadAndKind) + const signatureNeeded: ChainNoncePair[] = determineSignatureNonceTuplesNeeded(orderData, blockchainData) const rawData = signatureNeeded.map(chainNoncePair => { switch (chainNoncePair.chain) { @@ -566,10 +563,9 @@ export function addRawPresignBlockchainOrderData( raw: buildNEOOrderSignatureData( apiKey.child_keys[BIP44.NEO].address, apiKey.child_keys[BIP44.NEO].public_key, - config.assetData, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) } case 'eth': @@ -577,9 +573,9 @@ export function addRawPresignBlockchainOrderData( payload: payloadAndKind.payload, raw: buildETHOrderSignatureData( apiKey.child_keys[BIP44.ETH].address, - config.marketData, payloadAndKind, - chainNoncePair + chainNoncePair, + orderData ) } case 'btc': diff --git a/src/types/blockchainData.ts b/src/types/blockchainData.ts index a5f36f43..1b1822b1 100644 --- a/src/types/blockchainData.ts +++ b/src/types/blockchainData.ts @@ -1,3 +1,5 @@ +import BigNumber from 'bignumber.js' + /** * @TODO Add documentation. */ @@ -9,7 +11,7 @@ export default interface BlockchainData { readonly nonceOrder: number readonly noncesFrom: number[] readonly noncesTo: number[] - readonly limitPrice: string + readonly limitPrice: BigNumber } /** diff --git a/src/utils/blockchain/blockchain.spec.ts b/src/utils/blockchain/blockchain.spec.ts index 236500f8..2d41dd78 100644 --- a/src/utils/blockchain/blockchain.spec.ts +++ b/src/utils/blockchain/blockchain.spec.ts @@ -1,23 +1,18 @@ -import { BuyOrSellBuy, BuyOrSellSell } from '../../payload' +import { BuyOrSellBuy } from '../../payload' import { getLimitPrice } from './blockchain' test('unit rate conversion', async () => { - let price = { amount: '0.00123', currency_a: 'eth', currency_b: 'neo' } + let price = { amount: '0.00123000', currency_a: 'eth', currency_b: 'neo' } - let result = getLimitPrice('eth_neo', BuyOrSellBuy, price) + let result = getLimitPrice('eth_neo', BuyOrSellBuy, price).toFormat(8) expect(result).toBe('813.00813008') - result = getLimitPrice('eth_neo', BuyOrSellSell, price) - expect(result).toBe(price.amount) - - price = { amount: '0.00128', currency_a: 'eth', currency_b: 'neo' } - result = getLimitPrice('eth_neo', BuyOrSellSell, price) - expect(result).toBe(price.amount) - result = getLimitPrice('eth_neo', BuyOrSellBuy, price) + price = { amount: '0.00128000', currency_a: 'eth', currency_b: 'neo' } + result = getLimitPrice('eth_neo', BuyOrSellBuy, price).toFormat(8) expect(result).toBe('781.25000000') - price = { amount: '0.001249', currency_a: 'eth', currency_b: 'neo' } - result = getLimitPrice('eth_neo', BuyOrSellBuy, price) - expect(result).toBe('800.64051240') + price = { amount: '0.00124900', currency_a: 'eth', currency_b: 'neo' } + result = getLimitPrice('eth_neo', BuyOrSellBuy, price).toFormat(8) + expect(result).toBe('800.64051241') }) diff --git a/src/utils/blockchain/blockchain.ts b/src/utils/blockchain/blockchain.ts index 08ce843f..f9fc6c6e 100644 --- a/src/utils/blockchain/blockchain.ts +++ b/src/utils/blockchain/blockchain.ts @@ -1,4 +1,11 @@ -import { PayloadAndKind, SigningPayloadID, kindToOrderPrefix, isLimitOrderPayload, BuyOrSellSell } from '../../payload' +import { + PayloadAndKind, + SigningPayloadID, + kindToOrderPrefix, + BuyOrSellSell, + BuyOrSellBuy, + isLimitOrderPayload +} from '../../payload' import { Config, BlockchainData, BlockchainMovement, Asset } from '../../types' import getNEOScriptHash from '../getNEOScriptHash' import { normalizeAmount, toLittleEndianHex } from '../currency' @@ -9,9 +16,9 @@ import * as EC from 'elliptic' // only do this once export const ellipticContext = new EC.ec('secp256k1') -const BN = BigNumber.clone({ DECIMAL_PLACES: 16, ROUNDING_MODE: BigNumber.ROUND_FLOOR }) +export const BN = BigNumber.clone({ DECIMAL_PLACES: 16 }) -const bigNumberFormat = { +export const bigNumberFormat = { decimalSeparator: '.', groupSeparator: '', groupSize: 50, @@ -28,12 +35,10 @@ export function inferBlockchainData(payloadAndKind: PayloadAndKind): BlockchainD case SigningPayloadID.placeStopMarketOrderPayload: case SigningPayloadID.placeLimitOrderPayload: case SigningPayloadID.placeStopLimitOrderPayload: - let limitPrice: string = '' - + let limitPrice: BigNumber = new BigNumber(0) if (isLimitOrderPayload(kind)) { limitPrice = getLimitPrice(payload.marketName, payload.buyOrSell, payload.limitPrice) } - return { amount: payload.amount.amount, buyOrSell: payload.buyOrSell, @@ -112,18 +117,101 @@ export function getBlockchainMovement( } } -export function getLimitPrice(marketName: string, buyOrSell: string, limitPrice: any): string { - const { unitA, unitB } = getUnitPairs(marketName) - let assetFrom = unitB - if (buyOrSell === BuyOrSellSell) { - assetFrom = unitA +export interface AssetAmount { + amount: string + asset: Asset + symbol: string +} + +export interface OrderSignatureData { + destination: AssetAmount + meAmount: string + meRate: string + precision: number + rate: BigNumber + source: AssetAmount +} + +export function buildOrderSignatureData( + marketData: Config['marketData'], + assetData: Config['assetData'], + payloadAndKind: PayloadAndKind +): OrderSignatureData { + const blockchainData = inferBlockchainData(payloadAndKind) + const { unitA, unitB } = getUnitPairs(blockchainData.marketName) + const amountPrecision = marketData[blockchainData.marketName].minTradeIncrement + const assetA = assetData[unitA] + const assetB = assetData[unitB] + + // Amount of order always in asset A in ME + const amountOfA = blockchainData.amount + // Price is always in terms of asset B in ME + const bPerA = blockchainData.limitPrice + const aPerB = invertPrice(blockchainData.limitPrice) + const amountOfB = exchangeAmount(bPerA, amountOfA) + + switch (blockchainData.buyOrSell) { + case BuyOrSellBuy: + return { + destination: { + amount: amountOfA, + asset: assetA, + symbol: unitA + }, + meAmount: amountOfA, + meRate: bPerA.toFormat(bigNumberFormat), + precision: amountPrecision, + rate: aPerB, + source: { + amount: amountOfB, + asset: assetB, + symbol: unitB + } + } + case BuyOrSellSell: + return { + destination: { + amount: amountOfB, + asset: assetB, + symbol: unitB + }, + meAmount: amountOfA, + meRate: bPerA.toFormat(bigNumberFormat), + precision: amountPrecision, + rate: bPerA, + source: { + amount: amountOfA, + asset: assetA, + symbol: unitA + } + } + default: + throw new Error('Invalid order side') } +} + +export function invertPrice(amount: BigNumber): BigNumber { + return new BN(1).div(amount) +} + +export function exchangeAmount(price: BigNumber, amount: string): string { + const total = new BigNumber(amount).times(price) + return total.toFormat(8, BigNumber.ROUND_DOWN, bigNumberFormat) +} + +export function rateWithFees(rate: BigNumber): BigNumber { + return rate.times(399).div(400) +} + +export function getLimitPrice(marketName: string, buyOrSell: string, limitPrice: any): BigNumber { + const { unitB } = getUnitPairs(marketName) + const assetFrom = unitB + if (limitPrice.currency_a === assetFrom) { - return limitPrice.amount + return new BigNumber(limitPrice.amount) } else if (limitPrice.currency_b === assetFrom) { const amount = new BN(limitPrice.amount) - const reciprocal = new BN(1).div(amount) - return reciprocal.toFormat(8, bigNumberFormat) + return new BN(1).div(amount) } throw Error( diff --git a/yarn.lock b/yarn.lock index e1945f40..77a1180b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -129,10 +129,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.18.tgz#1d3ca764718915584fcd9f6344621b7672665c67" integrity sha512-fh+pAqt4xRzPfqA6eh3Z2y6fyZavRIumvjhaCL753+TVkGKGhpPeyrJG2JftD0T9q4GF00KjefsQ+PQNDdWQaQ== -"@types/node@13.7.0": - version "13.7.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.7.0.tgz#b417deda18cf8400f278733499ad5547ed1abec4" - integrity sha512-GnZbirvmqZUzMgkFn70c74OQpTTUcCzlhQliTzYjQMqg+hVKcDnxdL19Ne3UdYzdMA/+W3eb646FWn/ZaT1NfQ== +"@types/node@^14.14.14": + version "14.14.14" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.14.tgz#f7fd5f3cc8521301119f63910f0fb965c7d761ae" + integrity sha512-UHnOPWVWV1z+VV8k6L1HhG7UbGBgIdghqF3l9Ny9ApPghbjICXkUJSd/b9gOgQfjM1r+37cipdw/HJ3F6ICEnQ== "@types/randombytes@2.0.0": version "2.0.0"