-
Notifications
You must be signed in to change notification settings - Fork 13
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from Kava-Labs/ro-add-further-tests
Add further tests
- Loading branch information
Showing
7 changed files
with
379 additions
and
199 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
}) | ||
) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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))) |
Oops, something went wrong.