From cf83ab07e13f1e96bd313438ea8a08f0364dd3c1 Mon Sep 17 00:00:00 2001 From: Daniel McNally Date: Sat, 24 Aug 2019 03:54:51 -0400 Subject: [PATCH] feat(swaps): dynamic lock buffer This modifies the logic around calculating the `makerCltvDelta` value for swaps which specifies the minimum expected lock duration for the final hop of the first leg. We use the poisson quantile function to determine a very high probability that the first leg final lock won't expire before the second leg payment. This also removes the recently added `lockBuffer` config option in favor of a `cltvDelta` for lnd that specifies the lock delta that is used for the final hop of the second leg and gets added to the lock buffer to determine the final lock delta for the first leg. Closes #1164. --- bin/xud | 9 +- lib/Config.ts | 5 +- lib/lndclient/LndClient.ts | 29 +- lib/lndclient/types.ts | 1 + lib/raidenclient/RaidenClient.ts | 6 +- lib/swaps/SwapClient.ts | 12 +- lib/swaps/SwapClientManager.ts | 2 - lib/swaps/Swaps.ts | 51 +- npm-shrinkwrap.json | 891 +++++++++++++++++++++ package.json | 2 + sample-xud.conf | 2 +- test/jest/LndClient.spec.ts | 24 +- test/jest/RaidenClient.spec.ts | 17 +- test/jest/SwapClientManager.spec.ts | 2 + test/jest/Swaps.spec.ts | 30 + test/jest/__snapshots__/Swaps.spec.ts.snap | 9 + test/jest/integration/Swaps.spec.ts | 14 +- 17 files changed, 1021 insertions(+), 85 deletions(-) create mode 100644 test/jest/Swaps.spec.ts create mode 100644 test/jest/__snapshots__/Swaps.spec.ts.snap diff --git a/bin/xud b/bin/xud index 9dc927abe..04c6e4f0f 100755 --- a/bin/xud +++ b/bin/xud @@ -14,10 +14,6 @@ const { argv } = require('yargs') type: 'boolean', default: undefined, }, - lockbuffer: { - describe: 'Lock time in hours to add to cross-chain hop for swaps', - type: 'number', - }, loglevel: { describe: 'Verbosity of the logger', type: 'string', @@ -93,6 +89,10 @@ const { argv } = require('yargs') describe: 'Path to the SSL certificate for lnd', type: 'string', }, + 'lnd.[currency].cltvdelta': { + describe: 'CLTV delta for the final timelock', + type: 'number', + }, 'lnd.[currency].disable': { describe: 'Disable lnd integration', type: 'boolean', @@ -191,6 +191,7 @@ const { argv } = require('yargs') currencies.forEach((currency) => { parseBoolean(arg[currency], 'disable'); parseBoolean(arg[currency], 'nomacaroons'); + parseNumber(arg[currency], 'cltvdelta'); parseNumber(arg[currency], 'port'); }); diff --git a/lib/Config.ts b/lib/Config.ts index ce3ee218f..9a277e07b 100644 --- a/lib/Config.ts +++ b/lib/Config.ts @@ -17,8 +17,6 @@ class Config { public logpath: string; public logdateformat: string; public network: XuNetwork; - /** The lock time in hours that we add to the cross-chain "hop" for swaps. */ - public lockbuffer: number; public rpc: { disable: boolean, host: string, port: number }; public http: { host: string, port: number }; public lnd: { [currency: string]: LndClientConfig | undefined } = {}; @@ -77,7 +75,6 @@ class Config { this.logdateformat = 'DD/MM/YYYY HH:mm:ss.SSS'; this.network = this.getDefaultNetwork(); this.dbpath = this.getDefaultDbPath(); - this.lockbuffer = 24; this.p2p = { listen: true, @@ -114,6 +111,7 @@ class Config { host: 'localhost', port: 10009, nomacaroons: false, + cltvdelta: 40, }; this.lnd.LTC = { disable: false, @@ -123,6 +121,7 @@ class Config { host: 'localhost', port: 10010, nomacaroons: false, + cltvdelta: 576, }; this.raiden = { disable: false, diff --git a/lib/lndclient/LndClient.ts b/lib/lndclient/LndClient.ts index 4e69fc7bd..4a2517b53 100644 --- a/lib/lndclient/LndClient.ts +++ b/lib/lndclient/LndClient.ts @@ -27,7 +27,6 @@ const MAXFEE = 0.03; /** A class representing a client to interact with lnd. */ class LndClient extends SwapClient { public readonly type = SwapClientType.Lnd; - public readonly lockBuffer: number; public readonly finalLock: number; public config: LndClientConfig; public currency: string; @@ -59,15 +58,13 @@ class LndClient extends SwapClient { * Creates an lnd client. */ constructor( - { config, logger, currency, lockBufferHours }: - { config: LndClientConfig, logger: Logger, currency: string, lockBufferHours: number }, + { config, logger, currency }: + { config: LndClientConfig, logger: Logger, currency: string }, ) { super(logger); this.config = config; this.currency = currency; - this.lockBuffer = Math.round(lockBufferHours * 60 / LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[currency]); - // we set the expected final lock to 400 minutes which is the default for bitcoin on lnd 0.7.1 - this.finalLock = Math.round(400 / LndClient.MINUTES_PER_BLOCK_BY_CURRENCY[currency]); + this.finalLock = config.cltvdelta; } private static waitForClientReady = (client: grpc.Client) => { @@ -91,8 +88,6 @@ class LndClient extends SwapClient { * @param awaitingCreate whether xud is waiting for its node key to be created */ public init = async (awaitingCreate = false) => { - assert(this.lockBuffer > 0, `lnd-${this.currency}: lock buffer must be a positive number`); - const { disable, certpath, macaroonpath, nomacaroons, host, port } = this.config; if (disable) { await this.setStatus(ClientStatus.Disabled); @@ -413,7 +408,7 @@ class LndClient extends SwapClient { // In case of sanity swaps we don't know the // takerCltvDelta or the makerCltvDelta. Using our // client's default. - finalCltvDelta: this.lockBuffer, + finalCltvDelta: this.finalLock, }); const preimage = await this.executeSendRequest(request); return preimage; @@ -622,10 +617,10 @@ class LndClient extends SwapClient { return this.unaryCall('listChannels', new lndrpc.ListChannelsRequest()); } - public getRoute = async (units: number, destination: string, _currency: string, finalCltvDelta = this.lockBuffer) => { + public getRoute = async (units: number, destination: string, _currency: string, finalLock = this.finalLock) => { const request = new lndrpc.QueryRoutesRequest(); request.setAmt(units); - request.setFinalCltvDelta(finalCltvDelta); + request.setFinalCltvDelta(finalLock); request.setPubKey(destination); const fee = new lndrpc.FeeLimit(); fee.setFixed(Math.floor(MAXFEE * request.getAmt())); @@ -641,15 +636,15 @@ class LndClient extends SwapClient { !err.message.includes('unable to find a path to destination') && !err.message.includes('target not found') )) { - this.logger.error(`error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalCltvDelta}`, err); + this.logger.error(`error calling queryRoutes to ${destination}, amount ${units}, finalCltvDelta ${finalLock}`, err); throw err; } } if (route) { - this.logger.debug(`found a route to ${destination} for ${units} units with finalCltvDelta ${finalCltvDelta}: ${route}`); + this.logger.debug(`found a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`); } else { - this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalCltvDelta}: ${route}`); + this.logger.debug(`could not find a route to ${destination} for ${units} units with finalCltvDelta ${finalLock}: ${route}`); } return route; } @@ -692,13 +687,13 @@ class LndClient extends SwapClient { return unlockWalletResponse.toObject(); } - public addInvoice = async (rHash: string, units: number, cltvExpiry: number) => { + public addInvoice = async (rHash: string, units: number, expiry = this.finalLock) => { const addHoldInvoiceRequest = new lndinvoices.AddHoldInvoiceRequest(); addHoldInvoiceRequest.setHash(hexToUint8Array(rHash)); addHoldInvoiceRequest.setValue(units); - addHoldInvoiceRequest.setCltvExpiry(cltvExpiry); + addHoldInvoiceRequest.setCltvExpiry(expiry); await this.addHoldInvoice(addHoldInvoiceRequest); - this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${cltvExpiry}`); + this.logger.debug(`added invoice of ${units} for ${rHash} with cltvExpiry ${expiry}`); this.subscribeSingleInvoice(rHash); } diff --git a/lib/lndclient/types.ts b/lib/lndclient/types.ts index 6f2761bfd..2083d2298 100644 --- a/lib/lndclient/types.ts +++ b/lib/lndclient/types.ts @@ -6,6 +6,7 @@ export type LndClientConfig = { host: string; port: number; nomacaroons: boolean; + cltvdelta: number; }; /** General information about the state of this lnd client. */ diff --git a/lib/raidenclient/RaidenClient.ts b/lib/raidenclient/RaidenClient.ts index b816b1559..ea9c67403 100644 --- a/lib/raidenclient/RaidenClient.ts +++ b/lib/raidenclient/RaidenClient.ts @@ -45,7 +45,6 @@ async function parseResponseBody(res: http.IncomingMessage): Promise { */ class RaidenClient extends SwapClient { public readonly type = SwapClientType.Raiden; - public readonly lockBuffer: number; public readonly finalLock = 100; public address?: string; /** A map of currency symbols to token addresses. */ @@ -61,13 +60,12 @@ class RaidenClient extends SwapClient { * Creates a raiden client. */ constructor( - { config, logger, unitConverter, directChannelChecks = false, lockBufferHours }: - { config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter, directChannelChecks: boolean, lockBufferHours: number }, + { config, logger, unitConverter, directChannelChecks = false }: + { config: RaidenClientConfig, logger: Logger, unitConverter: UnitConverter, directChannelChecks: boolean }, ) { super(logger); const { disable, host, port } = config; - this.lockBuffer = Math.round(lockBufferHours * 60 / this.minutesPerBlock); this.port = port; this.host = host; this.disable = disable; diff --git a/lib/swaps/SwapClient.ts b/lib/swaps/SwapClient.ts index b53879006..f010112af 100644 --- a/lib/swaps/SwapClient.ts +++ b/lib/swaps/SwapClient.ts @@ -45,15 +45,7 @@ interface SwapClient { */ abstract class SwapClient extends EventEmitter { /** - * The number of blocks to use for determining the minimum delay for an incoming payment in excess - * of the total time delay of the contingent outgoing payment. This buffer ensures that the lock - * for incoming payments does not expire before the contingent outgoing payment lock. - */ - public abstract readonly lockBuffer: number; - /** - * The number of blocks of lock time to expect on the final incoming hop of a swap. This affects - * only the second leg of a swap where knowledge of the preimage is not contingent on making a - * separate payment. + * The number of blocks of lock time to expect on the final hop of an incoming swap payment. */ public abstract readonly finalLock: number; public abstract readonly type: SwapClientType; @@ -161,7 +153,7 @@ abstract class SwapClient extends EventEmitter { /** * @param units the amount of the invoice denominated in the smallest units supported by its currency */ - public abstract async addInvoice(rHash: string, units: number, cltvExpiry: number): Promise; + public abstract async addInvoice(rHash: string, units: number, expiry?: number): Promise; public abstract async settleInvoice(rHash: string, rPreimage: string): Promise; diff --git a/lib/swaps/SwapClientManager.ts b/lib/swaps/SwapClientManager.ts index d392f0ae6..b098d162b 100644 --- a/lib/swaps/SwapClientManager.ts +++ b/lib/swaps/SwapClientManager.ts @@ -51,7 +51,6 @@ class SwapClientManager extends EventEmitter { this.raidenClient = new RaidenClient({ unitConverter, config: config.raiden, - lockBufferHours: config.lockbuffer, logger: loggers.raiden, directChannelChecks: config.debug.raidenDirectChannelChecks, }); @@ -71,7 +70,6 @@ class SwapClientManager extends EventEmitter { const lndClient = new LndClient({ currency, config: lndConfig, - lockBufferHours: this.config.lockbuffer, logger: this.loggers.lnd.createSubLogger(currency), }); this.swapClients.set(currency, lndClient); diff --git a/lib/swaps/Swaps.ts b/lib/swaps/Swaps.ts index bbe0c289d..226524b4d 100644 --- a/lib/swaps/Swaps.ts +++ b/lib/swaps/Swaps.ts @@ -15,6 +15,7 @@ import { PacketType } from '../p2p/packets'; import SwapClientManager from './SwapClientManager'; import { errors, errorCodes } from './errors'; import SwapRecovery from './SwapRecovery'; +import poissonQuantile from 'distributions-poisson-quantile'; export type OrderToAccept = Pick & { quantity: number; @@ -77,6 +78,26 @@ class Swaps extends EventEmitter { return proposedQuantity > 0 && rHash.length === 64; } + /** + * Calculates the minimum expected lock delta for the final hop of the first leg to ensure a + * very high probability that it won't expire before the second leg payment. We use a Poisson + * distribution to model the possible block times of two independent chains, first calculating + * a probabilistic upper bound for the lock time in minuntes of the second leg then a + * probabilistic lower bound for the number of blocks for the lock time extended to the final + * hop of the first leg. + * @param secondLegLockDuration The lock duration (aka time lock or cltv delta) of the second + * leg (maker to taker) denominated in blocks of that chain. + * @returns A number of blocks for the chain of the first leg that is highly likely to take + * more time in minutes than the provided second leg lock duration. + */ + private static calculateLockBuffer = (secondLegLockDuration: number, secondLegMinutesPerBlock: number, firstLegMinutesPerBlock: number) => { + /** A probabilistic upper bound for the time it will take for the second leg route time lock to expire. */ + const secondLegLockMinutes = poissonQuantile(.9999, { lambda: secondLegLockDuration }) * secondLegMinutesPerBlock; + const firstLegLockBuffer = poissonQuantile(.9999, { lambda: secondLegLockMinutes / firstLegMinutesPerBlock }); + + return firstLegLockBuffer; + } + /** * Calculates the currencies and amounts of subunits/satoshis each side of a swap should receive. * @param quantity The quantity being swapped @@ -161,7 +182,7 @@ class Swaps extends EventEmitter { this.sanitySwaps.set(rHash, sanitySwap); const swapClient = this.swapClientManager.get(currency)!; try { - await swapClient.addInvoice(rHash, 1, swapClient.lockBuffer); + await swapClient.addInvoice(rHash, 1); } catch (err) { this.logger.error('could not add invoice for sanity swap', err); return; @@ -356,7 +377,7 @@ class Swaps extends EventEmitter { try { await Promise.all([ - swapClient.addInvoice(rHash, 1, swapClient.lockBuffer), + swapClient.addInvoice(rHash, 1), peer.sendPacket(sanitySwapInitPacket), peer.wait(sanitySwapInitPacket.header.id, PacketType.SanitySwapAck, Swaps.SANITY_SWAP_INIT_TIMEOUT), ]); @@ -575,27 +596,23 @@ class Swaps extends EventEmitter { if (height) { this.logger.debug(`got ${takerCurrency} block height of ${height}`); - const routeAbsoluteTimeLock = makerToTakerRoute.getTotalTimeLock(); - const routeLockDuration = routeAbsoluteTimeLock - height; + const routeTotalTimeLock = makerToTakerRoute.getTotalTimeLock(); + const routeLockDuration = routeTotalTimeLock - height; const routeLockHours = Math.round(routeLockDuration * takerSwapClient.minutesPerBlock / 60); this.logger.debug(`found route to taker with total lock duration of ${routeLockDuration} ${takerCurrency} blocks (~${routeLockHours}h)`); deal.takerMaxTimeLock = routeLockDuration; - const makerClientLockBuffer = this.swapClientManager.get(makerCurrency)!.lockBuffer; - const makerClientLockBufferHours = Math.round(makerClientLockBuffer * makerSwapClient.minutesPerBlock / 60); - this.logger.debug(`maker client lock buffer: ${makerClientLockBuffer} ${makerCurrency} blocks (~${makerClientLockBufferHours}h)`); - - /** The ratio of the average time for blocks on the taker (2nd leg) currency per blocks on the maker (1st leg) currency. */ - const blockTimeFactor = takerSwapClient.minutesPerBlock / makerSwapClient.minutesPerBlock; - this.logger.debug(`block time factor of ${makerCurrency} to ${takerCurrency}: ${blockTimeFactor}`); + // Here we calculate the minimum lock delta we will expect as maker on the final hop to us on + // the first leg of the swap. This should ensure a very high probability that the final hop + // of the payment to us won't expire before our payment to the taker with time leftover to + // satisfy our finalLock/cltvDelta requirement for the incoming payment swap client. + const lockBuffer = Swaps.calculateLockBuffer(routeLockDuration, takerSwapClient.minutesPerBlock, makerSwapClient.minutesPerBlock); + const lockBufferHours = Math.round(lockBuffer * makerSwapClient.minutesPerBlock / 60); + this.logger.debug(`calculated lock buffer for first leg: ${lockBuffer} ${makerCurrency} blocks (~${lockBufferHours}h)`); - // Here we calculate the minimum lock duration the maker will expect on the final hop to him - // on the first leg of the swap. This is equal to the maker's configurable lock buffer plus - // the total lock duration of the taker route times a factor to convert taker blocks to maker - // blocks. This is to ensure that the 1st leg payment HTLC doesn't expire before the 2nd leg. - deal.makerCltvDelta = makerClientLockBuffer + Math.ceil(routeLockDuration * blockTimeFactor); + deal.makerCltvDelta = lockBuffer + makerSwapClient.finalLock; const makerCltvDeltaHours = Math.round(deal.makerCltvDelta * makerSwapClient.minutesPerBlock / 60); - this.logger.debug(`calculated lock delta for final hop to maker: ${deal.makerCltvDelta} ${makerCurrency} blocks (~${makerCltvDeltaHours}h)`); + this.logger.debug(`lock delta for final hop to maker: ${deal.makerCltvDelta} ${makerCurrency} blocks (~${makerCltvDeltaHours}h)`); } if (!deal.makerCltvDelta) { diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 3d5171562..8ae3ab18c 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -833,6 +833,12 @@ "integrity": "sha512-EIjmpvnHj+T4nMcKwHwxZKUfDmphIKJc2qnEMhSoOvr1lYEQpuRKRz8orWr//krYIIArS/KGGLfL2YGVUYXmIA==", "dev": true }, + "@types/distributions-poisson-quantile": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@types/distributions-poisson-quantile/-/distributions-poisson-quantile-0.0.0.tgz", + "integrity": "sha512-3TsiQv03R89kBUv3IJOWHohZEw5Pnp1ynownmpgOfjvGxI0lZd/UCdSLcKHHC/Du+pZT0ZugOcR5ABiraQtRxw==", + "dev": true + }, "@types/dotenv": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-4.0.3.tgz", @@ -2355,6 +2361,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.0", "braces": "^2.3.0", + "fsevents": "^1.1.2", "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", @@ -2671,6 +2678,89 @@ "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" }, + "compute-array-constructors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-constructors/-/compute-array-constructors-1.0.1.tgz", + "integrity": "sha1-h+16ZZZo8yv0cM4hybsiSWSJJmU=" + }, + "compute-array-dtype": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-dtype/-/compute-array-dtype-1.0.1.tgz", + "integrity": "sha1-0EkmtoSQIkWQU5pK6BLxikmY2/M=" + }, + "compute-dtype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/compute-dtype/-/compute-dtype-1.0.0.tgz", + "integrity": "sha1-90ZfwmgWbitaK4pktXzsaRGNNXQ=", + "requires": { + "compute-array-dtype": "^1.0.0", + "type-name": "^1.0.1" + } + }, + "compute-erfcinv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/compute-erfcinv/-/compute-erfcinv-3.0.1.tgz", + "integrity": "sha1-psw7qtF6E+g4h7k1iR0sfNlqS8s=", + "requires": { + "compute-array-constructors": "^1.0.0", + "compute-polynomial": "^1.1.0", + "dstructs-matrix": "^2.0.0", + "utils-deep-get": "^1.0.0", + "utils-deep-set": "^1.0.1", + "validate.io-array-like": "^1.0.1", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-nan": "^1.0.3", + "validate.io-number-primitive": "^1.0.0", + "validate.io-object": "^1.0.4", + "validate.io-string-primitive": "^1.0.0", + "validate.io-typed-array-like": "^1.0.0" + } + }, + "compute-gammainc": { + "version": "git+https://github.com/compute-io/gammainc.git#bc70aa2136c91877a590ebc4a08bac3d9843adcc", + "from": "git+https://github.com/compute-io/gammainc.git", + "requires": { + "compute-array-constructors": "^1.0.0", + "dstructs-matrix": "^2.0.0", + "gamma": "^1.0.0", + "utils-deep-get": "^1.0.0", + "utils-deep-set": "^1.0.1", + "validate.io-array": "^1.0.6", + "validate.io-array-like": "^1.0.1", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-nan": "^1.0.3", + "validate.io-number-primitive": "^1.0.0", + "validate.io-object": "^1.0.4", + "validate.io-string-primitive": "^1.0.0", + "validate.io-typed-array-like": "^1.0.0" + } + }, + "compute-indexspace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-indexspace/-/compute-indexspace-1.0.1.tgz", + "integrity": "sha1-2B0KWCFFyW5Ls5k2rr2ubMV+hX8=", + "requires": { + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, + "compute-polynomial": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/compute-polynomial/-/compute-polynomial-1.1.0.tgz", + "integrity": "sha1-WAeC5spdzzxZuuzH/vP3ojUgJ14=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-number-primitive": "^1.0.0", + "validate.io-number-primitive-array": "^1.0.0", + "validate.io-object": "^1.0.3" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2811,6 +2901,16 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "const-max-uint32": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/const-max-uint32/-/const-max-uint32-1.0.2.tgz", + "integrity": "sha1-8Am7YjDmeO2HTdLWqc2ePL+rtnY=" + }, + "const-pinf-float64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/const-pinf-float64/-/const-pinf-float64-1.0.0.tgz", + "integrity": "sha1-9u+w15+cCYbT558pI6v5twtj1yY=" + }, "content-disposition": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", @@ -3550,6 +3650,53 @@ "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", "dev": true }, + "distributions-poisson-cdf": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/distributions-poisson-cdf/-/distributions-poisson-cdf-0.0.2.tgz", + "integrity": "sha1-jbqS+1z06Sa7MRLM0nIjGOwuAOI=", + "requires": { + "compute-array-constructors": "^1.0.0", + "compute-gammainc": "git+https://github.com/compute-io/gammainc.git", + "dstructs-matrix": "^2.0.0", + "utils-deep-get": "^1.0.0", + "utils-deep-set": "^1.0.1", + "validate.io-array-like": "^1.0.1", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-nan": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-number-primitive": "^1.0.0", + "validate.io-object": "^1.0.4", + "validate.io-positive-primitive": "^1.0.0", + "validate.io-string-primitive": "^1.0.0", + "validate.io-typed-array-like": "^1.0.0" + } + }, + "distributions-poisson-quantile": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/distributions-poisson-quantile/-/distributions-poisson-quantile-0.0.0.tgz", + "integrity": "sha1-wxdo673e2KXmo8L8wBPckM4TDeg=", + "requires": { + "compute-array-constructors": "^1.0.0", + "compute-erfcinv": "^3.0.0", + "distributions-poisson-cdf": "0.0.2", + "dstructs-matrix": "^2.0.0", + "utils-deep-get": "^1.0.0", + "utils-deep-set": "^1.0.1", + "validate.io-array-like": "^1.0.1", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-nan": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-number-primitive": "^1.0.0", + "validate.io-object": "^1.0.4", + "validate.io-positive-primitive": "^1.0.0", + "validate.io-string-primitive": "^1.0.0", + "validate.io-typed-array-like": "^1.0.0" + } + }, "doctrine": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz", @@ -3612,6 +3759,47 @@ "create-hmac": "^1.1.4" } }, + "dstructs-array-constructors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-constructors/-/dstructs-array-constructors-1.0.2.tgz", + "integrity": "sha1-DwaK1bN9AUpEAhpPnJ3Q2JpSZFo=" + }, + "dstructs-array-dtype": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-dtype/-/dstructs-array-dtype-1.0.2.tgz", + "integrity": "sha1-w2j7bkYinfokCfcAzEOiCbSA524=" + }, + "dstructs-cast-arrays": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dstructs-cast-arrays/-/dstructs-cast-arrays-1.0.3.tgz", + "integrity": "sha1-8sugaep0MdrV13lgGlHVmXbCvCU=", + "requires": { + "dstructs-array-constructors": "^1.0.2", + "dstructs-array-dtype": "^1.0.2", + "type-name": "^1.0.1", + "validate.io-array-like": "^1.0.1" + } + }, + "dstructs-matrix": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/dstructs-matrix/-/dstructs-matrix-2.1.2.tgz", + "integrity": "sha1-tJYlYAogtCHGCdGRG7HYOCkHR9o=", + "requires": { + "compute-dtype": "^1.0.0", + "compute-indexspace": "^1.0.1", + "dstructs-cast-arrays": "^1.0.2", + "utils-copy": "^1.0.0", + "validate.io-array": "^1.0.6", + "validate.io-contains": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0", + "validate.io-nan": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-nonnegative-integer-array": "^1.0.1", + "validate.io-number-primitive": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", @@ -4481,6 +4669,487 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", + "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", + "optional": true, + "requires": { + "nan": "^2.12.1", + "node-pre-gyp": "^0.12.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "aproba": { + "version": "1.2.0", + "bundled": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.5", + "bundled": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "brace-expansion": { + "version": "1.1.11", + "bundled": true, + "optional": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.1.1", + "bundled": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "concat-map": { + "version": "0.0.1", + "bundled": true, + "optional": true + }, + "console-control-strings": { + "version": "1.1.0", + "bundled": true, + "optional": true + }, + "core-util-is": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "debug": { + "version": "4.1.1", + "bundled": true, + "optional": true, + "requires": { + "ms": "^2.1.1" + } + }, + "deep-extend": { + "version": "0.6.0", + "bundled": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "bundled": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "bundled": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.3", + "bundled": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.24", + "bundled": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ignore-walk": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "bundled": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "bundled": true, + "optional": true + }, + "ini": { + "version": "1.3.5", + "bundled": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "bundled": true, + "optional": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "bundled": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "bundled": true, + "optional": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "bundled": true, + "optional": true + }, + "minipass": { + "version": "2.3.5", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.2.1", + "bundled": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "bundled": true, + "optional": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.1.1", + "bundled": true, + "optional": true + }, + "needle": { + "version": "2.3.0", + "bundled": true, + "optional": true, + "requires": { + "debug": "^4.1.0", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.12.0", + "bundled": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "bundled": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.6", + "bundled": true, + "optional": true + }, + "npm-packlist": { + "version": "1.4.1", + "bundled": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "bundled": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "object-assign": { + "version": "4.1.1", + "bundled": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "bundled": true, + "optional": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "bundled": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "bundled": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "rc": { + "version": "1.2.8", + "bundled": true, + "optional": true, + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "bundled": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "bundled": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.3", + "bundled": true, + "optional": true, + "requires": { + "glob": "^7.1.3" + } + }, + "safe-buffer": { + "version": "5.1.2", + "bundled": true, + "optional": true + }, + "safer-buffer": { + "version": "2.1.2", + "bundled": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "bundled": true, + "optional": true + }, + "semver": { + "version": "5.7.0", + "bundled": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "bundled": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "bundled": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "bundled": true, + "optional": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "bundled": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "bundled": true, + "optional": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "bundled": true, + "optional": true + }, + "tar": { + "version": "4.4.8", + "bundled": true, + "optional": true, + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.4", + "minizlib": "^1.1.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "wide-align": { + "version": "1.1.3", + "bundled": true, + "optional": true, + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "wrappy": { + "version": "1.0.2", + "bundled": true, + "optional": true + }, + "yallist": { + "version": "3.0.3", + "bundled": true, + "optional": true + } + } + }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -4509,6 +5178,11 @@ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, + "gamma": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gamma/-/gamma-1.0.0.tgz", + "integrity": "sha1-mDwck5/iPZMnAVhXEeHZpDDLdMs=" + }, "gauge": { "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", @@ -6885,6 +7559,7 @@ "@jest/types": "^24.8.0", "anymatch": "^2.0.0", "fb-watchman": "^2.0.0", + "fsevents": "^1.2.7", "graceful-fs": "^4.1.15", "invariant": "^2.2.4", "jest-serializer": "^24.4.0", @@ -8639,6 +9314,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.1", "braces": "^2.3.2", + "fsevents": "^1.2.7", "glob-parent": "^3.1.0", "inherits": "^2.0.3", "is-binary-path": "^1.0.0", @@ -9700,6 +10376,11 @@ "safe-regex": "^1.1.0" } }, + "regex-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regex-regex/-/regex-regex-1.0.0.tgz", + "integrity": "sha1-kEih6uuHD01IDavHb8Qs3MC8OnI=" + }, "registry-auth-token": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.4.0.tgz", @@ -11653,6 +12334,11 @@ "mime-types": "~2.1.18" } }, + "type-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-1.1.0.tgz", + "integrity": "sha1-rZw/fDMPWy8I3n159W0rlFHkKw4=" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -11958,11 +12644,82 @@ "object.getownpropertydescriptors": "^2.0.3" } }, + "utils-copy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/utils-copy/-/utils-copy-1.1.1.tgz", + "integrity": "sha1-biuXmCqozXPhGCo+b4vsPA9AWKc=", + "requires": { + "const-pinf-float64": "^1.0.0", + "object-keys": "^1.0.9", + "type-name": "^2.0.0", + "utils-copy-error": "^1.0.0", + "utils-indexof": "^1.0.0", + "utils-regex-from-string": "^1.0.0", + "validate.io-array": "^1.0.3", + "validate.io-buffer": "^1.0.1", + "validate.io-nonnegative-integer": "^1.0.0" + }, + "dependencies": { + "type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=" + } + } + }, + "utils-copy-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-copy-error/-/utils-copy-error-1.0.1.tgz", + "integrity": "sha1-eR3jk8DwmJCv1Z88vqY18HmpT6U=", + "requires": { + "object-keys": "^1.0.9", + "utils-copy": "^1.1.0" + } + }, + "utils-deep-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-deep-get/-/utils-deep-get-1.0.0.tgz", + "integrity": "sha1-SEBKd/om+E5pbkhaY4nCvs3woMk=", + "requires": { + "validate.io-array": "^1.0.6", + "validate.io-object": "^1.0.4", + "validate.io-string-primitive": "^1.0.0" + } + }, + "utils-deep-set": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-deep-set/-/utils-deep-set-1.0.1.tgz", + "integrity": "sha1-Zmr5PM0dIhw/yv3yFRoi7gvLBv4=", + "requires": { + "validate.io-array": "^1.0.6", + "validate.io-boolean-primitive": "^1.0.0", + "validate.io-object": "^1.0.4", + "validate.io-string-primitive": "^1.0.0" + } + }, + "utils-indexof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-indexof/-/utils-indexof-1.0.0.tgz", + "integrity": "sha1-IP6r8J7xAYtSNkPoOA57yD7GG1w=", + "requires": { + "validate.io-array-like": "^1.0.1", + "validate.io-integer-primitive": "^1.0.0" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "utils-regex-from-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-regex-from-string/-/utils-regex-from-string-1.0.0.tgz", + "integrity": "sha1-/hopCfjeD/DVGCyA+8ZU1qaH0Yk=", + "requires": { + "regex-regex": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -11985,6 +12742,140 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=" + }, + "validate.io-array-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-array-like/-/validate.io-array-like-1.0.2.tgz", + "integrity": "sha1-evn363tRcVvrIhVmjsXM5U+t21o=", + "requires": { + "const-max-uint32": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0" + } + }, + "validate.io-boolean-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-boolean-primitive/-/validate.io-boolean-primitive-1.0.0.tgz", + "integrity": "sha1-uxTJy4aDw8nz0TKalad2jj7inrU=" + }, + "validate.io-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-buffer/-/validate.io-buffer-1.0.2.tgz", + "integrity": "sha1-hS1nNAIZFNXROvwyUxdh43IO1E4=" + }, + "validate.io-contains": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-contains/-/validate.io-contains-1.0.0.tgz", + "integrity": "sha1-vwm6TyfGQlB7CQXbs6dKUncInP4=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-nan-primitive": "^1.0.0" + } + }, + "validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=" + }, + "validate.io-integer": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", + "integrity": "sha1-FoSWSAuVviJH7EQ/IjPeT4mHgGg=", + "requires": { + "validate.io-number": "^1.0.3" + } + }, + "validate.io-integer-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-primitive/-/validate.io-integer-primitive-1.0.0.tgz", + "integrity": "sha1-qaoBA1X+hoHA/qbBp0rSQZyt3cY=", + "requires": { + "validate.io-number-primitive": "^1.0.0" + } + }, + "validate.io-matrix-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-matrix-like/-/validate.io-matrix-like-1.0.2.tgz", + "integrity": "sha1-XsMqddCInaxzbepovdYUWxVe38M=" + }, + "validate.io-nan": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-nan/-/validate.io-nan-1.0.3.tgz", + "integrity": "sha1-1DjhOGjJy9N/26EllN5Nj+FEMKQ=" + }, + "validate.io-nan-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nan-primitive/-/validate.io-nan-primitive-1.0.0.tgz", + "integrity": "sha1-R1zC0DXQuvLQCRItg+opGjLX+ww=" + }, + "validate.io-nonnegative-integer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer/-/validate.io-nonnegative-integer-1.0.0.tgz", + "integrity": "sha1-gGkkOgjF+Y6VQTySnf17GPP28p8=", + "requires": { + "validate.io-integer": "^1.0.5" + } + }, + "validate.io-nonnegative-integer-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer-array/-/validate.io-nonnegative-integer-array-1.0.1.tgz", + "integrity": "sha1-ZjMKZl9VmLlvJfaQgfgfYy6k208=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0" + } + }, + "validate.io-number": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", + "integrity": "sha1-9j/+2iSL8opnqNSODjtGGhZluvg=" + }, + "validate.io-number-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-number-primitive/-/validate.io-number-primitive-1.0.0.tgz", + "integrity": "sha1-0uAfICmJNp3PEVVElWQgOv5YTlU=" + }, + "validate.io-number-primitive-array": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-number-primitive-array/-/validate.io-number-primitive-array-1.0.0.tgz", + "integrity": "sha1-cGRECMWU1C8fu9ZaxrIxYx6GmVQ=", + "requires": { + "validate.io-array": "^1.0.3" + } + }, + "validate.io-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/validate.io-object/-/validate.io-object-1.0.4.tgz", + "integrity": "sha1-3KAezu45DhENvCr4Q8gfe/M6Qas=", + "requires": { + "validate.io-array": "^1.0.1" + } + }, + "validate.io-positive-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-positive-primitive/-/validate.io-positive-primitive-1.0.0.tgz", + "integrity": "sha1-vGEkQWseoX7b8DWhdmnQCxhYd8A=", + "requires": { + "validate.io-number-primitive": "^1.0.0" + } + }, + "validate.io-string-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-string-primitive/-/validate.io-string-primitive-1.0.1.tgz", + "integrity": "sha1-uBNbn7E3K94C/dU60dDM1t55j+4=" + }, + "validate.io-typed-array-like": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-typed-array-like/-/validate.io-typed-array-like-1.0.1.tgz", + "integrity": "sha1-vJ0I7R2b+nvZ7FRMbuNzWnht9wo=", + "requires": { + "const-max-uint32": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0" + } + }, "validator": { "version": "10.11.0", "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", diff --git a/package.json b/package.json index 9056d752f..86f402958 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "cli-table3": "^0.5.1", "colors": "^1.3.3", "cross-os": "^1.3.0", + "distributions-poisson-quantile": "0.0.0", "dotenv": "^5.0.1", "express": "^4.16.3", "fastpriorityqueue": "^0.6.3", @@ -150,6 +151,7 @@ "@types/chai": "^4.1.7", "@types/chai-as-promised": "^7.1.0", "@types/chai-http": "^3.0.5", + "@types/distributions-poisson-quantile": "0.0.0", "@types/dotenv": "^4.0.3", "@types/express": "^4.16.0", "@types/gulp": "^4.0.5", diff --git a/sample-xud.conf b/sample-xud.conf index 812ffdfe9..120f6dc7b 100644 --- a/sample-xud.conf +++ b/sample-xud.conf @@ -46,7 +46,7 @@ host = "localhost" port = 8887 [lnd.BTC] -cltvdelta = 144 +cltvdelta = 40 disable = false host = "localhost" nomacaroons = false diff --git a/test/jest/LndClient.spec.ts b/test/jest/LndClient.spec.ts index 8a6db074b..e9fe036f6 100644 --- a/test/jest/LndClient.spec.ts +++ b/test/jest/LndClient.spec.ts @@ -25,7 +25,6 @@ describe('LndClient', () => { let config: LndClientConfig; let currency: string; let logger: Logger; - const lockBufferHours = 24; beforeEach(() => { config = { @@ -35,6 +34,7 @@ describe('LndClient', () => { host: '127.0.0.1', port: 4321, nomacaroons: true, + cltvdelta: 40, }; currency = 'BTC'; logger = new mockedLogger(); @@ -60,7 +60,7 @@ describe('LndClient', () => { test('it throws when connectPeer fails', async () => { expect.assertions(3); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['connectPeer'] = jest.fn().mockImplementation(() => { throw new Error('connectPeer failed'); }); @@ -80,7 +80,7 @@ describe('LndClient', () => { test('it tries all 2 lnd uris when connectPeer to first one fails', async () => { expect.assertions(3); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); const connectPeerFail = () => { throw new Error('connectPeer failed'); @@ -104,7 +104,7 @@ describe('LndClient', () => { test('it does succeed when connecting to already connected peer', async () => { expect.assertions(4); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); const alreadyConnected = () => { throw new Error('already connected'); @@ -127,7 +127,7 @@ describe('LndClient', () => { test('it throws when timeout reached', async () => { expect.assertions(3); jest.useFakeTimers(); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); const timeOut = () => { jest.runAllTimers(); @@ -151,7 +151,7 @@ describe('LndClient', () => { test('it stops trying to connect to lnd uris when first once succeeds', async () => { expect.assertions(3); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['openChannelSync'] = jest.fn().mockReturnValue(Promise.resolve()); lnd['connectPeer'] = jest.fn() .mockImplementationOnce(() => { @@ -171,7 +171,7 @@ describe('LndClient', () => { test('it throws when openchannel fails', async () => { expect.assertions(2); - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['connectPeer'] = jest.fn().mockReturnValue(Promise.resolve()); lnd['openChannelSync'] = jest.fn().mockImplementation(() => { throw new Error('openChannelSync error'); @@ -193,7 +193,7 @@ describe('LndClient', () => { describe('sendPayment', () => { test('it resolves upon maker success', async () => { - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['sendPaymentSync'] = jest.fn() .mockReturnValue(Promise.resolve(getSendPaymentSyncResponse())); const deal = getValidDeal(); @@ -210,7 +210,7 @@ describe('LndClient', () => { }); test('it resolves upon taker success', async () => { - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['sendPaymentSync'] = jest.fn() .mockReturnValue(Promise.resolve(getSendPaymentSyncResponse())); const deal = { @@ -229,7 +229,7 @@ describe('LndClient', () => { }); test('it rejects upon sendPaymentSync error', async () => { - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['sendPaymentSync'] = jest.fn() .mockReturnValue(Promise.resolve(getSendPaymentSyncErrorResponse())); await expect(lnd.sendPayment(getValidDeal())) @@ -237,7 +237,7 @@ describe('LndClient', () => { }); test('it resolves upon sendSmallestAmount success', async () => { - lnd = new LndClient({ config, currency, lockBufferHours, logger }); + lnd = new LndClient({ config, currency, logger }); lnd['sendPaymentSync'] = jest.fn() .mockReturnValue(Promise.resolve(getSendPaymentSyncResponse())); const buildSendRequestSpy = jest.spyOn(lnd as any, 'buildSendRequest'); @@ -248,7 +248,7 @@ describe('LndClient', () => { expect(buildSendRequestSpy).toHaveBeenCalledWith({ destination, rHash, - finalCltvDelta: lnd.lockBuffer, + finalCltvDelta: lnd.finalLock, amount: 1, }); }); diff --git a/test/jest/RaidenClient.spec.ts b/test/jest/RaidenClient.spec.ts index eca924856..8462a32fe 100644 --- a/test/jest/RaidenClient.spec.ts +++ b/test/jest/RaidenClient.spec.ts @@ -70,7 +70,6 @@ describe('RaidenClient', () => { let config: RaidenClientConfig; let raidenLogger: Logger; let unitConverter: UnitConverter; - const lockBufferHours = 24; beforeEach(() => { config = { @@ -92,7 +91,7 @@ describe('RaidenClient', () => { describe('sendPayment', () => { test('it removes 0x from secret', async () => { - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); const validTokenPaymentResponse: TokenPaymentResponse = getValidTokenPaymentResponse(); raiden['tokenPayment'] = jest.fn() @@ -104,7 +103,7 @@ describe('RaidenClient', () => { }); test('it rejects in case of empty secret response', async () => { - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); const invalidTokenPaymentResponse: TokenPaymentResponse = { ...getValidTokenPaymentResponse(), @@ -132,7 +131,7 @@ describe('RaidenClient', () => { test('it fails when tokenAddress for currency not found', async () => { expect.assertions(1); - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init([] as CurrencyInstance[]); try { await raiden.openChannel({ @@ -147,7 +146,7 @@ describe('RaidenClient', () => { test('it throws when openChannel fails', async () => { expect.assertions(1); - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -170,7 +169,7 @@ describe('RaidenClient', () => { test('it opens a channel', async () => { expect.assertions(2); - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -197,7 +196,7 @@ describe('RaidenClient', () => { const paymentHash = '63699fb42306ea693c7c9c038c18ecc8c7dbc8095b8ddd8d2e4421f2ce7b4c0c'; test('it detects payment in pending state', async () => { - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -216,7 +215,7 @@ describe('RaidenClient', () => { }); test('it checks if payment has failed or completed if it is not pending', async () => { - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); const peerRaidenAddress = '0x10D8CCAD85C7dc123090B43aA1f98C00a303BFC5'; const currency = 'WETH'; const mockTokenAddresses = new Map(); @@ -233,7 +232,7 @@ describe('RaidenClient', () => { }); test('channelBalance calculates the total balance of open channels for a currency', async () => { - raiden = new RaidenClient({ unitConverter, config, lockBufferHours, directChannelChecks: true, logger: raidenLogger }); + raiden = new RaidenClient({ unitConverter, config, directChannelChecks: true, logger: raidenLogger }); await raiden.init(currencyInstances as CurrencyInstance[]); raiden.tokenAddresses.get = jest.fn().mockReturnValue(channelBalanceTokenAddress); raiden['getChannels'] = jest.fn() diff --git a/test/jest/SwapClientManager.spec.ts b/test/jest/SwapClientManager.spec.ts index cb71848d8..2e2581224 100644 --- a/test/jest/SwapClientManager.spec.ts +++ b/test/jest/SwapClientManager.spec.ts @@ -95,6 +95,7 @@ describe('Swaps.SwapClientManager', () => { port: 10009, nomacaroons: true, macaroonpath: '', + cltvdelta: 40, }, LTC: { disable: false, @@ -103,6 +104,7 @@ describe('Swaps.SwapClientManager', () => { port: 10009, nomacaroons: true, macaroonpath: '', + cltvdelta: 576, }, }; config.raiden = { diff --git a/test/jest/Swaps.spec.ts b/test/jest/Swaps.spec.ts new file mode 100644 index 000000000..d9e65e6bd --- /dev/null +++ b/test/jest/Swaps.spec.ts @@ -0,0 +1,30 @@ +import Swaps from '../../lib/swaps/Swaps'; +import LndClient from '../../lib/lndclient/LndClient'; + +describe('Swaps', () => { + describe('calculateLockBuffer', () => { + const ltcMinutesPerBlock = LndClient['MINUTES_PER_BLOCK_BY_CURRENCY']['LTC']; + const btcMinutesPerBlock = LndClient['MINUTES_PER_BLOCK_BY_CURRENCY']['BTC']; + const raidenMinutesPerBlock = 0.25; + + test('it calculates a lock buffer with BTC first leg and LTC second leg', async () => { + expect(Swaps['calculateLockBuffer'](1152, ltcMinutesPerBlock, btcMinutesPerBlock)) + .toMatchSnapshot(); + }); + + test('it calculates a lock buffer with LTC first leg and BTC second leg', async () => { + expect(Swaps['calculateLockBuffer'](80, btcMinutesPerBlock, ltcMinutesPerBlock)) + .toMatchSnapshot(); + }); + + test('it calculates a lock buffer with BTC first leg and WETH second leg', async () => { + expect(Swaps['calculateLockBuffer'](100, raidenMinutesPerBlock, btcMinutesPerBlock)) + .toMatchSnapshot(); + }); + + test('it calculates a lock buffer with WETH first leg and BTC second leg', async () => { + expect(Swaps['calculateLockBuffer'](80, btcMinutesPerBlock, raidenMinutesPerBlock)) + .toMatchSnapshot(); + }); + }); +}); diff --git a/test/jest/__snapshots__/Swaps.spec.ts.snap b/test/jest/__snapshots__/Swaps.spec.ts.snap new file mode 100644 index 000000000..ce3421ea2 --- /dev/null +++ b/test/jest/__snapshots__/Swaps.spec.ts.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Swaps calculateLockBuffer it calculates a lock buffer with BTC first leg and LTC second leg 1`] = `389`; + +exports[`Swaps calculateLockBuffer it calculates a lock buffer with BTC first leg and WETH second leg 1`] = `12`; + +exports[`Swaps calculateLockBuffer it calculates a lock buffer with LTC first leg and BTC second leg 1`] = `542`; + +exports[`Swaps calculateLockBuffer it calculates a lock buffer with WETH first leg and BTC second leg 1`] = `4854`; diff --git a/test/jest/integration/Swaps.spec.ts b/test/jest/integration/Swaps.spec.ts index 6a266230e..086d7b569 100644 --- a/test/jest/integration/Swaps.spec.ts +++ b/test/jest/integration/Swaps.spec.ts @@ -27,12 +27,14 @@ jest.mock('../../../lib/swaps/SwapRepository', () => { }; }); }); -const getMockedLnd = (lockBuffer: number) => { +const getMockedLnd = (cltvDelta: number, minutesPerBlock: number) => { const lnd = new mockedLnd(); // @ts-ignore - lnd.lockBuffer = lockBuffer; + lnd.finalLock = cltvDelta; // @ts-ignore lnd.type = SwapClientType.Lnd; + // @ts-ignore + lnd.minutesPerBlock = minutesPerBlock; lnd.isConnected = jest.fn().mockReturnValue(true); const removeInvoice = jest.fn().mockImplementation(() => { return { catch: () => {} }; @@ -58,7 +60,7 @@ const getSwapRequestBody = (): SwapRequestPacketBody => { proposedQuantity: 10000, }; }; -describe('Swaps', () => { +describe('Swaps Integration', () => { let swaps: Swaps; let pool: Pool; let logger: Logger; @@ -80,8 +82,8 @@ describe('Swaps', () => { swapClientManager.get = jest.fn(); peer = new mockedPeer(); peer.sendPacket = jest.fn(); - lndBtc = getMockedLnd(144); - lndLtc = getMockedLnd(576); + lndBtc = getMockedLnd(40, 10); + lndLtc = getMockedLnd(576, 2.5); makerCurrency = 'LTC'; takerCurrency = 'BTC'; }); @@ -277,7 +279,7 @@ describe('Swaps', () => { swapRequestBody.takerCltvDelta, ); expect(lndLtc.addInvoice).toHaveBeenCalledTimes(1); - const expectedMakerCltvDelta = 1152; + const expectedMakerCltvDelta = 1445; expect(lndLtc.addInvoice).toHaveBeenCalledWith( swapRequestBody.rHash, swapRequestBody.proposedQuantity,