Skip to content
This repository has been archived by the owner on Dec 27, 2022. It is now read-only.

Single signed withdrawal commitment #615

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
5 changes: 3 additions & 2 deletions modules/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
MinimalTransaction,
WITHDRAWAL_RESOLVED_EVENT,
VectorErrorJson,
getConfirmationsForChain,
} from "@connext/vector-types";
import {
generateMerkleTreeData,
Expand All @@ -42,6 +41,7 @@ import {
import pino from "pino";
import Ajv from "ajv";
import { Evt } from "evt";
import { WithdrawCommitment } from "@connext/vector-contracts";

import { version } from "../package.json";

Expand All @@ -55,7 +55,8 @@ import {
import { setupEngineListeners } from "./listeners";
import { getEngineEvtContainer } from "./utils";
import { sendIsAlive } from "./isAlive";
import { WithdrawCommitment } from "@connext/vector-contracts";

export * from "./paramConverter";

export const ajv = new Ajv();

Expand Down
15 changes: 15 additions & 0 deletions modules/server-node/examples/admin.http
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
@rogerUrl = http://localhost:8007
@carolPublicIdentifier = vector8ZaxNSdUM83kLXJSsmj5jrcq17CpZUwBirmboaNPtQMEXjVNrL
@rogerPublicIdentifier = vector8Uz1BdpA9hV5uTm6QUv5jj1PsUyCH8m8ciA94voCzsxVmrBRor

##############
### Retry Withdrawal
Expand All @@ -20,4 +22,17 @@ Content-Type: application/json
"adminToken": "cxt1234",
"transactionHash": "0x9ed0c28027a045c2de9fae61e06eade573e9ddfcbab3a6514c5662781c874104",
"publicIdentifier": "vector7tbbTxQp8ppEQUgPsbGiTrVdapLdU5dH7zTbVuXRf1M4CEBU9Q"
}

##############
### Unsafe Withdraw Generate
POST {{rogerUrl}}/withdraw/generate
Content-Type: application/json

{
"adminToken": "cxt1234",
"publicIdentifier": "{{rogerPublicIdentifier}}",
"assetId": "0x0000000000000000000000000000000000000000",
"chainId": "1337",
"counterpartyIdentifier": "{{carolPublicIdentifier}}"
}
2 changes: 2 additions & 0 deletions modules/server-node/src/helpers/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class ServerNodeError extends NodeError {
static readonly type = "ServerNodeError";

static readonly reasons = {
BadRequest: "Problem with request",
ChainServiceNotFound: "Chain service not found",
ChannelNotFound: "Channel not found",
ClearStoreFailed: "Failed to clear store",
Expand All @@ -23,6 +24,7 @@ export class ServerNodeError extends NodeError {
TransactionNotFound: "Transaction not found",
TransferNotFound: "Transfer not found",
Unauthorized: "Unauthorized",
UnexpectedError: "Unexpected server error",
} as const;

readonly context: ServerNodeErrorContext;
Expand Down
132 changes: 130 additions & 2 deletions modules/server-node/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,24 @@ import {
VectorErrorJson,
StoredTransaction,
} from "@connext/vector-types";
import { constructRpcRequest, getPublicIdentifierFromPublicKey, hydrateProviders } from "@connext/vector-utils";
import {
ChannelSigner,
constructRpcRequest,
getBalanceForAssetId,
getParticipant,
getPublicIdentifierFromPublicKey,
getRandomBytes32,
getSignerAddressFromPublicIdentifier,
hydrateProviders,
} from "@connext/vector-utils";
import { WithdrawCommitment } from "@connext/vector-contracts";
import { Static, Type } from "@sinclair/typebox";
import { Wallet } from "@ethersproject/wallet";
import { BigNumber } from "@ethersproject/bignumber";

import { PrismaStore } from "./services/store";
import { config } from "./config";
import { createNode, deleteNodes, getChainService, getNode, getNodes } from "./helpers/nodes";
import { createNode, deleteNodes, getChainService, getNode, getNodes, getPath, nodes } from "./helpers/nodes";
import { ServerNodeError } from "./helpers/errors";
import {
ResubmitWithdrawalResult,
Expand Down Expand Up @@ -1086,6 +1096,124 @@ server.post<{ Body: NodeParams.RetryWithdrawTransaction }>(
},
);

server.post<{ Body: NodeParams.GenerateWithdrawCommitment }>(
"/withdraw/generate",
{
schema: {
body: NodeParams.GenerateWithdrawCommitmentSchema,
response: NodeResponses.GenerateWithdrawCommitmentSchema,
},
},
async (request, reply) => {
if (request.body.adminToken !== config.adminToken) {
return reply
.status(401)
.send(new ServerNodeError(ServerNodeError.reasons.Unauthorized, "", request.body).toJson());
}
try {
const engine = getNode(request.body.publicIdentifier);
if (!engine) {
return reply
.status(400)
.send(
jsonifyError(
new ServerNodeError(ServerNodeError.reasons.NodeNotFound, request.body.publicIdentifier, request.body),
),
);
}

const index = nodes[request.body.publicIdentifier].index;
const pk = Wallet.fromMnemonic(config.mnemonic, getPath(index)).privateKey;
const signer = new ChannelSigner(pk);

const channel = await store.getChannelStateByParticipants(
request.body.publicIdentifier,
request.body.counterpartyIdentifier,
request.body.chainId,
);
if (!channel) {
return reply
.status(404)
.send(
new ServerNodeError(
ServerNodeError.reasons.ChannelNotFound,
request.body.publicIdentifier,
request.body,
).toJson(),
);
}

if (request.body.nonce && request.body.nonce < channel.nonce) {
return reply.status(400).send(
new ServerNodeError(ServerNodeError.reasons.BadRequest, request.body.publicIdentifier, {
...request.body,
message: "Channel nonce is >= provided nonce",
}).toJson(),
);
}

const nonce = request.body.nonce ? request.body.nonce : channel.nonce;

const participant = getParticipant(channel, request.body.publicIdentifier);
if (!participant) {
return reply.status(400).send(
new ServerNodeError(ServerNodeError.reasons.BadRequest, request.body.publicIdentifier, {
...request.body,
message: "Participant not in channel",
}).toJson(),
);
}

const withdrawAmount = request.body.amount ?? getBalanceForAssetId(channel, request.body.assetId, participant);
if (BigNumber.from(withdrawAmount).isZero()) {
return reply.status(400).send(
new ServerNodeError(ServerNodeError.reasons.BadRequest, request.body.publicIdentifier, {
...request.body,
message: "Zero balance",
}).toJson(),
);
}

const commitment = new WithdrawCommitment(
channel.channelAddress,
channel.alice,
channel.bob,
request.body.recipient
? request.body.recipient
: getSignerAddressFromPublicIdentifier(request.body.publicIdentifier),
request.body.assetId,
withdrawAmount,
nonce.toString(),
request.body.callTo,
request.body.callData,
);

let initiatorSignature: string;
try {
initiatorSignature = await signer.signMessage(commitment.hashToSign());
} catch (err) {
return reply.status(400).send(
new ServerNodeError(ServerNodeError.reasons.BadRequest, request.body.publicIdentifier, {
...request.body,
message: "Signature error",
}).toJson(),
);
}
commitment.addSignatures(initiatorSignature);

// generate random transferId since this is not part of a real transfer
const transferId = getRandomBytes32();
await store.saveWithdrawalCommitment(transferId, commitment.toJson());
return reply.status(200).send({
commitment: commitment.toJson(),
transferId,
});
} catch (e) {
return reply.status(500).send(jsonifyError(e));
}
},
);

server.post<{ Body: NodeParams.CreateNode }>(
"/node",
{ schema: { body: NodeParams.CreateNodeSchema, response: NodeResponses.CreateNodeSchema } },
Expand Down
28 changes: 28 additions & 0 deletions modules/types/src/schemas/node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
TransferDisputeSchema,
ChannelDisputeSchema,
TVectorErrorJson,
TDecimalString,
} from "./basic";

////////////////////////////////////////
Expand Down Expand Up @@ -454,6 +455,27 @@ const PostAdminRetryWithdrawTransactionResponseSchema = {
}),
};

// GENERATE SINGLE SIGNED WITHDRAW COMMITMENT
const PostAdminGenerateWithdrawCommitmentBodySchema = Type.Object({
adminToken: Type.String(),
publicIdentifier: TPublicIdentifier,
counterpartyIdentifier: TPublicIdentifier,
chainId: TChainId,
assetId: TAddress,
amount: Type.Optional(TDecimalString),
recipient: Type.Optional(TAddress),
nonce: Type.Optional(Type.Number()),
callTo: Type.Optional(TAddress),
callData: Type.Optional(Type.String()),
});

const PostAdminGenerateWithdrawCommitmentResponseSchema = {
200: Type.Object({
commitment: Type.Any(),
transferId: TBytes32,
}),
};

// SUBMIT UNSUBMITTED WITHDRAWALS
const PostAdminSubmitWithdrawalsBodySchema = Type.Object({
adminToken: Type.String(),
Expand Down Expand Up @@ -708,6 +730,9 @@ export namespace NodeParams {

export const SubmitWithdrawalsSchema = PostAdminSubmitWithdrawalsBodySchema;
export type SubmitWithdrawals = Static<typeof SubmitWithdrawalsSchema>;

export const GenerateWithdrawCommitmentSchema = PostAdminGenerateWithdrawCommitmentBodySchema;
export type GenerateWithdrawCommitment = Static<typeof GenerateWithdrawCommitmentSchema>;
}

// eslint-disable-next-line @typescript-eslint/no-namespace
Expand Down Expand Up @@ -841,4 +866,7 @@ export namespace NodeResponses {

export const SubmitWithdrawalsSchema = PostAdminSubmitWithdrawalsResponseSchema;
export type SubmitWithdrawals = Static<typeof PostAdminSubmitWithdrawalsResponseSchema["200"]>;

export const GenerateWithdrawCommitmentSchema = PostAdminGenerateWithdrawCommitmentResponseSchema;
export type GenerateWithdrawCommitment = Static<typeof GenerateWithdrawCommitmentSchema["200"]>;
}