diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 00000000..dae7b314 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,54 @@ +# Development + +## Skipped go-nitro Commits / Features + +* Usage of bearer auth tokens + * | +* Kademlia-dht peer discovery + * + * Use libp2p notifications + * + * Skipping `libp2p.NATPortMap()` +* Implement a basic reverse payment proxy + * + +## Known issues (ts-nitro) + +* Metamask caching issue after chain restart + * Error occurs during direct fund transfer in mobymask-ui when the Nitro Node makes an eth_call to the Nitro Adjudicator contract + + ```bash + Received invalid block tag 1270. Latest block number is 99 + ``` + + * + + * To resolve this issue, attempt changing the network in Metamask and then switch back to the network you're using to connect to the local node + +## Known issues (go-nitro) + +* Error is thrown when trying to fund virtual channels with amounts more than their ledger channel supports + + ```bash + panic: error updating ledger funding: error proposing ledger update: propose could not add new state vars: insufficient funds + goroutine 88 [running]: + github.com/statechannels/go-nitro/node/engine.(*Engine).checkError(0x748a4ac48e62a6aa?, {0x1e60900, 0xc00048c940}) + go-nitro/node/engine/engine.go:867 +0x139 + github.com/statechannels/go-nitro/node/engine.(*Engine).run(0xc000306500, {0x1e6f948, 0xc0006a09b0}) + go-nitro/node/engine/engine.go:211 +0x852 + created by github.com/statechannels/go-nitro/node/engine.New in goroutine 1 + go-nitro/node/engine/engine.go:164 +0x54b + ``` + +* Error is thrown when conducting direct defund while virtual channel is running + + ```bash + panic: handleAPIEvent: Could not create directdefund objective for {ChannelId:0xecb0d8f2cdd9222b56dc24daa6b10fc2143f7b8861695071e260417d4ad289f6 objectiveStarted:0xc000743da0}: ledger channel has running guarantees + goroutine 200 [running]: + github.com/statechannels/go-nitro/node/engine.(*Engine).checkError(0x0?, {0x1e60900, 0xc000b8a240}) + go-nitro/node/engine/engine.go:867 +0x139 + github.com/statechannels/go-nitro/node/engine.(*Engine).run(0xc0009800a0, {0x1e6f948, 0xc000984000}) + go-nitro/node/engine/engine.go:211 +0x852 + created by github.com/statechannels/go-nitro/node/engine.New in goroutine 1 + go-nitro/node/engine/engine.go:164 +0x54b + ``` diff --git a/README.md b/README.md index 86ad3cf1..009c2941 100644 --- a/README.md +++ b/README.md @@ -182,3 +182,7 @@ Run relay node using v2 watcher ```bash clearNodeStorage() ``` + +### Development + +* [README](./DEVELOPMENT.md) diff --git a/packages/nitro-node/package.json b/packages/nitro-node/package.json index 98acdd4f..e3c840e9 100644 --- a/packages/nitro-node/package.json +++ b/packages/nitro-node/package.json @@ -61,7 +61,7 @@ "@libp2p/crypto": "^1.0.4", "@libp2p/tcp": "^6.0.0", "@multiformats/multiaddr": "^11.1.4", - "@statechannels/nitro-protocol": "^2.0.1-alpha.5", + "@statechannels/nitro-protocol": "^2.0.1-alpha.6", "assert": "^2.0.0", "async-mutex": "^0.4.0", "debug": "^4.3.4", diff --git a/packages/nitro-node/src/channel/channel.ts b/packages/nitro-node/src/channel/channel.ts index 9fe407f4..9cd3758a 100644 --- a/packages/nitro-node/src/channel/channel.ts +++ b/packages/nitro-node/src/channel/channel.ts @@ -4,7 +4,7 @@ import { Buffer } from 'buffer'; import { ethers } from 'ethers'; import { - fromJSON, toJSON, FieldDescription, Uint, Uint64, NitroSigner, + fromJSON, toJSON, FieldDescription, Uint, Uint64, NitroSigner, WrappedError, } from '@cerc-io/nitro-util'; import { Bytes32 } from '@statechannels/nitro-protocol'; @@ -124,7 +124,7 @@ export class Channel extends FixedPart { try { props = fromJSON(this.jsonEncodingMap, data); } catch (err) { - throw new Error(`error unmarshaling channel data: ${err}`); + throw new WrappedError('error unmarshaling channel data', err as Error); } return new Channel(props); @@ -389,14 +389,14 @@ export class Channel extends FixedPart { try { sig = await s.sign(signer); } catch (err) { - throw new Error(`Could not sign prefund ${err}`); + throw new WrappedError('Could not sign prefund', err as Error); } const ss = SignedState.newSignedState(s); try { ss.addSignature(sig); } catch (err) { - throw new Error(`could not add own signature ${err}`); + throw new WrappedError('could not add own signature', err as Error); } const ok = this.addSignedState(ss); diff --git a/packages/nitro-node/src/channel/consensus-channel/consensus-channel.ts b/packages/nitro-node/src/channel/consensus-channel/consensus-channel.ts index f1041cd0..aa5f6c5b 100644 --- a/packages/nitro-node/src/channel/consensus-channel/consensus-channel.ts +++ b/packages/nitro-node/src/channel/consensus-channel/consensus-channel.ts @@ -6,7 +6,7 @@ import { Buffer } from 'buffer'; import { ethers } from 'ethers'; import { - FieldDescription, NitroSigner, Uint, Uint64, fromJSON, toJSON, + FieldDescription, NitroSigner, Uint, Uint64, fromJSON, toJSON, WrappedError, } from '@cerc-io/nitro-util'; import { Bytes32 } from '@statechannels/nitro-protocol'; @@ -979,7 +979,7 @@ export class ConsensusChannel { throw new Error(`Follower did not sign initial state: ${followerAddr}, ${fp.participants![Number(Follower)]}`); } } catch (err) { - throw new Error(`could not verify sig: ${err}`); + throw new WrappedError('could not verify sig', err as Error); } const current = new SignedVars({ @@ -1266,7 +1266,7 @@ export class ConsensusChannel { try { signer = consensusCandidate.asState(this.fp).recoverSigner(countersigned.signature); } catch (err) { - throw new Error(`unable to recover signer: ${err}`); + throw new WrappedError('unable to recover signer', err as Error); } if (signer !== this.fp.participants![Number(Follower)]) { @@ -1303,20 +1303,20 @@ export class ConsensusChannel { try { vars = this.latestProposedVars(); } catch (err) { - throw new Error(`unable to construct latest proposed vars: ${err}`); + throw new WrappedError('unable to construct latest proposed vars', err as Error); } try { vars.handleProposal(proposal); } catch (err) { - throw new Error(`propose could not add new state vars: ${err}`); + throw new WrappedError('propose could not add new state vars', err as Error); } let signature: Signature; try { signature = await this.sign(vars, signer); } catch (err) { - throw new Error(`unable to sign state update: ${err}`); + throw new Error('unable to sign state update'); } const signed = new SignedProposal({ proposal, signature, turnNum: vars.turnNum }); @@ -1324,7 +1324,7 @@ export class ConsensusChannel { try { this.appendToProposalQueue(signed); } catch (err) { - throw new Error(`could not append to proposal queue: ${err}`); + throw new WrappedError('could not append to proposal queue', err as Error); } return signed; @@ -1358,7 +1358,7 @@ export class ConsensusChannel { // Get the latest proposal vars we have vars = this.latestProposedVars(); } catch (err) { - throw new Error(`could not generate the current proposal: ${err}`); + throw new WrappedError('could not generate the current proposal', err as Error); } if (p.turnNum !== vars.turnNum + BigInt(1)) { @@ -1369,7 +1369,7 @@ export class ConsensusChannel { try { vars.handleProposal(p.proposal); } catch (err) { - throw new Error(`receive could not add new state vars: ${err}`); + throw new WrappedError('receive could not add new state vars', err as Error); } // Validate the signature @@ -1377,7 +1377,7 @@ export class ConsensusChannel { try { signer = this.recoverSigner(vars, p.signature); } catch (err) { - throw new Error(`receive could not recover signature: ${err}`); + throw new WrappedError('receive could not recover signature', err as Error); } if (signer !== this.leader()) { diff --git a/packages/nitro-node/src/channel/state/signedstate.ts b/packages/nitro-node/src/channel/state/signedstate.ts index 6aa9ecbd..6338351b 100644 --- a/packages/nitro-node/src/channel/state/signedstate.ts +++ b/packages/nitro-node/src/channel/state/signedstate.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import { Buffer } from 'buffer'; import { - FieldDescription, Uint, Uint64, fromJSON, toJSON, + FieldDescription, Uint, Uint64, fromJSON, toJSON, WrappedError, } from '@cerc-io/nitro-util'; import { Signature } from '../../crypto/signatures'; @@ -55,7 +55,7 @@ export class SignedState { try { signer = this.state().recoverSigner(sig); } catch (err) { - throw new Error('AddSignature failed to recover signer'); + throw new WrappedError('AddSignature failed to recover signer', err as Error); } for (let i = 0; i < (this.state().participants ?? []).length; i += 1) { diff --git a/packages/nitro-node/src/node/engine/chainservice/eth-chainservice.ts b/packages/nitro-node/src/node/engine/chainservice/eth-chainservice.ts index 144d1e75..db9f9d8d 100644 --- a/packages/nitro-node/src/node/engine/chainservice/eth-chainservice.ts +++ b/packages/nitro-node/src/node/engine/chainservice/eth-chainservice.ts @@ -352,7 +352,7 @@ export class EthChainService implements ChainService { newBlockUnsubscribe = this.chain.subscribeNewHead(newBlockChan); } catch (subErr) { errorChan.push(new WrappedError( - `subscribeNewHead failed to resubscribe: ${subErr}`, + 'subscribeNewHead failed to resubscribe', subErr as Error, )); @@ -389,7 +389,7 @@ export class EthChainService implements ChainService { // eslint-disable-next-line no-await-in-loop await this.checkForMissedEvents(latestBlockNum); } catch (checkErr) { - errorChan.push(new Error(`subscribeFilterLogs failed during checkForMissedEvents: ${checkErr}`)); + errorChan.push(new WrappedError('subscribeFilterLogs failed during checkForMissedEvents', checkErr as Error)); return; } @@ -532,7 +532,7 @@ export class EthChainService implements ChainService { ); await this.out.push(event); } catch (err) { - throw new Error(`error in ParseDeposited: ${err}`); + throw new WrappedError('error in ParseDeposited', err as Error); } break; } @@ -542,7 +542,7 @@ export class EthChainService implements ChainService { try { au = this.na.interface.parseLog(l).args as unknown as AllocationUpdatedEventObject; } catch (err) { - throw new Error(`error in ParseAllocationUpdated: ${err}`); + throw new WrappedError('error in ParseAllocationUpdated', err as Error); } let tx; @@ -553,7 +553,7 @@ export class EthChainService implements ChainService { throw new Error('Expected transaction to be part of the chain, but the transaction is pending'); } } catch (err) { - throw new Error(`error in TransactionByHash: ${err}`); + throw new WrappedError('error in TransactionByHash', err as Error); } assert(tx !== undefined); @@ -563,7 +563,7 @@ export class EthChainService implements ChainService { assetAddress = assetAddressForIndex(this.na, tx, au.assetIndex.toBigInt()); } catch (err) { throw new WrappedError( - `error in assetAddressForIndex: ${err}`, + 'error in assetAddressForIndex', err as Error, ); } @@ -590,7 +590,7 @@ export class EthChainService implements ChainService { const event = new ConcludedEvent({ _channelID: new Destination(ce.channelId), _blockNum: BigInt(l.blockNumber) }); await this.out.push(event); } catch (err) { - throw new Error(`error in ParseConcluded: ${err}`); + throw new WrappedError('error in ParseConcluded', err as Error); } break; } @@ -613,7 +613,7 @@ export class EthChainService implements ChainService { this.out.push(event); } catch (err) { - throw new Error(`error in ParseChallengeRegistered: ${err}`); + throw new WrappedError('error in ParseChallengeRegistered', err as Error); } break; } @@ -772,7 +772,7 @@ export class EthChainService implements ChainService { await this.dispatchChainEvents(eventsToDispatch); } catch (err) { await errorChan.push(new WrappedError( - `failed dispatchChainEvents: ${err}`, + 'failed dispatchChainEvents', err as Error, )); } @@ -798,7 +798,7 @@ export class EthChainService implements ChainService { try { eventUnsubscribe = this.chain.subscribeFilterLogs(eventQuery, eventChan); } catch (err) { - throw new WrappedError(`subscribeFilterLogs failed: ${err}`, err as Error); + throw new WrappedError('subscribeFilterLogs failed', err as Error); } this.eventUnsubscribe = eventUnsubscribe.bind(this.chain); @@ -810,7 +810,7 @@ export class EthChainService implements ChainService { try { newBlockUnsubscribe = this.chain.subscribeNewHead(newBlockChan); } catch (err) { - throw new WrappedError(`subscribeNewHead failed: ${err}`, err as Error); + throw new WrappedError('subscribeNewHead failed', err as Error); } this.newBlockUnsubscribe = newBlockUnsubscribe.bind(this.chain); diff --git a/packages/nitro-node/src/node/engine/engine.ts b/packages/nitro-node/src/node/engine/engine.ts index 221935c3..75d9227f 100644 --- a/packages/nitro-node/src/node/engine/engine.ts +++ b/packages/nitro-node/src/node/engine/engine.ts @@ -214,7 +214,7 @@ export class Engine { e.paymentRequestsFromAPI = Channel(); e.fromChain = chain.eventFeed(); - e.fromMsg = msg.out(); + e.fromMsg = msg.p2pMessages(); e.chain = chain; e.msg = msg; @@ -398,7 +398,7 @@ export class Engine { if (obj.getStatus() === ObjectiveStatus.Completed) { this.logger(JSON.stringify({ - msg: 'Ignoring proposal for complected objective', + msg: 'Ignoring proposal for completed objective', ...withObjectiveIdAttribute(id), })); return [new EngineEvent({}), null]; @@ -484,7 +484,7 @@ export class Engine { if (objective.getStatus() === ObjectiveStatus.Completed) { this.logger(JSON.stringify({ - msg: 'Ignoring payload for complected objective', + msg: 'Ignoring payload for completed objective', ...withObjectiveIdAttribute(objective.id()), })); @@ -593,7 +593,7 @@ export class Engine { // TODO: return the amount we paid? await this.vm.receive(voucher); } catch (err) { - return [new EngineEvent({}), new Error(`error accepting payment voucher: ${err}`)]; + return [new EngineEvent({}), new WrappedError('error accepting payment voucher', err as Error)]; } finally { allCompleted.receivedVouchers.push(voucher); } @@ -719,7 +719,7 @@ export class Engine { try { chainId = await this.chain.getChainId(); } catch (err) { - return [new EngineEvent({}), new Error(`could not get chain id from chain service: ${err}`)]; + return [new EngineEvent({}), new WrappedError('could not get chain id from chain service', err as Error)]; } const objectiveId = or.id(myAddress, chainId); @@ -746,7 +746,7 @@ export class Engine { this.store.getConsensusChannel.bind(this.store), ); } catch (err) { - return [failedEngineEvent, new Error(`handleAPIEvent: Could not create virtualfund objective for ${or}: ${err}`)]; + return [failedEngineEvent, new WrappedError(`handleAPIEvent: Could not create virtualfund objective for ${or}`, err as Error)]; } if (METRICS_ENABLED) { @@ -763,7 +763,7 @@ export class Engine { return [ failedEngineEvent, new WrappedError( - `could not register channel with payment/receipt manager: ${err}`, + 'could not register channel with payment/receipt manager', err, )]; } @@ -784,7 +784,10 @@ export class Engine { } catch (err) { return [ failedEngineEvent, - new Error(`handleAPIEvent: Could not create virtualdefund objective for ${JSONbigNative.stringify(request)}: ${err}`), + new WrappedError( + `handleAPIEvent: Could not create virtualdefund objective for ${JSONbigNative.stringify(request)}`, + err as Error, + ), ]; } } @@ -806,7 +809,10 @@ export class Engine { } catch (err) { return [ failedEngineEvent, - new Error(`handleAPIEvent: Could not create virtualdefund objective for ${JSONbigNative.stringify(request)}: ${err}`), + new WrappedError( + `handleAPIEvent: Could not create virtualdefund objective for ${JSONbigNative.stringify(request)}`, + err as Error, + ), ]; } @@ -831,7 +837,7 @@ export class Engine { } catch (err) { return [ failedEngineEvent, - new Error(`handleAPIEvent: Could not create directfund objective for ${JSONbigNative.stringify(or)}: ${err}`), + new WrappedError(`handleAPIEvent: Could not create directfund objective for ${JSONbigNative.stringify(or)}`, err as Error), ]; } @@ -850,7 +856,10 @@ export class Engine { } catch (err) { return [ failedEngineEvent, - new Error(`handleAPIEvent: Could not create directdefund objective for ${JSONbigNative.stringify(request)}: ${err}`), + new WrappedError( + `handleAPIEvent: Could not create directdefund objective for ${JSONbigNative.stringify(request)}`, + err as Error, + ), ]; } @@ -863,7 +872,7 @@ export class Engine { } catch (err) { return [ failedEngineEvent, - new Error(`handleAPIEvent: Could not destroy consensus channel for ${JSONbigNative.stringify(request)}: ${err}`), + new WrappedError(`handleAPIEvent: Could not destroy consensus channel for ${JSONbigNative.stringify(request)}`, err as Error), ]; } @@ -897,7 +906,7 @@ export class Engine { try { voucher = await this.vm!.pay(cId, request.amount, this.store!.getChannelSigner()); } catch (err) { - return [ee, new Error(`handleAPIEvent: Error making payment: ${err}`)]; + return [ee, new WrappedError('handleAPIEvent: Error making payment', err as Error)]; } const [c, ok] = await this.store!.getChannelById(cId); @@ -917,7 +926,7 @@ export class Engine { try { info = await getPaymentChannelInfo(cId, this.store!, this.vm!); } catch (err) { - return [ee, new Error(`handleAPIEvent: Error querying channel info: ${err}`)]; + return [ee, new WrappedError('handleAPIEvent: Error querying channel info', err as Error)]; } ee.paymentChannelUpdates = [...ee.paymentChannelUpdates, info]; @@ -1177,20 +1186,29 @@ export class Engine { try { c = dfo.createConsensusChannel(); } catch (err) { - throw new Error(`could not create consensus channel for objective ${crankedObjective.id()}: ${err}`); + throw new WrappedError( + `could not create consensus channel for objective ${crankedObjective.id()}`, + err as Error, + ); } try { await this.store.setConsensusChannel(c); } catch (err) { - throw new Error(`could not store consensus channel for objective ${crankedObjective.id()}: ${err}`); + throw new WrappedError( + `could not store consensus channel for objective ${crankedObjective.id()}`, + err as Error, + ); } try { // Destroy the channel since the consensus channel takes over governance: await this.store.destroyChannel(c.id); } catch (err) { - throw new Error(`Could not destroy consensus channel for objective ${crankedObjective.id()}: ${err}`); + throw new WrappedError( + `Could not destroy consensus channel for objective ${crankedObjective.id()}`, + err as Error, + ); } } } finally { @@ -1223,7 +1241,7 @@ export class Engine { try { newObj = await this.constructObjectiveFromMessage(id, p); } catch (constructErr) { - throw new Error(`error constructing objective from message: ${constructErr}`); + throw new WrappedError('error constructing objective from message', constructErr as Error); } if (METRICS_ENABLED) { @@ -1233,7 +1251,7 @@ export class Engine { try { await this.store.setObjective(newObj); } catch (setErr) { - throw new Error(`error setting objective in store: ${setErr}`); + throw new WrappedError('error setting objective in store', setErr as Error); } this.logger(JSON.stringify({ @@ -1290,7 +1308,10 @@ export class Engine { try { await this.registerPaymentChannel(vfo); } catch (err) { - throw new Error(`could not register channel with payment/receipt manager.\n\ttarget channel: ${id}\n\terr: ${err}`); + throw new WrappedError( + `could not register channel with payment/receipt manager.\n\ttarget channel: ${id}\n\terr`, + err as Error, + ); } return vfo; @@ -1300,7 +1321,7 @@ export class Engine { try { vId = getVirtualChannelFromObjectiveId(id); } catch (err) { - throw new Error(`could not determine virtual channel id from objective ${id}: ${err}`); + throw new WrappedError(`could not determine virtual channel id from objective ${id}`, err as Error); } let minAmount: bigint | undefined = BigInt(0); @@ -1309,7 +1330,7 @@ export class Engine { try { paid = await this.vm.paid(vId); } catch (err) { - throw new Error(`could not determine virtual channel id from objective ${id}: ${err}`); + throw new WrappedError(`could not determine virtual channel id from objective ${id}`, err as Error); } minAmount = paid; @@ -1431,7 +1452,7 @@ type MessageDirection = string; // fromMsgErr wraps errors from objective construction functions and // returns an error bundled with the objectiveID function fromMsgErr(id: ObjectiveId, err: Error): Error { - return new Error(`could not create objective from message.\n\ttarget objective: ${id}\n\terr: ${err}`); + return new WrappedError(`could not create objective from message.\n\ttarget objective: ${id}\n\terr`, err as Error); } // getProposalObjectiveId returns the objectiveId for a proposal. diff --git a/packages/nitro-node/src/node/engine/messageservice/messageservice.ts b/packages/nitro-node/src/node/engine/messageservice/messageservice.ts index 967b0404..decd4a70 100644 --- a/packages/nitro-node/src/node/engine/messageservice/messageservice.ts +++ b/packages/nitro-node/src/node/engine/messageservice/messageservice.ts @@ -5,8 +5,8 @@ import { Message } from '../../../protocols/messages'; // TODO: Add tests export interface MessageService { - // Out returns a chan for receiving messages from the message service - out (): ReadChannel; + // P2PMessages returns a chan for receiving messages from the message service + p2pMessages (): ReadChannel; // Send is for sending messages with the message service send (msg: Message): Promise; diff --git a/packages/nitro-node/src/node/engine/messageservice/p2p-message-service/service.ts b/packages/nitro-node/src/node/engine/messageservice/p2p-message-service/service.ts index b7511127..935ea84d 100644 --- a/packages/nitro-node/src/node/engine/messageservice/p2p-message-service/service.ts +++ b/packages/nitro-node/src/node/engine/messageservice/p2p-message-service/service.ts @@ -9,8 +9,6 @@ import type { ReadChannel, ReadWriteChannel } from '@cerc-io/ts-channel'; // @ts-expect-error import type { Libp2p } from '@cerc-io/libp2p'; // @ts-expect-error -import type { PrivateKey } from '@libp2p/interface-keys'; -// @ts-expect-error import type { Stream, Connection } from '@libp2p/interface-connection'; // @ts-expect-error import type { IncomingStreamData } from '@libp2p/interface-registrar'; @@ -67,7 +65,6 @@ interface ConstructorOptions { scAddr: Address; newPeerInfo: ReadWriteChannel; logger: debug.Debugger; - key?: PrivateKey; p2pHost?: Libp2p; } @@ -80,8 +77,6 @@ export class P2PMessageService implements MessageService { private scAddr: Address = ethers.constants.AddressZero; - private privateKey?: PrivateKey; - private p2pHost?: Libp2p; private newPeerInfo?: ReadWriteChannel; @@ -110,19 +105,6 @@ export class P2PMessageService implements MessageService { }); ms.peer = opts.peer; - assert(ms.peer.peerId); - const { unmarshalPrivateKey } = await import('@libp2p/crypto/keys'); - - let messageKey; - try { - messageKey = await unmarshalPrivateKey(ms.peer.peerId.privateKey!); - } catch (err) { - ms.checkError(err as Error); - } - - assert(messageKey); - ms.privateKey = messageKey; - assert(ms.peer.node); ms.p2pHost = ms.peer.node; assert(ms.p2pHost); @@ -139,10 +121,7 @@ export class P2PMessageService implements MessageService { // id returns the libp2p peer ID of the message service. async id(): Promise { - const PeerIdFactory = await import('@libp2p/peer-id-factory'); - - assert(this.privateKey); - return PeerIdFactory.createFromPrivKey(this.privateKey); + return this.p2pHost.peerId; } // Custom Method to exchange info with already connected peers @@ -452,8 +431,8 @@ export class P2PMessageService implements MessageService { throw err; } - // out returns a channel that can be used to receive messages from the message service - out(): ReadChannel { + // p2pMessages returns a channel that can be used to receive messages from the message service + p2pMessages(): ReadChannel { return this.toEngine!.readOnly(); } diff --git a/packages/nitro-node/src/node/engine/store/durablestore.ts b/packages/nitro-node/src/node/engine/store/durablestore.ts index 8c2e68d9..d5ef775d 100644 --- a/packages/nitro-node/src/node/engine/store/durablestore.ts +++ b/packages/nitro-node/src/node/engine/store/durablestore.ts @@ -103,7 +103,7 @@ export class DurableStore implements Store { try { obj = decodeObjective(id, objJSON); } catch (err) { - throw new Error(`error decoding objective ${id}: ${err}`); + throw new WrappedError(`error decoding objective ${id}`, err as Error); } try { @@ -113,7 +113,7 @@ export class DurableStore implements Store { // return existing objective data along with error // return obj, fmt.Errorf("error populating channel data for objective %s: %w", id, err) - throw new Error(`error populating channel data for objective ${id}: ${err}`); + throw new WrappedError(`error populating channel data for objective ${id}`, err as Error); } return obj; @@ -125,7 +125,7 @@ export class DurableStore implements Store { try { objJSON = Buffer.from(JSONbigNative.stringify(obj), 'utf-8'); } catch (err) { - throw new Error(`error setting objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting objective ${obj.id()}`, err as Error); } await this.objectives!.put(obj.id(), objJSON); @@ -137,7 +137,7 @@ export class DurableStore implements Store { try { await this.setChannel(ch); } catch (err) { - throw new Error(`error setting virtual channel ${ch.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting virtual channel ${ch.id} from objective ${obj.id()}`, err as Error); } break; @@ -148,7 +148,7 @@ export class DurableStore implements Store { try { await this.setChannel(channel); } catch (err) { - throw new Error(`error setting channel ${channel.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting channel ${channel.id} from objective ${obj.id()}`, err as Error); } break; @@ -159,7 +159,7 @@ export class DurableStore implements Store { try { await this.setConsensusChannel(consensusChannel); } catch (err) { - throw new Error(`error setting consensus channel ${consensusChannel.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting consensus channel ${consensusChannel.id} from objective ${obj.id()}`, err as Error); } break; @@ -187,7 +187,7 @@ export class DurableStore implements Store { try { await this.channelToObjective!.put(obj.ownsChannel().string(), obj.id()); } catch (err) { - throw new Error(`cannot transfer ownership of channel: ${err}`); + throw new WrappedError('cannot transfer ownership of channel', err as Error); } } @@ -473,7 +473,7 @@ export class DurableStore implements Store { try { ch = await this._getChannelById(o.c!.id); } catch (err) { - throw new Error(`error retrieving channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving channel data for objective ${id}`, err as Error); } o.c = ch; @@ -487,7 +487,7 @@ export class DurableStore implements Store { try { ch = await this._getChannelById(o.c!.id); } catch (err) { - throw new Error(`error retrieving channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving channel data for objective ${id}`, err as Error); } o.c = ch; @@ -501,7 +501,7 @@ export class DurableStore implements Store { try { v = await this._getChannelById(o.v!.id); } catch (err) { - throw new Error(`error retrieving virtual channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving virtual channel data for objective ${id}`, err as Error); } o.v = new VirtualChannel(v); @@ -516,7 +516,7 @@ export class DurableStore implements Store { try { left = await this.getConsensusChannelById(o.toMyLeft.channel.id); } catch (err) { - throw new Error(`error retrieving left ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving left ledger channel data for objective ${id}`, err as Error); } o.toMyLeft.channel = left; @@ -530,7 +530,7 @@ export class DurableStore implements Store { try { right = await this.getConsensusChannelById(o.toMyRight.channel.id); } catch (err) { - throw new Error(`error retrieving right ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving right ledger channel data for objective ${id}`, err as Error); } o.toMyRight.channel = right; @@ -545,7 +545,7 @@ export class DurableStore implements Store { try { v = await this._getChannelById(o.v!.id); } catch (err) { - throw new Error(`error retrieving virtual channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving virtual channel data for objective ${id}`, err as Error); } o.v = new VirtualChannel(v); @@ -558,7 +558,10 @@ export class DurableStore implements Store { try { left = await this.getConsensusChannelById(o.toMyLeft.id); } catch (err) { - throw new Error(`error retrieving left ledger channel data for objective ${id}: ${err}`); + throw new WrappedError( + `error retrieving left ledger channel data for objective ${id}`, + err as Error, + ); } o.toMyLeft = left; @@ -571,7 +574,10 @@ export class DurableStore implements Store { try { right = await this.getConsensusChannelById(o.toMyRight.id); } catch (err) { - throw new Error(`error retrieving right ledger channel data for objective ${id}: ${err}`); + throw new WrappedError( + `error retrieving right ledger channel data for objective ${id}`, + err as Error, + ); } o.toMyRight = right; @@ -602,7 +608,7 @@ export class DurableStore implements Store { vJSON = await this.vouchers!.get(channelId.string()); } catch (err) { throw new WrappedError( - `channelId ${channelId.string()}: ${ErrLoadVouchers}`, + `channelId ${channelId.string()}`, ErrLoadVouchers, ); } diff --git a/packages/nitro-node/src/node/engine/store/memstore.ts b/packages/nitro-node/src/node/engine/store/memstore.ts index 7d855b55..6932c5e7 100644 --- a/packages/nitro-node/src/node/engine/store/memstore.ts +++ b/packages/nitro-node/src/node/engine/store/memstore.ts @@ -91,7 +91,7 @@ export class MemStore implements Store { /* eslint-disable @typescript-eslint/no-use-before-define */ obj = decodeObjective(id, objJSON); } catch (err) { - throw new Error(`error decoding objective ${id}: ${err}`); + throw new WrappedError(`error decoding objective ${id}`, err as Error); } try { @@ -101,7 +101,7 @@ export class MemStore implements Store { // return existing objective data along with error // return obj, fmt.Errorf("error populating channel data for objective %s: %w", id, err) - throw new Error(`error populating channel data for objective ${id}: ${err}`); + throw new WrappedError(`error populating channel data for objective ${id}`, err as Error); } return obj; @@ -113,7 +113,7 @@ export class MemStore implements Store { try { objJSON = Buffer.from(JSONbigNative.stringify(obj), 'utf-8'); } catch (err) { - throw new Error(`error setting objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting objective ${obj.id()}`, err as Error); } this.objectives!.store(obj.id(), objJSON); @@ -125,7 +125,7 @@ export class MemStore implements Store { try { this.setChannel(ch); } catch (err) { - throw new Error(`error setting virtual channel ${ch.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting virtual channel ${ch.id} from objective ${obj.id()}`, err as Error); } break; @@ -136,7 +136,7 @@ export class MemStore implements Store { try { this.setChannel(channel); } catch (err) { - throw new Error(`error setting channel ${channel.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError(`error setting channel ${channel.id} from objective ${obj.id()}`, err as Error); } break; @@ -147,7 +147,10 @@ export class MemStore implements Store { try { this.setConsensusChannel(consensusChannel); } catch (err) { - throw new Error(`error setting consensus channel ${consensusChannel.id} from objective ${obj.id()}: ${err}`); + throw new WrappedError( + `error setting consensus channel ${consensusChannel.id} from objective ${obj.id()}`, + err as Error, + ); } break; @@ -440,7 +443,7 @@ export class MemStore implements Store { try { ch = this._getChannelById(o.c!.id); } catch (err) { - throw new Error(`error retrieving channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving channel data for objective ${id}`, err as Error); } o.c = ch; @@ -454,7 +457,7 @@ export class MemStore implements Store { try { ch = this._getChannelById(o.c!.id); } catch (err) { - throw new Error(`error retrieving channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving channel data for objective ${id}`, err as Error); } o.c = ch; @@ -468,7 +471,7 @@ export class MemStore implements Store { try { ch = this._getChannelById(o.v!.id); } catch (err) { - throw new Error(`error retrieving virtual channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving virtual channel data for objective ${id}`, err as Error); } o.v = new VirtualChannel(ch); @@ -482,7 +485,7 @@ export class MemStore implements Store { try { left = this.getConsensusChannelById(o.toMyLeft.channel.id); } catch (err) { - throw new Error(`error retrieving left ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving left ledger channel data for objective ${id}`, err as Error); } o.toMyLeft.channel = left; @@ -496,7 +499,7 @@ export class MemStore implements Store { try { right = this.getConsensusChannelById(o.toMyRight.channel.id); } catch (err) { - throw new Error(`error retrieving right ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving right ledger channel data for objective ${id}`, err as Error); } o.toMyRight.channel = right; @@ -511,7 +514,7 @@ export class MemStore implements Store { try { ch = this._getChannelById(o.v!.id); } catch (err) { - throw new Error(`error retrieving virtual channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving virtual channel data for objective ${id}`, err as Error); } o.v = new VirtualChannel(ch); @@ -524,7 +527,7 @@ export class MemStore implements Store { try { left = this.getConsensusChannelById(o.toMyLeft.id); } catch (err) { - throw new Error(`error retrieving left ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving left ledger channel data for objective ${id}`, err as Error); } o.toMyLeft = left; @@ -537,7 +540,7 @@ export class MemStore implements Store { try { right = this.getConsensusChannelById(o.toMyRight.id); } catch (err) { - throw new Error(`error retrieving right ledger channel data for objective ${id}: ${err}`); + throw new WrappedError(`error retrieving right ledger channel data for objective ${id}`, err as Error); } o.toMyRight = right; @@ -564,7 +567,7 @@ export class MemStore implements Store { const [data, ok] = this.vouchers!.load(channelId.string()); if (!ok) { throw new WrappedError( - `channelId ${channelId.string()}: ${ErrLoadVouchers}`, + `channelId ${channelId.string()}`, ErrLoadVouchers, ); } diff --git a/packages/nitro-node/src/node/node.ts b/packages/nitro-node/src/node/node.ts index f7f8c886..065e9fab 100644 --- a/packages/nitro-node/src/node/node.ts +++ b/packages/nitro-node/src/node/node.ts @@ -122,7 +122,7 @@ export class Node { message: 'direct fund error', error: err, })); - throw new Error(`counterparty check failed: ${err}`); + throw new WrappedError('counterparty check failed', err as Error); } if (channelExists) { @@ -131,7 +131,7 @@ export class Node { error: ErrLedgerChannelExists, })); throw new WrappedError( - `counterparty ${ethers.utils.getAddress(counterparty)}: ${ErrLedgerChannelExists}`, + `counterparty ${ethers.utils.getAddress(counterparty)}`, ErrLedgerChannelExists, ); } @@ -339,4 +339,24 @@ export class Node { sentVouchers(): ReadChannel { return this.engine.sentVouchers; } + + // LedgerUpdates returns a chan that receives ledger channel info whenever that ledger channel is updated. Not suitable for multiple subscribers. + ledgerUpdates() { + return this.channelNotifier!.registerForAllLedgerUpdates(); + } + + // PaymentUpdates returns a chan that receives payment channel info whenever that payment channel is updated. Not suitable fo multiple subscribers. + paymentUpdates() { + return this.channelNotifier!.registerForAllPaymentUpdates(); + } + + // LedgerUpdatedChan returns a chan that receives a ledger channel info whenever the ledger with given id is updated + ledgerUpdatedChan(ledgerId: Destination) { + return this.channelNotifier!.registerForLedgerUpdates(ledgerId); + } + + // PaymentChannelUpdatedChan returns a chan that receives a payment channel info whenever the payment channel with given id is updated + paymentChannelUpdatedChan(ledgerId: Destination) { + return this.channelNotifier!.registerForPaymentChannelUpdates(ledgerId); + } } diff --git a/packages/nitro-node/src/node/notifier/channel-notifier.ts b/packages/nitro-node/src/node/notifier/channel-notifier.ts index e2911e91..4a22684d 100644 --- a/packages/nitro-node/src/node/notifier/channel-notifier.ts +++ b/packages/nitro-node/src/node/notifier/channel-notifier.ts @@ -5,6 +5,7 @@ import { Store } from '../engine/store/store'; import { PaymentChannelListeners, LedgerChannelListeners } from './listeners'; import { SafeSyncMap } from '../../internal/safesync/safesync'; import { LedgerChannelInfo, PaymentChannelInfo } from '../query/types'; +import { Destination } from '../../types/destination'; const ALL_NOTIFICATIONS = 'all'; @@ -37,6 +38,30 @@ export class ChannelNotifier { }); } + // RegisterForAllLedgerUpdates returns a buffered channel that will receive updates for all ledger channels. + registerForAllLedgerUpdates() { + const [li] = this.ledgerListeners!.loadOrStore(ALL_NOTIFICATIONS, LedgerChannelListeners.newLedgerChannelListeners()); + return li.getOrCreateListener(); + } + + // RegisterForLedgerUpdates returns a buffered channel that will receive updates or a specific ledger channel. + registerForLedgerUpdates(cId: Destination) { + const [li] = this.ledgerListeners!.loadOrStore(cId.string(), LedgerChannelListeners.newLedgerChannelListeners()); + return li.createNewListener(); + } + + // RegisterForAllPaymentUpdates returns a buffered channel that will receive updates for all payment channels. + registerForAllPaymentUpdates() { + const [li] = this.paymentListeners!.loadOrStore(ALL_NOTIFICATIONS, PaymentChannelListeners.newPaymentChannelListeners()); + return li.getOrCreateListener(); + } + + // RegisterForPaymentChannelUpdates returns a buffered channel that will receive updates or a specific payment channel. + registerForPaymentChannelUpdates(cId: Destination) { + const [li] = this.paymentListeners!.loadOrStore(cId.string(), PaymentChannelListeners.newPaymentChannelListeners()); + return li.createNewListener(); + } + // NotifyLedgerUpdated notifies all listeners of a ledger channel update. // It should be called whenever a ledger channel is updated. notifyLedgerUpdated(info: LedgerChannelInfo): void { diff --git a/packages/nitro-node/src/node/notifier/listeners.ts b/packages/nitro-node/src/node/notifier/listeners.ts index 5c7cf75c..36b09b31 100644 --- a/packages/nitro-node/src/node/notifier/listeners.ts +++ b/packages/nitro-node/src/node/notifier/listeners.ts @@ -1,4 +1,4 @@ -import { ReadWriteChannel } from '@cerc-io/ts-channel'; +import Channel, { ReadWriteChannel } from '@cerc-io/ts-channel'; import { Mutex } from 'async-mutex'; import { LedgerChannelInfo, PaymentChannelInfo } from '../query/types'; @@ -45,6 +45,33 @@ export class PaymentChannelListeners { } } + // createNewListener creates a new listener and adds it to the list of listeners. + async createNewListener() { + const release = await this.listenersLock.acquire(); + let listener; + try { + // Use a buffered channel to avoid blocking the notifier. + listener = Channel(1000); + this.listeners.push(listener); + } finally { + release(); + } + return listener; + } + + // getOrCreateListener returns the first listener, creating one if none exist. + async getOrCreateListener() { + const release = await this.listenersLock.acquire(); + if (this.listeners.length !== 0) { + const l = this.listeners[0]; + release(); + return l; + } + + release(); + return this.createNewListener(); + } + async close(): Promise { const release = await this.listenersLock.acquire(); @@ -101,6 +128,33 @@ export class LedgerChannelListeners { } } + // createNewListener creates a new listener and adds it to the list of listeners. + async createNewListener() { + const release = await this.listenersLock.acquire(); + let listener; + try { + // Use a buffered channel to avoid blocking the notifier. + listener = Channel(1000); + this.listeners.push(listener); + } finally { + release(); + } + return listener; + } + + // getOrCreateListener returns the first listener, creating one if none exist. + async getOrCreateListener() { + const release = await this.listenersLock.acquire(); + if (this.listeners.length !== 0) { + const l = this.listeners[0]; + release(); + return l; + } + + release(); + return this.createNewListener(); + } + async close(): Promise { const release = await this.listenersLock.acquire(); diff --git a/packages/nitro-node/src/node/query/query.ts b/packages/nitro-node/src/node/query/query.ts index 109849d1..d712680c 100644 --- a/packages/nitro-node/src/node/query/query.ts +++ b/packages/nitro-node/src/node/query/query.ts @@ -1,5 +1,9 @@ import _ from 'lodash'; +import { + WrappedError, +} from '@cerc-io/nitro-util'; + import { Exit } from '../../channel/state/outcome/exit'; import { Channel } from '../../channel/channel'; import { State } from '../../channel/state/state'; @@ -146,7 +150,7 @@ export const constructLedgerInfoFromConsensus = (con: ConsensusChannel, myAddres try { balance = getLedgerBalanceFromState(latest, myAddress); } catch (err) { - throw new Error(`failed to construct ledger channel info from consensus channel: ${err}`); + throw new WrappedError('failed to construct ledger channel info from consensus channel', err as Error); } return new LedgerChannelInfo({ @@ -163,7 +167,7 @@ export const constructLedgerInfoFromChannel = (c: Channel, myAddress: Address): try { balance = getLedgerBalanceFromState(latest, myAddress); } catch (err) { - throw new Error(`failed to construct ledger channel info from channel: ${err}`); + throw new WrappedError('failed to construct ledger channel info from channel', err as Error); } return new LedgerChannelInfo({ @@ -221,7 +225,7 @@ export const getPaymentChannelsByLedger = async (ledgerId: Destination, s: Store return []; } - throw new Error(`could not find any payment channels funded by ${ledgerId}: ${err}`); + throw new WrappedError(`could not find any payment channels funded by ${ledgerId}`, err as Error); } const toQuery = con.consensusVars().outcome.fundingTargets(); @@ -231,7 +235,7 @@ export const getPaymentChannelsByLedger = async (ledgerId: Destination, s: Store try { paymentChannels = await s.getChannelsByIds(toQuery); } catch (err) { - throw new Error(`could not query the store about ids ${toQuery}: ${err}`); + throw new WrappedError(`could not query the store about ids ${toQuery}`, err as Error); } const toReturn: PaymentChannelInfo[] = []; diff --git a/packages/nitro-node/src/protocols/directdefund/directdefund.ts b/packages/nitro-node/src/protocols/directdefund/directdefund.ts index d2bd03ed..232ba5d8 100644 --- a/packages/nitro-node/src/protocols/directdefund/directdefund.ts +++ b/packages/nitro-node/src/protocols/directdefund/directdefund.ts @@ -7,6 +7,7 @@ import cloneDeep from 'lodash/cloneDeep'; import Channel, { ReadWriteChannel } from '@cerc-io/ts-channel'; import { FieldDescription, NitroSigner, Uint64, fromJSON, toJSON, + WrappedError, } from '@cerc-io/nitro-util'; import { Destination } from '../../types/destination'; @@ -139,7 +140,7 @@ export class Objective implements ObjectiveInterface { try { cc = await getConsensusChannel(request.channelId) as ConsensusChannel; } catch (err) { - throw new Error(`could not find channel ${request.channelId}; ${err}`); + throw new WrappedError(`could not find channel ${request.channelId}`, err as Error); } if (cc.fundingTargets().length !== 0) { @@ -189,7 +190,7 @@ export class Objective implements ObjectiveInterface { try { ss = getSignedStatePayload(p.payloadData); } catch (err) { - throw new Error(`could not get signed state payload: ${err}`); + throw new WrappedError('could not get signed state payload', err as Error); } const s = ss.state(); @@ -257,7 +258,7 @@ export class Objective implements ObjectiveInterface { /* eslint-disable @typescript-eslint/no-use-before-define */ ss = getSignedStatePayload(p.payloadData); } catch (err) { - throw new Error(`could not get signed state payload: ${err}`); + throw new WrappedError('could not get signed state payload', err as Error); } if (ss.signatures().length !== 0) { @@ -306,14 +307,14 @@ export class Objective implements ObjectiveInterface { try { ss = await updated.c!.signAndAddState(stateToSign, signer); } catch (err) { - throw new Error(`could not sign final state ${err}`); + throw new WrappedError('could not sign final state', err as Error); } let messages: Message[]; try { messages = Message.createObjectivePayloadMessage(updated.id(), ss, SignedStatePayload, ...this.otherParticipants()); } catch (err) { - throw new Error(`could not create payload message ${err}`); + throw new WrappedError('could not create payload message', err as Error); } sideEffects.messagesToSend.push(...messages); @@ -323,7 +324,7 @@ export class Objective implements ObjectiveInterface { try { latestSupportedState = updated.c!.latestSupportedState(); } catch (err) { - throw new Error(`error finding a supported state: ${err}`); + throw new WrappedError('error finding a supported state', err as Error); } if (!latestSupportedState.isFinal) { @@ -428,7 +429,7 @@ function getSignedStatePayload(b: Buffer): SignedState { try { ss = SignedState.fromJSON(b.toString()); } catch (err) { - throw new Error(`could not unmarshal signed state: ${err}`); + throw new WrappedError('could not unmarshal signed state', err as Error); } return ss; diff --git a/packages/nitro-node/src/protocols/directfund/directfund.ts b/packages/nitro-node/src/protocols/directfund/directfund.ts index 7320ee67..5ec3a6be 100644 --- a/packages/nitro-node/src/protocols/directfund/directfund.ts +++ b/packages/nitro-node/src/protocols/directfund/directfund.ts @@ -64,7 +64,7 @@ const getSignedStatePayload = (b: Buffer): SignedState => { try { return SignedState.fromJSON(b.toString()); } catch (err) { - throw new Error(`could not unmarshal signed state: ${err}`); + throw new WrappedError('could not unmarshal signed state', err as Error); } }; @@ -153,12 +153,12 @@ export class Objective implements ObjectiveInterface { try { channelExists = await channelsExistWithCounterparty(request.counterParty, getChannels, getTwoPartyConsensusLedger); } catch (err) { - throw new Error(`counterparty check failed: ${err}`); + throw new WrappedError('counterparty check failed', err as Error); } if (channelExists) { throw new WrappedError( - `counterparty ${request.counterParty}: ${ErrLedgerChannelExists}`, + `counterparty ${request.counterParty}`, ErrLedgerChannelExists, ); } @@ -179,7 +179,7 @@ export class Objective implements ObjectiveInterface { try { b = Buffer.from(JSONbigNative.stringify(signedInitial), 'utf-8'); } catch (err) { - throw new Error(`could not create new objective: ${err}`); + throw new WrappedError('could not create new objective', err as Error); } const objectivePayload: ObjectivePayload = { @@ -192,7 +192,7 @@ export class Objective implements ObjectiveInterface { try { objective = Objective.constructFromPayload(preApprove, objectivePayload, myAddress); } catch (err) { - throw new Error(`could not create new objective: ${err}`); + throw new WrappedError('could not create new objective', err as Error); } return objective; @@ -209,7 +209,7 @@ export class Objective implements ObjectiveInterface { try { initialSignedState = getSignedStatePayload(op.payloadData); } catch (err) { - throw new Error(`could not get signed state payload: ${err}`); + throw new WrappedError('could not get signed state payload', err as Error); } const initialState = initialSignedState.state(); @@ -247,7 +247,7 @@ export class Objective implements ObjectiveInterface { try { init.c = channel.Channel.new(initialState, BigInt(myIndex)); } catch (err) { - throw new Error(`failed to initialize channel for direct-fund objective: ${err}`); + throw new WrappedError('failed to initialize channel for direct-fund objective', err as Error); } const myAllocatedAmount = initialState.outcome.totalAllocatedFor(Destination.addressToDestination(myAddress)); @@ -362,7 +362,7 @@ export class Objective implements ObjectiveInterface { try { ss = getSignedStatePayload(p.payloadData); } catch (err) { - throw new Error(`could not get signed state payload: ${err}`); + throw new WrappedError('could not get signed state payload', err as Error); } assert(updated.c); @@ -404,14 +404,14 @@ export class Objective implements ObjectiveInterface { try { ss = await updated.c.signAndAddPrefund(signer); } catch (err) { - throw new Error(`could not sign prefund ${err}`); + throw new WrappedError('could not sign prefund', err as Error); } let messages: Message[]; try { messages = Message.createObjectivePayloadMessage(updated.id(), ss, 'SignedStatePayload', ...updated.otherParticipants()); } catch (err) { - throw new Error(`could not create payload message ${err}`); + throw new WrappedError('could not create payload message', err as Error); } sideEffects.messagesToSend = sideEffects.messagesToSend.concat(messages); @@ -446,14 +446,14 @@ export class Objective implements ObjectiveInterface { try { ss = await updated.c.signAndAddPostfund(signer); } catch (err) { - throw new Error(`could not sign postfund ${err}`); + throw new WrappedError('could not sign postfund', err as Error); } let messages: Message[]; try { messages = Message.createObjectivePayloadMessage(updated.id(), ss, SignedStatePayload, ...updated.otherParticipants()); } catch (err) { - throw new Error('could not create payload message'); + throw new WrappedError('could not create payload message', err as Error); } sideEffects.messagesToSend = messages; diff --git a/packages/nitro-node/src/protocols/virtualdefund/virtualdefund.ts b/packages/nitro-node/src/protocols/virtualdefund/virtualdefund.ts index 89d43776..0f6ad369 100644 --- a/packages/nitro-node/src/protocols/virtualdefund/virtualdefund.ts +++ b/packages/nitro-node/src/protocols/virtualdefund/virtualdefund.ts @@ -11,6 +11,7 @@ import { Uint64, fromJSON, toJSON, + WrappedError, } from '@cerc-io/nitro-util'; import { Destination } from '../../types/destination'; @@ -80,7 +81,7 @@ export function getRequestFinalStatePayload(b: Buffer): Destination { try { cId = Destination.fromJSON(b.toString()); } catch (err) { - throw new Error(`could not unmarshal signatures: ${err}`); + throw new WrappedError('could not unmarshal signatures', err as Error); } return cId; } @@ -545,7 +546,7 @@ export class Objective implements ObjectiveInterface { try { s = updated.generateFinalState(); } catch (err) { - throw new Error(`could not generate final state: ${err}`); + throw new WrappedError('could not generate final state', err as Error); } } else { s = updated.finalState(); @@ -556,13 +557,13 @@ export class Objective implements ObjectiveInterface { try { ss = await updated.v!.signAndAddState(s, signer); } catch (err) { - throw new Error(`could not sign final state: ${err}`); + throw new WrappedError('could not sign final state', err as Error); } let messages: Message[]; try { messages = Message.createObjectivePayloadMessage(updated.id(), ss, SignedStatePayload, ...this.otherParticipants()); } catch (err) { - throw new Error(`could not get create payload message: ${err}`); + throw new WrappedError('could not get create payload message', err as Error); } sideEffects.messagesToSend.push(...messages); } @@ -577,7 +578,7 @@ export class Objective implements ObjectiveInterface { try { ledggerSideEffects = await updated.updateLedgerToRemoveGuarantee(updated.toMyLeft!, signer); } catch (err) { - throw new Error(`error updating ledger funding: ${err}`); + throw new WrappedError('error updating ledger funding', err as Error); } sideEffects.merge(ledggerSideEffects); } @@ -587,7 +588,7 @@ export class Objective implements ObjectiveInterface { try { ledgerSideEffects = await updated.updateLedgerToRemoveGuarantee(updated.toMyRight!, signer); } catch (err) { - throw new Error(`error updating ledger funding: ${err}`); + throw new WrappedError('error updating ledger funding', err as Error); } sideEffects.merge(ledgerSideEffects); } @@ -635,7 +636,7 @@ export class Objective implements ObjectiveInterface { try { await ledger.propose(this.ledgerProposal(ledger), signer); } catch (err) { - throw new Error(`error proposing ledger update: ${err}`); + throw new WrappedError('error proposing ledger update', err as Error); } const receipient = ledger.follower(); @@ -653,7 +654,7 @@ export class Objective implements ObjectiveInterface { try { sp = await ledger.signNextProposal(this.ledgerProposal(ledger), signer); } catch (err) { - throw new Error(`could not sign proposal: ${err}`); + throw new WrappedError('could not sign proposal', err as Error); } // ledger sideEffect @@ -723,7 +724,7 @@ export class Objective implements ObjectiveInterface { updated.minimumPaymentAmount, ); } catch (err) { - throw new Error(`outcome failed validation ${err}`); + throw new WrappedError('outcome failed validation', err as Error); } const ok = updated.v!.addSignedState(ss); @@ -794,7 +795,10 @@ export class Objective implements ObjectiveInterface { } if (err) { - throw new Error(`error incorporating signed proposal ${JSONbigNative.stringify(sp)} into objective: ${err}`); + throw new WrappedError( + `error incorporating signed proposal ${JSONbigNative.stringify(sp)} into objective`, + err as Error, + ); } } diff --git a/packages/nitro-node/src/protocols/virtualfund/virtualfund.ts b/packages/nitro-node/src/protocols/virtualfund/virtualfund.ts index 280128e9..2e655ea1 100644 --- a/packages/nitro-node/src/protocols/virtualfund/virtualfund.ts +++ b/packages/nitro-node/src/protocols/virtualfund/virtualfund.ts @@ -7,6 +7,7 @@ import Channel from '@cerc-io/ts-channel'; import type { ReadWriteChannel } from '@cerc-io/ts-channel'; import { FieldDescription, JSONbigNative, NitroSigner, Uint, Uint64, fromJSON, toJSON, + WrappedError, } from '@cerc-io/nitro-util'; import { Destination } from '../../types/destination'; @@ -347,7 +348,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { return objective; } catch (err) { - throw new Error(`Error creating objective: ${err}`); + throw new WrappedError('Error creating objective', err as Error); } } @@ -461,7 +462,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { ss = SignedState.fromJSON(b.toString()); } catch (err) { - throw new Error(`could not unmarshal signed state: ${err}`); + throw new WrappedError('could not unmarshal signed state', err as Error); } return ss; } @@ -478,7 +479,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { initialState = this.getSignedStatePayload(p.payloadData); } catch (err) { - throw new Error(`could not get signed state payload: ${err}`); + throw new WrappedError('could not get signed state payload', err as Error); } const { participants } = initialState.state(); assert(participants); @@ -628,7 +629,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { } if (err) { - throw new Error(`error incorporating signed proposal ${sp} into objective: ${err}`); + throw new WrappedError(`error incorporating signed proposal ${sp} into objective`, err as Error); } } @@ -645,7 +646,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { payload = this.getPayload(raw); } catch (err) { - throw new Error(`error parsing payload: ${err}`); + throw new WrappedError('error parsing payload', err as Error); } const updated = this.clone(); @@ -686,7 +687,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { ledgerSideEffects = await updated.updateLedgerWithGuarantee(updated.toMyLeft!, signer); } catch (err) { - throw new Error(`error updating ledger funding: ${err}`); + throw new WrappedError('error updating ledger funding', err as Error); } sideEffects.merge(ledgerSideEffects); } @@ -696,7 +697,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { ledgerSideEffects = await updated.updateLedgerWithGuarantee(updated.toMyRight!, signer); } catch (err) { - throw new Error(`error updating ledger funding: ${err}`); + throw new WrappedError('error updating ledger funding', err as Error); } sideEffects.merge(ledgerSideEffects); } @@ -825,7 +826,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { sp = await ledger.signNextProposal(c.expectedProposal(), signer); } catch (err) { - throw new Error(`no proposed state found for ledger channel ${err}`); + throw new WrappedError(`no proposed state found for ledger channel ${err}`, err as Error); } const sideEffects = new SideEffects({}); @@ -858,7 +859,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { se = await this.proposeLedgerUpdate(ledgerConnection, signer); } catch (err) { - throw new Error(`error proposing ledger update: ${err}`); + throw new WrappedError('error proposing ledger update', err as Error); } sideEffects = se; } else { @@ -869,7 +870,7 @@ export class Objective implements ObjectiveInterface, ProposalReceiver { try { se = await this.acceptLedgerUpdate(ledgerConnection, signer); } catch (err) { - throw new Error(`error proposing ledger update: ${err}`); + throw new WrappedError('error proposing ledger update', err as Error); } sideEffects = se; diff --git a/packages/nitro-node/src/utils/nitro.ts b/packages/nitro-node/src/utils/nitro.ts index e74d8cc2..6aff6379 100644 --- a/packages/nitro-node/src/utils/nitro.ts +++ b/packages/nitro-node/src/utils/nitro.ts @@ -3,13 +3,14 @@ import { providers } from 'ethers'; // @ts-expect-error import type { Peer } from '@cerc-io/peer'; -import { NitroSigner, DEFAULT_ASSET } from '@cerc-io/nitro-util'; +import { NitroSigner, DEFAULT_ASSET, Context } from '@cerc-io/nitro-util'; +import Channel from '@cerc-io/ts-channel'; import { Node } from '../node/node'; import { P2PMessageService } from '../node/engine/messageservice/p2p-message-service/service'; import { Store } from '../node/engine/store/store'; import { Destination } from '../types/destination'; -import { LedgerChannelInfo, PaymentChannelInfo } from '../node/query/types'; +import { ChannelStatus, LedgerChannelInfo, PaymentChannelInfo } from '../node/query/types'; import { createOutcome } from './helpers'; import { ChainService } from '../node/engine/chainservice/chainservice'; @@ -227,6 +228,97 @@ export class Nitro { return this.node.getPaymentChannelsByLedger(ledgerChannelId); } + async waitForPaymentChannelStatus( + channelId: string, + status: ChannelStatus, + ctx: Context, + ) { + const paymentUpdatesChannel = await this.node.paymentUpdates(); + + while (true) { + /* eslint-disable default-case */ + /* eslint-disable no-await-in-loop */ + switch (await Channel.select([ + paymentUpdatesChannel.shift(), + ctx.done.shift(), + ])) { + case paymentUpdatesChannel: { + const paymentInfo = paymentUpdatesChannel.value(); + if (paymentInfo.iD.string() === channelId && paymentInfo.status === status) { + return; + } + break; + } + + case ctx.done: { + return; + } + } + } + } + + async waitForLedgerChannelStatus( + channelId: string, + status: ChannelStatus, + ctx: Context, + ) { + const ledgerUpdatesChannel = await this.node.ledgerUpdates(); + + while (true) { + /* eslint-disable default-case */ + /* eslint-disable no-await-in-loop */ + switch (await Channel.select([ + ledgerUpdatesChannel.shift(), + ctx.done.shift(), + ])) { + case ledgerUpdatesChannel: { + const ledgerInfo = ledgerUpdatesChannel.value(); + if (ledgerInfo.iD.string() === channelId && ledgerInfo.status === status) { + return; + } + break; + } + + case ctx.done: { + return; + } + } + } + } + + async onPaymentChannelUpdated( + channelId: string, + callback: (info: PaymentChannelInfo) => void, + ctx: Context, + ) { + const wrapperFn = (info: PaymentChannelInfo) => { + if (info.iD.string().toLowerCase() === channelId.toLowerCase()) { + callback(info); + } + }; + + const paymentUpdatesChannel = await this.node.paymentUpdates(); + + while (true) { + /* eslint-disable default-case */ + /* eslint-disable no-await-in-loop */ + switch (await Channel.select([ + paymentUpdatesChannel.shift(), + ctx.done.shift(), + ])) { + case paymentUpdatesChannel: { + const paymentInfo = paymentUpdatesChannel.value(); + wrapperFn(paymentInfo); + break; + } + + case ctx.done: { + return; + } + } + } + } + async close() { await this.store.close(); await this.msgService.close(); diff --git a/packages/nitro-util/DEVELOPMENT.md b/packages/nitro-util/DEVELOPMENT.md new file mode 100644 index 00000000..a4c99833 --- /dev/null +++ b/packages/nitro-util/DEVELOPMENT.md @@ -0,0 +1,28 @@ +# Development + +## Generate contract bindings + +* Clone the go-nitro repo () and run `yarn` in root of repo. + +* Move to `go-nitro/packages/nitro-protocol/` and run `yarn hardhat compile` to compile the contract + + ```bash + $ yarn hardhat compile + Generating typings for: 43 artifacts in dir: typechain-types for target: ethers-v5 + Successfully generated 67 typings! + Compiled 42 Solidity files successfully + ``` + +* Copy files `ConsensusApp.json` `NitroAdjudicator.json` `VirtualPaymentApp.json` from go-nitro `packages/nitro-protocol/artifacts` to ts-nitro `packages/nitro-util/contracts` + +* In ts-nitro `packages/nitro-util` run `yarn generate-bindings` to generate the contract bindings + + ```bash + $ yarn generate-bindings + + yarn run v1.22.19 + $ ./scripts/generate-bindings.sh + $ /ts-nitro/node_modules/.bin/typechain --target ethers-v5 --out-dir ./src/contract-bindings ./contracts/NitroAdjudicator.json ./contracts/ConsensusApp.json ./contracts/VirtualPaymentApp.json ./contracts/Token.json + Successfully generated 11 typings! + Done in 1.27s. + ``` diff --git a/packages/nitro-util/README.md b/packages/nitro-util/README.md new file mode 100644 index 00000000..71f3412e --- /dev/null +++ b/packages/nitro-util/README.md @@ -0,0 +1,5 @@ +# nitro-util + +## Development + +* [README](./DEVELOPMENT.md) diff --git a/packages/nitro-util/contracts/NitroAdjudicator.json b/packages/nitro-util/contracts/NitroAdjudicator.json index 1d8539bb..1e7c6d43 100644 --- a/packages/nitro-util/contracts/NitroAdjudicator.json +++ b/packages/nitro-util/contracts/NitroAdjudicator.json @@ -288,6 +288,25 @@ "name": "ChallengeRegistered", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "channelId", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint48", + "name": "newTurnNumRecord", + "type": "uint48" + } + ], + "name": "Checkpointed", + "type": "event" + }, { "anonymous": false, "inputs": [ diff --git a/packages/nitro-util/package.json b/packages/nitro-util/package.json index 6e0879ae..ce7fb91b 100644 --- a/packages/nitro-util/package.json +++ b/packages/nitro-util/package.json @@ -39,7 +39,7 @@ }, "dependencies": { "@cerc-io/ts-channel": "1.0.3-ts-nitro-0.1.1", - "@statechannels/nitro-protocol": "^2.0.1-alpha.5", + "@statechannels/nitro-protocol": "^2.0.1-alpha.6", "assert": "^2.0.0", "debug": "^4.3.4", "ethers": "^5.7.2", diff --git a/packages/nitro-util/src/contract-bindings/NitroAdjudicator.ts b/packages/nitro-util/src/contract-bindings/NitroAdjudicator.ts index f80468ee..46b8006a 100644 --- a/packages/nitro-util/src/contract-bindings/NitroAdjudicator.ts +++ b/packages/nitro-util/src/contract-bindings/NitroAdjudicator.ts @@ -302,6 +302,7 @@ export interface NitroAdjudicatorInterface extends utils.Interface { "AllocationUpdated(bytes32,uint256,uint256,uint256)": EventFragment; "ChallengeCleared(bytes32,uint48)": EventFragment; "ChallengeRegistered(bytes32,uint48,tuple[],tuple)": EventFragment; + "Checkpointed(bytes32,uint48)": EventFragment; "Concluded(bytes32,uint48)": EventFragment; "Deposited(bytes32,address,uint256)": EventFragment; "Reclaimed(bytes32,uint256)": EventFragment; @@ -310,6 +311,7 @@ export interface NitroAdjudicatorInterface extends utils.Interface { getEvent(nameOrSignatureOrTopic: "AllocationUpdated"): EventFragment; getEvent(nameOrSignatureOrTopic: "ChallengeCleared"): EventFragment; getEvent(nameOrSignatureOrTopic: "ChallengeRegistered"): EventFragment; + getEvent(nameOrSignatureOrTopic: "Checkpointed"): EventFragment; getEvent(nameOrSignatureOrTopic: "Concluded"): EventFragment; getEvent(nameOrSignatureOrTopic: "Deposited"): EventFragment; getEvent(nameOrSignatureOrTopic: "Reclaimed"): EventFragment; @@ -360,6 +362,17 @@ export type ChallengeRegisteredEvent = TypedEvent< export type ChallengeRegisteredEventFilter = TypedEventFilter; +export interface CheckpointedEventObject { + channelId: string; + newTurnNumRecord: number; +} +export type CheckpointedEvent = TypedEvent< + [string, number], + CheckpointedEventObject +>; + +export type CheckpointedEventFilter = TypedEventFilter; + export interface ConcludedEventObject { channelId: string; finalizesAt: number; @@ -781,6 +794,15 @@ export interface NitroAdjudicator extends BaseContract { candidate?: null ): ChallengeRegisteredEventFilter; + "Checkpointed(bytes32,uint48)"( + channelId?: BytesLike | null, + newTurnNumRecord?: null + ): CheckpointedEventFilter; + Checkpointed( + channelId?: BytesLike | null, + newTurnNumRecord?: null + ): CheckpointedEventFilter; + "Concluded(bytes32,uint48)"( channelId?: BytesLike | null, finalizesAt?: null diff --git a/packages/nitro-util/src/contract-bindings/factories/NitroAdjudicator__factory.ts b/packages/nitro-util/src/contract-bindings/factories/NitroAdjudicator__factory.ts index e38a633e..e4259b11 100644 --- a/packages/nitro-util/src/contract-bindings/factories/NitroAdjudicator__factory.ts +++ b/packages/nitro-util/src/contract-bindings/factories/NitroAdjudicator__factory.ts @@ -295,6 +295,25 @@ const _abi = [ name: "ChallengeRegistered", type: "event", }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "bytes32", + name: "channelId", + type: "bytes32", + }, + { + indexed: false, + internalType: "uint48", + name: "newTurnNumRecord", + type: "uint48", + }, + ], + name: "Checkpointed", + type: "event", + }, { anonymous: false, inputs: [ diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index bb6be336..a8be87fc 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -279,8 +279,6 @@ const main = async () => { await new Promise((resolve) => { setTimeout(() => resolve(), 1000); }); await nitro.close(); - - process.exit(0); } }; diff --git a/yarn.lock b/yarn.lock index 8056888b..a20e85ba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3949,10 +3949,10 @@ ethers "^5.1.4" lodash "^4.17.21" -"@statechannels/nitro-protocol@^2.0.1-alpha.5": - version "2.0.1-alpha.5" - resolved "https://registry.yarnpkg.com/@statechannels/nitro-protocol/-/nitro-protocol-2.0.1-alpha.5.tgz#3aa09dacf6780a47ff415de23123f9aafe2534ec" - integrity sha512-CT32i5ZlZ0Jz8mAOmywALCnS/+Cl7ntrRlJr2r3jGBkkzNBWxHWR4qXg6HTUh7xxm5tY/g/TSmsGH8/z54K/Sw== +"@statechannels/nitro-protocol@^2.0.1-alpha.6": + version "2.0.1-alpha.6" + resolved "https://registry.yarnpkg.com/@statechannels/nitro-protocol/-/nitro-protocol-2.0.1-alpha.6.tgz#1bb59365eb78489eef2dfa73733bc1cad9890685" + integrity sha512-VehCgq3AOVTGCvGGIoF23YyQX+x1IbLzByFWbqPovucsm9vW0udR7r8Okw9hC0ZHanHOjQH5KxJL6aKUnrXinw== dependencies: "@openzeppelin/contracts" "^4.7.3" "@statechannels/exit-format" "^0.2.0"