diff --git a/modules/engine/src/index.ts b/modules/engine/src/index.ts index c68b37d64..9700a1cea 100644 --- a/modules/engine/src/index.ts +++ b/modules/engine/src/index.ts @@ -28,7 +28,6 @@ import { MinimalTransaction, WITHDRAWAL_RESOLVED_EVENT, VectorErrorJson, - getConfirmationsForChain, } from "@connext/vector-types"; import { generateMerkleTreeData, @@ -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"; @@ -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(); diff --git a/modules/server-node/examples/admin.http b/modules/server-node/examples/admin.http index 0b39c3d94..311684528 100644 --- a/modules/server-node/examples/admin.http +++ b/modules/server-node/examples/admin.http @@ -1,4 +1,6 @@ @rogerUrl = http://localhost:8007 +@carolPublicIdentifier = vector8ZaxNSdUM83kLXJSsmj5jrcq17CpZUwBirmboaNPtQMEXjVNrL +@rogerPublicIdentifier = vector8Uz1BdpA9hV5uTm6QUv5jj1PsUyCH8m8ciA94voCzsxVmrBRor ############## ### Retry Withdrawal @@ -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}}" } \ No newline at end of file diff --git a/modules/server-node/src/helpers/errors.ts b/modules/server-node/src/helpers/errors.ts index 8f93054af..0730ca008 100644 --- a/modules/server-node/src/helpers/errors.ts +++ b/modules/server-node/src/helpers/errors.ts @@ -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", @@ -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; diff --git a/modules/server-node/src/index.ts b/modules/server-node/src/index.ts index 7d8c343d7..3b9320ab8 100644 --- a/modules/server-node/src/index.ts +++ b/modules/server-node/src/index.ts @@ -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, @@ -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 } }, diff --git a/modules/types/src/schemas/node.ts b/modules/types/src/schemas/node.ts index d45db172d..a5e9cab86 100644 --- a/modules/types/src/schemas/node.ts +++ b/modules/types/src/schemas/node.ts @@ -21,6 +21,7 @@ import { TransferDisputeSchema, ChannelDisputeSchema, TVectorErrorJson, + TDecimalString, } from "./basic"; //////////////////////////////////////// @@ -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(), @@ -708,6 +730,9 @@ export namespace NodeParams { export const SubmitWithdrawalsSchema = PostAdminSubmitWithdrawalsBodySchema; export type SubmitWithdrawals = Static; + + export const GenerateWithdrawCommitmentSchema = PostAdminGenerateWithdrawCommitmentBodySchema; + export type GenerateWithdrawCommitment = Static; } // eslint-disable-next-line @typescript-eslint/no-namespace @@ -841,4 +866,7 @@ export namespace NodeResponses { export const SubmitWithdrawalsSchema = PostAdminSubmitWithdrawalsResponseSchema; export type SubmitWithdrawals = Static; + + export const GenerateWithdrawCommitmentSchema = PostAdminGenerateWithdrawCommitmentResponseSchema; + export type GenerateWithdrawCommitment = Static; }