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

Commit

Permalink
Merge pull request #1406 from connext/1405-pre-collateralization
Browse files Browse the repository at this point in the history
✨ Add new collateralization config and admin endpoints
  • Loading branch information
Rahul Sethuram authored Aug 28, 2020
2 parents ea23957 + 2dfbd54 commit b53d603
Show file tree
Hide file tree
Showing 16 changed files with 294 additions and 94 deletions.
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

## 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

0 comments on commit b53d603

Please sign in to comment.