Skip to content
This repository has been archived by the owner on May 28, 2021. It is now read-only.

✨ Add new collateralization config and admin endpoints #1406

Merged
merged 10 commits into from
Aug 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/src/reference/node.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Node
rhlsthrm marked this conversation as resolved.
Show resolved Hide resolved

## 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` |
| |
9 changes: 6 additions & 3 deletions modules/client/src/connext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -192,8 +192,11 @@ export class ConnextClient implements IConnextClient {
return this.node.getChannel();
};

public requestCollateral = async (assetId: string): Promise<PublicResults.RequestCollateral> => {
const requestCollateralResponse = await this.node.requestCollateral(assetId);
public requestCollateral = async (
assetId: string,
amount?: BigNumberish,
): Promise<PublicResults.RequestCollateral> => {
const requestCollateralResponse = await this.node.requestCollateral(assetId, amount);
if (!requestCollateralResponse) {
return undefined;
}
Expand Down
8 changes: 6 additions & 2 deletions modules/client/src/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -286,11 +286,15 @@ export class NodeApiClient implements INodeApiClient {
);
}

public async requestCollateral(assetId: string): Promise<NodeResponses.RequestCollateral> {
public async requestCollateral(
assetId: string,
amount?: BigNumberish,
): Promise<NodeResponses.RequestCollateral> {
return this.send(
`${this.userIdentifier}.${this.nodeIdentifier}.${this.chainId}.channel.request-collateral`,
{
assetId,
amount: amount?.toString(),
},
);
}
Expand Down
31 changes: 30 additions & 1 deletion modules/node/example.http
Original file line number Diff line number Diff line change
@@ -1,8 +1,37 @@
### 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

{
"multisigAddress": "0x93a8eAFC6436F3e238d962Cb429893ec22875705",
"assetId": "0x4E72770760c011647D4873f60A3CF6cDeA896CD8"
}
}

###
# 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
92 changes: 89 additions & 3 deletions modules/node/src/admin/admin.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<RebalanceProfile> {
// 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<RebalanceProfile> {
// 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);
}
}
}
20 changes: 10 additions & 10 deletions modules/node/src/channel/channel.provider.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -63,18 +63,20 @@ class ChannelMessaging extends AbstractMessagingProvider {
async requestCollateral(
userPublicIdentifier: string,
chainId: number,
data: { assetId?: string },
data: { assetId?: string; amount?: string },
): Promise<NodeResponses.RequestCollateral> {
// do not allow clients to specify an amount to collateralize with
const channel = await this.channelRepository.findByUserPublicIdentifierAndChainOrThrow(
userPublicIdentifier,
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 && {
Expand Down Expand Up @@ -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),
Expand Down
36 changes: 26 additions & 10 deletions modules/node/src/channel/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
getSignerAddressFromPublicIdentifier,
stringify,
calculateExchangeWad,
maxBN,
} from "@connext/utils";
import { Injectable, HttpService } from "@nestjs/common";
import { AxiosResponse } from "axios";
Expand All @@ -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 {
Expand Down Expand Up @@ -111,6 +112,7 @@ export class ChannelService {
multisigAddress: string,
assetId: string = AddressZero,
rebalanceType: RebalanceType,
requestedTarget: BigNumber = Zero,
): Promise<
| {
completed?: () => Promise<FreeBalanceResponse>;
Expand Down Expand Up @@ -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}`);
Expand All @@ -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) {
Expand All @@ -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 {
Expand Down Expand Up @@ -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)}`,
);
Expand Down
Loading