import { deployments, VectorChainService } from "@connext/vector-contracts"; import { VectorEngine } from "@connext/vector-engine"; import { ChainAddresses, ChainProviders, ChannelRpcMethods, CreateUpdateDetails, EngineEvent, EngineEventMap, IChannelSigner, INodeService, OptionalPublicIdentifier, Result, NodeParams, NodeResponses, EngineParams, FullChannelState, DEFAULT_CHANNEL_TIMEOUT, GetTransfersFilterOpts, TransferDispute, ChannelDispute, } from "@connext/vector-types"; import { constructRpcRequest, hydrateProviders, NatsMessagingService } from "@connext/vector-utils"; import pino, { BaseLogger } from "pino"; import { BrowserStore } from "./services/store"; import { BrowserLockService } from "./services/lock"; import { DirectProvider, IframeChannelProvider, IRpcChannelProvider } from "./channelProvider"; import { BrowserNodeError } from "./errors"; export * from "./constants"; export type BrowserNodeSignerConfig = { natsUrl?: string; authUrl?: string; messagingUrl?: string; logger?: BaseLogger; signer: IChannelSigner; chainProviders: ChainProviders; chainAddresses: ChainAddresses; }; export class BrowserNode implements INodeService { public channelProvider: IRpcChannelProvider | undefined; public publicIdentifier = ""; public signerAddress = ""; private readonly logger: pino.BaseLogger; // SDK specific config private supportedChains: number[] = []; private routerPublicIdentifier?: string; private iframeSrc?: string; private chainProviders: ChainProviders = {}; private chainAddresses?: ChainAddresses; private messagingUrl?: string; private natsUrl?: string; private authUrl?: string; constructor(params: { logger?: pino.BaseLogger; routerPublicIdentifier?: string; supportedChains?: number[]; iframeSrc?: string; chainProviders: ChainProviders; messagingUrl?: string; natsUrl?: string; authUrl?: string; chainAddresses?: ChainAddresses; }) { this.logger = params.logger || pino(); this.routerPublicIdentifier = params.routerPublicIdentifier; this.supportedChains = params.supportedChains || []; this.iframeSrc = params.iframeSrc; this.chainProviders = params.chainProviders; this.chainAddresses = params.chainAddresses; this.messagingUrl = params.messagingUrl; this.natsUrl = params.natsUrl; this.authUrl = params.authUrl; } // method for signer-based connections static async connect(config: BrowserNodeSignerConfig): Promise { if (!config.logger) { config.logger = pino({ name: config.signer.publicIdentifier }); } const node = new BrowserNode({ logger: config.logger, chainProviders: config.chainProviders }); // TODO: validate schema GH issue #429 config.logger.info( { method: "connect", publicIdentifier: config.signer.publicIdentifier, signerAddress: config.signer.address, messagingUrl: config.messagingUrl, natsUrl: config.natsUrl, authUrl: config.authUrl, }, "Connecting with provided signer", ); const chainJsonProviders = hydrateProviders(config.chainProviders!); const messaging = new NatsMessagingService({ logger: config.logger.child({ module: "MessagingService" }), messagingUrl: config.messagingUrl, natsUrl: config.natsUrl, authUrl: config.authUrl, signer: config.signer, }); await messaging.connect(); const store = await BrowserStore.create( config.signer.publicIdentifier, config.logger.child({ module: "BrowserStore" }), ); const lock = new BrowserLockService( config.signer.publicIdentifier, messaging, config.logger.child({ module: "BrowserLockService" }), ); const chainService = new VectorChainService( store, chainJsonProviders, config.signer, config.logger.child({ module: "VectorChainService" }), ); // Pull live network addresses out of public deployments if not provided explicitly for (const chainId of Object.keys(config.chainProviders)) { if (!config.chainAddresses) { config.chainAddresses = {} as any; } if (!config.chainAddresses[chainId]) { config.chainAddresses[chainId] = {} as any; } if ( !config.chainAddresses[chainId].channelFactoryAddress && deployments[chainId] && deployments[chainId].ChannelFactory ) { config.chainAddresses[chainId].channelFactoryAddress = deployments[chainId].ChannelFactory.address; } if ( !config.chainAddresses[chainId].transferRegistryAddress && deployments[chainId] && deployments[chainId].TransferRegistry ) { config.chainAddresses[chainId].transferRegistryAddress = deployments[chainId].TransferRegistry.address; } } const engine = await VectorEngine.connect( messaging, lock, store, config.signer, chainService, config.chainAddresses!, config.logger.child({ module: "VectorEngine" }), false, 100, ); node.channelProvider = new DirectProvider(engine); node.publicIdentifier = config.signer.publicIdentifier; node.signerAddress = config.signer.address; return node; } // method for non-signer based apps to connect to iframe async init(params: { signature?: string; signer?: string } = {}): Promise { // TODO: validate config GH issue #429 const method = "init"; this.logger.debug({ method }, "Method started"); const iframeSrc = this.iframeSrc ?? "https://wallet.connext.network"; this.logger.info( { method, iframeSrc, signer: params.signer, signature: params.signature }, "Connecting with iframe provider", ); this.channelProvider = await IframeChannelProvider.connect({ src: iframeSrc, id: "connext-iframe", }); this.logger.info({ method }, "Authenticating Connext"); const rpc = constructRpcRequest("connext_authenticate", { chainProviders: this.chainProviders, chainAddresses: this.chainAddresses, messagingUrl: this.messagingUrl, natsUrl: this.natsUrl, authUrl: this.authUrl, signature: params.signature, signer: params.signer, }); const auth = await this.channelProvider.send(rpc); this.logger.info({ method, response: auth }, "Received response from auth method"); const [nodeConfig] = await this.getConfig(); this.publicIdentifier = nodeConfig.publicIdentifier; this.signerAddress = nodeConfig.signerAddress; this.logger.info( { supportedChains: this.supportedChains, routerPublicIdentifier: this.routerPublicIdentifier, method }, "Checking for existing channels", ); await Promise.all( this.supportedChains.map(async (chainId) => { const channelRes = await this.getStateChannelByParticipants({ chainId, counterparty: this.routerPublicIdentifier!, }); if (channelRes.isError) { throw channelRes.getError(); } const channel = channelRes.getValue(); if (channel) { this.logger.info({ channelAddress: channel.channelAddress, chainId, method }, "Found setup channel"); return; } this.logger.info({ chainId, method }, "Setting up channel"); const address = await this.setup({ chainId, counterpartyIdentifier: this.routerPublicIdentifier!, timeout: DEFAULT_CHANNEL_TIMEOUT.toString(), }); if (address.isError) { throw address.getError(); } this.logger.info({ channelAddress: address.getValue(), chainId, method }, "Created channel"); }), ); this.logger.debug({ method }, "Method complete"); } createNode(params: NodeParams.CreateNode): Promise> { return Promise.resolve( Result.fail(new BrowserNodeError(BrowserNodeError.reasons.MultinodeProhibitted, this.publicIdentifier, params)), ); } async getConfig(): Promise { const rpc = constructRpcRequest("chan_getConfig", {}); return this.send(rpc); } async getRouterConfig( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest("chan_getRouterConfig", params); try { const res = await this.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getStatus(): Promise> { const rpc = constructRpcRequest("chan_getStatus", {}); try { const res = await this.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getStateChannelByParticipants( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getChannelStateByParticipants">( ChannelRpcMethods.chan_getChannelStateByParticipants, { alice: params.counterparty, bob: this.publicIdentifier, chainId: params.chainId, }, ); const res = await this.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getStateChannel( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getChannelState">(ChannelRpcMethods.chan_getChannelState, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getStateChannels(): Promise> { try { const rpc = constructRpcRequest<"chan_getChannelStates">(ChannelRpcMethods.chan_getChannelStates, {}); const res = await this.channelProvider!.send(rpc); return Result.ok(res.map((chan: FullChannelState) => chan.channelAddress)); } catch (e) { return Result.fail(e); } } async getTransferQuote( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getTransferQuote">(ChannelRpcMethods.chan_getTransferQuote, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getTransferByRoutingId( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getTransferStateByRoutingId">( ChannelRpcMethods.chan_getTransferStateByRoutingId, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getTransfersByRoutingId( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getTransferStatesByRoutingId">( ChannelRpcMethods.chan_getTransferStatesByRoutingId, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res as NodeResponses.GetTransferStatesByRoutingId); } catch (e) { return Result.fail(e); } } async getTransfer( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getTransferState">(ChannelRpcMethods.chan_getTransferState, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getActiveTransfers( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getActiveTransfers">(ChannelRpcMethods.chan_getActiveTransfers, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getTransfers( params: OptionalPublicIdentifier< NodeParams.GetTransfers & Omit & { startDate: Date; endDate: Date } // in the client, use Date type >, ): Promise> { try { const rpc = constructRpcRequest<"chan_getTransfers">(ChannelRpcMethods.chan_getTransfers, { filterOpts: { active: params.active, channelAddress: params.channelAddress, endDate: params.endDate, routingId: params.routingId, startDate: params.startDate, }, }); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getRegisteredTransfers( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getRegisteredTransfers">( ChannelRpcMethods.chan_getRegisteredTransfers, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getWithdrawalCommitment( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getWithdrawalCommitment">( ChannelRpcMethods.chan_getWithdrawalCommitment, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async getWithdrawalCommitmentByTransactionHash( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getWithdrawalCommitmentByTransactionHash">( ChannelRpcMethods.chan_getWithdrawalCommitmentByTransactionHash, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async setup( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_requestSetup">(ChannelRpcMethods.chan_requestSetup, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } // OK to leave unimplemented since browser node will never be Alice async internalSetup(): Promise> { return Result.fail(new BrowserNodeError(BrowserNodeError.reasons.MethodNotImplemented, this.publicIdentifier)); } // OK to leave unimplemented since all txes can be sent from outside the browser node async sendDepositTx(): Promise> { return Result.fail(new BrowserNodeError(BrowserNodeError.reasons.MethodNotImplemented, this.publicIdentifier)); } async reconcileDeposit( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_deposit">(ChannelRpcMethods.chan_deposit, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channelAddress }); } catch (e) { return Result.fail(e); } } async requestCollateral( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_requestCollateral">(ChannelRpcMethods.chan_requestCollateral, params); await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: params.channelAddress }); } catch (e) { return Result.fail(e); } } async conditionalTransfer( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_createTransfer">(ChannelRpcMethods.chan_createTransfer, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channelAddress, transferId: (res.latestUpdate.details as CreateUpdateDetails).transferId, routingId: (res.latestUpdate.details as CreateUpdateDetails).meta?.routingId, }); } catch (e) { return Result.fail(e); } } async resolveTransfer( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_resolveTransfer">(ChannelRpcMethods.chan_resolveTransfer, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channelAddress, transferId: (res.latestUpdate.details as CreateUpdateDetails).transferId, routingId: (res.latestUpdate.details as CreateUpdateDetails).meta?.routingId, }); } catch (e) { return Result.fail(e); } } async getWithdrawalQuote( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_getWithdrawalQuote">(ChannelRpcMethods.chan_getWithdrawalQuote, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async withdraw( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_withdraw">(ChannelRpcMethods.chan_withdraw, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channel.channelAddress, transferId: (res.channel.latestUpdate.details as CreateUpdateDetails).transferId, transactionHash: res.transactionHash, transaction: res.transaction, }); } catch (e) { return Result.fail(e); } } async withdrawRetry( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_withdrawRetry">(ChannelRpcMethods.chan_withdrawRetry, params); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async addTransactionToCommitment( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_addTransactionToCommitment">( ChannelRpcMethods.chan_addTransactionToCommitment, params, ); const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } async restoreState( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_restoreState">(ChannelRpcMethods.chan_restoreState, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channelAddress }); } catch (e) { return Result.fail(e); } } async signUtilityMessage( params: OptionalPublicIdentifier, ): Promise> { try { const rpc = constructRpcRequest<"chan_signUtilityMessage">(ChannelRpcMethods.chan_signUtilityMessage, params); const res = await this.channelProvider!.send(rpc); return Result.ok({ signedMessage: res, }); } catch (e) { return Result.fail(e); } } async sendIsAliveMessage( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_sendIsAlive, params); try { const res = await this.channelProvider!.send(rpc); return Result.ok({ channelAddress: res.channelAddress }); } catch (e) { return Result.fail(e); } } async send(payload: EngineParams.RpcRequest): Promise { return this.channelProvider!.send(payload); } ////////////////////// /// DISPUTE METHODS async getChannelDispute( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_getDispute, params); try { const res = (await this.channelProvider!.send(rpc)) as ChannelDispute | undefined; return Result.ok(res); } catch (e) { return Result.fail(e); } } async sendDisputeChannelTx( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_dispute, params); try { const res = await this.channelProvider!.send(rpc); return Result.ok({ transactionHash: res.transactionHash }); } catch (e) { return Result.fail(e); } } async sendDefundChannelTx( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_defund, params); try { const res = await this.channelProvider!.send(rpc); return Result.ok({ transactionHash: res.transactionHash }); } catch (e) { return Result.fail(e); } } async getTransferDispute( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_getTransferDispute, params); try { const res = (await this.channelProvider!.send(rpc)) as TransferDispute | undefined; return Result.ok(res); } catch (e) { return Result.fail(e); } } async sendDisputeTransferTx( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_disputeTransfer, params); try { const res = await this.channelProvider!.send(rpc); return Result.ok({ transactionHash: res.transactionHash }); } catch (e) { return Result.fail(e); } } async sendDefundTransferTx( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_defundTransfer, params); try { const res = await this.channelProvider!.send(rpc); return Result.ok({ transactionHash: res.transactionHash }); } catch (e) { return Result.fail(e); } } async sendExitChannelTx( params: OptionalPublicIdentifier, ): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_exit, params); try { const res = (await this.channelProvider!.send(rpc)) as NodeResponses.SendExitChannelTx; return Result.ok(res); } catch (e) { return Result.fail(e); } } async syncDisputes(): Promise> { const rpc = constructRpcRequest(ChannelRpcMethods.chan_syncDisputes, {}); try { const res = await this.channelProvider!.send(rpc); return Result.ok(res); } catch (e) { return Result.fail(e); } } waitFor( event: T, timeout: number, filter?: (payload: EngineEventMap[T]) => boolean, ): Promise { throw new Error("TODO"); // return this.engine.waitFor(event, timeout, filter); } async once( event: T, callback: (payload: EngineEventMap[T]) => void | Promise, filter?: (payload: EngineEventMap[T]) => boolean, ): Promise { return this.channelProvider!.once(event, callback, filter); } async on( event: T, callback: (payload: EngineEventMap[T]) => void | Promise, filter?: (payload: EngineEventMap[T]) => boolean, ): Promise { return this.channelProvider!.on(event, callback, filter); } async off(event: T): Promise { throw new Error("TODO"); // return this.engine.off(event); } }