diff --git a/docs/src/reference/node.md b/docs/src/reference/node.md new file mode 100644 index 0000000000..fbbbce0d8a --- /dev/null +++ b/docs/src/reference/node.md @@ -0,0 +1,29 @@ +# Node + +## Configuration + +### Environment Variables + +The following environment variables are used by the node: + +| Variable Name | Type | Description | Example | +| --------------------------------------- | ----------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `INDRA_ADMIN_TOKEN` | String | Token for administrative functions. | cxt1234 | +| `INDRA_CHAIN_PROVIDERS` | JSON String | Mapping of chainId to ethProviderUrl | '{"1":"https://mainnet.infura.io/v3/TOKEN","4":"https://rinkeby.infura.io/v3/TOKEN"}' | +| `INDRA_CONTRACT_ADDRESSES` | JSON String | Contract information, keyed by chainId | '{ "1337": { "ChallengeRegistry": { "address": "0x8CdaF0CD259887258Bc13a92C0a6dA92698644C0", "creationCodeHash": "0x42eba77f58ecb5c1352e9a62df1eed73aa1a89890ff73be1939f884f62d88c46", "runtimeCodeHash": "0xc38bff65185807f2babc2ae1334b0bdcf5fe0192ae041e3033b2084c61f80950", "txHash": "0x89f705aefdffa59061d97488e4507a7af4a4751462e100b8ed3fb1f5cc2238af" }, ...}' | +| `INDRA_DEFAULT_REBALANCE_PROFILE_ETH` | JSON String | Rebalance Profile to use by default | '{"collateralizeThreshold":"500000000000","target":"1500000000000","reclaimThreshold":"10000000000000"}' | +| `INDRA_DEFAULT_REBALANCE_PROFILE_TOKEN` | JSON String | Rebalance Profile to use by default (real units) | '{"collateralizeThreshold":"500000000000","target":"1500000000000","reclaimThreshold":"10000000000000"} | +| `INDRA_LOG_LEVEL` | Number | Log level - 1 = Error, 4 = Debug | 3 | +| `INDRA_MNEMONIC_FILE` | +| `INDRA_NATS_JWT_SIGNER_PRIVATE_KEY` | +| `INDRA_NATS_JWT_SIGNER_PUBLIC_KEY` | +| `INDRA_NATS_SERVERS` | +| `INDRA_NATS_WS_ENDPOINT` | +| `INDRA_PG_DATABASE` | +| `INDRA_PG_HOST` | +| `INDRA_PG_PASSWORD_FILE` | +| `INDRA_PG_PORT` | +| `INDRA_PG_USERNAME` | +| `INDRA_PORT` | +| `INDRA_REDIS_URL` | +| | diff --git a/modules/client/src/connext.ts b/modules/client/src/connext.ts index 79e923043b..768613d2b9 100644 --- a/modules/client/src/connext.ts +++ b/modules/client/src/connext.ts @@ -46,7 +46,7 @@ import { stringify, computeCancelDisputeHash, } from "@connext/utils"; -import { BigNumber, Contract, providers, constants, utils } from "ethers"; +import { BigNumber, Contract, providers, constants, utils, BigNumberish } from "ethers"; import { DepositController, @@ -192,8 +192,11 @@ export class ConnextClient implements IConnextClient { return this.node.getChannel(); }; - public requestCollateral = async (assetId: string): Promise => { - const requestCollateralResponse = await this.node.requestCollateral(assetId); + public requestCollateral = async ( + assetId: string, + amount?: BigNumberish, + ): Promise => { + const requestCollateralResponse = await this.node.requestCollateral(assetId, amount); if (!requestCollateralResponse) { return undefined; } diff --git a/modules/client/src/node.ts b/modules/client/src/node.ts index 98def92c8a..0e2fc0e254 100644 --- a/modules/client/src/node.ts +++ b/modules/client/src/node.ts @@ -17,7 +17,7 @@ import { } from "@connext/types"; import { bigNumberifyJson, isNode, logTime, stringify } from "@connext/utils"; import axios, { AxiosResponse } from "axios"; -import { utils, providers } from "ethers"; +import { utils, providers, BigNumberish } from "ethers"; import { v4 as uuid } from "uuid"; import { createCFChannelProvider } from "./channelProvider"; @@ -286,11 +286,15 @@ export class NodeApiClient implements INodeApiClient { ); } - public async requestCollateral(assetId: string): Promise { + public async requestCollateral( + assetId: string, + amount?: BigNumberish, + ): Promise { return this.send( `${this.userIdentifier}.${this.nodeIdentifier}.${this.chainId}.channel.request-collateral`, { assetId, + amount: amount?.toString(), }, ); } diff --git a/modules/node/example.http b/modules/node/example.http index 7764ef8e58..99303d4f09 100644 --- a/modules/node/example.http +++ b/modules/node/example.http @@ -1,3 +1,8 @@ +### ADMIN REQUESTS + +### +# Force uninstall deposit app (only works if node has deposit rights) + POST http://localhost:3000/api/admin/uninstall-deposit Content-Type: application/json x-auth-token: cxt1234 @@ -5,4 +10,28 @@ x-auth-token: cxt1234 { "multisigAddress": "0x93a8eAFC6436F3e238d962Cb429893ec22875705", "assetId": "0x4E72770760c011647D4873f60A3CF6cDeA896CD8" -} \ No newline at end of file +} + +### +# Set rebalance profile + +POST http://localhost:3000/api/admin/rebalance-profile +Content-Type: application/json +x-auth-token: cxt1234 + +{ + "multisigAddress": "0x93a8eAFC6436F3e238d962Cb429893ec22875705", + "rebalanceProfile": { + "assetId": "0x4E72770760c011647D4873f60A3CF6cDeA896CD8", + "collateralizeThreshold": "5", + "target": "15", + "reclaimThreshold": "0" + } +} + +### +# GET rebalance profile + +GET http://localhost:3000/api/admin/rebalance-profile/0x93a8eAFC6436F3e238d962Cb429893ec22875705/0x0000000000000000000000000000000000000000 +Content-Type: application/json +x-auth-token: cxt1234 \ No newline at end of file diff --git a/modules/node/src/admin/admin.controller.ts b/modules/node/src/admin/admin.controller.ts index 3271f4ac21..44d39ca299 100644 --- a/modules/node/src/admin/admin.controller.ts +++ b/modules/node/src/admin/admin.controller.ts @@ -5,23 +5,37 @@ import { Headers, UnauthorizedException, NotFoundException, - BadRequestException, + Get, + Param, + InternalServerErrorException, } from "@nestjs/common"; import { ConfigService } from "../config/config.service"; import { AdminService } from "./admin.service"; +import { RebalanceProfile } from "@connext/types"; +import { ChannelService } from "../channel/channel.service"; +import { ChannelRepository } from "../channel/channel.repository"; +import { BigNumber } from "ethers"; export class UninstallDepositAppDto { multisigAddress!: string; assetId?: string; } +export class AddRebalanceProfileDto { + multisigAddress!: string; + rebalanceProfile!: RebalanceProfile; +} + @Controller("admin") export class AdminController { constructor( private readonly adminService: AdminService, private readonly configService: ConfigService, + private readonly channelService: ChannelService, + private readonly channelRepository: ChannelRepository, ) {} + @Post("uninstall-deposit") async uninstallDepositApp( @Body() { multisigAddress, assetId }: UninstallDepositAppDto, @@ -35,9 +49,81 @@ export class AdminController { return res; } catch (e) { if (e.message.includes("Channel does not exist for multisig")) { - throw new NotFoundException(); + throw new NotFoundException("Channel not found"); + } + throw new InternalServerErrorException(e.message); + } + } + + @Post("rebalance-profile") + async addRebalanceProfile( + @Body() { multisigAddress, rebalanceProfile }: AddRebalanceProfileDto, + @Headers("x-auth-token") token: string, + ): Promise { + // not ideal to do this everywhere, can be refactored into a "guard" (see nest docs) + if (token !== this.configService.getAdminToken()) { + throw new UnauthorizedException(); + } + try { + const channel = await this.channelRepository.findByMultisigAddressOrThrow(multisigAddress); + const res = await this.channelService.addRebalanceProfileToChannel( + channel.userIdentifier, + channel.chainId, + { + ...rebalanceProfile, + collateralizeThreshold: BigNumber.from(rebalanceProfile.collateralizeThreshold), + target: BigNumber.from(rebalanceProfile.target), + reclaimThreshold: BigNumber.from(rebalanceProfile.reclaimThreshold), + }, + ); + return { + assetId: res.assetId, + collateralizeThreshold: res.collateralizeThreshold.toString(), + target: res.target.toString(), + reclaimThreshold: res.reclaimThreshold.toString(), + } as any; + } catch (e) { + if (e.message.includes("Channel does not exist for multisig")) { + throw new NotFoundException("Channel not found"); + } + throw new InternalServerErrorException(e.message); + } + } + + @Get("rebalance-profile/:multisigAddress/:assetId") + async getRebalanceProfile( + @Param("multisigAddress") multisigAddress: string, + @Param("assetId") assetId: string, + @Headers("x-auth-token") token: string, + ): Promise { + // not ideal to do this everywhere, can be refactored into a "guard" (see nest docs) + if (token !== this.configService.getAdminToken()) { + throw new UnauthorizedException(); + } + try { + const channel = await this.channelRepository.findByMultisigAddressOrThrow(multisigAddress); + let res = await this.channelRepository.getRebalanceProfileForChannelAndAsset( + channel.userIdentifier, + channel.chainId, + assetId, + ); + if (!res) { + res = this.configService.getDefaultRebalanceProfile(assetId); + } + if (!res) { + throw new NotFoundException("Rebalance profile not found"); + } + return { + assetId: res.assetId, + collateralizeThreshold: res.collateralizeThreshold.toString(), + target: res.target.toString(), + reclaimThreshold: res.reclaimThreshold.toString(), + } as any; + } catch (e) { + if (e.message.includes("Channel does not exist for multisig")) { + throw new NotFoundException("Channel not found"); } - throw new BadRequestException(e.message); + throw new InternalServerErrorException(e.message); } } } diff --git a/modules/node/src/channel/channel.provider.ts b/modules/node/src/channel/channel.provider.ts index 63e28231a5..c4da2c39be 100644 --- a/modules/node/src/channel/channel.provider.ts +++ b/modules/node/src/channel/channel.provider.ts @@ -1,7 +1,7 @@ import { MethodResults, NodeResponses } from "@connext/types"; import { MessagingService } from "@connext/messaging"; import { FactoryProvider } from "@nestjs/common/interfaces"; -import { utils, constants } from "ethers"; +import { utils, constants, BigNumber } from "ethers"; import { AuthService } from "../auth/auth.service"; import { LoggerService } from "../logger/logger.service"; @@ -63,7 +63,7 @@ class ChannelMessaging extends AbstractMessagingProvider { async requestCollateral( userPublicIdentifier: string, chainId: number, - data: { assetId?: string }, + data: { assetId?: string; amount?: string }, ): Promise { // do not allow clients to specify an amount to collateralize with const channel = await this.channelRepository.findByUserPublicIdentifierAndChainOrThrow( @@ -71,10 +71,12 @@ class ChannelMessaging extends AbstractMessagingProvider { chainId, ); try { + const requestedTarget = data.amount ? BigNumber.from(data.amount) : undefined; const response = await this.channelService.rebalance( channel.multisigAddress, getAddress(data.assetId || constants.AddressZero), RebalanceType.COLLATERALIZE, + requestedTarget, ); return ( response && { @@ -136,16 +138,14 @@ class ChannelMessaging extends AbstractMessagingProvider { throw new Error(`Found channel, but no setup commitment. This should not happen.`); } // get active app set state commitments - const setStateCommitments = - await this.setStateCommitmentRepository.findAllActiveCommitmentsByMultisig( - channel.multisigAddress, - ); + const setStateCommitments = await this.setStateCommitmentRepository.findAllActiveCommitmentsByMultisig( + channel.multisigAddress, + ); // get active app conditional transaction commitments - const conditionalCommitments = - await this.conditionalTransactionCommitmentRepository.findAllActiveCommitmentsByMultisig( - channel.multisigAddress, - ); + const conditionalCommitments = await this.conditionalTransactionCommitmentRepository.findAllActiveCommitmentsByMultisig( + channel.multisigAddress, + ); return { channel, setupCommitment: convertSetupEntityToMinimalTransaction(setupCommitment), diff --git a/modules/node/src/channel/channel.service.ts b/modules/node/src/channel/channel.service.ts index 842df0642f..c4d7e2dcc4 100644 --- a/modules/node/src/channel/channel.service.ts +++ b/modules/node/src/channel/channel.service.ts @@ -12,6 +12,7 @@ import { getSignerAddressFromPublicIdentifier, stringify, calculateExchangeWad, + maxBN, } from "@connext/utils"; import { Injectable, HttpService } from "@nestjs/common"; import { AxiosResponse } from "axios"; @@ -28,7 +29,7 @@ import { DEFAULT_DECIMALS } from "../constants"; import { Channel } from "./channel.entity"; import { ChannelRepository } from "./channel.repository"; -const { AddressZero } = constants; +const { AddressZero, Zero } = constants; const { getAddress, toUtf8Bytes, sha256 } = utils; export enum RebalanceType { @@ -111,6 +112,7 @@ export class ChannelService { multisigAddress: string, assetId: string = AddressZero, rebalanceType: RebalanceType, + requestedTarget: BigNumber = Zero, ): Promise< | { completed?: () => Promise; @@ -149,10 +151,10 @@ export class ChannelService { normalizedAssetId, ); - const { collateralizeThreshold, target, reclaimThreshold } = rebalancingTargets; + const { collateralizeThreshold, target: profileTarget, reclaimThreshold } = rebalancingTargets; if ( - (collateralizeThreshold.gt(target) || reclaimThreshold.lt(target)) && + (collateralizeThreshold.gt(profileTarget) || reclaimThreshold.lt(profileTarget)) && !reclaimThreshold.isZero() ) { throw new Error(`Rebalancing targets not properly configured: ${rebalancingTargets}`); @@ -174,15 +176,26 @@ export class ChannelService { if (rebalanceType === RebalanceType.COLLATERALIZE) { // If free balance is too low, collateralize up to upper bound - if (nodeFreeBalance.lt(collateralizeThreshold)) { + + // make sure requested target is under reclaim threshold + if (requestedTarget?.gt(reclaimThreshold)) { + throw new Error( + `Requested target ${requestedTarget.toString()} is greater than reclaim threshold ${reclaimThreshold.toString()}`, + ); + } + + const targetToUse = maxBN([profileTarget, requestedTarget]); + const thresholdToUse = maxBN([collateralizeThreshold, requestedTarget]); + + if (nodeFreeBalance.lt(thresholdToUse)) { this.log.info( - `nodeFreeBalance ${nodeFreeBalance.toString()} < collateralizeThreshold ${collateralizeThreshold.toString()}, depositing`, + `nodeFreeBalance ${nodeFreeBalance.toString()} < thresholdToUse ${thresholdToUse.toString()}, depositing to target ${requestedTarget.toString()}`, ); - const amount = target.sub(nodeFreeBalance); + const amount = targetToUse.sub(nodeFreeBalance); rebalanceRes = (await this.depositService.deposit(channel, amount, normalizedAssetId))!; } else { this.log.info( - `Free balance ${nodeFreeBalance} is greater than or equal to lower collateralization bound: ${collateralizeThreshold.toString()}`, + `Free balance ${nodeFreeBalance} is greater than or equal to lower collateralization bound: ${thresholdToUse.toString()}`, ); } } else if (rebalanceType === RebalanceType.RECLAIM) { @@ -191,7 +204,7 @@ export class ChannelService { this.log.info( `nodeFreeBalance ${nodeFreeBalance.toString()} > reclaimThreshold ${reclaimThreshold.toString()}, withdrawing`, ); - const amount = nodeFreeBalance.sub(target); + const amount = nodeFreeBalance.sub(profileTarget); const transaction = await this.withdrawService.withdraw(channel, amount, normalizedAssetId); rebalanceRes.transaction = transaction; } else { @@ -305,13 +318,16 @@ export class ChannelService { )}`, ); const { assetId, collateralizeThreshold, target, reclaimThreshold } = profile; - if (reclaimThreshold.lt(target) || collateralizeThreshold.gt(target)) { + if ( + (!reclaimThreshold.isZero() && reclaimThreshold.lt(target)) || + collateralizeThreshold.gt(target) + ) { throw new Error(`Rebalancing targets not properly configured: ${stringify(profile)}`); } // reclaim targets cannot be less than collateralize targets, otherwise we get into a loop of // collateralize/reclaim - if (reclaimThreshold.lt(collateralizeThreshold)) { + if (!reclaimThreshold.isZero() && reclaimThreshold.lt(collateralizeThreshold)) { throw new Error( `Reclaim targets cannot be less than collateralize targets: ${stringify(profile)}`, ); diff --git a/modules/node/src/config/config.service.ts b/modules/node/src/config/config.service.ts index b1493a6819..5a63aa992d 100644 --- a/modules/node/src/config/config.service.ts +++ b/modules/node/src/config/config.service.ts @@ -29,6 +29,10 @@ type PostgresConfig = { username: string; }; +type MaxCollateralMap = { + [assetId: string]: T; +}; + @Injectable() export class ConfigService implements OnModuleInit { private readonly envConfig: { [key: string]: string }; @@ -293,32 +297,55 @@ export class ConfigService implements OnModuleInit { return parseInt(this.get(`INDRA_APP_CLEANUP_INTERVAL`) || "3600000"); } - async getDefaultRebalanceProfile( - assetId: string = AddressZero, - ): Promise { + getDefaultRebalanceProfile(assetId: string = AddressZero): RebalanceProfile | undefined { if (assetId === AddressZero) { + let defaultProfileEth = { + collateralizeThreshold: parseEther(`0.05`), + target: parseEther(`0.1`), + reclaimThreshold: parseEther(`0.5`), + }; + try { + const parsed = JSON.parse(this.get("INDRA_DEFAULT_REBALANCE_PROFILE_ETH")); + if (parsed) { + defaultProfileEth = { + collateralizeThreshold: BigNumber.from(parsed.collateralizeThreshold), + target: BigNumber.from(parsed.target), + reclaimThreshold: BigNumber.from(parsed.reclaimThreshold), + }; + } + } catch (e) {} return { assetId: AddressZero, channels: [], id: 0, - collateralizeThreshold: parseEther(`0.05`), - target: parseEther(`0.1`), - reclaimThreshold: Zero, + ...defaultProfileEth, }; } + let defaultProfileToken = { + collateralizeThreshold: parseEther(`5`), + target: parseEther(`20`), + reclaimThreshold: parseEther(`100`), + }; + try { + defaultProfileToken = JSON.parse(this.get("INDRA_DEFAULT_REBALANCE_PROFILE_TOKEN")); + const parsed = JSON.parse(this.get("INDRA_DEFAULT_REBALANCE_PROFILE_TOKEN")); + if (parsed) { + defaultProfileToken = { + collateralizeThreshold: BigNumber.from(parsed.collateralizeThreshold), + target: BigNumber.from(parsed.target), + reclaimThreshold: BigNumber.from(parsed.reclaimThreshold), + }; + } + } catch (e) {} return { assetId, channels: [], id: 0, - collateralizeThreshold: parseEther(`5`), - target: parseEther(`20`), - reclaimThreshold: Zero, + ...defaultProfileToken, }; } - async getZeroRebalanceProfile( - assetId: string = AddressZero, - ): Promise { + getZeroRebalanceProfile(assetId: string = AddressZero): RebalanceProfile | undefined { if (assetId === AddressZero) { return { assetId: AddressZero, diff --git a/modules/test-runner/src/collateral/profiles.test.ts b/modules/test-runner/src/collateral/profiles.test.ts index fc67c0741c..47023bdfba 100644 --- a/modules/test-runner/src/collateral/profiles.test.ts +++ b/modules/test-runner/src/collateral/profiles.test.ts @@ -2,15 +2,8 @@ import { IConnextClient, RebalanceProfile } from "@connext/types"; import { toBN } from "@connext/utils"; import { constants } from "ethers"; import { before } from "mocha"; -import { Client } from "ts-nats"; -import { - addRebalanceProfile, - createClient, - expect, - getNatsClient, - getTestLoggers, -} from "../util"; +import { addRebalanceProfile, createClient, expect, getTestLoggers } from "../util"; const { AddressZero } = constants; @@ -18,12 +11,9 @@ const name = "Collateralization Profiles"; const { timeElapsed } = getTestLoggers(name); describe(name, () => { let client: IConnextClient; - let nats: Client; let start: number; - before(async () => { - nats = getNatsClient(); - }); + before(async () => {}); beforeEach(async () => { start = Date.now(); @@ -32,7 +22,7 @@ describe(name, () => { }); afterEach(async () => { - await client.off(); + client.off(); }); it("throws error if collateral targets are higher than reclaim", async () => { @@ -42,7 +32,7 @@ describe(name, () => { target: toBN("10"), reclaimThreshold: toBN("15"), }; - const profileResponse = await addRebalanceProfile(nats, client, REBALANCE_PROFILE, false); + const profileResponse = await addRebalanceProfile(client, REBALANCE_PROFILE, false); expect(profileResponse).to.match(/Rebalancing targets not properly configured/); }); @@ -53,7 +43,7 @@ describe(name, () => { target: toBN("1"), reclaimThreshold: toBN("9"), }; - const profileResponse = await addRebalanceProfile(nats, client, REBALANCE_PROFILE, false); + const profileResponse = await addRebalanceProfile(client, REBALANCE_PROFILE, false); expect(profileResponse).to.match(/Rebalancing targets not properly configured/); }); @@ -64,7 +54,7 @@ describe(name, () => { target: toBN("10"), reclaimThreshold: toBN("9"), }; - const profileResponse = await addRebalanceProfile(nats, client, REBALANCE_PROFILE, false); + const profileResponse = await addRebalanceProfile(client, REBALANCE_PROFILE, false); expect(profileResponse).to.match(/Rebalancing targets not properly configured/); }); }); diff --git a/modules/test-runner/src/collateral/reclaim.test.ts b/modules/test-runner/src/collateral/reclaim.test.ts index b97ff99d94..c595ce8055 100644 --- a/modules/test-runner/src/collateral/reclaim.test.ts +++ b/modules/test-runner/src/collateral/reclaim.test.ts @@ -3,7 +3,6 @@ import { IConnextClient, Contract, RebalanceProfile } from "@connext/types"; import { getRandomBytes32, toBN } from "@connext/utils"; import { BigNumber, constants } from "ethers"; import { before, describe } from "mocha"; -import { Client } from "ts-nats"; import { addRebalanceProfile, @@ -11,7 +10,6 @@ import { createClient, expect, fundChannel, - getNatsClient, getTestLoggers, } from "../util"; @@ -24,12 +22,9 @@ describe(name, () => { let clientB: IConnextClient; let tokenAddress: string; let nodeSignerAddress: string; - let nats: Client; let start: number; - before(async () => { - nats = getNatsClient(); - }); + before(async () => {}); beforeEach(async () => { start = Date.now(); @@ -45,8 +40,8 @@ describe(name, () => { }); afterEach(async () => { - await clientA.off(); - await clientB.off(); + clientA.off(); + clientB.off(); }); it("should reclaim ETH with async transfer", async () => { @@ -58,7 +53,7 @@ describe(name, () => { }; // set rebalancing profile to reclaim collateral - await addRebalanceProfile(nats, clientA, REBALANCE_PROFILE); + await addRebalanceProfile(clientA, REBALANCE_PROFILE); // deposit client await fundChannel( @@ -109,7 +104,7 @@ describe(name, () => { }; // set rebalancing profile to reclaim collateral - await addRebalanceProfile(nats, clientA, REBALANCE_PROFILE); + await addRebalanceProfile(clientA, REBALANCE_PROFILE); // deposit client await fundChannel( diff --git a/modules/test-runner/src/collateral/request.test.ts b/modules/test-runner/src/collateral/request.test.ts index 290a12d7a8..7ac14ca135 100644 --- a/modules/test-runner/src/collateral/request.test.ts +++ b/modules/test-runner/src/collateral/request.test.ts @@ -1,5 +1,5 @@ import { IConnextClient, EventNames } from "@connext/types"; -import { constants } from "ethers"; +import { constants, utils } from "ethers"; import { createClient, ETH_AMOUNT_MD, expect, getTestLoggers, TOKEN_AMOUNT } from "../util"; @@ -22,7 +22,7 @@ describe(name, () => { }); afterEach(async () => { - await client.off(); + client.off(); }); it("should collateralize ETH", async () => { @@ -49,6 +49,19 @@ describe(name, () => { expect(freeBalance[nodeSignerAddress]).to.be.least(TOKEN_AMOUNT); }); + it("should collateralize tokens with a target", async () => { + const requestedTarget = utils.parseEther("50"); // 20 < requested < 100 + const response = (await client.requestCollateral(tokenAddress, requestedTarget))!; + expect(response).to.be.ok; + expect(response.completed).to.be.ok; + expect(response.transaction).to.be.ok; + expect(response.transaction.hash).to.be.ok; + expect(response.depositAppIdentityHash).to.be.ok; + const { freeBalance } = await response.completed(); + expect(freeBalance[client.signerAddress]).to.be.eq(Zero); + expect(freeBalance[nodeSignerAddress]).to.be.least(requestedTarget); + }); + it("should properly handle concurrent collateral requests", async () => { const appDef = client.config.contractAddresses[client.chainId].DepositApp; let depositAppCount = 0; diff --git a/modules/test-runner/src/store/restoreState.test.ts b/modules/test-runner/src/store/restoreState.test.ts index 0111d85bad..a698ba12fb 100644 --- a/modules/test-runner/src/store/restoreState.test.ts +++ b/modules/test-runner/src/store/restoreState.test.ts @@ -17,7 +17,6 @@ import { ethProviderUrl, expect, fundChannel, - getNatsClient, getTestLoggers, TOKEN_AMOUNT, TOKEN_AMOUNT_SM, @@ -37,7 +36,6 @@ describe(name, () => { beforeEach(async () => { start = Date.now(); - const nats = getNatsClient(); signerA = getRandomChannelSigner(ethProviderUrl); store = getLocalStore(); clientA = await createClient({ signer: signerA, store, id: "A" }); @@ -50,12 +48,12 @@ describe(name, () => { reclaimThreshold: toBN("0"), }; // set rebalancing profile to reclaim collateral - await addRebalanceProfile(nats, clientA, REBALANCE_PROFILE); + await addRebalanceProfile(clientA, REBALANCE_PROFILE); timeElapsed("beforeEach complete", start); }); afterEach(async () => { - await clientA.off(); + clientA.off(); }); it("client can delete its store and restore from a remote backup", async () => { diff --git a/modules/test-runner/src/util/helpers/rebalanceProfile.ts b/modules/test-runner/src/util/helpers/rebalanceProfile.ts index 532254ab2c..28f9103af5 100644 --- a/modules/test-runner/src/util/helpers/rebalanceProfile.ts +++ b/modules/test-runner/src/util/helpers/rebalanceProfile.ts @@ -1,29 +1,34 @@ import { RebalanceProfile, IConnextClient } from "@connext/types"; -import { Client } from "ts-nats"; import { expect } from ".."; import { env } from "../env"; +import Axios from "axios"; export const addRebalanceProfile = async ( - nats: Client, client: IConnextClient, profile: RebalanceProfile, assertProfile: boolean = true, ) => { - const msg = await nats.request( - `admin.${client.publicIdentifier}.${client.chainId}.channel.add-profile`, - 5000, - JSON.stringify({ - id: Date.now(), - profile, - token: env.adminToken, - }), - ); + try { + const msg = await Axios.post( + `${env.nodeUrl}/admin/rebalance-profile`, + { + multisigAddress: client.multisigAddress, + rebalanceProfile: profile, + }, + { + headers: { + "x-auth-token": env.adminToken, + }, + }, + ); + if (assertProfile) { + const returnedProfile = await client.getRebalanceProfile(profile.assetId); + expect(returnedProfile).to.deep.include(profile); + } - if (assertProfile) { - const returnedProfile = await client.getRebalanceProfile(profile.assetId); - expect(returnedProfile).to.deep.include(profile); + return msg.data; + } catch (e) { + return e.response.data.message; } - - return msg.data; }; diff --git a/modules/types/src/api.ts b/modules/types/src/api.ts index 1d86bb816e..4c06529ed6 100644 --- a/modules/types/src/api.ts +++ b/modules/types/src/api.ts @@ -1,5 +1,5 @@ import { AppRegistry } from "./app"; -import { providers } from "ethers"; +import { providers, BigNumberish } from "ethers"; import { Address, @@ -78,7 +78,10 @@ export interface INodeApiClient { paymentId: string, conditionType: ConditionalTransferTypes, ): Promise; - requestCollateral(assetId: Address): Promise; + requestCollateral( + assetId: Address, + amount?: BigNumberish, + ): Promise; fetchLinkedTransfer(paymentId: Bytes32): Promise; fetchSignedTransfer(paymentId: Bytes32): Promise; fetchGraphTransfer(paymentId: Bytes32): Promise; diff --git a/modules/types/src/client.ts b/modules/types/src/client.ts index 5682ccc036..e1b456494e 100644 --- a/modules/types/src/client.ts +++ b/modules/types/src/client.ts @@ -1,4 +1,4 @@ -import { providers } from "ethers"; +import { providers, BigNumberish } from "ethers"; import { AppRegistry, DefaultApp, AppInstanceJson } from "./app"; import { Address, Bytes32, DecString, PublicIdentifier } from "./basic"; @@ -117,7 +117,7 @@ export interface IConnextClient { subscribeToSwapRates(from: Address, to: Address, callback: any): Promise; getLatestSwapRate(from: Address, to: Address): Promise; unsubscribeToSwapRates(from: Address, to: Address): Promise; - requestCollateral(tokenAddress: Address): Promise; + requestCollateral(tokenAddress: Address, amount?: BigNumberish): Promise; getRebalanceProfile(assetId?: Address): Promise; getTransferHistory(): Promise; reclaimPendingAsyncTransfers(): Promise; diff --git a/ops/start-indra.sh b/ops/start-indra.sh index 90bfa5938c..6cc85eb308 100644 --- a/ops/start-indra.sh +++ b/ops/start-indra.sh @@ -356,8 +356,10 @@ services: INDRA_ADMIN_TOKEN: '$INDRA_ADMIN_TOKEN' INDRA_CHAIN_PROVIDERS: '$INDRA_CHAIN_PROVIDERS' INDRA_CONTRACT_ADDRESSES: '$INDRA_CONTRACT_ADDRESSES' - INDRA_MNEMONIC_FILE: '$INDRA_MNEMONIC_FILE' + INDRA_DEFAULT_REBALANCE_PROFILE_ETH: '$INDRA_DEFAULT_REBALANCE_PROFILE_ETH' + INDRA_DEFAULT_REBALANCE_PROFILE_TOKEN: '$INDRA_DEFAULT_REBALANCE_PROFILE_TOKEN' INDRA_LOG_LEVEL: '$INDRA_LOG_LEVEL' + INDRA_MNEMONIC_FILE: '$INDRA_MNEMONIC_FILE' INDRA_NATS_JWT_SIGNER_PRIVATE_KEY: '$INDRA_NATS_JWT_SIGNER_PRIVATE_KEY' INDRA_NATS_JWT_SIGNER_PUBLIC_KEY: '$INDRA_NATS_JWT_SIGNER_PUBLIC_KEY' INDRA_NATS_SERVERS: 'nats://nats:$nats_port'