Skip to content

Commit

Permalink
Merge pull request #5 from Kava-Labs/ro-add-further-tests
Browse files Browse the repository at this point in the history
Add further tests
  • Loading branch information
kincaidoneil authored Feb 28, 2019
2 parents 7941759 + 16671b3 commit 01707d4
Show file tree
Hide file tree
Showing 7 changed files with 379 additions and 199 deletions.
191 changes: 191 additions & 0 deletions src/__tests__/_helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { AssetUnit, convert, usd } from '@kava-labs/crypto-rate-utils'
import 'envkey'
import { ExecutionContext } from 'ava'
import BigNumber from 'bignumber.js'
import { performance } from 'perf_hooks'
import { SwitchApi, SettlementEngineType, ReadyUplinks } from '..'
import { CredentialConfigs } from '../credential'

// Return configs for connecting to accounts set up in env vars.
const ethConfig = (n: number): CredentialConfigs => {
return {
settlerType: SettlementEngineType.Machinomy,
privateKey: process.env[`ETH_PRIVATE_KEY_CLIENT_${n}`]!
}
}
const btcConfig = (n: number): CredentialConfigs => {
return {
settlerType: SettlementEngineType.Lnd,
hostname: process.env[`LIGHTNING_LND_HOST_CLIENT_${n}`]!,
tlsCert: process.env[`LIGHTNING_TLS_CERT_PATH_CLIENT_${n}`]!,
macaroon: process.env[`LIGHTNING_MACAROON_PATH_CLIENT_${n}`]!,
grpcPort: parseInt(process.env[`LIGHTNING_LND_GRPCPORT_CLIENT_${n}`]!, 10)
}
}
const xrpConfig = (n: number): CredentialConfigs => {
return {
settlerType: SettlementEngineType.XrpPaychan,
secret: process.env[`XRP_SECRET_CLIENT_${n}`]!
}
}

export const addEth = (n: number) => ({
add
}: SwitchApi): Promise<ReadyUplinks> => add(ethConfig(n))
export const addBtc = (n: number) => ({
add
}: SwitchApi): Promise<ReadyUplinks> => add(btcConfig(n))
export const addXrp = (n: number) => ({
add
}: SwitchApi): Promise<ReadyUplinks> => add(xrpConfig(n))

// Helper to test deposit and withdraw on uplinks
export const testFunding = (
createUplink: (api: SwitchApi) => Promise<ReadyUplinks>
) => async (t: ExecutionContext<SwitchApi>) => {
const { state, deposit, withdraw, streamMoney } = t.context
const uplink = await createUplink(t.context)

const settler = state.settlers[uplink.settlerType]

// Instead down to the base unit of the ledger if there's more precision than that
const toUplinkUnit = (unit: AssetUnit) =>
convert(unit, settler.exchangeUnit(), state.rateBackend).decimalPlaces(
settler.exchangeUnit().exchangeUnit,
BigNumber.ROUND_DOWN
)

t.true(uplink.balance$.value.isZero(), 'initial layer 2 balance is 0')

// TODO Check base layer balances to make sure fees are also correctly reported!
// TODO Check that incoming capacity is opened!

/**
* TODO Issue with xrp: openAmount has 9 digits of precision, but balance$ only has 6!
* e.g. openAmount === "2.959676012", uplink.balance$ === "2.959676"
*/

const openAmount = toUplinkUnit(usd(1))
await t.notThrowsAsync(
deposit({
uplink,
amount: openAmount,
authorize: () => Promise.resolve()
}),
'opens channel without throwing an error'
)

t.true(
uplink.balance$.value.isEqualTo(openAmount),
'balance$ correctly reflects the initial channel open'
)

const depositAmount = toUplinkUnit(usd(2))
await t.notThrowsAsync(
deposit({
uplink,
amount: depositAmount,
authorize: () => Promise.resolve()
}),
'deposits to channel without throwing an error'
)

t.true(
uplink.balance$.value.isEqualTo(openAmount.plus(depositAmount)),
'balance$ correctly reflects the deposit to the channel'
)

// Rebalance so there's some money in both the incoming & outgoing channels
await t.notThrowsAsync(
streamMoney({
amount: toUplinkUnit(usd(1.1)),
source: uplink,
dest: uplink
}),
'uplink can stream money to itself'
)

await t.notThrowsAsync(
withdraw({ uplink, authorize: () => Promise.resolve() }),
'withdraws from channel without throwing an error'
)

t.true(
uplink.balance$.value.isZero(),
'balance$ of uplink goes back to zero following a withdraw'
)
}

// Helper to test streaming between different uplinks
export const testExchange = (
createSource: (api: SwitchApi) => Promise<ReadyUplinks>,
createDest: (api: SwitchApi) => Promise<ReadyUplinks>
) => async (t: ExecutionContext<SwitchApi>) => {
const { state, deposit, streamMoney } = t.context

const createFundedUplink = async (
createUplink: (api: SwitchApi) => Promise<ReadyUplinks>
) => {
const uplink = await createUplink(t.context)
await deposit({
uplink,
amount: convert(
usd(3),
state.settlers[uplink.settlerType].exchangeUnit(),
state.rateBackend
),
authorize: () => Promise.resolve()
})
return uplink
}

const [sourceUplink, destUplink] = await Promise.all([
createFundedUplink(createSource),
createFundedUplink(createDest)
])

// Without this pause after creating the uplinks, a stream from lnd to lnd fails.
// TODO fix
await new Promise(r => setTimeout(r, 500))

const initialSourceBalance = sourceUplink.balance$.value
const initialDestBalance = destUplink.balance$.value

const sourceUnit = state.settlers[sourceUplink.settlerType].exchangeUnit
const destUnit = state.settlers[destUplink.settlerType].exchangeUnit

const amountToSend = convert(usd(2), sourceUnit(), state.rateBackend)
const start = performance.now()
await t.notThrowsAsync(
streamMoney({
amount: amountToSend,
source: sourceUplink,
dest: destUplink
})
)
t.log(`time: ${performance.now() - start} ms`)

// Wait up to 2 seconds for the final settlements to come in (sigh)
await new Promise(r => setTimeout(r, 2000))

const finalSourceBalance = sourceUplink.balance$.value
t.true(
initialSourceBalance.minus(amountToSend).isEqualTo(finalSourceBalance),
'source balance accurately represents the amount that was sent'
)

const estimatedReceiveAmount = convert(
sourceUnit(amountToSend),
destUnit(),
state.rateBackend
)
const estimatedDestFinalBalance = initialDestBalance.plus(
estimatedReceiveAmount
)
const finalDestBalance = destUplink.balance$.value
t.true(
finalDestBalance.isGreaterThan(estimatedDestFinalBalance.times(0.99)) &&
finalDestBalance.isLessThan(estimatedDestFinalBalance.times(1.01)),
'destination balance accounts for the amount that was sent, with margin for exchange rate fluctuations'
)
}
105 changes: 105 additions & 0 deletions src/__tests__/add-remove.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import anyTest, { TestInterface, ExecutionContext } from 'ava'
import 'envkey'
import {
SwitchApi,
connect,
LedgerEnv,
SettlementEngineType,
ReadyUplinks
} from '..'
import { addXrp, addEth, addBtc } from './_helpers'

const test = anyTest as TestInterface<SwitchApi>

// Before & after each test, construct and disconnect the API
test.beforeEach(async t => {
t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv)
})
test.afterEach(async t => t.context.disconnect())

// Test adding and removing uplinks
const testAddRemove = (
createUplink: (api: SwitchApi) => Promise<ReadyUplinks>
) => async (t: ExecutionContext<SwitchApi>) => {
const uplink = await createUplink(t.context)
t.true(t.context.state.uplinks.includes(uplink))

await t.context.remove(uplink)
t.false(t.context.state.uplinks.includes(uplink))
}
test('add then remove btc', testAddRemove(addBtc(1)))
test('add then remove eth without deposit', testAddRemove(addEth(1)))
test('add then remove xrp without deposit', testAddRemove(addXrp(1)))

// Test that uplinks with the same credentials cannot be added
test('cannot add duplicate eth uplink', async t => {
await addEth(1)(t.context)
await t.throwsAsync(addEth(1)(t.context))
})
test('cannot add duplicate xrp uplink', async t => {
await addXrp(1)(t.context)
await t.throwsAsync(addXrp(1)(t.context))
})
test('cannot add duplicate btc uplink', async t => {
await addBtc(1)(t.context)
await t.throwsAsync(addBtc(1)(t.context))
})

// Test credential config input validation
// Private key and credential validation is done by lower level libraries.

test('add with invalid xrp secret throws', async t => {
await t.throwsAsync(
t.context.add({
settlerType: SettlementEngineType.XrpPaychan,
secret: 'this is not a valid xrpSecret' // invalid but correct length
}),
'Non-base58 character'
)
})
test('add with un-activated xrp secret throws', async t => {
await t.throwsAsync(
t.context.add({
settlerType: SettlementEngineType.XrpPaychan,
secret: 'sn5s78zYX1i9mzFmd8jXooDFYgfj2' // un-activated but valid secret
}),
'actNotFound'
)
})
// Test eth private keys. As long as they contain correct characters and are the right length they are a valid key.
test('add with invalid eth secret throws', async t => {
await t.throwsAsync(
t.context.add({
settlerType: SettlementEngineType.Machinomy,
privateKey:
'this is not a valid eth secret despite being the correct leength'
})
)
// Note: if the secret is correct length but contains invalid characters, an invalid length error is thrown ('private key length is invalid').
})
// Test valid lnd uri, but invalid credentials.
test('add with invalid lnd credentials throws', async t => {
await t.throwsAsync(
t.context.add({
settlerType: SettlementEngineType.Lnd,
hostname: process.env.LIGHTNING_LND_HOST_CLIENT_1!,
grpcPort: parseInt(process.env.LIGHTNING_LND_GRPCPORT_CLIENT_1!, 10),
tlsCert:
'LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUNpRENDQWkrZ0F3SUJBZ0lRZG81djBRQlhIbmppNGhSYWVlTWpOREFLQmdncWhrak9QUVFEQWpCSE1SOHcKSFFZRFZRUUtFeFpzYm1RZ1lYVjBiMmRsYm1WeVlYUmxaQ0JqWlhKME1TUXdJZ1lEVlFRREV4dEtkWE4wZFhOegpMVTFoWTBKdmIyc3RVSEp2TFRNdWJHOWpZV3d3SGhjTk1UZ3dPREl6TURVMU9ERXdXaGNOTVRreE1ERTRNRFUxCk9ERXdXakJITVI4d0hRWURWUVFLRXhac2JtUWdZWFYwYjJkbGJtVnlZWFJsWkNCalpYSjBNU1F3SWdZRFZRUUQKRXh0S2RYTjBkWE56TFUxaFkwSnZiMnN0VUhKdkxUTXViRzlqWVd3d1dUQVRCZ2NxaGtqT1BRSUJCZ2dxaGtpTwpQUU1CQndOQ0FBU0ZoUm0rdy9UMTBQb0t0ZzRsbTloQk5KakpENDczZmt6SHdQVUZ3eTkxdlRyUVNmNzU0M2oyCkpyZ0ZvOG1iVFYwVnRwZ3FrZksxSU1WS01MckYyMXhpbzRIOE1JSDVNQTRHQTFVZER3RUIvd1FFQXdJQ3BEQVAKQmdOVkhSTUJBZjhFQlRBREFRSC9NSUhWQmdOVkhSRUVnYzB3Z2NxQ0cwcDFjM1IxYzNNdFRXRmpRbTl2YXkxUQpjbTh0TXk1c2IyTmhiSUlKYkc5allXeG9iM04wZ2dSMWJtbDRnZ3AxYm1sNGNHRmphMlYwaHdSL0FBQUJoeEFBCkFBQUFBQUFBQUFBQUFBQUFBQUFCaHhEK2dBQUFBQUFBQUFBQUFBQUFBQUFCaHhEK2dBQUFBQUFBQUF3bGM5WmMKazdiRGh3VEFxQUVFaHhEK2dBQUFBQUFBQUJpTnAvLytHeFhHaHhEK2dBQUFBQUFBQUtXSjV0bGlET1JqaHdRSwpEd0FDaHhEK2dBQUFBQUFBQUc2V3ovLyszYXRGaHhEOTJ0RFF5djRUQVFBQUFBQUFBQkFBTUFvR0NDcUdTTTQ5CkJBTUNBMGNBTUVRQ0lBOU85eHRhem1keENLajBNZmJGSFZCcTVJN0pNbk9GUHB3UlBKWFFmcllhQWlCZDVOeUoKUUN3bFN4NUVDblBPSDVzUnB2MjZUOGFVY1hibXlueDlDb0R1ZkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==',
macaroon:
'AgEDbG5kArsBAwoQ3/I9f6kgSE6aUPd85lWpOBIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV32ml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFgoHbWVzc2FnZRIEcmVhZBIFd3JpdGUaFwoIb2ZmY2hhaW4SBHJlYWQSBXdyaXRlGhYKB29uY2hhaW4SBHJlYWQSBXdyaXRlGhQKBXBlZXJzEgRyZWFkEgV3cml0ZQAABiAiUTBv3Eh6iDbdjmXCfNxp4HBEcOYNzXhrm+ncLHf5jA=='
}),
'Failed to connect before the deadline'
)
})
test('add with invalid lnd uri throws', async t => {
await t.throwsAsync(
t.context.add({
settlerType: SettlementEngineType.Lnd,
hostname: 'nonsense',
grpcPort: 2000,
tlsCert: 'nonsense',
macaroon: 'nonsense'
})
)
})
48 changes: 48 additions & 0 deletions src/__tests__/disconnect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import anyTest, { TestInterface, ExecutionContext } from 'ava'
import 'envkey'
import { SwitchApi, connect, LedgerEnv, ReadyUplinks } from '..'
import { addXrp, addEth, addBtc, testExchange } from './_helpers'
import BigNumber from 'bignumber.js'

const test = anyTest as TestInterface<SwitchApi>

// Before & after each test, construct and disconnect the API

test.beforeEach(async t => {
t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv)
})

test('after connect', async t => {
await t.notThrowsAsync(t.context.disconnect())
})

test('after add eth', async t => {
const uplink = await addEth(1)(t.context)
await t.notThrowsAsync(t.context.disconnect())
})

test('after deposit eth', async t => {
const uplink = await addEth(1)(t.context)
const openAmount = new BigNumber(0.01)
await t.context.deposit({
uplink,
amount: openAmount,
authorize: () => Promise.resolve()
})
await t.notThrowsAsync(t.context.disconnect())
})

test('after withdraw eth', async t => {
const uplink = await addEth(1)(t.context)
const openAmount = new BigNumber(0.01)
await t.context.deposit({
uplink,
amount: openAmount,
authorize: () => Promise.resolve()
})
await t.context.withdraw({ uplink, authorize: () => Promise.resolve() })
await t.notThrowsAsync(t.context.disconnect())
})

// TODO test the other assets
// TODO maybe refactor the helpers to include a generic deposit method
18 changes: 18 additions & 0 deletions src/__tests__/stream-same-asset.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import anyTest, { TestInterface } from 'ava'
import 'envkey'
import { SwitchApi, connect, LedgerEnv, ReadyUplinks } from '..'
import { addXrp, addEth, addBtc, testExchange } from './_helpers'

const test = anyTest as TestInterface<SwitchApi>

// Before & after each test, construct and disconnect the API

test.beforeEach(async t => {
t.context = await connect(process.env.LEDGER_ENV! as LedgerEnv)
})

test.afterEach(async t => t.context.disconnect())

test('xrp -> xrp different credentials', testExchange(addXrp(1), addXrp(2)))
test('eth -> eth different credentials', testExchange(addEth(1), addEth(2)))
test('btc -> btc different credentials', testExchange(addBtc(2), addBtc(1)))
Loading

0 comments on commit 01707d4

Please sign in to comment.