From 7058a15909407df076e7e503ca6fb26c8d43ebd7 Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Thu, 17 Sep 2020 12:58:23 -0400 Subject: [PATCH] feat: bigint units & decimalplaces from db This commit makes two significant changes. 1. It uses the new `bigint` type - supported as of node LTS v14 - for internal representations of the base "units" of any currencies/tokens. ETH wei, having 10^18 units per ether, can exceed the capacity of javascript's `number` type and lead to unexpected rounding or scientific notation when converting to string that has required hacky workarounds. Using `bigint` gives us native integer support for base tokens for all tokens, regardless of how many decimal places they can be split into. 2. It reads the aforementioned decimal places from the database for every token. Previously, we hardcoded the units per tokens for a handful of known ERC20 tokens. Adding other currencies to be supported by xud would have required a code change. Now, new tokens can be supported with changes to the database via rpc calls, without requiring code updates. Closes #1054. Closes #1888. --- lib/Xud.ts | 9 +- lib/connextclient/ConnextClient.ts | 48 ++++---- lib/connextclient/types.ts | 4 +- lib/http/HttpService.ts | 2 +- lib/lndclient/LndClient.ts | 20 ++-- lib/orderbook/OrderBook.ts | 17 +-- lib/swaps/SwapClient.ts | 6 +- lib/swaps/SwapClientManager.ts | 6 +- lib/swaps/Swaps.ts | 74 +++++++------ lib/swaps/types.ts | 12 +- lib/utils/UnitConverter.ts | 66 ++++++----- test/integration/OrderBook.spec.ts | 17 ++- test/integration/Swaps.spec.ts | 9 +- test/jest/Connext.spec.ts | 34 ++++-- test/jest/DB.spec.ts | 4 +- test/jest/LndClient.spec.ts | 10 +- test/jest/Orderbook.spec.ts | 8 +- test/jest/SwapClientManager.spec.ts | 15 ++- test/jest/Swaps.spec.ts | 29 +++++ test/jest/UnitConverter.spec.ts | 66 +++++------ .../__snapshots__/UnitConverter.spec.ts.snap | 4 +- test/jest/integration/Swaps.spec.ts | 103 +++++++++++++++++- test/unit/Swaps.spec.ts | 91 +--------------- test/utils.ts | 14 ++- 24 files changed, 380 insertions(+), 288 deletions(-) diff --git a/lib/Xud.ts b/lib/Xud.ts index c639c804e..75e6ee840 100644 --- a/lib/Xud.ts +++ b/lib/Xud.ts @@ -44,7 +44,6 @@ class Xud extends EventEmitter { private swaps!: Swaps; private shuttingDown = false; private swapClientManager?: SwapClientManager; - private unitConverter?: UnitConverter; private simnetChannels$?: Subscription; /** @@ -118,8 +117,8 @@ class Xud extends EventEmitter { this.db = new DB(loggers.db, this.config.dbpath); await this.db.init(this.config.network, this.config.initdb); - this.unitConverter = new UnitConverter(); - this.unitConverter.init(); + const currencies = await this.db.models.Currency.findAll(); + const unitConverter = new UnitConverter(currencies); const nodeKeyPath = NodeKey.getPath(this.config.xudir, this.config.instanceid); const nodeKeyExists = await fs @@ -127,7 +126,7 @@ class Xud extends EventEmitter { .then(() => true) .catch(() => false); - this.swapClientManager = new SwapClientManager(this.config, loggers, this.unitConverter, this.db.models); + this.swapClientManager = new SwapClientManager(this.config, loggers, unitConverter, this.db.models); await this.swapClientManager.init(); let nodeKey: NodeKey | undefined; @@ -182,6 +181,7 @@ class Xud extends EventEmitter { const initPromises: Promise[] = []; this.swaps = new Swaps({ + unitConverter, logger: loggers.swaps, models: this.db.models, pool: this.pool, @@ -191,6 +191,7 @@ class Xud extends EventEmitter { initPromises.push(this.swaps.init()); this.orderBook = new OrderBook({ + unitConverter, logger: loggers.orderbook, models: this.db.models, thresholds: this.config.orderthresholds, diff --git a/lib/connextclient/ConnextClient.ts b/lib/connextclient/ConnextClient.ts index 9ea7c7736..e8b160630 100644 --- a/lib/connextclient/ConnextClient.ts +++ b/lib/connextclient/ConnextClient.ts @@ -47,11 +47,11 @@ import { interface ConnextClient { on(event: 'preimage', listener: (preimageRequest: ProvidePreimageEvent) => void): void; on(event: 'transferReceived', listener: (transferReceivedRequest: TransferReceivedEvent) => void): void; - on(event: 'htlcAccepted', listener: (rHash: string, amount: number, currency: string) => void): this; + on(event: 'htlcAccepted', listener: (rHash: string, units: bigint, currency: string) => void): this; on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this; on(event: 'depositConfirmed', listener: (hash: string) => void): this; once(event: 'initialized', listener: () => void): this; - emit(event: 'htlcAccepted', rHash: string, amount: number, currency: string): boolean; + emit(event: 'htlcAccepted', rHash: string, units: bigint, currency: string): boolean; emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean; emit(event: 'initialized'): boolean; emit(event: 'preimage', preimageRequest: ProvidePreimageEvent): void; @@ -249,7 +249,7 @@ class ConnextClient extends SwapClient { * if one doesn't exist, starts a new request for the specified amount. Then * calls channelBalance to refresh the inbound capacity for the currency. */ - private requestCollateralInBackground = (currency: string, units: number) => { + private requestCollateralInBackground = (currency: string, units: bigint) => { // first check whether we already have a pending collateral request for this currency // if not start a new request, and when it completes call channelBalance to refresh our inbound capacity const requestCollateralPromise = @@ -257,7 +257,7 @@ class ConnextClient extends SwapClient { this.sendRequest('/request-collateral', 'POST', { channelAddress: this.channelAddress, assetId: this.tokenAddresses.get(currency), - amount: units.toLocaleString('fullwide', { useGrouping: false }), + amount: units.toString(), publicIdentifier: this.publicIdentifier, }) .then(() => { @@ -452,7 +452,7 @@ class ConnextClient extends SwapClient { let secret; if (deal.role === SwapRole.Maker) { // we are the maker paying the taker - amount = deal.takerUnits.toLocaleString('fullwide', { useGrouping: false }); + amount = deal.takerUnits.toString(); tokenAddress = this.tokenAddresses.get(deal.takerCurrency)!; const expiry = await this.getExpiry(this.finalLock); const executeTransfer = this.executeHashLockTransfer({ @@ -477,7 +477,7 @@ class ConnextClient extends SwapClient { secret = preimage; } else { // we are the taker paying the maker - amount = deal.makerUnits.toLocaleString('fullwide', { useGrouping: false }); + amount = deal.makerUnits.toString(); tokenAddress = this.tokenAddresses.get(deal.makerCurrency)!; secret = deal.rPreimage!; assert(deal.makerCltvDelta, 'cannot send transfer without deal.makerCltvDelta'); @@ -518,7 +518,7 @@ class ConnextClient extends SwapClient { currency: expectedCurrency, }: { rHash: string; - units: number; + units: bigint; expiry?: number; currency?: string; }) => { @@ -841,12 +841,12 @@ class ConnextClient extends SwapClient { const freeBalanceAmount = this.unitConverter.unitsToAmount({ currency, - units: Number(freeBalanceOffChain), + units: BigInt(freeBalanceOffChain), }); const nodeFreeBalanceAmount = this.unitConverter.unitsToAmount({ currency, - units: Number(nodeFreeBalanceOffChain), + units: BigInt(nodeFreeBalanceOffChain), }); this.outboundAmounts.set(currency, freeBalanceAmount); @@ -890,7 +890,7 @@ class ConnextClient extends SwapClient { const confirmedBalanceAmount = this.unitConverter.unitsToAmount({ currency, - units: Number(freeBalanceOnChain), + units: BigInt(freeBalanceOnChain), }); return { @@ -985,7 +985,7 @@ class ConnextClient extends SwapClient { const withdrawResponse = await this.sendRequest('/withdraw', 'POST', { publicIdentifier: this.publicIdentifier, channelAddress: this.channelAddress, - amount: amount.toLocaleString('fullwide', { useGrouping: false }), + amount: amount.toString(), assetId: this.tokenAddresses.get(currency), recipient: destination, fee: gasPriceGwei, @@ -1012,19 +1012,13 @@ class ConnextClient extends SwapClient { }; // Withdraw on-chain funds - public withdraw = async ({ - all, - currency, - amount: argAmount, - destination, - fee, - }: WithdrawArguments): Promise => { + public withdraw = async ({ all, currency, amount, destination, fee }: WithdrawArguments): Promise => { if (fee) { // TODO: allow overwriting gas price throw Error('setting fee for Ethereum withdrawals is not supported yet'); } - let units = ''; + let unitsStr: string; const { freeBalanceOnChain } = await this.getBalance(currency); @@ -1033,21 +1027,23 @@ class ConnextClient extends SwapClient { // TODO: query Ether balance, subtract gas price times 21000 (gas usage of transferring Ether), and set that as amount throw new Error('withdrawing all ETH is not supported yet'); } - units = freeBalanceOnChain; - } else if (argAmount) { - const argUnits = this.unitConverter.amountToUnits({ + unitsStr = freeBalanceOnChain; + } else if (amount) { + const units = this.unitConverter.amountToUnits({ currency, - amount: argAmount, + amount, }); - if (Number(freeBalanceOnChain) < argUnits) { + if (Number(freeBalanceOnChain) < units) { throw errors.INSUFFICIENT_BALANCE; } - units = argUnits.toString(); + unitsStr = units.toString(); + } else { + throw new Error('either all must be true or amount must be non-zero'); } const res = await this.sendRequest('/onchain-transfer', 'POST', { assetId: this.getTokenAddress(currency), - amount: units, + amount: unitsStr, recipient: destination, }); const { txhash } = await parseResponseBody(res); diff --git a/lib/connextclient/types.ts b/lib/connextclient/types.ts index 162697a77..f1e0981db 100644 --- a/lib/connextclient/types.ts +++ b/lib/connextclient/types.ts @@ -213,7 +213,7 @@ export type TransfersByRoutingIdResponse = ConnextTransfer[]; export type ExpectedIncomingTransfer = { rHash: string; - units: number; + units: bigint; expiry: number; tokenAddress: string; routingId: string; @@ -316,7 +316,7 @@ export type TransferReceivedEvent = { tokenAddress: string; rHash: string; expiry: number; - units: number; + units: bigint; routingId: string; }; diff --git a/lib/http/HttpService.ts b/lib/http/HttpService.ts index 279ebb356..4daa9b623 100644 --- a/lib/http/HttpService.ts +++ b/lib/http/HttpService.ts @@ -37,7 +37,7 @@ class HttpService { const rHash = lockHash.slice(2); const expiry = parseInt(expiryString, 10); const { amount } = balance; - const units = parseInt(amount[0], 10); + const units = BigInt(amount[0]); await this.service.transferReceived({ rHash, expiry, diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 550b3f564..f196a2bcb 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -30,7 +30,7 @@ import { Chain, ChannelCount, ClientMethods, LndClientConfig, LndInfo } from './ interface LndClient { on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this; - on(event: 'htlcAccepted', listener: (rHash: string, amount: number) => void): this; + on(event: 'htlcAccepted', listener: (rHash: string, units: bigint) => void): this; on(event: 'channelBackup', listener: (channelBackup: Uint8Array) => void): this; on(event: 'channelBackupEnd', listener: () => void): this; on(event: 'locked', listener: () => void): this; @@ -38,7 +38,7 @@ interface LndClient { once(event: 'initialized', listener: () => void): this; emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean; - emit(event: 'htlcAccepted', rHash: string, amount: number): boolean; + emit(event: 'htlcAccepted', rHash: string, units: bigint): boolean; emit(event: 'channelBackup', channelBackup: Uint8Array): boolean; emit(event: 'channelBackupEnd'): boolean; emit(event: 'locked'): boolean; @@ -502,7 +502,7 @@ class LndClient extends SwapClient { const randomHash = crypto.randomBytes(32).toString('hex'); this.logger.debug(`checking hold invoice support with hash: ${randomHash}`); - await this.addInvoice({ rHash: randomHash, units: 1 }); + await this.addInvoice({ rHash: randomHash, units: 1n }); await this.removeInvoice(randomHash); } catch (err) { if (err.code !== grpc.status.UNAVAILABLE) { @@ -846,7 +846,7 @@ class LndClient extends SwapClient { remoteIdentifier, units, uris, - pushUnits = 0, + pushUnits = 0n, fee = 0, }: OpenChannelParams): Promise => { if (!remoteIdentifier) { @@ -857,7 +857,7 @@ class LndClient extends SwapClient { await this.connectPeerAddresses(uris); } - const openResponse = await this.openChannelSync(remoteIdentifier, units, pushUnits, fee); + const openResponse = await this.openChannelSync(remoteIdentifier, Number(units), Number(pushUnits), fee); return openResponse.hasFundingTxidStr() ? openResponse.getFundingTxidStr() : base64ToHex(openResponse.getFundingTxidBytes_asB64()); @@ -927,9 +927,9 @@ class LndClient extends SwapClient { ); }; - public getRoute = async (units: number, destination: string, _currency: string, finalLock = this.finalLock) => { + public getRoute = async (units: bigint, destination: string, _currency: string, finalLock = this.finalLock) => { const request = new lndrpc.QueryRoutesRequest(); - request.setAmt(units); + request.setAmt(Number(units)); request.setFinalCltvDelta(finalLock); request.setPubKey(destination); const fee = new lndrpc.FeeLimit(); @@ -1090,12 +1090,12 @@ class LndClient extends SwapClient { expiry = this.finalLock, }: { rHash: string; - units: number; + units: bigint; expiry?: number; }) => { const addHoldInvoiceRequest = new lndinvoices.AddHoldInvoiceRequest(); addHoldInvoiceRequest.setHash(hexToUint8Array(rHash)); - addHoldInvoiceRequest.setValue(units); + addHoldInvoiceRequest.setValue(Number(units)); addHoldInvoiceRequest.setCltvExpiry(expiry); await this.addHoldInvoice(addHoldInvoiceRequest); this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${expiry}`); @@ -1224,7 +1224,7 @@ class LndClient extends SwapClient { if (invoice.getState() === lndrpc.Invoice.InvoiceState.ACCEPTED) { // we have accepted an htlc for this invoice this.logger.debug(`accepted htlc for invoice ${rHash}`); - this.emit('htlcAccepted', rHash, invoice.getValue()); + this.emit('htlcAccepted', rHash, BigInt(invoice.getValue())); } }) .on('end', deleteInvoiceSubscription) diff --git a/lib/orderbook/OrderBook.ts b/lib/orderbook/OrderBook.ts index 48c5e751d..decc2fbcc 100644 --- a/lib/orderbook/OrderBook.ts +++ b/lib/orderbook/OrderBook.ts @@ -1,7 +1,6 @@ import assert from 'assert'; import { EventEmitter } from 'events'; import uuidv1 from 'uuid/v1'; -import { UnitConverter } from '../utils/UnitConverter'; import { SwapClientType, SwapFailureReason, SwapPhase, SwapRole } from '../constants/enums'; import { Models } from '../db/DB'; import { CurrencyCreationAttributes, CurrencyInstance, OrderCreationAttributes, PairInstance } from '../db/types'; @@ -12,6 +11,7 @@ import Pool from '../p2p/Pool'; import Swaps from '../swaps/Swaps'; import { SwapDeal, SwapFailure, SwapSuccess } from '../swaps/types'; import { pubKeyToAlias } from '../utils/aliasUtils'; +import { UnitConverter } from '../utils/UnitConverter'; import { derivePairId, ms, setTimeoutPromise } from '../utils/utils'; import errors, { errorCodes } from './errors'; import OrderBookRepository from './OrderBookRepository'; @@ -92,6 +92,7 @@ class OrderBook extends EventEmitter { private strict: boolean; private pool: Pool; private swaps: Swaps; + private unitConverter: UnitConverter; /** Max time for placeOrder iterations (due to swaps failures retries). */ private static readonly MAX_PLACEORDER_ITERATIONS_TIME = 60000; // 1 min @@ -113,6 +114,7 @@ class OrderBook extends EventEmitter { thresholds, pool, swaps, + unitConverter, nosanityswaps, nobalancechecks, nomatching = false, @@ -123,6 +125,7 @@ class OrderBook extends EventEmitter { thresholds: OrderBookThresholds; pool: Pool; swaps: Swaps; + unitConverter: UnitConverter; nosanityswaps: boolean; nobalancechecks: boolean; nomatching?: boolean; @@ -133,6 +136,7 @@ class OrderBook extends EventEmitter { this.logger = logger; this.pool = pool; this.swaps = swaps; + this.unitConverter = unitConverter; this.nomatching = nomatching; this.nosanityswaps = nosanityswaps; this.nobalancechecks = nobalancechecks; @@ -147,7 +151,7 @@ class OrderBook extends EventEmitter { outboundCurrency, inboundAmount, outboundAmount, - } = UnitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); + } = this.unitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); this.swaps.swapClientManager.subtractInboundReservedAmount(inboundCurrency, inboundAmount); this.swaps.swapClientManager.subtractOutboundReservedAmount(outboundCurrency, outboundAmount); }; @@ -160,7 +164,7 @@ class OrderBook extends EventEmitter { outboundCurrency, inboundAmount, outboundAmount, - } = UnitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); + } = this.unitConverter.calculateInboundOutboundAmounts(order.quantity, order.price, order.isBuy, order.pairId); this.swaps.swapClientManager.addInboundReservedAmount(inboundCurrency, inboundAmount); this.swaps.swapClientManager.addOutboundReservedAmount(outboundCurrency, outboundAmount); }); @@ -371,10 +375,9 @@ class OrderBook extends EventEmitter { if (currency.swapClient === SwapClientType.Connext && !currency.tokenAddress) { throw errors.CURRENCY_MISSING_ETHEREUM_CONTRACT_ADDRESS(currency.id); } - const currencyInstance = await this.repository.addCurrency({ - ...currency, - decimalPlaces: currency.decimalPlaces || 8, - }); + const decimalPlaces = currency.decimalPlaces || 8; + const currencyInstance = await this.repository.addCurrency({ ...currency, decimalPlaces }); + this.unitConverter.setDecimalPlacesPerCurrency(currency.id, decimalPlaces); this.currencyInstances.set(currencyInstance.id, currencyInstance); await this.swaps.swapClientManager.add(currencyInstance); }; diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index c8b77c22c..8cecff130 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -71,8 +71,10 @@ export type WithdrawArguments = { interface SwapClient { on(event: 'connectionVerified', listener: (swapClientInfo: SwapClientInfo) => void): this; + on(event: 'htlcAccepted', listener: (rHash: string, units: bigint, currency?: string) => void): this; once(event: 'initialized', listener: () => void): this; emit(event: 'connectionVerified', swapClientInfo: SwapClientInfo): boolean; + emit(event: 'htlcAccepted', rHash: string, units: bigint, currency?: string): boolean; emit(event: 'initialized'): boolean; } @@ -287,7 +289,7 @@ abstract class SwapClient extends EventEmitter { * @returns routes */ public abstract async getRoute( - units: number, + units: bigint, destination: string, currency: string, finalCltvDelta?: number, @@ -314,7 +316,7 @@ abstract class SwapClient extends EventEmitter { currency, }: { rHash: string; - units: number; + units: bigint; expiry?: number; currency?: string; }): Promise; diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index 79852a91c..2c102f640 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -37,11 +37,11 @@ interface SwapClientManager { on(event: 'connextUpdate', listener: (tokenAddresses: Map, pubKey?: string) => void): this; on( event: 'htlcAccepted', - listener: (swapClient: SwapClient, rHash: string, amount: number, currency: string) => void, + listener: (swapClient: SwapClient, rHash: string, units: bigint, currency: string) => void, ): this; emit(event: 'lndUpdate', lndUpdate: LndUpdate): boolean; emit(event: 'connextUpdate', tokenAddresses: Map, pubKey?: string): boolean; - emit(event: 'htlcAccepted', swapClient: SwapClient, rHash: string, amount: number, currency: string): boolean; + emit(event: 'htlcAccepted', swapClient: SwapClient, rHash: string, units: bigint, currency: string): boolean; } class SwapClientManager extends EventEmitter { @@ -212,7 +212,7 @@ class SwapClientManager extends EventEmitter { inboundCurrency, outboundAmount, inboundAmount, - } = UnitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); + } = this.unitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); // check if clients exists const outboundSwapClient = this.get(outboundCurrency); diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index 89a3c43b6..4457c441a 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -51,6 +51,8 @@ class Swaps extends EventEmitter { private timeouts = new Map(); private usedHashes = new Set(); private repository: SwapRepository; + private unitConverter: UnitConverter; + /** The maximum time in milliseconds we will wait for a swap to be accepted before failing it. */ private static readonly SWAP_ACCEPT_TIMEOUT = 10000; /** The maximum time in milliseconds we will wait for a swap to be completed before failing it. */ @@ -79,12 +81,14 @@ class Swaps extends EventEmitter { models, pool, swapClientManager, + unitConverter, strict = true, }: { logger: Logger; models: Models; pool: Pool; swapClientManager: SwapClientManager; + unitConverter: UnitConverter; strict?: boolean; }) { super(); @@ -93,6 +97,7 @@ class Swaps extends EventEmitter { this.models = models; this.pool = pool; this.swapClientManager = swapClientManager; + this.unitConverter = unitConverter; this.strict = strict; this.swapRecovery = new SwapRecovery(swapClientManager, logger.createSubLogger('RECOVERY')); this.repository = new SwapRepository(this.models); @@ -141,7 +146,7 @@ class Swaps extends EventEmitter { * @param isBuy Whether the maker order in the swap is a buy * @returns An object with the calculated maker and taker values. */ - private static calculateMakerTakerAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { + private calculateMakerTakerAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { const { inboundCurrency, inboundAmount, @@ -149,7 +154,7 @@ class Swaps extends EventEmitter { outboundCurrency, outboundAmount, outboundUnits, - } = UnitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); + } = this.unitConverter.calculateInboundOutboundAmounts(quantity, price, isBuy, pairId); return { makerCurrency: inboundCurrency, makerAmount: inboundAmount, @@ -201,7 +206,7 @@ class Swaps extends EventEmitter { this.sanitySwaps.set(rHash, sanitySwap); const swapClient = this.swapClientManager.get(currency)!; try { - await swapClient.addInvoice({ rHash, units: 1 }); + await swapClient.addInvoice({ rHash, units: 1n }); } catch (err) { this.logger.error('could not add invoice for sanity swap', err); return; @@ -302,7 +307,13 @@ class Swaps extends EventEmitter { public addDeal = (deal: SwapDeal) => { this.deals.set(deal.rHash, deal); this.usedHashes.add(deal.rHash); - this.logger.debug(`New deal: ${JSON.stringify(deal)}`); + this.logger.debug( + `New deal: ${JSON.stringify({ + ...deal, + makerUnits: deal.makerUnits.toString(), + takerUnits: deal.takerUnits.toString(), + })}`, + ); }; /** @@ -320,7 +331,7 @@ class Swaps extends EventEmitter { throw SwapFailureReason.SwapClientNotSetup; } - const { makerCurrency, makerUnits } = Swaps.calculateMakerTakerAmounts( + const { makerCurrency, makerUnits } = this.calculateMakerTakerAmounts( taker.quantity, maker.price, maker.isBuy, @@ -414,7 +425,7 @@ class Swaps extends EventEmitter { try { await Promise.all([ - swapClient.addInvoice({ rHash, units: 1 }), + swapClient.addInvoice({ rHash, units: 1n }), peer.sendPacket(sanitySwapInitPacket), peer.wait(sanitySwapInitPacket.header.id, PacketType.SanitySwapAck, Swaps.SANITY_SWAP_INIT_TIMEOUT), ]); @@ -456,7 +467,7 @@ class Swaps extends EventEmitter { takerCurrency, takerAmount, takerUnits, - } = Swaps.calculateMakerTakerAmounts(quantity, maker.price, maker.isBuy, maker.pairId); + } = this.calculateMakerTakerAmounts(quantity, maker.price, maker.isBuy, maker.pairId); const clientType = this.swapClientManager.get(makerCurrency)!.type; const destination = peer.getIdentifier(clientType, makerCurrency)!; @@ -544,7 +555,7 @@ class Swaps extends EventEmitter { takerCurrency, takerAmount, takerUnits, - } = Swaps.calculateMakerTakerAmounts(quantity, price, isBuy, pairId); + } = this.calculateMakerTakerAmounts(quantity, price, isBuy, pairId); const makerSwapClient = this.swapClientManager.get(makerCurrency)!; if (!makerSwapClient) { @@ -765,7 +776,7 @@ class Swaps extends EventEmitter { return true; }; - private handleHtlcAccepted = async (swapClient: SwapClient, rHash: string, amount: number, currency: string) => { + private handleHtlcAccepted = async (swapClient: SwapClient, rHash: string, units: bigint, currency: string) => { let rPreimage: string; const deal = this.getDeal(rHash); @@ -778,7 +789,7 @@ class Swaps extends EventEmitter { } try { - rPreimage = await this.resolveHash(rHash, amount, currency); + rPreimage = await this.resolveHash(rHash, units, currency); } catch (err) { this.logger.error(`could not resolve hash for deal ${rHash}`, err); return; @@ -888,7 +899,7 @@ class Swaps extends EventEmitter { // TODO: penalize peer return; } else if (quantity < deal.proposedQuantity) { - const { makerAmount, takerAmount } = Swaps.calculateMakerTakerAmounts( + const { makerAmount, takerAmount } = this.calculateMakerTakerAmounts( quantity, deal.price, deal.isBuy, @@ -963,16 +974,16 @@ class Swaps extends EventEmitter { * @returns `true` if the resolve request is valid, `false` otherwise */ private validateResolveRequest = (deal: SwapDeal, resolveRequest: ResolveRequest) => { - const { amount, tokenAddress, expiration, chainHeight } = resolveRequest; + const { units, tokenAddress, expiration, chainHeight } = resolveRequest; const peer = this.pool.getPeer(deal.peerPubKey); - let expectedAmount: number; + let expectedUnits: bigint; let expectedTokenAddress: string | undefined; let expectedCurrency: string; let source: string; let destination: string; switch (deal.role) { case SwapRole.Maker: { - expectedAmount = deal.makerUnits; + expectedUnits = deal.makerUnits; expectedCurrency = deal.makerCurrency; expectedTokenAddress = this.swapClientManager.connextClient?.tokenAddresses.get(deal.makerCurrency); source = 'Taker'; @@ -1001,7 +1012,7 @@ class Swaps extends EventEmitter { break; } case SwapRole.Taker: - expectedAmount = deal.takerUnits; + expectedUnits = deal.takerUnits; expectedCurrency = deal.takerCurrency; expectedTokenAddress = this.swapClientManager.connextClient?.tokenAddresses.get(deal.takerCurrency); source = 'Maker'; @@ -1030,8 +1041,8 @@ class Swaps extends EventEmitter { return false; } - if (amount < expectedAmount) { - this.logger.error(`received ${amount}, expected ${expectedAmount}`); + if (units < expectedUnits) { + this.logger.error(`received ${units}, expected ${expectedUnits}`); this.failDeal({ deal, peer, @@ -1046,12 +1057,7 @@ class Swaps extends EventEmitter { }; /** Attempts to resolve the preimage for the payment hash of a pending sanity swap. */ - private resolveSanitySwap = async (rHash: string, amount: number, htlcCurrency?: string) => { - assert( - amount === 1, - 'sanity swaps must have an amount of exactly 1 of the smallest unit supported by the currency', - ); - + private resolveSanitySwap = async (rHash: string, htlcCurrency?: string) => { const sanitySwap = this.sanitySwaps.get(rHash); if (sanitySwap) { @@ -1097,17 +1103,17 @@ class Swaps extends EventEmitter { /** * Resolves the hash for an incoming HTLC to its preimage. * @param rHash the payment hash to resolve - * @param amount the amount in satoshis + * @param units the amount in base units of the currency * @param htlcCurrency the currency of the HTLC * @returns the preimage for the provided payment hash */ - public resolveHash = async (rHash: string, amount: number, htlcCurrency?: string): Promise => { + public resolveHash = async (rHash: string, units: bigint, htlcCurrency?: string): Promise => { const deal = this.getDeal(rHash); if (!deal) { - if (amount === 1) { - // if we don't have a deal for this hash, but its amount is exactly 1 satoshi, try to resolve it as a sanity swap - return this.resolveSanitySwap(rHash, amount, htlcCurrency); + if (units === 1n) { + // if we don't have a deal for this hash, but the amount is exactly 1 unit, try to resolve it as a sanity swap + return this.resolveSanitySwap(rHash, htlcCurrency); } else { throw errors.PAYMENT_HASH_NOT_FOUND(rHash); } @@ -1236,7 +1242,7 @@ class Swaps extends EventEmitter { }; public handleResolveRequest = async (resolveRequest: ResolveRequest): Promise => { - const { amount, rHash } = resolveRequest; + const { units, rHash } = resolveRequest; this.logger.debug(`handleResolveRequest starting with hash ${rHash}`); @@ -1257,7 +1263,7 @@ class Swaps extends EventEmitter { } try { - const preimage = await this.resolveHash(rHash, amount); + const preimage = await this.resolveHash(rHash, units); // we treat responding to a resolve request as having received payment and persist the state await this.setDealPhase(deal, SwapPhase.PaymentReceived); @@ -1479,7 +1485,13 @@ class Swaps extends EventEmitter { rHash, setTimeout(this.handleSwapTimeout, Swaps.SWAP_ACCEPT_TIMEOUT, rHash, SwapFailureReason.DealTimedOut), ); - this.logger.debug(`Requesting deal: ${JSON.stringify(deal)}`); + this.logger.debug( + `Requesting deal: ${JSON.stringify({ + ...deal, + makerUnits: deal.makerUnits.toString(), + takerUnits: deal.takerUnits.toString(), + })}`, + ); break; case SwapPhase.SwapAccepted: assert(deal.role === SwapRole.Maker, 'SwapAccepted can only be set by the maker'); diff --git a/lib/swaps/types.ts b/lib/swaps/types.ts index db2ae5e39..1f9f1898e 100644 --- a/lib/swaps/types.ts +++ b/lib/swaps/types.ts @@ -30,7 +30,7 @@ export type SwapDeal = { /** The amount the taker is expecting to receive denominated in satoshis. */ takerAmount: number; /** The number of the smallest base units of the currency (like satoshis or wei) the maker is expecting to receive. */ - takerUnits: number; + takerUnits: bigint; /** The currency the taker is expecting to receive. */ takerCurrency: string; /** Taker's lnd pubkey on the taker currency's network. */ @@ -40,7 +40,7 @@ export type SwapDeal = { /** The amount the maker is expecting to receive denominated in satoshis. */ makerAmount: number; /** The number of the smallest base units of the currency (like satoshis or wei) the maker is expecting to receive. */ - makerUnits: number; + makerUnits: bigint; /** The currency the maker is expecting to receive. */ makerCurrency: string; /** The CLTV delta from the current height that should be used to set the timelock for the final hop when sending to maker. */ @@ -130,7 +130,7 @@ export type TradingLimits = { export type ResolveRequest = { /** The amount of the incoming payment pending resolution, in the smallest units supported by the token. */ - amount: number; + units: bigint; rHash: string; tokenAddress: string; /** The number of blocks before the incoming payment expires. */ @@ -145,7 +145,7 @@ export type CloseChannelParams = { * The amount to extract from the channel, if applicable. If 0 or unspecified, * the entire off-chain balance for the specified currency will be extracted. */ - units?: number; + units?: bigint; currency?: string; /** * The on-chain address to send funds extracted from the channel. If unspecified @@ -161,12 +161,12 @@ export type OpenChannelParams = { /** The remote node with which to open the channel. */ remoteIdentifier?: string; /** The size of the channel. */ - units: number; + units: bigint; currency?: string; /** Uris with which to connect to the remote node. */ uris?: string[]; /** The balance to assign to the remote node. */ - pushUnits?: number; + pushUnits?: bigint; /** The fee in sat per byte. */ fee?: number; }; diff --git a/lib/utils/UnitConverter.ts b/lib/utils/UnitConverter.ts index 39a5af3b9..75415c3d6 100644 --- a/lib/utils/UnitConverter.ts +++ b/lib/utils/UnitConverter.ts @@ -1,16 +1,17 @@ -const UNITS_PER_CURRENCY: { [key: string]: number } = { - BTC: 1, - LTC: 1, - ETH: 10 ** 10, - USDT: 10 ** -2, - WETH: 10 ** 10, - DAI: 10 ** 10, - XUC: 10 ** 10, -}; +import { Currency } from '../orderbook/types'; class UnitConverter { - /** Number of smallest units per currency. */ - private UNITS_PER_CURRENCY: { [key: string]: number } = UNITS_PER_CURRENCY; + private decimalPlacesPerCurrency = new Map(); + + constructor(currencies: Currency[]) { + currencies.forEach((currency) => { + this.decimalPlacesPerCurrency.set(currency.id, currency.decimalPlaces); + }); + } + + public setDecimalPlacesPerCurrency = (currency: string, decimalPlaces: number) => { + this.decimalPlacesPerCurrency.set(currency, decimalPlaces); + }; /** * Calculates the incoming and outgoing currencies and amounts of subunits/satoshis for an order if it is swapped. @@ -20,12 +21,12 @@ class UnitConverter { * @returns An object with the calculated incoming and outgoing values. The quote currency * amount is returned as zero if the price is 0 or infinity, indicating a market order. */ - public static calculateInboundOutboundAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { + public calculateInboundOutboundAmounts = (quantity: number, price: number, isBuy: boolean, pairId: string) => { const [baseCurrency, quoteCurrency] = pairId.split('/'); const baseCurrencyAmount = quantity; const quoteCurrencyAmount = price > 0 && price < Number.POSITIVE_INFINITY ? Math.round(quantity * price) : 0; // if price is zero or infinity, this is a market order and we can't know the quote currency amount - const baseCurrencyUnits = Math.floor(baseCurrencyAmount * UNITS_PER_CURRENCY[baseCurrency]); - const quoteCurrencyUnits = Math.floor(quoteCurrencyAmount * UNITS_PER_CURRENCY[quoteCurrency]); + const baseCurrencyUnits = this.amountToUnits({ currency: baseCurrency, amount: baseCurrencyAmount }); + const quoteCurrencyUnits = this.amountToUnits({ currency: quoteCurrency, amount: quoteCurrencyAmount }); const inboundCurrency = isBuy ? baseCurrency : quoteCurrency; const inboundAmount = isBuy ? baseCurrencyAmount : quoteCurrencyAmount; @@ -43,29 +44,36 @@ class UnitConverter { }; }; - public init = () => { - // TODO: Populate the mapping from the database (Currency.decimalPlaces). - // this.UNITS_PER_CURRENCY = await fetchUnitsPerCurrencyFromDatabase(); - }; - - public amountToUnits = ({ currency, amount }: { currency: string; amount: number }): number => { - const unitsPerCurrency = this.UNITS_PER_CURRENCY[currency]; - if (!unitsPerCurrency) { + public amountToUnits = ({ currency, amount }: { currency: string; amount: number }): bigint => { + const decimalPlaces = this.decimalPlacesPerCurrency.get(currency); + if (!decimalPlaces) { throw new Error( - `cannot convert ${currency} amount of ${amount} to units because units per currency was not found in the database`, + `cannot convert ${currency} amount of ${amount} to units because decimal places per currency was not found in the database`, ); } - return Math.floor(amount * unitsPerCurrency); + if (decimalPlaces < 8) { + return BigInt(amount) / 10n ** (8n - BigInt(decimalPlaces)); + } else if (decimalPlaces > 8n) { + return BigInt(amount) * 10n ** (BigInt(decimalPlaces) - 8n); + } else { + return BigInt(amount); + } }; - public unitsToAmount = ({ currency, units }: { currency: string; units: number }): number => { - const unitsPerCurrency = this.UNITS_PER_CURRENCY[currency]; - if (!unitsPerCurrency) { + public unitsToAmount = ({ currency, units }: { currency: string; units: bigint }): number => { + const decimalPlaces = this.decimalPlacesPerCurrency.get(currency); + if (!decimalPlaces) { throw new Error( - `cannot convert ${currency} units of ${units} to amount because units per currency was not found in the database`, + `cannot convert ${currency} units of ${units} to units because decimal places per currency was not found in the database`, ); } - return Math.floor(units / unitsPerCurrency); + if (decimalPlaces < 8) { + return Number(units) * 10 ** (8 - decimalPlaces); + } else if (decimalPlaces > 8n) { + return Number(units) / 10 ** (decimalPlaces - 8); + } else { + return Number(units); + } }; } diff --git a/test/integration/OrderBook.spec.ts b/test/integration/OrderBook.spec.ts index 808203776..aa23c351c 100644 --- a/test/integration/OrderBook.spec.ts +++ b/test/integration/OrderBook.spec.ts @@ -13,10 +13,11 @@ import Pool from '../../lib/p2p/Pool'; import SwapClient from '../../lib/swaps/SwapClient'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import Swaps from '../../lib/swaps/Swaps'; +import { UnitConverter } from '../../lib/utils/UnitConverter'; import { createOwnOrder } from '../utils'; const PAIR_ID = 'LTC/BTC'; -const currencies = PAIR_ID.split('/'); +const currencyIds = PAIR_ID.split('/'); const loggers = Logger.createLoggers(Level.Warn); const getMockPool = (sandbox: sinon.SinonSandbox) => { @@ -50,14 +51,16 @@ const getMockSwaps = (sandbox: sinon.SinonSandbox) => { return swaps; }; +const currencies = [ + { id: currencyIds[0], swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, + { id: currencyIds[1], swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, +]; + const initValues = async (db: DB) => { const orderBookRepository = new OrderBookRepository(db.models); - await orderBookRepository.addCurrencies([ - { id: currencies[0], swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, - { id: currencies[1], swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, - ]); - await orderBookRepository.addPairs([{ baseCurrency: currencies[0], quoteCurrency: currencies[1] }]); + await orderBookRepository.addCurrencies(currencies); + await orderBookRepository.addPairs([{ baseCurrency: currencyIds[0], quoteCurrency: currencyIds[1] }]); }; describe('OrderBook', () => { @@ -81,6 +84,7 @@ describe('OrderBook', () => { orderBook = new OrderBook({ pool, swaps, + unitConverter: new UnitConverter(currencies), thresholds: config.orderthresholds, logger: loggers.orderbook, models: db.models, @@ -233,6 +237,7 @@ describe('nomatching OrderBook', () => { orderBook = new OrderBook({ pool, swaps, + unitConverter: new UnitConverter(currencies), thresholds: config.orderthresholds, logger: loggers.orderbook, models: db.models, diff --git a/test/integration/Swaps.spec.ts b/test/integration/Swaps.spec.ts index d8e7c950c..1ac149757 100644 --- a/test/integration/Swaps.spec.ts +++ b/test/integration/Swaps.spec.ts @@ -1,7 +1,7 @@ import chai, { expect } from 'chai'; import chaiAsPromised from 'chai-as-promised'; import sinon, { SinonSandbox } from 'sinon'; -import { SwapFailureReason } from '../../lib/constants/enums'; +import { SwapClientType, SwapFailureReason } from '../../lib/constants/enums'; import DB from '../../lib/db/DB'; import Logger, { Level } from '../../lib/Logger'; import Peer from '../../lib/p2p/Peer'; @@ -9,6 +9,7 @@ import Pool from '../../lib/p2p/Pool'; import SwapClient from '../../lib/swaps/SwapClient'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import Swaps from '../../lib/swaps/Swaps'; +import { UnitConverter } from '../../lib/utils/UnitConverter'; import { getValidDeal, waitForSpy } from '../utils'; chai.use(chaiAsPromised); @@ -58,6 +59,11 @@ const validSwapSuccess = () => { }; }; +const currencies = [ + { id: 'LTC', swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, + { id: 'BTC', swapClient: SwapClientType.Lnd, decimalPlaces: 8 }, +]; + describe('Swaps.Integration', () => { const loggers = Logger.createLoggers(Level.Warn); let db: DB; @@ -114,6 +120,7 @@ describe('Swaps.Integration', () => { swaps = new Swaps({ pool, swapClientManager, + unitConverter: new UnitConverter(currencies), logger: loggers.swaps, models: db.models, }); diff --git a/test/jest/Connext.spec.ts b/test/jest/Connext.spec.ts index 8d9f7cdd9..52dadfe75 100644 --- a/test/jest/Connext.spec.ts +++ b/test/jest/Connext.spec.ts @@ -1,10 +1,10 @@ import ConnextClient from '../../lib/connextclient/ConnextClient'; -import { UnitConverter } from '../../lib/utils/UnitConverter'; -import Logger from '../../lib/Logger'; +import errors from '../../lib/connextclient/errors'; import { SwapClientType } from '../../lib/constants/enums'; import { CurrencyInstance } from '../../lib/db/types'; +import Logger from '../../lib/Logger'; import { PaymentState } from '../../lib/swaps/SwapClient'; -import errors from '../../lib/connextclient/errors'; +import { getUnitConverter } from '../utils'; const MOCK_TX_HASH = '0x5544332211'; jest.mock('../../lib/utils/utils', () => { @@ -84,17 +84,27 @@ describe('ConnextClient', () => { config, currencyInstances, logger, - unitConverter: new UnitConverter(), + unitConverter: getUnitConverter(), network: 'mainnet', }); }); describe('withdraw', () => { - const MOCK_FREE_BALANCE_ON_CHAIN = 10000; + const MOCK_ETH_FREE_BALANCE_ON_CHAIN = 10n ** 18n; // 1 ETH + const MOCK_USDT_FREE_BALANCE_ON_CHAIN = 10000n * 10n ** 6n; // 10000 USDT const DESTINATION_ADDRESS = '0x12345'; beforeEach(() => { - connext['getBalance'] = jest.fn().mockReturnValue({ freeBalanceOnChain: MOCK_FREE_BALANCE_ON_CHAIN }); + connext['getBalance'] = jest.fn().mockImplementation((currency: string) => { + switch (currency) { + case 'ETH': + return { freeBalanceOnChain: MOCK_ETH_FREE_BALANCE_ON_CHAIN }; + case 'USDT': + return { freeBalanceOnChain: MOCK_USDT_FREE_BALANCE_ON_CHAIN }; + default: + return { freeBalanceOnChain: 0 }; + } + }); connext['sendRequest'] = jest.fn(); }); @@ -134,7 +144,7 @@ describe('ConnextClient', () => { await connext.withdraw({ currency: 'ETH', destination: DESTINATION_ADDRESS, - amount: 0.0000011, + amount: 2 * 10 ** 8, }); } catch (e) { expect(e).toMatchSnapshot(); @@ -154,7 +164,7 @@ describe('ConnextClient', () => { 'POST', expect.objectContaining({ assetId: USDT_ASSET_ID, - amount: MOCK_FREE_BALANCE_ON_CHAIN, + amount: MOCK_USDT_FREE_BALANCE_ON_CHAIN, recipient: DESTINATION_ADDRESS, }), ); @@ -166,7 +176,7 @@ describe('ConnextClient', () => { const txhash = await connext.withdraw({ currency: 'USDT', destination: DESTINATION_ADDRESS, - amount: 5000, + amount: 5000 * 10 ** 8, }); expect(connext['sendRequest']).toHaveBeenCalledTimes(1); expect(connext['sendRequest']).toHaveBeenCalledWith( @@ -174,7 +184,7 @@ describe('ConnextClient', () => { 'POST', expect.objectContaining({ assetId: USDT_ASSET_ID, - amount: '50', + amount: '5000000000', recipient: DESTINATION_ADDRESS, }), ); @@ -186,7 +196,7 @@ describe('ConnextClient', () => { const txhash = await connext.withdraw({ currency: 'ETH', destination: DESTINATION_ADDRESS, - amount: 0.0000005, + amount: 1 * 10 ** 2, }); expect(connext['sendRequest']).toHaveBeenCalledTimes(1); expect(connext['sendRequest']).toHaveBeenCalledWith( @@ -194,7 +204,7 @@ describe('ConnextClient', () => { 'POST', expect.objectContaining({ assetId: ETH_ASSET_ID, - amount: '5000', + amount: '1000000000000', recipient: DESTINATION_ADDRESS, }), ); diff --git a/test/jest/DB.spec.ts b/test/jest/DB.spec.ts index 33bcaa32c..42dc7af3a 100644 --- a/test/jest/DB.spec.ts +++ b/test/jest/DB.spec.ts @@ -37,8 +37,8 @@ const deal: SwapDeal = { makerCurrency: 'LTC', takerAmount: 5000, makerAmount: 1000000, - takerUnits: 5000, - makerUnits: 1000000, + takerUnits: 5000n, + makerUnits: 1000000n, takerCltvDelta: 144, makerCltvDelta: 144, rPreimage: '60743C0B6BFA885E30F101705764F43F8EF7E613DD0F07AD5178C7D9B1682B9E', diff --git a/test/jest/LndClient.spec.ts b/test/jest/LndClient.spec.ts index f48d85119..ba647eef1 100644 --- a/test/jest/LndClient.spec.ts +++ b/test/jest/LndClient.spec.ts @@ -50,7 +50,7 @@ describe('LndClient', () => { describe('openChannel', () => { const peerPubKey = '02f8895eb03c37b2665415be4d83b20228acc0abc55ebf6728565141c66cfc164a'; - const units = 16000000; + const units = 16000000n; const externalIp1 = '123.456.789.321:9735'; const externalIp2 = '192.168.63.155:9777'; const lndListeningUris = [`${peerPubKey}@${externalIp1}`, `${peerPubKey}@${externalIp2}`]; @@ -92,12 +92,12 @@ describe('LndClient', () => { expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); expect(lnd['connectPeer']).toHaveBeenCalledWith(peerPubKey, externalIp1); expect(lnd['openChannelSync']).toHaveBeenCalledTimes(1); - expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, units, 0, 0); + expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, Number(units), 0, 0); }); test('it pushes satoshis to the peer when specified', async () => { expect.assertions(4); - const pushUnits = 481824; + const pushUnits = 481824n; lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve(openChannelSyncResponse)); lnd['connectPeer'] = jest.fn().mockImplementationOnce(() => { return Promise.resolve(); @@ -111,7 +111,7 @@ describe('LndClient', () => { expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); expect(lnd['connectPeer']).toHaveBeenCalledWith(peerPubKey, externalIp1); expect(lnd['openChannelSync']).toHaveBeenCalledTimes(1); - expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, units, pushUnits, 0); + expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, Number(units), Number(pushUnits), 0); }); test('it should set fee when specified', async () => { @@ -130,7 +130,7 @@ describe('LndClient', () => { expect(lnd['connectPeer']).toHaveBeenCalledTimes(1); expect(lnd['connectPeer']).toHaveBeenCalledWith(peerPubKey, externalIp1); expect(lnd['openChannelSync']).toHaveBeenCalledTimes(1); - expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, units, 0, fee); + expect(lnd['openChannelSync']).toHaveBeenCalledWith(peerPubKey, Number(units), 0, fee); }); test('it stops trying to connect to lnd uris when first once succeeds', async () => { diff --git a/test/jest/Orderbook.spec.ts b/test/jest/Orderbook.spec.ts index a13d69724..45cd2e7c5 100644 --- a/test/jest/Orderbook.spec.ts +++ b/test/jest/Orderbook.spec.ts @@ -11,6 +11,7 @@ import Pool from '../../lib/p2p/Pool'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import Swaps from '../../lib/swaps/Swaps'; import { UnitConverter } from '../../lib/utils/UnitConverter'; +import { getUnitConverter } from '../utils'; jest.mock('../../lib/db/DB', () => { return jest.fn().mockImplementation(() => { @@ -50,7 +51,7 @@ jest.mock('../../lib/db/DB', () => { }; }); }); -const advertisedPairs = ['LTC/BTC', 'WETH/BTC']; +const advertisedPairs = ['LTC/BTC', 'ETH/BTC']; const mockActivatePair = jest.fn(); const mockActivateCurrency = jest.fn(); const mockGetIdentifier = jest.fn(() => 'pubkeyoraddress'); @@ -161,11 +162,11 @@ describe('OrderBook', () => { nodeKey: new mockedNodeKey(), }); pool.broadcastOrder = jest.fn(); - unitConverter = new UnitConverter(); - unitConverter.init(); + unitConverter = getUnitConverter(); swapClientManager = new SwapClientManager(config, loggers, unitConverter, db.models); swaps = new Swaps({ pool, + unitConverter, swapClientManager, logger: loggers.swaps, models: db.models, @@ -178,6 +179,7 @@ describe('OrderBook', () => { orderbook = new Orderbook({ pool, swaps, + unitConverter, thresholds: config.orderthresholds, logger: loggers.orderbook, models: db.models, diff --git a/test/jest/SwapClientManager.spec.ts b/test/jest/SwapClientManager.spec.ts index 0bddeb99e..dbd6e2a65 100644 --- a/test/jest/SwapClientManager.spec.ts +++ b/test/jest/SwapClientManager.spec.ts @@ -2,11 +2,12 @@ import Config from '../../lib/Config'; import { SwapClientType } from '../../lib/constants/enums'; import DB from '../../lib/db/DB'; import Logger from '../../lib/Logger'; +import { OwnLimitOrder } from '../../lib/orderbook/types'; +import { errorCodes } from '../../lib/swaps/errors'; import SwapClient from '../../lib/swaps/SwapClient'; import SwapClientManager from '../../lib/swaps/SwapClientManager'; import { UnitConverter } from '../../lib/utils/UnitConverter'; -import { OwnLimitOrder } from '../../lib/orderbook/types'; -import { errorCodes } from '../../lib/swaps/errors'; +import { getUnitConverter } from '../utils'; jest.mock('../../lib/db/DB', () => { return jest.fn().mockImplementation(() => { @@ -108,8 +109,7 @@ describe('Swaps.SwapClientManager', () => { }; config.strict = true; db = new DB(loggers.db, config.dbpath); - unitConverter = new UnitConverter(); - unitConverter.init(); + unitConverter = getUnitConverter(); tokenAddresses.set('WETH', '0x1234'); }); @@ -287,7 +287,7 @@ describe('Swaps.SwapClientManager', () => { expect(mockLndOpenChannel).toHaveBeenCalledWith( expect.objectContaining({ remoteIdentifier, - units: amount, + units: BigInt(amount), uris: lndListeningUris, }), ); @@ -422,9 +422,8 @@ describe('Swaps.SwapClientManager', () => { localId: 'test', }; - await expect(swapClientManager.checkSwapCapacities(order)).rejects.toHaveProperty( - 'code', - errorCodes.SWAP_CLIENT_NOT_FOUND, + await expect(swapClientManager.checkSwapCapacities(order)).rejects.toThrow( + 'cannot convert AAA amount of 600000 to units because decimal places per currency was not found in the database', ); }); }); diff --git a/test/jest/Swaps.spec.ts b/test/jest/Swaps.spec.ts index a13a54e09..af10fc18a 100644 --- a/test/jest/Swaps.spec.ts +++ b/test/jest/Swaps.spec.ts @@ -1,4 +1,5 @@ import LndClient from '../../lib/lndclient/LndClient'; +import { SwapRequestPacketBody } from '../../lib/p2p/packets'; import Swaps from '../../lib/swaps/Swaps'; describe('Swaps', () => { @@ -23,4 +24,32 @@ describe('Swaps', () => { expect(Swaps['calculateLockBuffer'](80, btcMinutesPerBlock, ethMinutesPerBlock)).toMatchSnapshot(); }); }); + + describe('validateSwapRequest', () => { + const quantity = 1000000; + const takerCltvDelta = 144; + const orderId = 'f8a85c66-7e73-43cd-9ac4-176ff4cc28a8'; + const rHash = '62c8bbef4587cff4286246e63044dc3e454b5693fb5ebd0171b7e58644bfafe2'; + + const swapRequest: SwapRequestPacketBody = { + takerCltvDelta, + orderId, + rHash, + proposedQuantity: quantity, + pairId: 'LTC/BTC', + }; + + it('should validate a good swap request', () => { + expect(Swaps.validateSwapRequest(swapRequest)).toEqual(true); + }); + + it('should flag a swap request with a non-positive proposed quantity', () => { + expect(Swaps.validateSwapRequest({ ...swapRequest, proposedQuantity: 0 })).toEqual(false); + expect(Swaps.validateSwapRequest({ ...swapRequest, proposedQuantity: -1 })).toEqual(false); + }); + + it('should flag a swap request with an rHash that is not 64 characters', () => { + expect(Swaps.validateSwapRequest({ ...swapRequest, rHash: 'notavalidhash' })).toEqual(false); + }); + }); }); diff --git a/test/jest/UnitConverter.spec.ts b/test/jest/UnitConverter.spec.ts index 3b74efd9e..eeced7605 100644 --- a/test/jest/UnitConverter.spec.ts +++ b/test/jest/UnitConverter.spec.ts @@ -1,33 +1,36 @@ -import { UnitConverter } from '../../lib/utils/UnitConverter'; +import { getUnitConverter } from '../utils'; + +export const UNITS_PER_CURRENCY: { [key: string]: bigint } = { + BTC: 1n, + LTC: 1n, + ETH: 10n ** 10n, +}; describe('UnitConverter', () => { - const unitConverter = new UnitConverter(); + const unitConverter = getUnitConverter(); describe('amountToUnits', () => { test('converts BTC amount to units', () => { - unitConverter.init(); const amount = 99999999; expect( unitConverter.amountToUnits({ amount, currency: 'BTC', }), - ).toEqual(amount); + ).toEqual(BigInt(amount)); }); - test('converts WETH amount to units', () => { - unitConverter.init(); + test('converts ETH amount to units', () => { expect( unitConverter.amountToUnits({ amount: 7500000, - currency: 'WETH', + currency: 'ETH', }), - ).toEqual(75000000000000000); + ).toEqual(75000000000000000n); }); test('throws error upon unknown currency', () => { expect.assertions(1); - unitConverter.init(); try { unitConverter.amountToUnits({ amount: 123, @@ -41,32 +44,29 @@ describe('UnitConverter', () => { describe('unitsToAmount', () => { test('converts BTC units to amount', () => { - unitConverter.init(); - const units = 99999999; + const units = 99999999n; expect( unitConverter.unitsToAmount({ units, currency: 'BTC', }), - ).toEqual(units); + ).toEqual(Number(units)); }); - test('converts WETH units to amount', () => { - unitConverter.init(); + test('converts ETH units to amount', () => { expect( unitConverter.unitsToAmount({ - units: 75000000000000000, - currency: 'WETH', + units: 75000000000000000n, + currency: 'ETH', }), ).toEqual(7500000); }); test('throws error upon unknown currency', () => { expect.assertions(1); - unitConverter.init(); try { unitConverter.unitsToAmount({ - units: 123, + units: 123n, currency: 'ABC', }); } catch (e) { @@ -75,10 +75,12 @@ describe('UnitConverter', () => { }); }); - describe('calculateSwapAmounts', () => { + describe('calculateInboundOutboundAmounts', () => { const pairId = 'LTC/BTC'; const quantity = 250000; + const bigQuantity = 250000n; const price = 0.01; + const priceDivisor = 100n; test('calculate inbound and outbound amounts and currencies for a buy order', () => { const { @@ -88,13 +90,13 @@ describe('UnitConverter', () => { outboundAmount, inboundUnits, outboundUnits, - } = UnitConverter.calculateInboundOutboundAmounts(quantity, price, true, pairId); + } = unitConverter.calculateInboundOutboundAmounts(quantity, price, true, pairId); expect(inboundCurrency).toEqual('LTC'); expect(inboundAmount).toEqual(quantity); - expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['LTC'] * quantity); + expect(inboundUnits).toEqual(UNITS_PER_CURRENCY['LTC'] * bigQuantity); expect(outboundCurrency).toEqual('BTC'); expect(outboundAmount).toEqual(quantity * price); - expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + expect(outboundUnits).toEqual((UNITS_PER_CURRENCY['BTC'] * bigQuantity) / priceDivisor); }); test('calculate inbound and outbound amounts and currencies for a sell order', () => { @@ -105,17 +107,17 @@ describe('UnitConverter', () => { outboundAmount, inboundUnits, outboundUnits, - } = UnitConverter.calculateInboundOutboundAmounts(quantity, price, false, pairId); + } = unitConverter.calculateInboundOutboundAmounts(quantity, price, false, pairId); expect(inboundCurrency).toEqual('BTC'); expect(inboundAmount).toEqual(quantity * price); - expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + expect(inboundUnits).toEqual((UNITS_PER_CURRENCY['BTC'] * bigQuantity) / priceDivisor); expect(outboundCurrency).toEqual('LTC'); expect(outboundAmount).toEqual(quantity); - expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['LTC'] * quantity); + expect(outboundUnits).toEqual(UNITS_PER_CURRENCY['LTC'] * bigQuantity); }); test('calculate 0 outbound amount for a market buy order', () => { - const { outboundCurrency, outboundAmount, outboundUnits } = UnitConverter.calculateInboundOutboundAmounts( + const { outboundCurrency, outboundAmount, outboundUnits } = unitConverter.calculateInboundOutboundAmounts( quantity, 0, true, @@ -123,11 +125,11 @@ describe('UnitConverter', () => { ); expect(outboundCurrency).toEqual('BTC'); expect(outboundAmount).toEqual(0); - expect(outboundUnits).toEqual(0); + expect(outboundUnits).toEqual(0n); }); test('calculate 0 inbound amount for a market sell order', () => { - const { inboundCurrency, inboundAmount, inboundUnits } = UnitConverter.calculateInboundOutboundAmounts( + const { inboundCurrency, inboundAmount, inboundUnits } = unitConverter.calculateInboundOutboundAmounts( quantity, Number.POSITIVE_INFINITY, false, @@ -135,7 +137,7 @@ describe('UnitConverter', () => { ); expect(inboundCurrency).toEqual('BTC'); expect(inboundAmount).toEqual(0); - expect(inboundUnits).toEqual(0); + expect(inboundUnits).toEqual(0n); }); test('calculate inbound and outbound amounts and currencies for a Connext order', () => { @@ -146,13 +148,13 @@ describe('UnitConverter', () => { outboundAmount, inboundUnits, outboundUnits, - } = UnitConverter.calculateInboundOutboundAmounts(quantity, price, true, 'ETH/BTC'); + } = unitConverter.calculateInboundOutboundAmounts(quantity, price, true, 'ETH/BTC'); expect(inboundCurrency).toEqual('ETH'); expect(inboundAmount).toEqual(quantity); - expect(inboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['ETH'] * quantity); + expect(inboundUnits).toEqual(UNITS_PER_CURRENCY['ETH'] * bigQuantity); expect(outboundCurrency).toEqual('BTC'); expect(outboundAmount).toEqual(quantity * price); - expect(outboundUnits).toEqual(unitConverter['UNITS_PER_CURRENCY']['BTC'] * quantity * price); + expect(outboundUnits).toEqual((UNITS_PER_CURRENCY['BTC'] * bigQuantity) / priceDivisor); }); }); }); diff --git a/test/jest/__snapshots__/UnitConverter.spec.ts.snap b/test/jest/__snapshots__/UnitConverter.spec.ts.snap index 216973aa9..e564b0739 100644 --- a/test/jest/__snapshots__/UnitConverter.spec.ts.snap +++ b/test/jest/__snapshots__/UnitConverter.spec.ts.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnitConverter amountToUnits throws error upon unknown currency 1`] = `[Error: cannot convert ABC amount of 123 to units because units per currency was not found in the database]`; +exports[`UnitConverter amountToUnits throws error upon unknown currency 1`] = `[Error: cannot convert ABC amount of 123 to units because decimal places per currency was not found in the database]`; -exports[`UnitConverter unitsToAmount throws error upon unknown currency 1`] = `[Error: cannot convert ABC units of 123 to amount because units per currency was not found in the database]`; +exports[`UnitConverter unitsToAmount throws error upon unknown currency 1`] = `[Error: cannot convert ABC units of 123 to units because decimal places per currency was not found in the database]`; diff --git a/test/jest/integration/Swaps.spec.ts b/test/jest/integration/Swaps.spec.ts index 39aee2ea3..77f7fc2de 100644 --- a/test/jest/integration/Swaps.spec.ts +++ b/test/jest/integration/Swaps.spec.ts @@ -1,8 +1,8 @@ import { + ReputationEvent, SwapClientType, SwapFailureReason, SwapPhase, - ReputationEvent, SwapRole, SwapState, } from '../../../lib/constants/enums'; @@ -15,7 +15,7 @@ import Pool from '../../../lib/p2p/Pool'; import SwapClientManager from '../../../lib/swaps/SwapClientManager'; import Swaps, { OrderToAccept } from '../../../lib/swaps/Swaps'; import { SwapDeal } from '../../../lib/swaps/types'; -import { getValidDeal } from '../../utils'; +import { getUnitConverter, getValidDeal } from '../../utils'; const addReputationEvent = jest.fn().mockImplementation(() => { return { catch: () => {} }; @@ -93,6 +93,7 @@ describe('Swaps Integration', () => { let lndLtc: LndClient; let makerCurrency: string; let takerCurrency: string; + const unitConverter = getUnitConverter(); beforeEach(() => { logger = new mockedLogger(); @@ -117,6 +118,7 @@ describe('Swaps Integration', () => { logger, pool, swapClientManager, + unitConverter, models: db.models, }); }); @@ -298,7 +300,7 @@ describe('Swaps Integration', () => { const dealAccepted = await swaps.acceptDeal(orderToAccept, swapRequestPacket, peer); expect(dealAccepted).toEqual(true); expect(lndBtc.getRoute).toHaveBeenCalledWith( - 1000, + 1000n, peerLndBtcPubKey, takerCurrency, swapRequestBody.takerCltvDelta, @@ -307,7 +309,7 @@ describe('Swaps Integration', () => { const expectedMakerCltvDelta = 1641; expect(lndLtc.addInvoice).toHaveBeenCalledWith({ rHash: swapRequestBody.rHash, - units: swapRequestBody.proposedQuantity, + units: BigInt(swapRequestBody.proposedQuantity), expiry: expectedMakerCltvDelta, currency: makerCurrency, }); @@ -397,4 +399,97 @@ describe('Swaps Integration', () => { expect(deal.state).toEqual(SwapState.Error); }); }); + + describe('calculateMakerTakerAmounts', () => { + const quantity = 1000000; + const price = 0.005; + const takerCltvDelta = 144; + const orderId = 'f8a85c66-7e73-43cd-9ac4-176ff4cc28a8'; + const rHash = '62c8bbef4587cff4286246e63044dc3e454b5693fb5ebd0171b7e58644bfafe2'; + const pairId = 'LTC/BTC'; + const peerPubKey = '03029c6a4d80c91da9e40529ec41c93b17cc9d7956b59c7d8334b0318d4a86aef8'; + /** A swap deal for a buy order. */ + const buyDeal: SwapDeal = { + quantity, + price, + takerCltvDelta, + orderId, + rHash, + pairId, + peerPubKey, + role: SwapRole.Maker, + phase: SwapPhase.SwapCreated, + state: SwapState.Active, + localId: '1', + proposedQuantity: quantity, + isBuy: true, + makerCurrency: 'LTC', + takerCurrency: 'BTC', + makerAmount: quantity, + takerAmount: quantity * price, + makerUnits: BigInt(quantity), + takerUnits: BigInt(quantity * price), + createTime: 1540716251106, + }; + + const buyDealEth = { + ...buyDeal, + pairId: 'ETH/BTC', + makerCurrency: 'ETH', + takerCurrency: 'BTC', + makerAmount: quantity, + takerAmount: quantity * price, + makerUnits: 10n ** 10n * BigInt(quantity), + takerUnits: BigInt(quantity * price), + }; + + /** A swap deal for a sell order, mirrored from the buy deal for convenience. */ + const sellDeal = { + ...buyDeal, + isBuy: false, + takerCurrency: buyDeal.makerCurrency, + makerCurrency: buyDeal.takerCurrency, + takerAmount: buyDeal.makerAmount, + makerAmount: buyDeal.takerAmount, + takerUnits: buyDeal.makerUnits, + makerUnits: buyDeal.takerUnits, + }; + + it('should calculate swap amounts and currencies for a buy order', () => { + const { makerCurrency, makerAmount, takerCurrency, takerAmount, makerUnits, takerUnits } = swaps[ + 'calculateMakerTakerAmounts' + ](buyDeal.quantity!, buyDeal.price, buyDeal.isBuy, buyDeal.pairId); + expect(makerAmount).toEqual(buyDeal.makerAmount); + expect(takerAmount).toEqual(buyDeal.takerAmount); + expect(makerUnits).toEqual(buyDeal.makerUnits); + expect(takerUnits).toEqual(buyDeal.takerUnits); + expect(makerCurrency).toEqual(buyDeal.makerCurrency); + expect(takerCurrency).toEqual(buyDeal.takerCurrency); + }); + + it('should calculate swap amounts and currencies for a sell order', () => { + const { makerCurrency, makerAmount, takerCurrency, takerAmount, makerUnits, takerUnits } = swaps[ + 'calculateMakerTakerAmounts' + ](sellDeal.quantity!, sellDeal.price, sellDeal.isBuy, sellDeal.pairId); + expect(makerAmount).toEqual(sellDeal.makerAmount); + expect(takerAmount).toEqual(sellDeal.takerAmount); + expect(makerUnits).toEqual(sellDeal.makerUnits); + expect(takerUnits).toEqual(sellDeal.takerUnits); + expect(makerCurrency).toEqual(sellDeal.makerCurrency); + expect(takerCurrency).toEqual(sellDeal.takerCurrency); + }); + + it('should calculate swap amounts and currencies for an ETH buy order', () => { + const { makerCurrency, makerAmount, takerCurrency, takerAmount } = swaps['calculateMakerTakerAmounts']( + buyDealEth.quantity!, + buyDealEth.price, + buyDealEth.isBuy, + buyDealEth.pairId, + ); + expect(makerAmount).toEqual(buyDealEth.makerAmount); + expect(takerAmount).toEqual(buyDealEth.takerAmount); + expect(makerCurrency).toEqual(buyDealEth.makerCurrency); + expect(takerCurrency).toEqual(buyDealEth.takerCurrency); + }); + }); }); diff --git a/test/unit/Swaps.spec.ts b/test/unit/Swaps.spec.ts index 2435141f2..bdc71f316 100644 --- a/test/unit/Swaps.spec.ts +++ b/test/unit/Swaps.spec.ts @@ -1,64 +1,12 @@ import { expect } from 'chai'; -import Swaps from '../../lib/swaps/Swaps'; -import { SwapDeal } from '../../lib/swaps/types'; -import { SwapPhase, SwapState, SwapRole } from '../../lib/constants/enums'; import { SwapRequestPacketBody } from '../../lib/p2p/packets'; +import Swaps from '../../lib/swaps/Swaps'; describe('Swaps', () => { const quantity = 1000000; - const price = 0.005; const takerCltvDelta = 144; const orderId = 'f8a85c66-7e73-43cd-9ac4-176ff4cc28a8'; const rHash = '62c8bbef4587cff4286246e63044dc3e454b5693fb5ebd0171b7e58644bfafe2'; - const pairId = 'LTC/BTC'; - const peerPubKey = '03029c6a4d80c91da9e40529ec41c93b17cc9d7956b59c7d8334b0318d4a86aef8'; - - /** A swap deal for a buy order. */ - const buyDeal: SwapDeal = { - quantity, - price, - takerCltvDelta, - orderId, - rHash, - pairId, - peerPubKey, - role: SwapRole.Maker, - phase: SwapPhase.SwapCreated, - state: SwapState.Active, - localId: '1', - proposedQuantity: quantity, - isBuy: true, - makerCurrency: 'LTC', - takerCurrency: 'BTC', - makerAmount: quantity, - takerAmount: quantity * price, - makerUnits: quantity, - takerUnits: quantity * price, - createTime: 1540716251106, - }; - - const buyDealEth = { - ...buyDeal, - pairId: 'WETH/BTC', - makerCurrency: 'WETH', - takerCurrency: 'BTC', - makerAmount: quantity, - takerAmount: quantity * price, - makerUnits: 10 ** 10 * quantity, - takerUnits: quantity * price, - }; - - /** A swap deal for a sell order, mirrored from the buy deal for convenience. */ - const sellDeal = { - ...buyDeal, - isBuy: false, - takerCurrency: buyDeal.makerCurrency, - makerCurrency: buyDeal.takerCurrency, - takerAmount: buyDeal.makerAmount, - makerAmount: buyDeal.takerAmount, - takerUnits: buyDeal.makerUnits, - makerUnits: buyDeal.takerUnits, - }; const swapRequest: SwapRequestPacketBody = { takerCltvDelta, @@ -68,43 +16,6 @@ describe('Swaps', () => { pairId: 'LTC/BTC', }; - it('should calculate swap amounts and currencies for a buy order', () => { - const { makerCurrency, makerAmount, takerCurrency, takerAmount, makerUnits, takerUnits } = Swaps[ - 'calculateMakerTakerAmounts' - ](buyDeal.quantity!, buyDeal.price, buyDeal.isBuy, buyDeal.pairId); - expect(makerAmount).to.equal(buyDeal.makerAmount); - expect(takerAmount).to.equal(buyDeal.takerAmount); - expect(makerUnits).to.equal(buyDeal.makerUnits); - expect(takerUnits).to.equal(buyDeal.takerUnits); - expect(makerCurrency).to.equal(buyDeal.makerCurrency); - expect(takerCurrency).to.equal(buyDeal.takerCurrency); - }); - - it('should calculate swap amounts and currencies for a sell order', () => { - const { makerCurrency, makerAmount, takerCurrency, takerAmount, makerUnits, takerUnits } = Swaps[ - 'calculateMakerTakerAmounts' - ](sellDeal.quantity!, sellDeal.price, sellDeal.isBuy, sellDeal.pairId); - expect(makerAmount).to.equal(sellDeal.makerAmount); - expect(takerAmount).to.equal(sellDeal.takerAmount); - expect(makerUnits).to.equal(sellDeal.makerUnits); - expect(takerUnits).to.equal(sellDeal.takerUnits); - expect(makerCurrency).to.equal(sellDeal.makerCurrency); - expect(takerCurrency).to.equal(sellDeal.takerCurrency); - }); - - it('should calculate swap amounts and currencies for a WETH buy order', () => { - const { makerCurrency, makerAmount, takerCurrency, takerAmount } = Swaps['calculateMakerTakerAmounts']( - buyDealEth.quantity!, - buyDealEth.price, - buyDealEth.isBuy, - buyDealEth.pairId, - ); - expect(makerAmount).to.equal(buyDealEth.makerAmount); - expect(takerAmount).to.equal(buyDealEth.takerAmount); - expect(makerCurrency).to.equal(buyDealEth.makerCurrency); - expect(takerCurrency).to.equal(buyDealEth.takerCurrency); - }); - it('should validate a good swap request', () => { expect(Swaps.validateSwapRequest(swapRequest)).to.be.true; }); diff --git a/test/utils.ts b/test/utils.ts index 31499bf01..881e58281 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -6,6 +6,7 @@ import { SinonSpy } from 'sinon'; import uuidv1 from 'uuid'; import { SwapPhase, SwapRole, SwapState } from '../lib/constants/enums'; import { OwnOrder, PeerOrder } from '../lib/orderbook/types'; +import { UnitConverter } from '../lib/utils/UnitConverter'; import { ms } from '../lib/utils/utils'; /** @@ -112,9 +113,9 @@ export const getValidDeal = (phase = SwapPhase.SendingPayment, role = SwapRole.M isBuy: true, quantity: 10000, makerAmount: 10000, - makerUnits: 10000, + makerUnits: 10000n, takerAmount: 1000, - takerUnits: 1000, + takerUnits: 1000n, makerCurrency: 'LTC', takerCurrency: 'BTC', destination: '034c5266591bff232d1647f45bcf6bbc548d3d6f70b2992d28aba0afae067880ac', @@ -125,3 +126,12 @@ export const getValidDeal = (phase = SwapPhase.SendingPayment, role = SwapRole.M takerMaxTimeLock: 100, }; }; + +export const getUnitConverter = (): UnitConverter => + new UnitConverter([ + { id: 'BTC', decimalPlaces: 8, swapClient: 0 }, + { id: 'LTC', decimalPlaces: 8, swapClient: 0 }, + { id: 'ETH', decimalPlaces: 18, swapClient: 2 }, + { id: 'USDT', decimalPlaces: 6, swapClient: 2 }, + { id: 'XUC', decimalPlaces: 18, swapClient: 2 }, + ]);