diff --git a/.yarn/cache/chnl-npm-1.2.0-0147cf365c-78044132c0.zip b/.yarn/cache/chnl-npm-1.2.0-0147cf365c-78044132c0.zip deleted file mode 100644 index ba3aab3f..00000000 Binary files a/.yarn/cache/chnl-npm-1.2.0-0147cf365c-78044132c0.zip and /dev/null differ diff --git a/.yarn/cache/es-abstract-npm-1.18.0-ac2faa8a98-98b2dd3778.zip b/.yarn/cache/es-abstract-npm-1.18.0-ac2faa8a98-98b2dd3778.zip deleted file mode 100644 index 3b2441ed..00000000 Binary files a/.yarn/cache/es-abstract-npm-1.18.0-ac2faa8a98-98b2dd3778.zip and /dev/null differ diff --git a/.yarn/cache/promise-controller-npm-1.0.0-3e6f67a83d-80e1a43d37.zip b/.yarn/cache/promise-controller-npm-1.0.0-3e6f67a83d-80e1a43d37.zip deleted file mode 100644 index e29144a3..00000000 Binary files a/.yarn/cache/promise-controller-npm-1.0.0-3e6f67a83d-80e1a43d37.zip and /dev/null differ diff --git a/.yarn/cache/promise.prototype.finally-npm-3.1.2-18b6014744-e0b6e94d32.zip b/.yarn/cache/promise.prototype.finally-npm-3.1.2-18b6014744-e0b6e94d32.zip deleted file mode 100644 index 8c9fdcd4..00000000 Binary files a/.yarn/cache/promise.prototype.finally-npm-3.1.2-18b6014744-e0b6e94d32.zip and /dev/null differ diff --git a/.yarn/cache/promised-map-npm-1.0.0-22c41839f5-716cc4b1be.zip b/.yarn/cache/promised-map-npm-1.0.0-22c41839f5-716cc4b1be.zip deleted file mode 100644 index fee23f5f..00000000 Binary files a/.yarn/cache/promised-map-npm-1.0.0-22c41839f5-716cc4b1be.zip and /dev/null differ diff --git a/.yarn/cache/websocket-as-promised-npm-2.0.1-289ab937b7-68dfd25be9.zip b/.yarn/cache/websocket-as-promised-npm-2.0.1-289ab937b7-68dfd25be9.zip deleted file mode 100644 index f448c115..00000000 Binary files a/.yarn/cache/websocket-as-promised-npm-2.0.1-289ab937b7-68dfd25be9.zip and /dev/null differ diff --git a/README.md b/README.md index 72a598fc..e9c73a62 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,15 @@ The worker uses the webcrypto api if it is run in the browser. This library is o defined if you access the webpage with `localhost` in firefox. It is not available on `127.0.0.1` or `0.0.0.0` due to browser security policies. +## Testing +Use the below command to only execute a particular test suite. + +**Note:** The worker tests are skipped by default, as they need a running setup. + +```bash +// execute worker tests +yarn test --runTestsByPath packages/worker-api/src/integriteeWorker.spec.ts +``` ```bash yarn add @encointer/node-api @encointer/worker-api diff --git a/lerna.json b/lerna.json index cc5ff0be..6744e2b8 100644 --- a/lerna.json +++ b/lerna.json @@ -6,5 +6,5 @@ "publishConfig": { "directory": "build" }, - "version": "0.15.2" + "version": "0.16.0-alpha.0" } diff --git a/packages/node-api/package.json b/packages/node-api/package.json index 9e22b709..0ced22ee 100644 --- a/packages/node-api/package.json +++ b/packages/node-api/package.json @@ -18,10 +18,10 @@ }, "sideEffects": false, "type": "module", - "version": "0.15.2", + "version": "0.16.0-alpha.0", "main": "index.js", "dependencies": { - "@encointer/types": "^0.15.2", + "@encointer/types": "^0.16.0-alpha.0", "@polkadot/api": "^11.2.1", "tslib": "^2.6.2" }, diff --git a/packages/types/package.json b/packages/types/package.json index 7ef71931..9c8900c5 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -18,7 +18,7 @@ }, "sideEffects": false, "type": "module", - "version": "0.15.2", + "version": "0.16.0-alpha.0", "main": "index.js", "scripts": { "generate:defs": "node --experimental-specifier-resolution=node --loader ts-node/esm ../../node_modules/.bin/polkadot-types-from-defs --package @encointer/types/interfaces --input ./src/interfaces", diff --git a/packages/util/package.json b/packages/util/package.json index 298e6415..8aee88f6 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -20,7 +20,7 @@ "sideEffects": false, "type": "module", "types": "./index.d.ts", - "version": "0.15.2", + "version": "0.16.0-alpha.0", "main": "index.js", "dependencies": { "@babel/runtime": "^7.18.9", diff --git a/packages/worker-api/package.json b/packages/worker-api/package.json index ae0f7f59..ff9d9dae 100644 --- a/packages/worker-api/package.json +++ b/packages/worker-api/package.json @@ -19,12 +19,12 @@ "sideEffects": false, "type": "module", "types": "./index.d.ts", - "version": "0.15.2", + "version": "0.16.0-alpha.0", "main": "index.js", "dependencies": { - "@encointer/node-api": "^0.15.2", - "@encointer/types": "^0.15.2", - "@encointer/util": "^0.15.2", + "@encointer/node-api": "^0.16.0-alpha.0", + "@encointer/types": "^0.16.0-alpha.0", + "@encointer/util": "^0.16.0-alpha.0", "@peculiar/webcrypto": "^1.4.6", "@polkadot/api": "^11.2.1", "@polkadot/keyring": "^12.6.2", @@ -32,13 +32,15 @@ "@polkadot/util": "^12.6.2", "@polkadot/util-crypto": "^12.6.2", "@polkadot/wasm-crypto": "^7.3.2", + "@polkadot/x-global": "^12.6.2", + "@polkadot/x-ws": "^12.6.2", "bs58": "^4.0.1", - "promised-map": "^1.0.0", - "websocket": "^1.0.34", - "websocket-as-promised": "^2.0.1" + "eventemitter3": "^5.0.1", + "tslib": "^2.6.2" }, "devDependencies": { - "@types/bs58": "^4.0.4" + "@types/bs58": "^4.0.4", + "websocket": "^1.0.34" }, "peerDependencies": { "@polkadot/x-randomvalues": "^12.3.2" diff --git a/packages/worker-api/src/encointerWorker.spec.ts b/packages/worker-api/src/encointerWorker.spec.ts deleted file mode 100644 index d9751912..00000000 --- a/packages/worker-api/src/encointerWorker.spec.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { Keyring } from '@polkadot/api'; -import { cryptoWaitReady } from '@polkadot/util-crypto'; -import { localDockerNetwork } from './testUtils/networks.js'; -import { EncointerWorker } from './encointerWorker.js'; -import WS from 'websocket'; - -const {w3cwebsocket: WebSocket} = WS; - -describe('worker', () => { - const network = localDockerNetwork(); - let keyring: Keyring; - let worker: EncointerWorker; - // let alice: KeyringPair; - // let charlie: KeyringPair; - beforeAll(async () => { - jest.setTimeout(90000); - await cryptoWaitReady(); - keyring = new Keyring({type: 'sr25519'}); - // alice = keyring.addFromUri('//Alice', {name: 'Alice default'}); - // charlie = keyring.addFromUri('//Charlie', {name: 'Bob default'}); - - worker = new EncointerWorker(network.worker, { - keyring: keyring, - types: network.customTypes, - // @ts-ignore - createWebSocket: (url) => new WebSocket( - url, - undefined, - undefined, - undefined, - // Allow the worker's self-signed certificate - { rejectUnauthorized: false } - ), - api: null, - }); - }); - - // skip it, as this requires a worker (and hence a node) to be running - // To my knowledge jest does not have an option to run skipped tests specifically, does it? - // Todo: add proper CI to test this too. - describe.skip('needs worker and node running', () => { - // Tests specific for the encointer protocol - describe('encointer-worker', () => { - describe('getTotalIssuance', () => { - it('should return value', async () => { - const result = await worker.getTotalIssuance(network.chosenCid); - // console.debug('getTotalIssuance', result); - expect(result).toBeDefined(); - }); - }); - - describe('getParticipantCount', () => { - it('should return default value', async () => { - const result = await worker.getParticipantCount(network.chosenCid); - expect(result).toBe(0); - }); - }); - - describe('getMeetupCount', () => { - it('should return default value', async () => { - const result = await worker.getMeetupCount(network.chosenCid); - expect(result).toBe(0); - }); - }); - - describe('getCeremonyReward', () => { - it('should return default value', async () => { - const result = await worker.getCeremonyReward(network.chosenCid); - expect(result).toBe(1); - }); - }); - - describe('getLocationTolerance', () => { - it('should return default value', async () => { - const result = await worker.getLocationTolerance(network.chosenCid); - expect(result).toBe(1000); - }); - }); - - describe('getTimeTolerance', () => { - it('should return default value', async () => { - const result = await worker.getTimeTolerance(network.chosenCid); - expect(result.toNumber()).toBe(600000); - }); - }); - - describe('getSchedulerState', () => { - it('should return value', async () => { - const result = await worker.getSchedulerState(network.chosenCid); - // console.debug('schedulerStateResult', result); - expect(result).toBeDefined(); - }); - }); - - describe('getRegistration', () => { - it('should return default value', async () => { - await cryptoWaitReady(); - const bob = keyring.addFromUri('//Bob', {name: 'Bob default'}); - const result = await worker.getParticipantIndex(bob, network.chosenCid); - expect(result.toNumber()).toBe(0); - }); - }); - - describe('getMeetupIndex', () => { - it('should return default value', async () => { - await cryptoWaitReady(); - const bob = keyring.addFromUri('//Bob', {name: 'Bob default'}); - const result = await worker.getMeetupIndex(bob, network.chosenCid); - expect(result.toNumber()).toBe(0); - }); - }); - - describe('getAttestations', () => { - it('should be empty', async () => { - await cryptoWaitReady(); - const bob = keyring.addFromUri('//Bob', {name: 'Bob default'}); - const result = await worker.getAttestations(bob, network.chosenCid); - expect(result.toJSON()).toStrictEqual([]); - }); - }); - - describe('getMeetupRegistry method', () => { - it('should be empty', async () => { - await cryptoWaitReady(); - const bob = keyring.addFromUri('//Bob', {name: 'Bob default'}); - const result = await worker.getMeetupRegistry(bob, network.chosenCid); - expect(result.toJSON()).toStrictEqual([]); - }); - }); - }); - }); -}); diff --git a/packages/worker-api/src/encointerWorker.ts b/packages/worker-api/src/encointerWorker.ts deleted file mode 100644 index cdf0b47d..00000000 --- a/packages/worker-api/src/encointerWorker.ts +++ /dev/null @@ -1,107 +0,0 @@ -import type {u32, u64, Vec} from '@polkadot/types'; -import {communityIdentifierFromString} from '@encointer/util'; - -import type { - CommunityIdentifier, - MeetupIndexType, - ParticipantIndexType, - SchedulerState, - Attestation, -} from '@encointer/types'; - -import {type RequestOptions, Request} from './interface.js'; -import {callGetter} from './sendRequest.js'; -import {PubKeyPinPair, toAccount} from "@encointer/util/common"; -import type {KeyringPair} from "@polkadot/keyring/types"; -import {Worker} from "./worker.js"; -import type {AccountId, Balance, Moment} from "@polkadot/types/interfaces/runtime"; - -// Todo: This code is a WIP and will not work as is: https://github.com/encointer/encointer-js/issues/91 - -export class EncointerWorker extends Worker { - - public cidFromStr(cidStr: String): CommunityIdentifier { - return communityIdentifierFromString(this.registry(), cidStr); - } - - public async getNonce(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.TrustedGetter, 'nonce', 'u32'], { - shard: cid, - account: toAccount(accountOrPubKey, this.keyring()) - }, options) - } - - public async getTotalIssuance(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.PublicGetter, 'total_issuance', 'Balance'], {cid}, options) - } - - public async getParticipantCount(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return (await callGetter(this, [Request.PublicGetter, 'participant_count', 'u64'], {cid}, options)).toNumber() - } - - public async getMeetupCount(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return (await callGetter(this, [Request.PublicGetter, 'meetup_count', 'u64'], {cid}, options)).toNumber() - } - - public async getCeremonyReward(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.PublicGetter, 'ceremony_reward', 'I64F64'], {cid}, options) - } - - public async getLocationTolerance(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return (await callGetter(this, [Request.PublicGetter, 'location_tolerance', 'u32'], {cid}, options)).toNumber() - } - - public async getTimeTolerance(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.PublicGetter, 'time_tolerance', 'Moment'], {cid}, options) - } - - public async getSchedulerState(cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.PublicGetter, 'scheduler_state', 'SchedulerState'], {cid}, options) - } - - public async getBalance(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.TrustedGetter, 'free_balance', 'Balance'], { - shard: cid, - account: toAccount(accountOrPubKey,this.keyring()) - }, options) - } - - public async getParticipantIndex(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.TrustedGetter, 'participant_index', 'ParticipantIndexType'], { - cid, - account: toAccount(accountOrPubKey,this.keyring()) - }, options) - } - - public async getMeetupIndex(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.TrustedGetter, 'meetup_index', 'MeetupIndexType'], { - cid, - account: toAccount(accountOrPubKey,this.keyring()) - }, options) - } - - public async getAttestations(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise> { - return await callGetter>(this, [Request.TrustedGetter, 'attestations', 'Vec'], { - cid, - account: toAccount(accountOrPubKey,this.keyring()) - }, options) - } - - public async getMeetupRegistry(accountOrPubKey: KeyringPair | PubKeyPinPair, cid: string, options: RequestOptions = {} as RequestOptions): Promise> { - return await callGetter>(this, [Request.TrustedGetter, 'meetup_registry', 'Vec'], { - cid, - account: toAccount(accountOrPubKey, this.keyring()) - }, options) - } - - // Todo: `sendTrustedCall` must be generic over the trusted call or we have to duplicate code for encointer. - // async sendTrustedCall(call: EncointerTrustedCallSigned, shard: ShardIdentifier, options: CallOptions = {} as CallOptions): Promise { - // if (this.shieldingKey() == undefined) { - // const key = await this.getShieldingKey(options); - // console.log(`Setting the shielding pubKey of the worker.`) - // this.setShieldingKey(key); - // } - // - // return sendTrustedCall(this, call, shard, true, 'TrustedOperationResult', options); - // } -} diff --git a/packages/worker-api/src/index.ts b/packages/worker-api/src/index.ts index 90f94eda..56d16b90 100644 --- a/packages/worker-api/src/index.ts +++ b/packages/worker-api/src/index.ts @@ -1,5 +1,4 @@ export { Worker } from './worker.js'; -export { EncointerWorker } from './encointerWorker.js'; export { IntegriteeWorker } from './integriteeWorker.js'; export * from './interface.js'; diff --git a/packages/worker-api/src/integriteeWorker.spec.ts b/packages/worker-api/src/integriteeWorker.spec.ts index 0c3e3ff4..1cdfd156 100644 --- a/packages/worker-api/src/integriteeWorker.spec.ts +++ b/packages/worker-api/src/integriteeWorker.spec.ts @@ -2,9 +2,10 @@ import { Keyring } from '@polkadot/api'; import { cryptoWaitReady } from '@polkadot/util-crypto'; import {localDockerNetwork} from './testUtils/networks.js'; import { IntegriteeWorker } from './integriteeWorker.js'; -import WS from 'websocket'; import {type KeyringPair} from "@polkadot/keyring/types"; +import WS from 'websocket'; + const {w3cwebsocket: WebSocket} = WS; describe('worker', () => { @@ -22,7 +23,6 @@ describe('worker', () => { worker = new IntegriteeWorker(network.worker, { keyring: keyring, - types: network.customTypes, // @ts-ignore createWebSocket: (url) => new WebSocket( url, @@ -32,10 +32,12 @@ describe('worker', () => { // Allow the worker's self-signed certificate { rejectUnauthorized: false } ), - api: null, }); }); + afterAll(async () => { + await worker.closeWs() + }); // skip it, as this requires a worker (and hence a node) to be running // To my knowledge jest does not have an option to run skipped tests specifically, does it? @@ -59,8 +61,8 @@ describe('worker', () => { describe('getNonce', () => { it('should return value', async () => { - const result = await worker.getNonce(alice, network.mrenclave); - console.log('Nonce', result); + const result = await worker.getNonce(alice, network.shard); + console.log('Nonce', result.toHuman); expect(result).toBeDefined(); }); }); @@ -68,55 +70,55 @@ describe('worker', () => { describe('getAccountInfo', () => { it('should return value', async () => { - const result = await worker.getAccountInfo(alice, network.mrenclave); - console.log('getAccountInfo', result); + const result = await worker.getAccountInfo(alice, network.shard); + console.log('getAccountInfo', result.toHuman()); expect(result).toBeDefined(); }); }); describe('accountInfoGetter', () => { it('should return value', async () => { - const getter = await worker.accountInfoGetter(charlie, network.mrenclave); + const getter = await worker.accountInfoGetter(charlie, network.shard); console.log(`AccountInfoGetter: ${JSON.stringify(getter)}`); const result = await getter.send(); - console.log('getAccountInfo:', result); + console.log('getAccountInfo:', result.toHuman()); expect(result).toBeDefined(); }); }); describe('parentchainsInfoGetter', () => { it('should return value', async () => { - const getter = worker.parentchainsInfoGetter(network.mrenclave); + const getter = worker.parentchainsInfoGetter(network.shard); console.log(`parentchainsInfoGetter: ${JSON.stringify(getter)}`); const result = await getter.send(); - console.log('parentchainsInfoGetter:', result); + console.log('parentchainsInfoGetter:', result.toHuman()); expect(result).toBeDefined(); }); }); describe('guessTheNumberInfoGetter', () => { it('should return value', async () => { - const getter = worker.guessTheNumberInfoGetter(network.mrenclave); + const getter = worker.guessTheNumberInfoGetter(network.shard); console.log(`GuessTheNumberInfo: ${JSON.stringify(getter)}`); const result = await getter.send(); - console.log('GuessTheNumberInfo:', result); + console.log('GuessTheNumberInfo:', result.toHuman()); expect(result).toBeDefined(); }); }); describe('guessTheNumberAttemptsGetter', () => { it('should return value', async () => { - const getter = await worker.guessTheNumberAttemptsTrustedGetter(charlie, network.mrenclave); + const getter = await worker.guessTheNumberAttemptsTrustedGetter(charlie, network.shard); console.log(`Attempts: ${JSON.stringify(getter)}`); const result = await getter.send(); - console.log('Attempts:', result); + console.log('Attempts:', result.toHuman()); expect(result).toBeDefined(); }); }); describe('balance transfer should work', () => { it('should return value', async () => { - const shard = network.chosenCid; + const shard = network.shard; const result = await worker.trustedBalanceTransfer( alice, shard, @@ -125,14 +127,15 @@ describe('worker', () => { charlie.address, 1100000000000 ); - console.log('balance transfer result', result.toHuman()); + console.log('balance transfer result', JSON.stringify(result)); expect(result).toBeDefined(); }); }); - describe('balance unshield should work', () => { + // race condition so skipped + describe.skip('balance unshield should work', () => { it('should return value', async () => { - const shard = network.chosenCid; + const shard = network.shard; const result = await worker.balanceUnshieldFunds( alice, @@ -142,14 +145,15 @@ describe('worker', () => { charlie.address, 1100000000000, ); - console.log('balance unshield result', result.toHuman()); + console.log('balance unshield result', JSON.stringify(result)); expect(result).toBeDefined(); }); }); - describe('guess the number should work', () => { + // race condition, so skipped + describe.skip('guess the number should work', () => { it('should return value', async () => { - const shard = network.chosenCid; + const shard = network.shard; const result = await worker.guessTheNumber( alice, @@ -157,7 +161,7 @@ describe('worker', () => { network.mrenclave, 1, ); - console.log('guess the number result', result.toHuman()); + console.log('guess the number result', JSON.stringify(result)); expect(result).toBeDefined(); }); }); diff --git a/packages/worker-api/src/integriteeWorker.ts b/packages/worker-api/src/integriteeWorker.ts index 37765424..aa5968ba 100644 --- a/packages/worker-api/src/integriteeWorker.ts +++ b/packages/worker-api/src/integriteeWorker.ts @@ -1,4 +1,3 @@ -import type {Hash} from '@polkadot/types/interfaces/runtime'; import type { ShardIdentifier, IntegriteeTrustedCallSigned, @@ -7,19 +6,15 @@ import type { GuessTheNumberTrustedCall, GuessTheNumberPublicGetter, GuessTheNumberTrustedGetter, AttemptsArg, } from '@encointer/types'; import { - type RequestOptions, type ISubmittableGetter, - type JsonRpcRequest, type TrustedGetterArgs, type TrustedSignerOptions, type PublicGetterArgs, - type IWorker, - type PublicGetterParams, type TrustedGetterParams, + type PublicGetterParams, type TrustedGetterParams, type TrustedCallResult, } from './interface.js'; import {Worker} from "./worker.js"; -import {sendTrustedCall, sendWorkerRequest} from './sendRequest.js'; import { - createGetterRpc, createIntegriteeGetterPublic, + createIntegriteeGetterPublic, createSignedGetter, createTrustedCall, signTrustedCall, type TrustedCallArgs, type TrustedCallVariant, @@ -32,14 +27,14 @@ import {asString} from "@encointer/util"; export class IntegriteeWorker extends Worker { - public async getNonce(accountOrPubKey: AddressOrPair, shard: string, singerOptions?: TrustedSignerOptions, requestOptions?: RequestOptions): Promise { - const info = await this.getAccountInfo(accountOrPubKey, shard, singerOptions, requestOptions); + public async getNonce(accountOrPubKey: AddressOrPair, shard: string, singerOptions?: TrustedSignerOptions): Promise { + const info = await this.getAccountInfo(accountOrPubKey, shard, singerOptions); return info.nonce; } - public async getAccountInfo(accountOrPubKey: AddressOrPair, shard: string, singerOptions?: TrustedSignerOptions, requestOptions?: RequestOptions): Promise { + public async getAccountInfo(accountOrPubKey: AddressOrPair, shard: string, singerOptions?: TrustedSignerOptions): Promise { const getter = await this.accountInfoGetter(accountOrPubKey, shard, singerOptions); - return getter.send(requestOptions); + return getter.send(); } public async accountInfoGetter(accountOrPubKey: AddressOrPair, shard: string, signerOptions?: TrustedSignerOptions): Promise> { @@ -85,14 +80,13 @@ export class IntegriteeWorker extends Worker { to: String, amount: number, signerOptions?: TrustedSignerOptions, - requestOptions?: RequestOptions, - ): Promise { - const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions, requestOptions) + ): Promise { + const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions) const shardT = this.createType('ShardIdentifier', bs58.decode(shard)); const params = this.createType('BalanceTransferArgs', [from, to, amount]) const call = createTrustedCall(this, ['balance_transfer', 'BalanceTransferArgs'], params); const signed = await signTrustedCall(this, call, account, shardT, mrenclave, nonce, signerOptions); - return this.sendTrustedCall(signed, shardT, requestOptions); + return this.sendTrustedCall(signed, shardT); } public async balanceUnshieldFunds( @@ -103,15 +97,14 @@ export class IntegriteeWorker extends Worker { toPublicAddress: string, amount: number, signerOptions?: TrustedSignerOptions, - requestOptions?: RequestOptions, - ): Promise { - const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions, requestOptions) + ): Promise { + const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions) const shardT = this.createType('ShardIdentifier', bs58.decode(shard)); const params = this.createType('BalanceUnshieldArgs', [fromIncognitoAddress, toPublicAddress, amount, shardT]) const call = createTrustedCall(this, ['balance_unshield', 'BalanceUnshieldArgs'], params); const signed = await signTrustedCall(this, call, account, shardT, mrenclave, nonce, signerOptions); - return this.sendTrustedCall(signed, shardT, requestOptions); + return this.sendTrustedCall(signed, shardT); } public async guessTheNumber( @@ -120,9 +113,8 @@ export class IntegriteeWorker extends Worker { mrenclave: string, guess: number, signerOptions?: TrustedSignerOptions, - requestOptions?: RequestOptions, - ): Promise { - const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions, requestOptions) + ): Promise { + const nonce = signerOptions?.nonce ?? await this.getNonce(account, shard, signerOptions) const shardT = this.createType('ShardIdentifier', bs58.decode(shard)); const params = this.createType('GuessArgs', [asString(account), guess]) @@ -131,16 +123,20 @@ export class IntegriteeWorker extends Worker { const signed = await signTrustedCall(this, call, account, shardT, mrenclave, nonce, signerOptions); console.debug(`GuessTheNumber ${JSON.stringify(signed)}`); - return this.sendTrustedCall(signed, shardT, requestOptions); + return this.sendTrustedCall(signed, shardT); } - async sendTrustedCall(call: IntegriteeTrustedCallSigned, shard: ShardIdentifier, requestOptions?: RequestOptions): Promise { + async sendTrustedCall(call: IntegriteeTrustedCallSigned, shard: ShardIdentifier): Promise { if (this.shieldingKey() == undefined) { console.debug(`[sentTrustedCall] Setting the shielding pubKey of the worker.`) - await this.getShieldingKey(requestOptions); + await this.getShieldingKey(); } - return sendTrustedCall(this, call, shard, true, 'TrustedOperationResult', requestOptions); + const top = this.createType('IntegriteeTrustedOperation', { + direct_call: call + }) + + return this.submitAndWatchTop(top, shard); } } @@ -157,44 +153,40 @@ export class SubmittableGetter implements ISubmittableGe this.returnType = returnType; } - into_rpc(): JsonRpcRequest { - return createGetterRpc(this.worker, this.getter, this.shard); - } - - send(requestOptions?: RequestOptions): Promise { - const rpc = this.into_rpc(); - return sendWorkerRequest(this.worker, rpc, this.returnType, requestOptions); + async send(): Promise { + return this.worker.sendGetter(this.getter, this.shard, this.returnType); } } -export const submittableTrustedGetter = async (self: W, request: string, account: AddressOrPair, args: TrustedGetterArgs, trustedGetterParams: TrustedGetterParams, returnType: string)=> { +async function submittableTrustedGetter(self: W, request: string, account: AddressOrPair, args: TrustedGetterArgs, trustedGetterParams: TrustedGetterParams, returnType: string): Promise> { const {shard} = args; const shardT = self.createType('ShardIdentifier', bs58.decode(shard)); const signedGetter = await createSignedGetter(self, request, account, trustedGetterParams, { signer: args?.signer }) return new SubmittableGetter(self, shardT, signedGetter, returnType); } -export const submittablePublicGetter = (self: W, request: string, args: PublicGetterArgs, publicGetterParams: PublicGetterParams, returnType: string)=> { + +function submittablePublicGetter(self: W, request: string, args: PublicGetterArgs, publicGetterParams: PublicGetterParams, returnType: string): SubmittableGetter { const {shard} = args; const shardT = self.createType('ShardIdentifier', bs58.decode(shard)); const signedGetter = createIntegriteeGetterPublic(self, request, publicGetterParams) return new SubmittableGetter(self, shardT, signedGetter, returnType); } -export const guessTheNumberPublicGetter = ( - self: IWorker, +function guessTheNumberPublicGetter( + self: Worker, getterVariant: string, -): GuessTheNumberPublicGetter => { +): GuessTheNumberPublicGetter { return self.createType('GuessTheNumberPublicGetter', { [getterVariant]: null }); } -export const guessTheNumberTrustedGetter = ( - self: IWorker, +function guessTheNumberTrustedGetter( + self: Worker, getterVariant: string, params: GuessTheNumberTrustedGetterParams -): GuessTheNumberTrustedGetter => { +): GuessTheNumberTrustedGetter { return self.createType('GuessTheNumberTrustedGetter', { [getterVariant]: params }); @@ -203,11 +195,11 @@ export const guessTheNumberTrustedGetter = ( export type GuessTheNumberTrustedGetterParams = AttemptsArg | null; -export const guessTheNumberCall = ( - self: IWorker, +function guessTheNumberCall( + self: Worker, callVariant: TrustedCallVariant, params: TrustedCallArgs -): GuessTheNumberTrustedCall => { +): GuessTheNumberTrustedCall { const [variant, argType] = callVariant; return self.createType('GuessTheNumberTrustedCall', { diff --git a/packages/worker-api/src/interface.ts b/packages/worker-api/src/interface.ts index 38edfbfa..e7cb3606 100644 --- a/packages/worker-api/src/interface.ts +++ b/packages/worker-api/src/interface.ts @@ -1,28 +1,38 @@ -import WebSocketAsPromised from 'websocket-as-promised'; import {Keyring} from "@polkadot/keyring"; import type {u8} from "@polkadot/types-codec"; import type {TypeRegistry, u32, Vec} from "@polkadot/types"; import type {RegistryTypes, Signer} from "@polkadot/types/types"; import type {AddressOrPair} from "@polkadot/api-base/types/submittable"; -import {Worker} from "./worker.js"; import type { GuessTheNumberPublicGetter, GuessTheNumberTrustedGetter, IntegriteeGetter, - ShardIdentifier + ShardIdentifier, TrustedOperationStatus } from "@encointer/types"; +import type {Hash} from "@polkadot/types/interfaces/runtime"; -export interface IWorker extends WebSocketAsPromised { - rsCount: number; - rqStack: string[]; - keyring: () => Keyring | undefined; +export interface IWorkerBase { createType: (apiType: string, obj?: any) => any; - open: () => Promise; encrypt: (data: Uint8Array) => Promise> registry: () => TypeRegistry } -export interface ISubmittableGetter { +export interface GenericGetter { + toU8a(): Uint8Array, + toHex(): string +} + +export interface GenericTop { + toU8a(): Uint8Array, + toHex(): string +} + +export interface TrustedCallResult { + topHash?: Hash, + status?: TrustedOperationStatus, +} + +export interface ISubmittableGetter { worker: W; @@ -32,27 +42,9 @@ export interface ISubmittableGetter { returnType: string, - into_rpc(): JsonRpcRequest; - send(): Promise; } -export interface JsonRpcRequest { - jsonrpc: string; - method: string; - params?: any; - id: number | string; -} - -export function createJsonRpcRequest(method: string, params: any, id: number | string): JsonRpcRequest { - return { - jsonrpc: '2.0', - method: method, - params: params, - id: id - }; -} - export interface WorkerOptions { keyring?: Keyring; types?: RegistryTypes; @@ -85,18 +77,3 @@ export interface PublicGetterArgs { } export type PublicGetterParams = GuessTheNumberPublicGetter | null - -export type RequestArgs = PublicGetterArgs | TrustedGetterArgs | { } - -export interface RequestOptions { - timeout?: number; - debug?: boolean; -} - -export enum Request { - TrustedGetter, - PublicGetter, - Worker -} - -export type WorkerMethod = [ Request, string, string ] diff --git a/packages/worker-api/src/parsers.ts b/packages/worker-api/src/parsers.ts deleted file mode 100644 index 80738d18..00000000 --- a/packages/worker-api/src/parsers.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {parseI64F64} from '@encointer/util'; -import {u8aToBn} from '@polkadot/util'; - -import type {IWorker} from './interface.js'; -import type {BalanceEntry} from "@encointer/types"; - -export function parseBalance(self: IWorker, data: any): BalanceEntry { - const balanceEntry = self.createType('BalanceEntry', data); - // Todo: apply demurrage - return self.createType('BalanceEntry', - { - principal: parseI64F64(balanceEntry.principal), - last_update: balanceEntry.last_update - } - ); -} - -export function parseBalanceType(data: any): number { - return parseI64F64(u8aToBn(data)); -} - - diff --git a/packages/worker-api/src/requests.ts b/packages/worker-api/src/requests.ts index d6a8cc3b..cf32b1da 100644 --- a/packages/worker-api/src/requests.ts +++ b/packages/worker-api/src/requests.ts @@ -1,6 +1,5 @@ import { - createJsonRpcRequest, - type IWorker, type PublicGetterParams, type TrustedGetterParams, + type IWorkerBase, type PublicGetterParams, type TrustedGetterParams, type TrustedSignerOptions } from "./interface.js"; import type { @@ -17,7 +16,7 @@ import type {u32} from "@polkadot/types"; import bs58 from "bs58"; import type {AddressOrPair} from "@polkadot/api-base/types/submittable"; -export const createIntegriteeGetterPublic = (self: IWorker, request: string, publicGetterParams: PublicGetterParams) => { +export const createIntegriteeGetterPublic = (self: IWorkerBase, request: string, publicGetterParams: PublicGetterParams) => { const g = self.createType('IntegriteeGetter', { public: { [request]: publicGetterParams , @@ -26,18 +25,18 @@ export const createIntegriteeGetterPublic = (self: IWorker, request: string, pub return g; } -export const createSignedGetter = async (self: IWorker, request: string, account: AddressOrPair, trustedGetterParams: TrustedGetterParams, options: TrustedSignerOptions) => { +export const createSignedGetter = async (self: IWorkerBase, request: string, account: AddressOrPair, trustedGetterParams: TrustedGetterParams, options: TrustedSignerOptions) => { const trustedGetter = createTrustedGetter(self, request, trustedGetterParams); return await signTrustedGetter(self, account, trustedGetter, options); } -export const createTrustedGetter = (self: IWorker, request: string, params: TrustedGetterParams) => { +export const createTrustedGetter = (self: IWorkerBase, request: string, params: TrustedGetterParams) => { return self.createType('IntegriteeTrustedGetter', { [request]: params }); } -export async function signTrustedGetter(self: IWorker, account: AddressOrPair, getter: IntegriteeTrustedGetter, options?: TrustedSignerOptions): Promise { +export async function signTrustedGetter(self: IWorkerBase, account: AddressOrPair, getter: IntegriteeTrustedGetter, options?: TrustedSignerOptions): Promise { const signature = await signPayload(account, getter.toU8a(), options?.signer); const g = self.createType('IntegriteeGetter', { trusted: { @@ -50,24 +49,12 @@ export async function signTrustedGetter(self: IWorker, account: AddressOrPair, g return g; } -export const createGetterRpc = (self: IWorker, getter: IntegriteeGetter, shard: ShardIdentifier) => { - const r = self.createType( - 'Request', { - shard: shard, - cyphertext: getter.toHex() - } - ); - - return createJsonRpcRequest('state_executeGetter', [r.toHex()], 1); -} - - export type TrustedCallArgs = (BalanceTransferArgs | BalanceUnshieldArgs | GuessTheNumberTrustedCall); export type TrustedCallVariant = [string, string] export const createTrustedCall = ( - self: IWorker, + self: IWorkerBase, trustedCall: TrustedCallVariant, params: TrustedCallArgs ): IntegriteeTrustedCall => { @@ -79,7 +66,7 @@ export const createTrustedCall = ( } export const signTrustedCall = async ( - self: IWorker, + self: IWorkerBase, call: IntegriteeTrustedCall, account: AddressOrPair, shard: ShardIdentifier, diff --git a/packages/worker-api/src/rpc-provider/README.md b/packages/worker-api/src/rpc-provider/README.md new file mode 100644 index 00000000..39c347bf --- /dev/null +++ b/packages/worker-api/src/rpc-provider/README.md @@ -0,0 +1,5 @@ +The RPC provider is a subset of https://github.com/polkadot-js/api/tree/master/packages/rpc-provider. + +However, it contains a small but crucial change for us: + +The reason it lives here, is only because we want to be able to inject a websocket for integration testing against local setups for the integritee worker, which means the websocket needs accept self-signed certificates. diff --git a/packages/worker-api/src/rpc-provider/src/bundle.ts b/packages/worker-api/src/rpc-provider/src/bundle.ts new file mode 100644 index 00000000..0a4d3a2b --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/bundle.ts @@ -0,0 +1,4 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export { WsProvider } from './ws/index.js'; diff --git a/packages/worker-api/src/rpc-provider/src/coder/decodeResponse.spec.ts b/packages/worker-api/src/rpc-provider/src/coder/decodeResponse.spec.ts new file mode 100644 index 00000000..293fd641 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/decodeResponse.spec.ts @@ -0,0 +1,70 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import type { JsonRpcResponse } from '../types.js'; + +import { RpcCoder } from './index.js'; + +describe('decodeResponse', (): void => { + let coder: RpcCoder; + + beforeEach((): void => { + coder = new RpcCoder(); + }); + + it('expects a non-empty input object', (): void => { + expect( + () => coder.decodeResponse(undefined as unknown as JsonRpcResponse) + ).toThrow(/Invalid jsonrpc/); + }); + + it('expects a valid jsonrpc field', (): void => { + expect( + () => coder.decodeResponse({} as JsonRpcResponse) + ).toThrow(/Invalid jsonrpc/); + }); + + it('expects a valid id field', (): void => { + expect( + () => coder.decodeResponse({ jsonrpc: '2.0' } as JsonRpcResponse) + ).toThrow(/Invalid id/); + }); + + it('expects a valid result field', (): void => { + expect( + () => coder.decodeResponse({ id: 1, jsonrpc: '2.0' } as JsonRpcResponse) + ).toThrow(/No result/); + }); + + it('throws any error found', (): void => { + expect( + () => coder.decodeResponse({ error: { code: 123, message: 'test error' }, id: 1, jsonrpc: '2.0' } as JsonRpcResponse) + ).toThrow(/123: test error/); + }); + + it('throws any error found, with data', (): void => { + expect( + () => coder.decodeResponse({ error: { code: 123, data: 'Error("Some random error description")', message: 'test error' }, id: 1, jsonrpc: '2.0' } as JsonRpcResponse) + ).toThrow(/123: test error: Some random error description/); + }); + + it('allows for number subscription ids', (): void => { + expect( + coder.decodeResponse({ id: 1, jsonrpc: '2.0', method: 'test', params: { result: 'test result', subscription: 1 } } as JsonRpcResponse) + ).toEqual('test result'); + }); + + it('allows for string subscription ids', (): void => { + expect( + coder.decodeResponse({ id: 1, jsonrpc: '2.0', method: 'test', params: { result: 'test result', subscription: 'abc' } } as JsonRpcResponse) + ).toEqual('test result'); + }); + + it('returns the result', (): void => { + expect( + coder.decodeResponse({ id: 1, jsonrpc: '2.0', result: 'some result' } as JsonRpcResponse) + ).toEqual('some result'); + }); +}); diff --git a/packages/worker-api/src/rpc-provider/src/coder/encodeJson.spec.ts b/packages/worker-api/src/rpc-provider/src/coder/encodeJson.spec.ts new file mode 100644 index 00000000..5e166e8b --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/encodeJson.spec.ts @@ -0,0 +1,20 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { RpcCoder } from './index.js'; + +describe('encodeJson', (): void => { + let coder: RpcCoder; + + beforeEach((): void => { + coder = new RpcCoder(); + }); + + it('encodes a valid JsonRPC JSON string', (): void => { + expect( + coder.encodeJson('method', ['params']) + ).toEqual([1, '{"id":1,"jsonrpc":"2.0","method":"method","params":["params"]}']); + }); +}); diff --git a/packages/worker-api/src/rpc-provider/src/coder/encodeObject.spec.ts b/packages/worker-api/src/rpc-provider/src/coder/encodeObject.spec.ts new file mode 100644 index 00000000..841a3257 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/encodeObject.spec.ts @@ -0,0 +1,25 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { RpcCoder } from './index.js'; + +describe('encodeObject', (): void => { + let coder: RpcCoder; + + beforeEach((): void => { + coder = new RpcCoder(); + }); + + it('encodes a valid JsonRPC object', (): void => { + expect( + coder.encodeObject('method', ['a', 'b']) + ).toEqual([1, { + id: 1, + jsonrpc: '2.0', + method: 'method', + params: ['a', 'b'] + }]); + }); +}); diff --git a/packages/worker-api/src/rpc-provider/src/coder/error.spec.ts b/packages/worker-api/src/rpc-provider/src/coder/error.spec.ts new file mode 100644 index 00000000..89ac16c2 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/error.spec.ts @@ -0,0 +1,111 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +/// + +import { isError } from '@polkadot/util'; + +import RpcError from './error.js'; + +describe('RpcError', (): void => { + describe('constructor', (): void => { + it('constructs an Error that is still an Error', (): void => { + expect( + isError( + new RpcError() + ) + ).toEqual(true); + }); + }); + + describe('static', (): void => { + it('exposes the .CODES as a static', (): void => { + expect( + Object.keys(RpcError.CODES) + ).not.toEqual(0); + }); + }); + + describe('constructor properties', (): void => { + it('sets the .message property', (): void => { + expect( + new RpcError('test message').message + ).toEqual('test message'); + }); + + it("sets the .message to '' when not set", (): void => { + expect( + new RpcError().message + ).toEqual(''); + }); + + it('sets the .code property', (): void => { + expect( + new RpcError('test message', 1234).code + ).toEqual(1234); + }); + + it('sets the .code to UKNOWN when not set', (): void => { + expect( + new RpcError('test message').code + ).toEqual(RpcError.CODES.UNKNOWN); + }); + + it('sets the .data property', (): void => { + const data = 'here'; + + expect( + new RpcError('test message', 1234, data).data + ).toEqual(data); + }); + + it('sets the .data property to generic value', (): void => { + const data = { custom: 'value' } as const; + + expect( + new RpcError('test message', 1234, data).data + ).toEqual(data); + }); + }); + + describe('stack traces', (): void => { + // eslint-disable-next-line @typescript-eslint/ban-types + let captureStackTrace: (targetObject: Record, constructorOpt?: Function | undefined) => void; + + beforeEach((): void => { + captureStackTrace = Error.captureStackTrace; + + Error.captureStackTrace = function (error): void { + Object.defineProperty(error, 'stack', { + configurable: true, + get: function getStack (): string { + const value = 'some stack returned'; + + Object.defineProperty(this, 'stack', { value }); + + return value; + } + }); + }; + }); + + afterEach((): void => { + Error.captureStackTrace = captureStackTrace; + }); + + it('captures via captureStackTrace when available', (): void => { + expect( + new RpcError().stack + ).toEqual('some stack returned'); + }); + + it('captures via stack when captureStackTrace not available', (): void => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + Error.captureStackTrace = null as any; + + expect( + new RpcError().stack.length + ).not.toEqual(0); + }); + }); +}); diff --git a/packages/worker-api/src/rpc-provider/src/coder/error.ts b/packages/worker-api/src/rpc-provider/src/coder/error.ts new file mode 100644 index 00000000..908d9ead --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/error.ts @@ -0,0 +1,66 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { RpcErrorInterface } from '../types.js'; + +import { isFunction } from '@polkadot/util'; + +const UNKNOWN = -99999; + +function extend> (that: RpcError, name: K, value: RpcError[K]): void { + Object.defineProperty(that, name, { + configurable: true, + enumerable: false, + value + }); +} + +/** + * @name RpcError + * @summary Extension to the basic JS Error. + * @description + * The built-in JavaScript Error class is extended by adding a code to allow for Error categorization. In addition to the normal `stack`, `message`, the numeric `code` and `data` (any types) parameters are available on the object. + * @example + *
+ * + * ```javascript + * const { RpcError } from '@polkadot/util'); + * + * throw new RpcError('some message', RpcError.CODES.METHOD_NOT_FOUND); // => error.code = -32601 + * ``` + */ +export default class RpcError extends Error implements RpcErrorInterface { + public code!: number; + + public data?: T; + + public override message!: string; + + public override name!: string; + + public override stack!: string; + + public constructor (message = '', code: number = UNKNOWN, data?: T) { + super(); + + extend(this, 'message', String(message)); + extend(this, 'name', this.constructor.name); + extend(this, 'data', data); + extend(this, 'code', code); + + if (isFunction(Error.captureStackTrace)) { + Error.captureStackTrace(this, this.constructor); + } else { + const { stack } = new Error(message); + + stack && extend(this, 'stack', stack); + } + } + + public static CODES = { + ASSERT: -90009, + INVALID_JSONRPC: -99998, + METHOD_NOT_FOUND: -32601, // Rust client + UNKNOWN + }; +} diff --git a/packages/worker-api/src/rpc-provider/src/coder/index.ts b/packages/worker-api/src/rpc-provider/src/coder/index.ts new file mode 100644 index 00000000..120e1a17 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/coder/index.ts @@ -0,0 +1,88 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { JsonRpcRequest, JsonRpcResponse, JsonRpcResponseBaseError } from '../types.js'; + +import { isNumber, isString, isUndefined, stringify } from '@polkadot/util'; + +import RpcError from './error.js'; + +function formatErrorData (data?: string | number): string { + if (isUndefined(data)) { + return ''; + } + + const formatted = `: ${isString(data) + ? data.replace(/Error\("/g, '').replace(/\("/g, '(').replace(/"\)/g, ')').replace(/\(/g, ', ').replace(/\)/g, '') + : stringify(data)}`; + + // We need some sort of cut-off here since these can be very large and + // very nested, pick a number and trim the result display to it + return formatted.length <= 256 + ? formatted + : `${formatted.substring(0, 255)}…`; +} + +function checkError (error?: JsonRpcResponseBaseError): void { + if (error) { + const { code, data, message } = error; + + throw new RpcError(`${code}: ${message}${formatErrorData(data)}`, code, data); + } +} + +/** @internal */ +export class RpcCoder { + #id = 0; + + public decodeResponse (response?: JsonRpcResponse): T { + if (!response || response.jsonrpc !== '2.0') { + throw new Error('Invalid jsonrpc field in decoded object'); + } + + const isSubscription = !isUndefined(response.params) && !isUndefined(response.method); + + if ( + !isNumber(response.id) && + ( + !isSubscription || ( + !isNumber(response.params.subscription) && + !isString(response.params.subscription) + ) + ) + ) { + throw new Error('Invalid id field in decoded object'); + } + + checkError(response.error); + + if (response.result === undefined && !isSubscription) { + throw new Error('No result found in jsonrpc response'); + } + + if (isSubscription) { + checkError(response.params.error); + + return response.params.result; + } + + return response.result; + } + + public encodeJson (method: string, params: unknown[]): [number, string] { + const [id, data] = this.encodeObject(method, params); + + return [id, stringify(data)]; + } + + public encodeObject (method: string, params: unknown[]): [number, JsonRpcRequest] { + const id = ++this.#id; + + return [id, { + id, + jsonrpc: '2.0', + method, + params + }]; + } +} diff --git a/packages/worker-api/src/rpc-provider/src/defaults.ts b/packages/worker-api/src/rpc-provider/src/defaults.ts new file mode 100644 index 00000000..55d19f21 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/defaults.ts @@ -0,0 +1,10 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +const HTTP_URL = 'http://127.0.0.1:9933'; +const WS_URL = 'ws://127.0.0.1:9944'; + +export default { + HTTP_URL, + WS_URL +}; diff --git a/packages/worker-api/src/rpc-provider/src/index.ts b/packages/worker-api/src/rpc-provider/src/index.ts new file mode 100644 index 00000000..3fb2827c --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/index.ts @@ -0,0 +1,4 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from './bundle.js'; diff --git a/packages/worker-api/src/rpc-provider/src/lru.ts b/packages/worker-api/src/rpc-provider/src/lru.ts new file mode 100644 index 00000000..b9afa882 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/lru.ts @@ -0,0 +1,134 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// Assuming all 1.5MB responses, we apply a default allowing for 192MB +// cache space (depending on the historic queries this would vary, metadata +// for Kusama/Polkadot/Substrate falls between 600-750K, 2x for estimate) +export const DEFAULT_CAPACITY = 128; + +class LRUNode { + readonly key: string; + + public next: LRUNode; + public prev: LRUNode; + + constructor (key: string) { + this.key = key; + this.next = this.prev = this; + } +} + +// https://en.wikipedia.org/wiki/Cache_replacement_policies#LRU +export class LRUCache { + readonly capacity: number; + + readonly #data = new Map(); + readonly #refs = new Map(); + + #length = 0; + #head: LRUNode; + #tail: LRUNode; + + constructor (capacity = DEFAULT_CAPACITY) { + this.capacity = capacity; + this.#head = this.#tail = new LRUNode(''); + } + + get length (): number { + return this.#length; + } + + get lengthData (): number { + return this.#data.size; + } + + get lengthRefs (): number { + return this.#refs.size; + } + + entries (): [string, unknown][] { + const keys = this.keys(); + const count = keys.length; + const entries = new Array<[string, unknown]>(count); + + for (let i = 0; i < count; i++) { + const key = keys[i]; + + entries[i] = [key, this.#data.get(key)]; + } + + return entries; + } + + keys (): string[] { + const keys: string[] = []; + + if (this.#length) { + let curr = this.#head; + + while (curr !== this.#tail) { + keys.push(curr.key); + curr = curr.next; + } + + keys.push(curr.key); + } + + return keys; + } + + get (key: string): T | null { + const data = this.#data.get(key); + + if (data) { + this.#toHead(key); + + return data as T; + } + + return null; + } + + set (key: string, value: T): void { + if (this.#data.has(key)) { + this.#toHead(key); + } else { + const node = new LRUNode(key); + + this.#refs.set(node.key, node); + + if (this.length === 0) { + this.#head = this.#tail = node; + } else { + this.#head.prev = node; + node.next = this.#head; + this.#head = node; + } + + if (this.#length === this.capacity) { + this.#data.delete(this.#tail.key); + this.#refs.delete(this.#tail.key); + + this.#tail = this.#tail.prev; + this.#tail.next = this.#head; + } else { + this.#length += 1; + } + } + + this.#data.set(key, value); + } + + #toHead (key: string): void { + const ref = this.#refs.get(key); + + if (ref && ref !== this.#head) { + ref.prev.next = ref.next; + ref.next.prev = ref.prev; + ref.next = this.#head; + + this.#head.prev = ref; + this.#head = ref; + } + } +} diff --git a/packages/worker-api/src/rpc-provider/src/mod.ts b/packages/worker-api/src/rpc-provider/src/mod.ts new file mode 100644 index 00000000..aa7b729d --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/mod.ts @@ -0,0 +1,4 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export * from './index.js'; diff --git a/packages/worker-api/src/rpc-provider/src/types.ts b/packages/worker-api/src/rpc-provider/src/types.ts new file mode 100644 index 00000000..ad030ec4 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/types.ts @@ -0,0 +1,99 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +export interface JsonRpcObject { + id: number; + jsonrpc: '2.0'; +} + +export interface JsonRpcRequest extends JsonRpcObject { + method: string; + params: unknown[]; +} + +export interface JsonRpcResponseBaseError { + code: number; + data?: number | string; + message: string; +} + +export interface RpcErrorInterface { + code: number; + data?: T; + message: string; + stack: string; +} + +interface JsonRpcResponseSingle { + error?: JsonRpcResponseBaseError; + result: T; +} + +interface JsonRpcResponseSubscription { + method?: string; + params: { + error?: JsonRpcResponseBaseError; + result: T; + subscription: number | string; + }; +} + +export type JsonRpcResponseBase = JsonRpcResponseSingle & JsonRpcResponseSubscription; + +export type JsonRpcResponse = JsonRpcObject & JsonRpcResponseBase; + +export type ProviderInterfaceCallback = (error: Error | null, result: any) => void; + +export type ProviderInterfaceEmitted = 'connected' | 'disconnected' | 'error'; + +export type ProviderInterfaceEmitCb = (value?: any) => any; + +export interface ProviderInterface { + /** true if the provider supports subscriptions (not available for HTTP) */ + readonly hasSubscriptions: boolean; + /** true if the clone() functionality is available on the provider */ + readonly isClonable: boolean; + /** true if the provider is currently connected (ws/sc has connection logic) */ + readonly isConnected: boolean; + /** (optional) stats for the provider with connections/bytes */ + readonly stats?: ProviderStats; + + clone (): ProviderInterface; + connect (): Promise; + disconnect (): Promise; + on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void; + send (method: string, params: unknown[], isCacheable?: boolean): Promise; + subscribe (type: string, method: string, params: unknown[], cb: ProviderInterfaceCallback): Promise; + unsubscribe (type: string, method: string, id: number | string): Promise; +} + +/** Stats for a specific endpoint */ +export interface EndpointStats { + /** The total number of bytes sent */ + bytesRecv: number; + /** The total number of bytes received */ + bytesSent: number; + /** The number of cached/in-progress requests made */ + cached: number; + /** The number of errors found */ + errors: number; + /** The number of requests */ + requests: number; + /** The number of subscriptions */ + subscriptions: number; + /** The number of request timeouts */ + timeout: number; +} + +/** Overall stats for the provider */ +export interface ProviderStats { + /** Details for the active/open requests */ + active: { + /** Number of active requests */ + requests: number; + /** Number of active subscriptions */ + subscriptions: number; + }; + /** The total requests that have been made */ + total: EndpointStats; +} diff --git a/packages/worker-api/src/rpc-provider/src/ws/errors.ts b/packages/worker-api/src/rpc-provider/src/ws/errors.ts new file mode 100644 index 00000000..ad5ff6cd --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/ws/errors.ts @@ -0,0 +1,41 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +// from https://stackoverflow.com/questions/19304157/getting-the-reason-why-websockets-closed-with-close-code-1006 + +const known: Record = { + 1000: 'Normal Closure', + 1001: 'Going Away', + 1002: 'Protocol Error', + 1003: 'Unsupported Data', + 1004: '(For future)', + 1005: 'No Status Received', + 1006: 'Abnormal Closure', + 1007: 'Invalid frame payload data', + 1008: 'Policy Violation', + 1009: 'Message too big', + 1010: 'Missing Extension', + 1011: 'Internal Error', + 1012: 'Service Restart', + 1013: 'Try Again Later', + 1014: 'Bad Gateway', + 1015: 'TLS Handshake' +}; + +export function getWSErrorString (code: number): string { + if (code >= 0 && code <= 999) { + return '(Unused)'; + } else if (code >= 1016) { + if (code <= 1999) { + return '(For WebSocket standard)'; + } else if (code <= 2999) { + return '(For WebSocket extensions)'; + } else if (code <= 3999) { + return '(For libraries and frameworks)'; + } else if (code <= 4999) { + return '(For applications)'; + } + } + + return known[code] || '(Unknown)'; +} diff --git a/packages/worker-api/src/rpc-provider/src/ws/index.ts b/packages/worker-api/src/rpc-provider/src/ws/index.ts new file mode 100644 index 00000000..21bf03d1 --- /dev/null +++ b/packages/worker-api/src/rpc-provider/src/ws/index.ts @@ -0,0 +1,639 @@ +// Copyright 2017-2024 @polkadot/rpc-provider authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { Class } from '@polkadot/util/types'; +import type { EndpointStats, JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted, ProviderStats } from '../types.js'; + +import { EventEmitter } from 'eventemitter3'; + +import {isChildClass, isNull, isUndefined, logger, noop, objectSpread, stringify} from '@polkadot/util'; +import { xglobal } from '@polkadot/x-global'; +import { WebSocket } from '@polkadot/x-ws'; + +import { RpcCoder } from '../coder/index.js'; +import defaults from '../defaults.js'; +import { DEFAULT_CAPACITY, LRUCache } from '../lru.js'; +import { getWSErrorString } from './errors.js'; + +interface SubscriptionHandler { + callback: ProviderInterfaceCallback; + type: string; +} + +interface WsStateAwaiting { + callback: ProviderInterfaceCallback; + method: string; + params: unknown[]; + start: number; + subscription?: SubscriptionHandler | undefined; +} + +interface WsStateSubscription extends SubscriptionHandler { + method: string; + params: unknown[]; +} + +const ALIASES: Record = { + chain_finalisedHead: 'chain_finalizedHead', + chain_subscribeFinalisedHeads: 'chain_subscribeFinalizedHeads', + chain_unsubscribeFinalisedHeads: 'chain_unsubscribeFinalizedHeads' +}; + +const RETRY_DELAY = 2_500; + +const DEFAULT_TIMEOUT_MS = 60 * 1000; +const TIMEOUT_INTERVAL = 5_000; + +const l = logger('api-ws'); + +/** @internal Clears a Record<*> of all keys, optionally with all callback on clear */ +function eraseRecord (record: Record, cb?: (item: T) => void): void { + Object.keys(record).forEach((key): void => { + if (cb) { + cb(record[key]); + } + + delete record[key]; + }); +} + +/** @internal Creates a default/empty stats object */ +function defaultEndpointStats (): EndpointStats { + return { bytesRecv: 0, bytesSent: 0, cached: 0, errors: 0, requests: 0, subscriptions: 0, timeout: 0 }; +} + +/** + * # @polkadot/rpc-provider/ws + * + * @name WsProvider + * + * @description The WebSocket Provider allows sending requests using WebSocket to a WebSocket RPC server TCP port. Unlike the [[HttpProvider]], it does support subscriptions and allows listening to events such as new blocks or balance changes. + * + * @example + *
+ * + * ```javascript + * import Api from '@polkadot/api/promise'; + * import { WsProvider } from '@polkadot/rpc-provider/ws'; + * + * const provider = new WsProvider('ws://127.0.0.1:9944'); + * const api = new Api(provider); + * ``` + * + * @see [[HttpProvider]] + */ +export class WsProvider implements ProviderInterface { + readonly #callCache: LRUCache; + readonly #coder: RpcCoder; + readonly #endpoints: string[]; + // @ts-ignore - + readonly #headers: Record; + readonly #eventemitter: EventEmitter; + readonly #handlers: Record = {}; + readonly #isReadyPromise: Promise; + readonly #stats: ProviderStats; + readonly #waitingForId: Record> = {}; + + // @encointer's only customization so that we can pass in + // node's websocket implementation in our integration tests + // to accept the worker's self-signed certificate. + readonly #createWebsocket?: (url: string) => WebSocket; + + #autoConnectMs: number; + #endpointIndex: number; + #endpointStats: EndpointStats; + #isConnected = false; + #subscriptions: Record = {}; + #timeoutId?: ReturnType | null = null; + #websocket: WebSocket | null; + #timeout: number; + + /** + * @param {string | string[]} endpoint The endpoint url. Usually `ws://ip:9944` or `wss://ip:9944`, may provide an array of endpoint strings. + * @param {number | false} autoConnectMs Whether to connect automatically or not (default). Provided value is used as a delay between retries. + * @param {Record} headers The headers provided to the underlying WebSocket + * @param {number} [timeout] Custom timeout value used per request . Defaults to `DEFAULT_TIMEOUT_MS` + */ + constructor (endpoint: string | string[] = defaults.WS_URL, autoConnectMs: number | false = RETRY_DELAY, headers: Record = {}, timeout?: number, cacheCapacity?: number, createWebsocket?: (url: string) => WebSocket) { + const endpoints = Array.isArray(endpoint) + ? endpoint + : [endpoint]; + + if (endpoints.length === 0) { + throw new Error('WsProvider requires at least one Endpoint'); + } + + endpoints.forEach((endpoint) => { + if (!/^(wss|ws):\/\//.test(endpoint)) { + throw new Error(`Endpoint should start with 'ws://', received '${endpoint}'`); + } + }); + this.#callCache = new LRUCache(cacheCapacity || DEFAULT_CAPACITY); + this.#eventemitter = new EventEmitter(); + this.#autoConnectMs = autoConnectMs || 0; + this.#coder = new RpcCoder(); + this.#endpointIndex = -1; + this.#endpoints = endpoints; + this.#headers = headers; + this.#websocket = null; + this.#stats = { + active: { requests: 0, subscriptions: 0 }, + total: defaultEndpointStats() + }; + this.#endpointStats = defaultEndpointStats(); + this.#timeout = timeout || DEFAULT_TIMEOUT_MS; + this.#createWebsocket = createWebsocket; + + if (autoConnectMs && autoConnectMs > 0) { + this.connectWithRetry().catch(noop); + } + + this.#isReadyPromise = new Promise((resolve): void => { + this.#eventemitter.once('connected', (): void => { + resolve(this); + }); + }); + } + + /** + * @summary `true` when this provider supports subscriptions + */ + public get hasSubscriptions (): boolean { + return !!true; + } + + /** + * @summary `true` when this provider supports clone() + */ + public get isClonable (): boolean { + return !!true; + } + + /** + * @summary Whether the node is connected or not. + * @return {boolean} true if connected + */ + public get isConnected (): boolean { + return this.#isConnected; + } + + /** + * @description Promise that resolves the first time we are connected and loaded + */ + public get isReady (): Promise { + return this.#isReadyPromise; + } + + public get endpoint (): string { + return this.#endpoints[this.#endpointIndex]; + } + + /** + * @description Returns a clone of the object + */ + public clone (): WsProvider { + return new WsProvider(this.#endpoints); + } + + protected selectEndpointIndex (endpoints: string[]): number { + return (this.#endpointIndex + 1) % endpoints.length; + } + + /** + * @summary Manually connect + * @description The [[WsProvider]] connects automatically by default, however if you decided otherwise, you may + * connect manually using this method. + */ + // eslint-disable-next-line @typescript-eslint/require-await + public async connect (): Promise { + if (this.#websocket) { + throw new Error('WebSocket is already connected'); + } + + try { + this.#endpointIndex = this.selectEndpointIndex(this.#endpoints); + + + if (this.#createWebsocket !== undefined) { + this.#websocket = this.#createWebsocket(this.endpoint); + } else { + // the as here is Deno-specific - not available on the globalThis + this.#websocket = typeof xglobal.WebSocket !== 'undefined' && isChildClass(xglobal.WebSocket as unknown as Class, WebSocket) + ? new WebSocket(this.endpoint) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - WS may be an instance of ws, which supports options + : new WebSocket(this.endpoint, undefined, { + headers: this.#headers + }); + + } + + if (this.#websocket) { + this.#websocket.onclose = this.#onSocketClose; + this.#websocket.onerror = this.#onSocketError; + this.#websocket.onmessage = this.#onSocketMessage; + this.#websocket.onopen = this.#onSocketOpen; + } + + // timeout any handlers that have not had a response + this.#timeoutId = setInterval(() => this.#timeoutHandlers(), TIMEOUT_INTERVAL); + } catch (error) { + l.error(error); + + this.#emit('error', error); + + throw error; + } + } + + /** + * @description Connect, never throwing an error, but rather forcing a retry + */ + public async connectWithRetry (): Promise { + if (this.#autoConnectMs > 0) { + try { + await this.connect(); + } catch { + setTimeout((): void => { + this.connectWithRetry().catch(noop); + }, this.#autoConnectMs); + } + } + } + + /** + * @description Manually disconnect from the connection, clearing auto-connect logic + */ + // eslint-disable-next-line @typescript-eslint/require-await + public async disconnect (): Promise { + // switch off autoConnect, we are in manual mode now + this.#autoConnectMs = 0; + + try { + if (this.#websocket) { + // 1000 - Normal closure; the connection successfully completed + this.#websocket.close(1000); + } + } catch (error) { + l.error(error); + + this.#emit('error', error); + + throw error; + } + } + + /** + * @description Returns the connection stats + */ + public get stats (): ProviderStats { + return { + active: { + requests: Object.keys(this.#handlers).length, + subscriptions: Object.keys(this.#subscriptions).length + }, + total: this.#stats.total + }; + } + + public get endpointStats (): EndpointStats { + return this.#endpointStats; + } + + /** + * @summary Listens on events after having subscribed using the [[subscribe]] function. + * @param {ProviderInterfaceEmitted} type Event + * @param {ProviderInterfaceEmitCb} sub Callback + * @return unsubscribe function + */ + public on (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): () => void { + this.#eventemitter.on(type, sub); + + return (): void => { + this.#eventemitter.removeListener(type, sub); + }; + } + + /** + * @summary Send JSON data using WebSockets to configured HTTP Endpoint or queue. + * @param method The RPC methods to execute + * @param params Encoded parameters as applicable for the method + * @param subscription Subscription details (internally used) + */ + public send (method: string, params: unknown[], isCacheable?: boolean, subscription?: SubscriptionHandler): Promise { + this.#endpointStats.requests++; + this.#stats.total.requests++; + + const [id, body] = this.#coder.encodeJson(method, params); + const cacheKey = isCacheable ? `${method}::${stringify(params)}` : ''; + let resultPromise: Promise | null = isCacheable + ? this.#callCache.get(cacheKey) + : null; + + if (!resultPromise) { + resultPromise = this.#send(id, body, method, params, subscription); + + if (isCacheable) { + this.#callCache.set(cacheKey, resultPromise); + } + } else { + this.#endpointStats.cached++; + this.#stats.total.cached++; + } + + return resultPromise; + } + + async #send (id: number, body: string, method: string, params: unknown[], subscription?: SubscriptionHandler): Promise { + return new Promise((resolve, reject): void => { + try { + if (!this.isConnected || this.#websocket === null) { + throw new Error('WebSocket is not connected'); + } + + const callback = (error?: Error | null, result?: T): void => { + error + ? reject(error) + : resolve(result as T); + }; + + l.debug(() => ['calling', method, body]); + + this.#handlers[id] = { + callback, + method, + params, + start: Date.now(), + subscription + }; + + const bytesSent = body.length; + + this.#endpointStats.bytesSent += bytesSent; + this.#stats.total.bytesSent += bytesSent; + + this.#websocket.send(body); + } catch (error) { + this.#endpointStats.errors++; + this.#stats.total.errors++; + + reject(error); + } + }); + } + + /** + * @name subscribe + * @summary Allows subscribing to a specific event. + * + * @example + *
+ * + * ```javascript + * const provider = new WsProvider('ws://127.0.0.1:9944'); + * const rpc = new Rpc(provider); + * + * rpc.state.subscribeStorage([[storage.system.account,
]], (_, values) => { + * console.log(values) + * }).then((subscriptionId) => { + * console.log('balance changes subscription id: ', subscriptionId) + * }) + * ``` + */ + public subscribe (type: string, method: string, params: unknown[], callback: ProviderInterfaceCallback): Promise { + this.#endpointStats.subscriptions++; + this.#stats.total.subscriptions++; + + // subscriptions are not cached, LRU applies to .at() only + return this.send(method, params, false, { callback, type }); + } + + /** + * @summary Allows unsubscribing to subscriptions made with [[subscribe]]. + */ + public async unsubscribe (type: string, method: string, id: number | string): Promise { + const subscription = `${type}::${id}`; + + // FIXME This now could happen with re-subscriptions. The issue is that with a re-sub + // the assigned id now does not match what the API user originally received. It has + // a slight complication in solving - since we cannot rely on the send id, but rather + // need to find the actual subscription id to map it + if (isUndefined(this.#subscriptions[subscription])) { + l.debug(() => `Unable to find active subscription=${subscription}`); + + return false; + } + + delete this.#subscriptions[subscription]; + + try { + return this.isConnected && !isNull(this.#websocket) + ? this.send(method, [id]) + : true; + } catch { + return false; + } + } + + #emit = (type: ProviderInterfaceEmitted, ...args: unknown[]): void => { + this.#eventemitter.emit(type, ...args); + }; + + #onSocketClose = (event: CloseEvent): void => { + const error = new Error(`disconnected from ${this.endpoint}: ${event.code}:: ${event.reason || getWSErrorString(event.code)}`); + + if (this.#autoConnectMs > 0) { + l.error(error.message); + } + + this.#isConnected = false; + + if (this.#websocket) { + this.#websocket.onclose = null; + this.#websocket.onerror = null; + this.#websocket.onmessage = null; + this.#websocket.onopen = null; + this.#websocket = null; + } + + if (this.#timeoutId) { + clearInterval(this.#timeoutId); + this.#timeoutId = null; + } + + // reject all hanging requests + eraseRecord(this.#handlers, (h) => { + try { + h.callback(error, undefined); + } catch (err) { + // does not throw + l.error(err); + } + }); + eraseRecord(this.#waitingForId); + + // Reset stats for active endpoint + this.#endpointStats = defaultEndpointStats(); + + this.#emit('disconnected'); + + if (this.#autoConnectMs > 0) { + setTimeout((): void => { + this.connectWithRetry().catch(noop); + }, this.#autoConnectMs); + } + }; + + #onSocketError = (error: Event): void => { + l.debug(() => ['socket error', error]); + this.#emit('error', error); + }; + + #onSocketMessage = (message: MessageEvent): void => { + l.debug(() => ['received', message.data]); + + const bytesRecv = message.data.length; + + this.#endpointStats.bytesRecv += bytesRecv; + this.#stats.total.bytesRecv += bytesRecv; + + const response = JSON.parse(message.data) as JsonRpcResponse; + + return isUndefined(response.method) + ? this.#onSocketMessageResult(response) + : this.#onSocketMessageSubscribe(response); + }; + + #onSocketMessageResult = (response: JsonRpcResponse): void => { + const handler = this.#handlers[response.id]; + + if (!handler) { + l.debug(() => `Unable to find handler for id=${response.id}`); + + return; + } + + try { + const { method, params, subscription } = handler; + const result = this.#coder.decodeResponse(response); + + // first send the result - in case of subs, we may have an update + // immediately if we have some queued results already + handler.callback(null, result); + + if (subscription) { + const subId = `${subscription.type}::${result}`; + + this.#subscriptions[subId] = objectSpread({}, subscription, { + method, + params + }); + + // if we have a result waiting for this subscription already + if (this.#waitingForId[subId]) { + this.#onSocketMessageSubscribe(this.#waitingForId[subId]); + } + } + } catch (error) { + this.#endpointStats.errors++; + this.#stats.total.errors++; + + handler.callback(error as Error, undefined); + } + + delete this.#handlers[response.id]; + }; + + #onSocketMessageSubscribe = (response: JsonRpcResponse): void => { + if (!response.method) { + throw new Error('No method found in JSONRPC response'); + } + + const method = ALIASES[response.method] || response.method; + const subId = `${method}::${response.params.subscription}`; + const handler = this.#subscriptions[subId]; + + if (!handler) { + // store the JSON, we could have out-of-order subid coming in + this.#waitingForId[subId] = response; + + l.debug(() => `Unable to find handler for subscription=${subId}`); + + return; + } + + // housekeeping + delete this.#waitingForId[subId]; + + try { + const result = this.#coder.decodeResponse(response); + + handler.callback(null, result); + } catch (error) { + this.#endpointStats.errors++; + this.#stats.total.errors++; + + handler.callback(error as Error, undefined); + } + }; + + #onSocketOpen = (): boolean => { + if (this.#websocket === null) { + throw new Error('WebSocket cannot be null in onOpen'); + } + + l.debug(() => ['connected to', this.endpoint]); + + this.#isConnected = true; + + this.#resubscribe(); + + this.#emit('connected'); + + return true; + }; + + #resubscribe = (): void => { + const subscriptions = this.#subscriptions; + + this.#subscriptions = {}; + + Promise.all(Object.keys(subscriptions).map(async (id): Promise => { + const { callback, method, params, type } = subscriptions[id]; + + // only re-create subscriptions which are not in author (only area where + // transactions are created, i.e. submissions such as 'author_submitAndWatchExtrinsic' + // are not included (and will not be re-broadcast) + if (type.startsWith('author_')) { + return; + } + + try { + await this.subscribe(type, method, params, callback); + } catch (error) { + l.error(error); + } + })).catch(l.error); + }; + + #timeoutHandlers = (): void => { + const now = Date.now(); + const ids = Object.keys(this.#handlers); + + for (let i = 0, count = ids.length; i < count; i++) { + const handler = this.#handlers[ids[i]]; + + if ((now - handler.start) > this.#timeout) { + try { + handler.callback(new Error(`No response received from RPC endpoint in ${this.#timeout / 1000}s`), undefined); + } catch { + // ignore + } + + this.#endpointStats.timeout++; + this.#stats.total.timeout++; + delete this.#handlers[ids[i]]; + } + } + }; +} diff --git a/packages/worker-api/src/sendRequest.ts b/packages/worker-api/src/sendRequest.ts deleted file mode 100644 index 49da9051..00000000 --- a/packages/worker-api/src/sendRequest.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { - type IWorker, - type RequestArgs, - type RequestOptions, - type WorkerMethod, - createJsonRpcRequest -} from './interface.js'; -import { Request } from './interface.js'; -import type {ShardIdentifier, IntegriteeTrustedCallSigned} from "@encointer/types"; - -export const sendWorkerRequest = async (self: IWorker, clientRequest: any, parserType: string, options?: RequestOptions): Promise =>{ - if( !self.isOpened ) { - await self.open(); - } - - const requestId = self.rqStack.push(parserType) + self.rsCount; - const timeout = options && options.timeout ? options.timeout : undefined; - return self.sendRequest( - clientRequest, { - timeout: timeout, - requestId - } - ) -} - -export const callGetter = async (self: IWorker, workerMethod: WorkerMethod, _args: RequestArgs, requestOptions?: RequestOptions): Promise => { - const [getterType, method, parser] = workerMethod; - let result: Promise; - let parserType: string = requestOptions?.debug ? 'raw': parser; - switch (getterType) { - case Request.Worker: - result = sendWorkerRequest(self, createJsonRpcRequest(method, [], 1), parserType, requestOptions) - break; - default: - throw "Invalid request variant, public and trusted have been removed for the integritee worker" - } - return result as Promise -} - -export const sendTrustedCall = async (self: IWorker, call: IntegriteeTrustedCallSigned, shard: ShardIdentifier, direct: boolean, parser: string, options: RequestOptions = {} as RequestOptions): Promise => { - let result: Promise; - let parserType: string = options.debug ? 'raw': parser; - - let top; - if (direct) { - top = self.createType('IntegriteeTrustedOperation', { - direct_call: call - }) - } else { - top = self.createType('IntegriteeTrustedOperation', { - indirect_call: call - }) - } - - console.debug(`Sending TrustedOperation: ${JSON.stringify(top)}`); - - const cyphertext = await self.encrypt(top.toU8a()); - - const r = self.createType( - 'Request', { shard, cyphertext: cyphertext } - ); - - const rpc = createJsonRpcRequest('author_submitExtrinsic', [r.toHex()], 1); - result = sendWorkerRequest(self, rpc, parserType, options) - - console.debug(`[sendTrustedCall] sent request: ${JSON.stringify(rpc)}`); - - return result as Promise -} - diff --git a/packages/worker-api/src/testUtils/networks.ts b/packages/worker-api/src/testUtils/networks.ts index 42d4b43d..786e3269 100644 --- a/packages/worker-api/src/testUtils/networks.ts +++ b/packages/worker-api/src/testUtils/networks.ts @@ -39,8 +39,9 @@ export const localDockerNetwork = () => { chain: 'ws://127.0.0.1:9944', worker: 'wss://127.0.0.1:2000', genesisHash: '0x388c446a804e24e77ae89f5bb099edb60cacc2ac7c898ce175bdaa08629c1439', - mrenclave: 'GopQCtWihHQ8Xw1sZP6DTWMDQuaijSBE2NM8GU2U4Erc', - chosenCid: 'GopQCtWihHQ8Xw1sZP6DTWMDQuaijSBE2NM8GU2U4Erc', + mrenclave: 'BrFk2gARyQxD56NLpDKVbZinDmD1Twt1GwMsEyQuWshx', + shard: 'BrFk2gARyQxD56NLpDKVbZinDmD1Twt1GwMsEyQuWshx', + chosenCid: 'BrFk2gARyQxD56NLpDKVbZinDmD1Twt1GwMsEyQuWshx', customTypes: {}, palletOverrides: {} }; @@ -52,8 +53,8 @@ export const paseoNetwork = () => { // reverse proxy to the worker worker: 'wss://scv1.paseo.api.incognitee.io:443', genesisHash: '', - mrenclave: '5wePd1LYa5M49ghwgZXs55cepKbJKhj5xfzQGfPeMS7c', - // abused as shard vault + mrenclave: '5BUCG8UXdgjWDDFQUd5kuRwnubDnMFhYEdbgxDZTnrBx', + shard: '5wePd1LYa5M49ghwgZXs55cepKbJKhj5xfzQGfPeMS7c', chosenCid: '5wePd1LYa5M49ghwgZXs55cepKbJKhj5xfzQGfPeMS7c', customTypes: {}, palletOverrides: {} diff --git a/packages/worker-api/src/worker.spec.ts b/packages/worker-api/src/worker.spec.ts index 8f20a4ab..5566e099 100644 --- a/packages/worker-api/src/worker.spec.ts +++ b/packages/worker-api/src/worker.spec.ts @@ -1,41 +1,42 @@ -import { Keyring } from '@polkadot/api'; import { cryptoWaitReady } from '@polkadot/util-crypto'; import {localDockerNetwork} from './testUtils/networks.js'; import { Worker } from './worker.js'; +import bs58 from "bs58"; + import WS from 'websocket'; const {w3cwebsocket: WebSocket} = WS; -describe.skip('worker', () => { +describe('worker', () => { const network = localDockerNetwork(); - let keyring: Keyring; let worker: Worker; beforeAll(async () => { jest.setTimeout(90000); await cryptoWaitReady(); - keyring = new Keyring({type: 'sr25519'}); - - worker = new Worker(network.worker, { - keyring: keyring, - types: network.customTypes, - // @ts-ignore - createWebSocket: (url) => new WebSocket( - url, - undefined, - undefined, - undefined, - // Allow the worker's self-signed certificate, needed in non-reverse proxy setups - // where we talk to the worker directly. - { rejectUnauthorized: false } + worker = new Worker(network.worker, + { + // @ts-ignore + createWebSocket: (url) => new WebSocket( + url, + undefined, + undefined, + undefined, + // Allow the worker's self-signed certificate, needed in non-reverse proxy setups + // where we talk to the worker directly. + { rejectUnauthorized: false } ), - api: null, - }); + } + ); + }); + + afterAll(async () => { + await worker.closeWs() }); // skip it, as this requires a worker (and hence a node) to be running // To my knowledge jest does not have an option to run skipped tests specifically, does it? // Todo: add proper CI to test this too. - describe('needs worker and node running', () => { + describe.skip('needs worker and node running', () => { describe('getWorkerPubKey', () => { it('should return value', async () => { const result = await worker.getShieldingKey(); @@ -54,9 +55,10 @@ describe.skip('worker', () => { describe('getFingerprint', () => { it('should return value', async () => { - const result = await worker.getFingerprint(); - console.log('Fingerprint', result.toString()); - expect(result).toBeDefined(); + const mrenclave = await worker.getFingerprint(); + + console.log('Fingerprint', bs58.encode(mrenclave.toU8a())); + expect(mrenclave).toBeDefined(); }); }); }); diff --git a/packages/worker-api/src/worker.ts b/packages/worker-api/src/worker.ts index 1ca34450..6ef29700 100644 --- a/packages/worker-api/src/worker.ts +++ b/packages/worker-api/src/worker.ts @@ -1,114 +1,50 @@ import type {Vec} from '@polkadot/types'; import {TypeRegistry} from '@polkadot/types'; import type {RegistryTypes} from '@polkadot/types/types'; -import {Keyring} from '@polkadot/keyring' import {compactAddLength, hexToU8a} from '@polkadot/util'; -import WebSocketAsPromised from 'websocket-as-promised'; import {options as encointerOptions} from '@encointer/node-api'; -import {parseI64F64} from '@encointer/util'; -import type {EnclaveFingerprint, Vault} from '@encointer/types'; - -import {type RequestOptions, type IWorker, Request, type WorkerOptions} from './interface.js'; -import {parseBalance} from './parsers.js'; -import {callGetter} from './sendRequest.js'; +import type { + EnclaveFingerprint, + RpcReturnValue, ShardIdentifier, + TrustedOperationStatus, + Vault, Request +} from '@encointer/types'; + +import { + type GenericGetter, + type GenericTop, + type IWorkerBase, + type TrustedCallResult, + type WorkerOptions +} from './interface.js'; import {encryptWithPublicKey, parseWebCryptoRSA} from "./webCryptoRSA.js"; -import type {u8} from "@polkadot/types-codec"; +import type {Bytes, u8} from "@polkadot/types-codec"; import BN from "bn.js"; +import {WsProvider} from "./rpc-provider/src/index.js"; +import {Keyring} from "@polkadot/keyring"; +import type {Hash} from "@polkadot/types/interfaces/runtime"; -const unwrapWorkerResponse = (self: IWorker, data: string) => { - /// Defaults to return `[]`, which is fine as `createType(api.registry, , [])` - /// instantiates the with its default value. - const dataTyped = self.createType('Option', data) - return dataTyped.unwrapOrDefault(); -} - -const parseGetterResponse = (self: IWorker, responseType: string, data: string) => { - if (data === 'Could not decode request') { - throw new Error(`Worker error: ${data}`); - } - - // console.debug(`Getter response: ${data}`); - const json = JSON.parse(data); - - const value = hexToU8a(json["result"]); - const returnValue = self.createType('RpcReturnValue', value); - console.debug(`RpcReturnValue ${JSON.stringify(returnValue)}`); - - if (returnValue.status.isError) { - const errorMsg = self.createType('String', returnValue.value); - throw new Error(`RPC Error: ${errorMsg}`); - } - - let parsedData: any; - try { - switch (responseType) { - case 'raw': - parsedData = unwrapWorkerResponse(self, returnValue.value); - break; - case 'BalanceEntry': - parsedData = unwrapWorkerResponse(self, returnValue.value); - parsedData = parseBalance(self, parsedData); - break; - case 'I64F64': - parsedData = unwrapWorkerResponse(self, returnValue.value); - parsedData = parseI64F64(self.createType('i128', parsedData)); - break; - case 'CryptoKey': - const jsonStr = self.createType('String', returnValue.value); - // Todo: For some reason there are 2 non-utf characters, where I don't know where - // they come from currently. - console.debug(`Got shielding key: ${jsonStr.toJSON().substring(2)}`); - parsedData = parseWebCryptoRSA(jsonStr.toJSON().substring(2)); - break - case 'Vault': - parsedData = self.createType(responseType, returnValue.value); - break - case 'EnclaveFingerprint': - parsedData = self.createType(responseType, returnValue.value); - break - case 'TrustedOperationResult': - console.debug(`Got TrustedOperationResult`) - parsedData = self.createType('Hash', returnValue.value); - break - default: - parsedData = unwrapWorkerResponse(self, returnValue.value); - console.debug(`unwrapped data ${parsedData}`); - parsedData = self.createType(responseType, parsedData); - break; - } - } catch (err) { - throw new Error(`Can't parse into ${responseType}:\n${err}`); - } - return parsedData; -} - -export class Worker extends WebSocketAsPromised implements IWorker { +export class Worker implements IWorkerBase { readonly #registry: TypeRegistry; - #keyring?: Keyring; - - #shieldingKey?: CryptoKey + #shieldingKey?: CryptoKey; - rsCount: number; + #keyring?: Keyring; - rqStack: string[]; + #ws: WsProvider; constructor(url: string, options: WorkerOptions = {} as WorkerOptions) { - super(url, { - createWebSocket: (options.createWebSocket || undefined), - packMessage: (data: any) => JSON.stringify(data), - unpackMessage: (data: any) => parseGetterResponse(this, this.rqStack.shift() || '', data), - attachRequestId: (data: any): any => data, - extractRequestId: () => this.rsCount = ++this.rsCount - }); - this.#keyring = (options.keyring || undefined); this.#registry = new TypeRegistry(); - this.rsCount = 0; - this.rqStack = [] as string[] + this.#keyring = (options.keyring || undefined); + + // We want to pass arguments to NodeJS' websocket implementation into the provider + // in our integration tests, so that we can accept the workers self-signed + // certificate. Hence, we inject the factory function. + this.#ws = new WsProvider(url, 100, undefined, undefined, undefined, options.createWebSocket); if (options.types != undefined) { this.#registry.register(encointerOptions({types: options.types}).types as RegistryTypes); @@ -117,6 +53,14 @@ export class Worker extends WebSocketAsPromised implements IWorker { } } + public async isReady(): Promise { + return this.#ws.isReady + } + + public async closeWs(): Promise { + return this.#ws.disconnect() + } + public async encrypt(data: Uint8Array): Promise> { const dataBE = new BN(data); const dataArrayBE = new Uint8Array(dataBE.toArray()); @@ -132,13 +76,6 @@ export class Worker extends WebSocketAsPromised implements IWorker { return this.createType('Vec', compactAddLength(beArray)) } - public registry(): TypeRegistry { - return this.#registry - } - - public createType(apiType: string, obj?: any): any { - return this.#registry.createType(apiType as never, obj) - } public keyring(): Keyring | undefined { return this.#keyring; @@ -148,6 +85,15 @@ export class Worker extends WebSocketAsPromised implements IWorker { this.#keyring = keyring; } + + public registry(): TypeRegistry { + return this.#registry + } + + public createType(apiType: string, obj?: any): any { + return this.#registry.createType(apiType as never, obj) + } + public shieldingKey(): CryptoKey | undefined { return this.#shieldingKey; } @@ -156,17 +102,155 @@ export class Worker extends WebSocketAsPromised implements IWorker { this.#shieldingKey = shieldingKey; } - public async getShieldingKey(options: RequestOptions = {} as RequestOptions): Promise { - const key = await callGetter(this, [Request.Worker, 'author_getShieldingKey', 'CryptoKey'], {}, options) + public async getShieldingKey(): Promise { + const res = await this.send( + 'author_getShieldingKey',[] + ); + + const jsonStr = this.createType('String', res.value); + // Todo: For some reason there are 2 non-utf characters, where I don't know where + // they come from currently. + // console.debug(`Got shielding key: ${jsonStr.toJSON().substring(2)}`); + const key = await parseWebCryptoRSA(jsonStr.toJSON().substring(2)); + + // @ts-ignore this.setShieldingKey(key); + // @ts-ignore return key; } - public async getShardVault(options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.Worker, 'author_getShardVault', 'Vault'], {}, options) + public async getShardVault(): Promise { + const res = await this.send( + 'author_getShardVault',[] + ); + + console.debug(`Got vault key: ${JSON.stringify(res)}`); + return this.createType('Vault', res.value); + } + + public async getFingerprint(): Promise { + const res = await this.send( + 'author_getFingerprint',[] + ); + + console.debug(`Got fingerprint: ${res}`); + + return this.createType('EnclaveFingerprint', res.value); + } + + public async sendGetter(getter: Getter, shard: ShardIdentifier, returnType: string): Promise { + const r = this.createType( + 'Request', { + shard: shard, + cyphertext: getter.toHex() + } + ); + const response = await this.send('state_executeGetter', [r.toHex()]) + const value = unwrapWorkerResponse(this, response.value) + return this.createType(returnType, value); + } + + async submitAndWatchTop(top: Top, shard: ShardIdentifier): Promise { + + console.debug(`Sending TrustedOperation: ${JSON.stringify(top)}`); + const cyphertext = await this.encrypt(top.toU8a()); + + const r = this.createType( + 'Request', { shard, cyphertext: cyphertext } + ); + + const returnValue = await this.submitAndWatch(r) + + console.debug(`[sendTrustedCall] result: ${JSON.stringify(returnValue)}`); + + return returnValue; + } + + + public async send(method: string, params: unknown[]): Promise { + await this.isReady(); + const result = await this.#ws.send( + method, params + ); + + return this.resultToRpcReturnValue(result); } - public async getFingerprint(options: RequestOptions = {} as RequestOptions): Promise { - return await callGetter(this, [Request.Worker, 'author_getFingerprint', 'EnclaveFingerprint'], {}, options) + public async submitAndWatch(request: Request): Promise { + await this.isReady(); + + let topHash: Hash; + + return new Promise( async (resolve, reject) => { + const onStatusChange = (error: Error | null, result: string) => { + if (error) { + reject(new Error(`Callback Error: ${error.message}`)); + return; + } + + console.debug(`DirectRequestStatus: ${JSON.stringify(result)}`); + const directRequestStatus = this.createType('DirectRequestStatus', result); + + if (directRequestStatus.isError) { + const errorMsg = this.createType('String', directRequestStatus.value); + reject(new Error(`DirectRequestStatus is Error: ${errorMsg}`)); + } else if (directRequestStatus.isOk) { + resolve({topHash, status: undefined}); + } else if (directRequestStatus.isTrustedOperationStatus) { + console.debug(`TrustedOperationStatus: ${directRequestStatus}`); + const status = directRequestStatus.asTrustedOperationStatus; + + if (status.isInvalid || status.isUsurped || status.isDropped) { + console.debug(`Trusted operation failed to execute: ${status.toHuman()}`); + resolve({topHash, status}); + } + + if (connection_can_be_closed(status)) { + resolve({topHash, status}); + } + } + } + + try { + const res = await this.#ws.subscribe('author_submitAndWatchExtrinsic', + 'author_submitAndWatchExtrinsic', [request.toHex()], onStatusChange + ); + topHash = this.createType('Hash', res); + console.debug(`topHash: ${topHash}`); + } catch (err) { + console.error(err); + reject(err); + } + }) } + + resultToRpcReturnValue(result: string): RpcReturnValue { + if (result === 'Could not decode request') { + throw new Error(`Worker error: ${result}`); + } + + const value = hexToU8a(result); + const returnValue = this.createType('RpcReturnValue', value); + console.debug(`RpcReturnValue ${JSON.stringify(returnValue)}`); + + if (returnValue.status.isError) { + const errorMsg = this.createType('String', returnValue.value); + throw new Error(`RPC: ${errorMsg}`); + } + + return returnValue; + } +} + +function connection_can_be_closed(status: TrustedOperationStatus): boolean { + return !(status.isSubmitted || status.isFuture || status.isReady || status.isBroadCast || status.isInvalid) +} + +/** + * Defaults to return `[]`, which is fine as `createType(api.registry, , [])` + * instantiates the with its default value. + */ +function unwrapWorkerResponse (self: Worker, data: Bytes) { + const dataTyped = self.createType('Option', data) + return dataTyped.unwrapOrDefault(); } diff --git a/yarn.lock b/yarn.lock index f64e63f7..17c0d237 100644 --- a/yarn.lock +++ b/yarn.lock @@ -755,18 +755,18 @@ __metadata: languageName: node linkType: hard -"@encointer/node-api@npm:^0.15.2, @encointer/node-api@workspace:packages/node-api": +"@encointer/node-api@npm:^0.16.0-alpha.0, @encointer/node-api@workspace:packages/node-api": version: 0.0.0-use.local resolution: "@encointer/node-api@workspace:packages/node-api" dependencies: - "@encointer/types": "npm:^0.15.2" + "@encointer/types": "npm:^0.16.0-alpha.0" "@polkadot/api": "npm:^11.2.1" "@polkadot/util-crypto": "npm:^12.6.2" tslib: "npm:^2.6.2" languageName: unknown linkType: soft -"@encointer/types@npm:^0.15.2, @encointer/types@workspace:packages/types": +"@encointer/types@npm:^0.16.0-alpha.0, @encointer/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@encointer/types@workspace:packages/types" dependencies: @@ -781,7 +781,7 @@ __metadata: languageName: unknown linkType: soft -"@encointer/util@npm:^0.15.2, @encointer/util@workspace:packages/util": +"@encointer/util@npm:^0.16.0-alpha.0, @encointer/util@workspace:packages/util": version: 0.0.0-use.local resolution: "@encointer/util@workspace:packages/util" dependencies: @@ -798,9 +798,9 @@ __metadata: version: 0.0.0-use.local resolution: "@encointer/worker-api@workspace:packages/worker-api" dependencies: - "@encointer/node-api": "npm:^0.15.2" - "@encointer/types": "npm:^0.15.2" - "@encointer/util": "npm:^0.15.2" + "@encointer/node-api": "npm:^0.16.0-alpha.0" + "@encointer/types": "npm:^0.16.0-alpha.0" + "@encointer/util": "npm:^0.16.0-alpha.0" "@peculiar/webcrypto": "npm:^1.4.6" "@polkadot/api": "npm:^11.2.1" "@polkadot/keyring": "npm:^12.6.2" @@ -808,11 +808,13 @@ __metadata: "@polkadot/util": "npm:^12.6.2" "@polkadot/util-crypto": "npm:^12.6.2" "@polkadot/wasm-crypto": "npm:^7.3.2" + "@polkadot/x-global": "npm:^12.6.2" + "@polkadot/x-ws": "npm:^12.6.2" "@types/bs58": "npm:^4.0.4" bs58: "npm:^4.0.1" - promised-map: "npm:^1.0.0" + eventemitter3: "npm:^5.0.1" + tslib: "npm:^2.6.2" websocket: "npm:^1.0.34" - websocket-as-promised: "npm:^2.0.1" peerDependencies: "@polkadot/x-randomvalues": ^12.3.2 languageName: unknown @@ -4837,13 +4839,6 @@ __metadata: languageName: node linkType: hard -"chnl@npm:^1.2.0": - version: 1.2.0 - resolution: "chnl@npm:1.2.0" - checksum: 10/78044132c0a002b40f4e388407d57d4e767b223bcc1f7dcb108ecb188a8999c0b6a366e0871070c0e661648f8aca9dc1bd42a8f39c893c272c9935bb92ad82c9 - languageName: node - linkType: hard - "chokidar@npm:^3.4.0": version: 3.5.2 resolution: "chokidar@npm:3.5.2" @@ -6222,30 +6217,6 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.17.0-next.0": - version: 1.18.0 - resolution: "es-abstract@npm:1.18.0" - dependencies: - call-bind: "npm:^1.0.2" - es-to-primitive: "npm:^1.2.1" - function-bind: "npm:^1.1.1" - get-intrinsic: "npm:^1.1.1" - has: "npm:^1.0.3" - has-symbols: "npm:^1.0.2" - is-callable: "npm:^1.2.3" - is-negative-zero: "npm:^2.0.1" - is-regex: "npm:^1.1.2" - is-string: "npm:^1.0.5" - object-inspect: "npm:^1.9.0" - object-keys: "npm:^1.1.1" - object.assign: "npm:^4.1.2" - string.prototype.trimend: "npm:^1.0.4" - string.prototype.trimstart: "npm:^1.0.4" - unbox-primitive: "npm:^1.0.0" - checksum: 10/98b2dd3778d0bc36b86302603681f26432aad85d2019834a09d5221ca350600eb4b1e95915d442644cfcc422d5996a806c13ca30435655150f4a4e081b5960cb - languageName: node - linkType: hard - "es-abstract@npm:^1.18.0-next.1, es-abstract@npm:^1.18.0-next.2, es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.1": version: 1.19.1 resolution: "es-abstract@npm:1.19.1" @@ -8818,7 +8789,7 @@ __metadata: languageName: node linkType: hard -"is-callable@npm:^1.1.4, is-callable@npm:^1.2.3, is-callable@npm:^1.2.4": +"is-callable@npm:^1.1.4, is-callable@npm:^1.2.4": version: 1.2.4 resolution: "is-callable@npm:1.2.4" checksum: 10/4e3d8c08208475e74a4108a9dc44dbcb74978782e38a1d1b55388342a4824685765d95917622efa2ca1483f7c4dbec631dd979cbb3ebd239f57a75c83a46d99f @@ -9103,7 +9074,7 @@ __metadata: languageName: node linkType: hard -"is-regex@npm:^1.1.2, is-regex@npm:^1.1.4": +"is-regex@npm:^1.1.4": version: 1.1.4 resolution: "is-regex@npm:1.1.4" dependencies: @@ -12709,13 +12680,6 @@ __metadata: languageName: node linkType: hard -"promise-controller@npm:^1.0.0": - version: 1.0.0 - resolution: "promise-controller@npm:1.0.0" - checksum: 10/80e1a43d377ab15e4bfd61a9826fb6feec50fde69f583544bf67fd2dad191b5190c39509d13180dd3adfd9449e651e0fb727cd827a2bf0ffc6bd3c0874dc619f - languageName: node - linkType: hard - "promise-inflight@npm:^1.0.1": version: 1.0.1 resolution: "promise-inflight@npm:1.0.1" @@ -12733,24 +12697,6 @@ __metadata: languageName: node linkType: hard -"promise.prototype.finally@npm:^3.1.2": - version: 3.1.2 - resolution: "promise.prototype.finally@npm:3.1.2" - dependencies: - define-properties: "npm:^1.1.3" - es-abstract: "npm:^1.17.0-next.0" - function-bind: "npm:^1.1.1" - checksum: 10/e0b6e94d32dd11ee3a3fdcac1aa9a2cc0d1af9391fc34fe3bd169cb365b35851e85f7e4ba47b20538974d08cd855c86f68aa214c7b12d31ca07c084b428781fd - languageName: node - linkType: hard - -"promised-map@npm:^1.0.0": - version: 1.0.0 - resolution: "promised-map@npm:1.0.0" - checksum: 10/716cc4b1be07467ee8ad204eb572d3149e12dd00a60e2267c7505261ce7756e45fcbfc4fcef606c0ffa56fa4984d8e4c5dbb411d21ed30730bd889f1264dd1d3 - languageName: node - linkType: hard - "prompts@npm:^2.0.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" @@ -15093,7 +15039,7 @@ __metadata: languageName: node linkType: hard -"unbox-primitive@npm:^1.0.0, unbox-primitive@npm:^1.0.1": +"unbox-primitive@npm:^1.0.1": version: 1.0.1 resolution: "unbox-primitive@npm:1.0.1" dependencies: @@ -15656,18 +15602,6 @@ __metadata: languageName: node linkType: hard -"websocket-as-promised@npm:^2.0.1": - version: 2.0.1 - resolution: "websocket-as-promised@npm:2.0.1" - dependencies: - chnl: "npm:^1.2.0" - promise-controller: "npm:^1.0.0" - promise.prototype.finally: "npm:^3.1.2" - promised-map: "npm:^1.0.0" - checksum: 10/68dfd25be95e648e18ac1cb996562f0fb1738850e7b1ec3bc3d641833b8dc30dfc1e02f63c5c245a6b64d09ad1bbe5326a3652e8ba38d00833f1e521bbeba0a7 - languageName: node - linkType: hard - "websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": version: 0.7.4 resolution: "websocket-driver@npm:0.7.4"