From 86c9d6c712be113ea91bd172c5d0b79bbdff6d78 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 May 2022 11:52:46 +0300 Subject: [PATCH 01/11] refactor(AeppRpc)!: make to init in sync BREAKING CHANGE: AeppRpc doesn't accept `connection` anymore Use `connectToWallet` method instead. --- src/utils/aepp-wallet-communication/rpc/aepp-rpc.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index b0db28863c..e8c5e784bb 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -11,7 +11,6 @@ import { v4 as uuid } from '@aeternity/uuid' import AccountResolver from '../../../account/resolver' import AccountRpc from '../../../account/rpc' import { decode } from '../../encoder' -import AsyncInit from '../../../utils/async-init' import RpcClient from './rpc-client' import { METHODS, RPC_STATUS, VERSION } from '../schema' import { @@ -102,10 +101,9 @@ const handleMessage = (instance) => async (msg) => { * @param {Object} connection Wallet connection object * @return {Object} */ -export default AccountResolver.compose(AsyncInit, { - async init ({ +export default AccountResolver.compose({ + init ({ name, - connection, debug = false, ...other }) { @@ -115,7 +113,6 @@ export default AccountResolver.compose(AsyncInit, { this[event] = handler }) - this.connection = connection this.name = name this.debug = debug @@ -133,11 +130,6 @@ export default AccountResolver.compose(AsyncInit, { if (!account) this._ensureAccountAccess() return resolveAccountBase(account) } - - if (connection) { - // Init RPCClient - await this.connectToWallet(connection) - } }, methods: { addresses () { From de4c21d4607ab0bfa4df0ef14cf8cc5b00e0d18a Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 May 2022 12:43:30 +0300 Subject: [PATCH 02/11] refactor(rpc): simplify handlers --- .../aepp-wallet-communication/rpc/aepp-rpc.js | 96 ++++++++----------- .../rpc/rpc-client.js | 2 +- .../rpc/wallet-rpc.js | 67 ++++++------- test/integration/rpc.js | 10 +- 4 files changed, 77 insertions(+), 98 deletions(-) diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index e8c5e784bb..475495ad36 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -24,67 +24,55 @@ import { import Node from '../../../node' const NOTIFICATIONS = { - [METHODS.updateAddress]: (instance) => - ({ params }) => { - instance.rpcClient.accounts = params - instance.onAddressChange(params) - }, - [METHODS.updateNetwork]: (instance) => - async ({ params }) => { - const { node } = params - if (node) instance.addNode(node.name, await Node(node), true) - instance.onNetworkChange(params) - }, - [METHODS.closeConnection]: (instance) => - (msg) => { - instance.disconnectWallet() - instance.onDisconnect(msg.params) - }, - [METHODS.readyToConnect]: () => () => {} + [METHODS.updateAddress]: (instance, { params }) => { + instance.rpcClient.accounts = params + instance.onAddressChange(params) + }, + [METHODS.updateNetwork]: async (instance, { params }) => { + const { node } = params + if (node) instance.addNode(node.name, await Node(node), true) + instance.onNetworkChange(params) + }, + [METHODS.closeConnection]: (instance, msg) => { + instance.disconnectWallet() + instance.onDisconnect(msg.params) + }, + [METHODS.readyToConnect]: () => {} } const RESPONSES = { - [METHODS.address]: (instance) => - (msg) => instance.rpcClient.processResponse(msg), - [METHODS.connect]: (instance) => - (msg) => { - if (msg.result) instance.rpcClient.info.status = RPC_STATUS.CONNECTED - instance.rpcClient.processResponse(msg) - }, - [METHODS.subscribeAddress]: (instance) => - (msg) => { - if (msg.result) { - if (msg.result.address) { - instance.rpcClient.accounts = msg.result.address - } - if (msg.result.subscription) { - instance.rpcClient.addressSubscription = msg.result.subscription - } + [METHODS.address]: (instance, msg) => instance.rpcClient.processResponse(msg), + [METHODS.connect]: (instance, msg) => { + if (msg.result) instance.rpcClient.info.status = RPC_STATUS.CONNECTED + instance.rpcClient.processResponse(msg) + }, + [METHODS.subscribeAddress]: (instance, msg) => { + if (msg.result) { + if (msg.result.address) { + instance.rpcClient.accounts = msg.result.address + } + if (msg.result.subscription) { + instance.rpcClient.addressSubscription = msg.result.subscription } - - instance.rpcClient.processResponse(msg, ({ result }) => [result]) - }, - [METHODS.sign]: (instance) => - (msg) => { - instance.rpcClient.processResponse( - msg, ({ result }) => [result.signedTransaction || result.transactionHash] - ) - }, - [METHODS.signMessage]: (instance) => - (msg) => { - instance.rpcClient.processResponse(msg, ({ result }) => [result.signature]) } -} -const REQUESTS = {} + instance.rpcClient.processResponse(msg, ({ result }) => [result]) + }, + [METHODS.sign]: (instance, msg) => { + instance.rpcClient.processResponse( + msg, ({ result }) => [result.signedTransaction || result.transactionHash] + ) + }, + [METHODS.signMessage]: (instance, msg) => { + instance.rpcClient.processResponse(msg, ({ result }) => [result.signature]) + } +} -const handleMessage = (instance) => async (msg) => { +const handleMessage = async (instance, msg) => { if (!msg.id) { - return NOTIFICATIONS[msg.method](instance)(msg) - } else if (Object.prototype.hasOwnProperty.call(instance.rpcClient.callbacks, msg.id)) { - return RESPONSES[msg.method](instance)(msg) - } else { - return REQUESTS[msg.method](instance)(msg) + return NOTIFICATIONS[msg.method](instance, msg) + } else if (instance.rpcClient.callbacks[msg.id] != null) { + return RESPONSES[msg.method](instance, msg) } } @@ -155,7 +143,7 @@ export default AccountResolver.compose({ ...walletInfo, connection, id: uuid(), - handlers: [handleMessage(this), this.onDisconnect] + handlers: [handleMessage.bind(null, this), this.onDisconnect] }) const { node } = await this.sendConnectRequest(connectNode) if (connectNode) { diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index d5f5bc6e5c..d3f3a501c9 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -212,7 +212,7 @@ export default stampit({ */ request (name, params) { const msgId = this.sendMessage({ method: name, params }) - if (Object.prototype.hasOwnProperty.call(this.callbacks, msgId)) { + if (this.callbacks[msgId] != null) { throw new DuplicateCallbackError() } return new Promise((resolve, reject) => { diff --git a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js index 11b1a89deb..3de56460d0 100644 --- a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js @@ -26,15 +26,12 @@ const resolveOnAccount = (addresses, onAccount, opt = {}) => { } const NOTIFICATIONS = { - [METHODS.closeConnection]: (instance, { client }) => - async (msg) => { - client.disconnect(true) - instance.onDisconnect(msg.params, client) - } + [METHODS.closeConnection]: async (instance, client, params) => { + client.disconnect(true) + instance.onDisconnect(params, client) + } } -const RESPONSES = {} - const REQUESTS = { // Store client info and prepare two fn for each client `connect` and `denyConnection` // which automatically prepare and send response for that client @@ -115,8 +112,8 @@ const REQUESTS = { (error) => ({ error: ERRORS.rejectedByUser(error) }) ) }, - [METHODS.sign] (callInstance, instance, client, options) { - const { tx, onAccount, returnSigned = false } = options + [METHODS.sign] (callInstance, instance, client, message) { + const { tx, onAccount, returnSigned = false } = message const address = onAccount || client.currentAccount // Authorization check if (!client.isConnected()) return { error: ERRORS.notAuthorize() } @@ -188,34 +185,27 @@ const REQUESTS = { } } -const handleMessage = (instance, id) => async (msg, origin) => { - const client = instance.rpcClients[id] - if (!msg.id) { - return NOTIFICATIONS[msg.method](instance, { client })(msg, origin) - } - if (Object.prototype.hasOwnProperty.call(client.callbacks, msg.id)) { - return RESPONSES[msg.method](instance, { client })(msg, origin) - } else { - const { id, method } = msg - const callInstance = (methodName, params, accept, deny) => () => new Promise(resolve => { - instance[methodName]( - client, - { - id, - method, - params, - accept: (...args) => resolve(accept(...args)), - deny: (...args) => resolve(deny(...args)) - }, - origin - ) - }) - // TODO make one structure for handler functions - const errorObjectOrHandler = REQUESTS[msg.method](callInstance, instance, client, msg.params) - const response = typeof errorObjectOrHandler === 'function' ? await errorObjectOrHandler() : errorObjectOrHandler - const { error, result } = response ?? {} - client.sendMessage({ id, method, ...error ? { error } : { result } }, true) +const handleMessage = async (instance, client, { id, method, params }, origin) => { + if (!id) { + return NOTIFICATIONS[method](instance, client, params, origin) } + + const callInstance = (methodName, params, accept, deny) => new Promise(resolve => { + instance[methodName]( + client, + { + id, + method, + params, + accept: (...args) => resolve(accept(...args)), + deny: (...args) => resolve(deny(...args)) + }, + origin + ) + }) + const response = await REQUESTS[method](callInstance, instance, client, params) + const { error, result } = response ?? {} + client.sendMessage({ id, method, ...error ? { error } : { result } }, true) } /** @@ -336,7 +326,10 @@ export default Ae.compose(AccountMultiple, { id, info: { status: RPC_STATUS.WAITING_FOR_CONNECTION_REQUEST }, connection: clientConnection, - handlers: [handleMessage(this, id), this.onDisconnect] + handlers: [ + (message, origin) => handleMessage(this, this.rpcClients[id], message, origin), + this.onDisconnect + ] }) return id }, diff --git a/test/integration/rpc.js b/test/integration/rpc.js index a03cf473b5..e5288fa524 100644 --- a/test/integration/rpc.js +++ b/test/integration/rpc.js @@ -368,14 +368,12 @@ describe('Aepp<->Wallet', function () { it('Receive update for wallet select account', async () => { const connectedAccount = Object.keys(aepp.rpcClient.accounts.connected)[0] - const received = await new Promise((resolve) => { - aepp.onAddressChange = ({ connected, current }) => { - Object.hasOwnProperty.call(current, connectedAccount).should.be.equal(true) - resolve(Object.keys(connected)[0] !== connectedAccount) - } + const { connected, current } = await new Promise((resolve) => { + aepp.onAddressChange = resolve wallet.selectAccount(connectedAccount) }) - received.should.be.equal(true) + expect(current[connectedAccount]).to.be.eql({}) + expect(Object.keys(connected).includes(connectedAccount)).to.be.equal(false) }) it('Aepp: receive notification for network update', async () => { From e2b5186940137a2f5d7dc06b06ce315a496ae669 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 May 2022 13:05:17 +0300 Subject: [PATCH 03/11] refactor(aepp-rpc): drop custom response handlers --- src/account/rpc.ts | 5 ++- .../aepp-wallet-communication/rpc/aepp-rpc.js | 43 +++++-------------- .../rpc/rpc-client.js | 15 +++---- 3 files changed, 19 insertions(+), 44 deletions(-) diff --git a/src/account/rpc.ts b/src/account/rpc.ts index 67a8e2f48b..b21155bbe9 100644 --- a/src/account/rpc.ts +++ b/src/account/rpc.ts @@ -44,11 +44,12 @@ class _AccountRpc extends _AccountBase { ): Promise> { if (innerTx != null) throw new NotImplementedError('innerTx option in AccountRpc') if (networkId !== this._networkId) throw new NotImplementedError('networkId should be equal to current network') - return this._rpcClient.request(METHODS.sign, { + const res = await this._rpcClient.request(METHODS.sign, { onAccount: this._address, tx, returnSigned: true }) + return res.signedTransaction } /** @@ -60,7 +61,7 @@ class _AccountRpc extends _AccountBase { async signMessage ( message: string, { returnHex = false }: Parameters<_AccountBase['signMessage']>[1] = {} ): Promise { - const signature = await this._rpcClient.request( + const { signature } = await this._rpcClient.request( METHODS.signMessage, { onAccount: this._address, message } ) return returnHex ? signature : Buffer.from(signature, 'hex') diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index 475495ad36..ce7ab79600 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -40,39 +40,9 @@ const NOTIFICATIONS = { [METHODS.readyToConnect]: () => {} } -const RESPONSES = { - [METHODS.address]: (instance, msg) => instance.rpcClient.processResponse(msg), - [METHODS.connect]: (instance, msg) => { - if (msg.result) instance.rpcClient.info.status = RPC_STATUS.CONNECTED - instance.rpcClient.processResponse(msg) - }, - [METHODS.subscribeAddress]: (instance, msg) => { - if (msg.result) { - if (msg.result.address) { - instance.rpcClient.accounts = msg.result.address - } - if (msg.result.subscription) { - instance.rpcClient.addressSubscription = msg.result.subscription - } - } - - instance.rpcClient.processResponse(msg, ({ result }) => [result]) - }, - [METHODS.sign]: (instance, msg) => { - instance.rpcClient.processResponse( - msg, ({ result }) => [result.signedTransaction || result.transactionHash] - ) - }, - [METHODS.signMessage]: (instance, msg) => { - instance.rpcClient.processResponse(msg, ({ result }) => [result.signature]) - } -} - const handleMessage = async (instance, msg) => { if (!msg.id) { return NOTIFICATIONS[msg.method](instance, msg) - } else if (instance.rpcClient.callbacks[msg.id] != null) { - return RESPONSES[msg.method](instance, msg) } } @@ -190,7 +160,14 @@ export default AccountResolver.compose({ */ async subscribeAddress (type, value) { this._ensureConnected() - return this.rpcClient.request(METHODS.subscribeAddress, { type, value }) + const result = await this.rpcClient.request(METHODS.subscribeAddress, { type, value }) + if (result.address) { + this.rpcClient.accounts = result.address + } + if (result.subscription) { + this.rpcClient.addressSubscription = result.subscription + } + return result }, /** * Send connection request to wallet @@ -201,13 +178,15 @@ export default AccountResolver.compose({ * @return {Promise} Connection response */ async sendConnectRequest (connectNode) { - return this.rpcClient.request( + const walletInfo = this.rpcClient.request( METHODS.connect, { name: this.name, version: VERSION, connectNode } ) + this.rpcClient.info.status = RPC_STATUS.CONNECTED + return walletInfo }, _ensureConnected () { if (this.rpcClient?.isConnected()) return diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index d3f3a501c9..c010c73e4b 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -51,7 +51,8 @@ export default stampit({ if (!msg || !msg.jsonrpc || msg.jsonrpc !== '2.0' || !msg.method) { throw new InvalidRpcMessageError(msg) } - onMessage(msg, origin) + if ((msg.result ?? msg.error) != null) this.processResponse(msg) + else onMessage(msg, origin) } const disconnect = (aepp, connection) => { @@ -225,18 +226,12 @@ export default stampit({ * @instance * @rtype (msg: Object, transformResult: Function) => void * @param {Object} msg Message object - * @param {Function=} transformResult Optional parser function for message * @return {void} */ - processResponse ({ id, error, result }, transformResult) { + processResponse ({ id, error, result }) { if (!this.callbacks[id]) throw new MissingCallbackError(id) - if (result) { - this.callbacks[id].resolve(...typeof transformResult === 'function' - ? transformResult({ id, result }) - : [result]) - } else { - this.callbacks[id].reject(error) - } + if (result) this.callbacks[id].resolve(result) + else this.callbacks[id].reject(error) delete this.callbacks[id] } } From 5f1a0075f35c1350d62e245317d4e5466590670f Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 May 2022 14:38:10 +0300 Subject: [PATCH 04/11] refactor(rpc-client)!: provide method handlers instead of onMessage BREAKING CHANGE: `handlers` parameter is removed in RpcClient Provide a `methods` parameter instead of `handlers[0]`. Provide an `onDisconnect` parameter instead of `handlers[1]`. --- .../aepp-wallet-communication/rpc/aepp-rpc.js | 20 +++---- .../rpc/rpc-client.js | 22 ++++--- .../rpc/wallet-rpc.js | 57 +++++++------------ 3 files changed, 45 insertions(+), 54 deletions(-) diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index ce7ab79600..5bfc247936 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -11,6 +11,7 @@ import { v4 as uuid } from '@aeternity/uuid' import AccountResolver from '../../../account/resolver' import AccountRpc from '../../../account/rpc' import { decode } from '../../encoder' +import { mapObject } from '../../other' import RpcClient from './rpc-client' import { METHODS, RPC_STATUS, VERSION } from '../schema' import { @@ -23,29 +24,23 @@ import { } from '../../errors' import Node from '../../../node' -const NOTIFICATIONS = { - [METHODS.updateAddress]: (instance, { params }) => { +const METHOD_HANDLERS = { + [METHODS.updateAddress]: (instance, params) => { instance.rpcClient.accounts = params instance.onAddressChange(params) }, - [METHODS.updateNetwork]: async (instance, { params }) => { + [METHODS.updateNetwork]: async (instance, params) => { const { node } = params if (node) instance.addNode(node.name, await Node(node), true) instance.onNetworkChange(params) }, - [METHODS.closeConnection]: (instance, msg) => { + [METHODS.closeConnection]: (instance, params) => { instance.disconnectWallet() - instance.onDisconnect(msg.params) + instance.onDisconnect(params) }, [METHODS.readyToConnect]: () => {} } -const handleMessage = async (instance, msg) => { - if (!msg.id) { - return NOTIFICATIONS[msg.method](instance, msg) - } -} - /** * Contain functionality for wallet interaction and connect it to sdk * @alias module:@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc @@ -113,7 +108,8 @@ export default AccountResolver.compose({ ...walletInfo, connection, id: uuid(), - handlers: [handleMessage.bind(null, this), this.onDisconnect] + onDisconnect: this.onDisconnect, + methods: mapObject(METHOD_HANDLERS, ([key, value]) => [key, value.bind(null, this)]) }) const { node } = await this.sendConnectRequest(connectNode) if (connectNode) { diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index c010c73e4b..d068f5e9db 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -23,13 +23,12 @@ import { * @param {Object} param Init params object * @param {String} param.name Client name * @param {Object} param.connection Connection object - * @param {Function[]} param.handlers Array with two function for message handling - * @param {Function} param.handlers[0] Message handler - * @param {Function} param.handlers[1] Disconnect callback + * @param {Function} param.onDisconnect Disconnect callback + * @param {Object} param.methods Object containing handlers for each request by name * @return {Object} */ export default stampit({ - init ({ id, name, icons, connection, handlers: [onMessage, onDisconnect] }) { + init ({ id, name, icons, connection, onDisconnect, methods }) { this.id = id this.connection = connection this.info = { name, icons } @@ -47,12 +46,21 @@ export default stampit({ this._messageId = 0 - const handleMessage = (msg, origin) => { + const handleMessage = async (msg, origin) => { if (!msg || !msg.jsonrpc || msg.jsonrpc !== '2.0' || !msg.method) { throw new InvalidRpcMessageError(msg) } - if ((msg.result ?? msg.error) != null) this.processResponse(msg) - else onMessage(msg, origin) + if ((msg.result ?? msg.error) != null) { + this.processResponse(msg) + return + } + + // TODO: remove methods as far it is not required in JSON RPC + const response = { id: msg.id, method: msg.method } + const { error, result } = await methods[msg.method](msg.params, origin) + if (error) response.error = error + else response.result = result + if (response.id) this.sendMessage(response, true) } const disconnect = (aepp, connection) => { diff --git a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js index 3de56460d0..e2659cb81d 100644 --- a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js @@ -14,7 +14,7 @@ import RpcClient from './rpc-client' import { ERRORS, METHODS, RPC_STATUS, VERSION } from '../schema' import { ArgumentError, TypeError, UnknownRpcClientError } from '../../errors' import { isAccountBase } from '../../../account/base' -import { filterObject } from '../../other' +import { filterObject, mapObject } from '../../other' import { unpackTx } from '../../../tx/builder' const resolveOnAccount = (addresses, onAccount, opt = {}) => { @@ -25,14 +25,11 @@ const resolveOnAccount = (addresses, onAccount, opt = {}) => { return onAccount } -const NOTIFICATIONS = { - [METHODS.closeConnection]: async (instance, client, params) => { +const METHOD_HANDLERS = { + [METHODS.closeConnection]: async (callInstance, instance, client, params) => { client.disconnect(true) instance.onDisconnect(params, client) - } -} - -const REQUESTS = { + }, // Store client info and prepare two fn for each client `connect` and `denyConnection` // which automatically prepare and send response for that client [METHODS.connect] ( @@ -185,29 +182,6 @@ const REQUESTS = { } } -const handleMessage = async (instance, client, { id, method, params }, origin) => { - if (!id) { - return NOTIFICATIONS[method](instance, client, params, origin) - } - - const callInstance = (methodName, params, accept, deny) => new Promise(resolve => { - instance[methodName]( - client, - { - id, - method, - params, - accept: (...args) => resolve(accept(...args)), - deny: (...args) => resolve(deny(...args)) - }, - origin - ) - }) - const response = await REQUESTS[method](callInstance, instance, client, params) - const { error, result } = response ?? {} - client.sendMessage({ id, method, ...error ? { error } : { result } }, true) -} - /** * Contain functionality for aepp interaction and managing multiple aepps * @alias module:@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/wallet-rpc @@ -322,15 +296,28 @@ export default Ae.compose(AccountMultiple, { // @TODO detect if aepp has some history based on origin???? // if yes use this instance for connection const id = uuid() - this.rpcClients[id] = RpcClient({ + const client = RpcClient({ id, info: { status: RPC_STATUS.WAITING_FOR_CONNECTION_REQUEST }, connection: clientConnection, - handlers: [ - (message, origin) => handleMessage(this, this.rpcClients[id], message, origin), - this.onDisconnect - ] + onDisconnect: this.onDisconnect, + methods: mapObject(METHOD_HANDLERS, ([key, value]) => [key, (params, origin) => { + const callInstance = (methodName, params, accept, deny) => new Promise(resolve => { + this[methodName]( + client, + { + method: key, + params, + accept: (...args) => resolve(accept(...args)), + deny: (...args) => resolve(deny(...args)) + }, + origin + ) + }) + return value(callInstance, this, client, params) + }]) }) + this.rpcClients[id] = client return id }, /** From c8151fffa352ec00571f58e556aa1763cfd30b8c Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Fri, 27 May 2022 17:27:53 +0300 Subject: [PATCH 05/11] refactor(rpc): add set of error classes --- docs/guides/error-handling.md | 10 ++ .../rpc/rpc-client.js | 18 +-- .../rpc/wallet-rpc.js | 106 +++++-------- src/utils/aepp-wallet-communication/schema.ts | 145 +++++++++++++----- test/integration/rpc.js | 4 +- 5 files changed, 165 insertions(+), 118 deletions(-) diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md index 16cfaf28b5..e42eea66a1 100644 --- a/docs/guides/error-handling.md +++ b/docs/guides/error-handling.md @@ -98,6 +98,16 @@ BaseError │ │ AlreadyConnectedError │ │ NoWalletConnectedError │ │ RpcConnectionError +│ +└̌───RpcError +│ │ RpcInvalidTransactionError +│ │ RpcBroadcastError +│ │ RpcRejectedByUserError +│ │ RpcUnsupportedProtocolError +│ │ RpcConnectionDenyError +│ │ RpcNotAuthorizeError +│ │ RpcPermissionDenyError +│ │ RpcInternalError ``` ## Usage diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index d068f5e9db..e9f956a0f0 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -8,12 +8,8 @@ */ import stampit from '@stamp/it' -import { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES } from '../schema' -import { - InvalidRpcMessageError, - DuplicateCallbackError, - MissingCallbackError -} from '../../errors' +import { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES, RpcError, RpcInternalError } from '../schema' +import { InvalidRpcMessageError, DuplicateCallbackError, MissingCallbackError } from '../../errors' /** * Contain functionality for using RPC conection @@ -57,9 +53,11 @@ export default stampit({ // TODO: remove methods as far it is not required in JSON RPC const response = { id: msg.id, method: msg.method } - const { error, result } = await methods[msg.method](msg.params, origin) - if (error) response.error = error - else response.result = result + try { + response.result = await methods[msg.method](msg.params, origin) + } catch (error) { + response.error = error instanceof RpcError ? error : new RpcInternalError() + } if (response.id) this.sendMessage(response, true) } @@ -239,7 +237,7 @@ export default stampit({ processResponse ({ id, error, result }) { if (!this.callbacks[id]) throw new MissingCallbackError(id) if (result) this.callbacks[id].resolve(result) - else this.callbacks[id].reject(error) + else this.callbacks[id].reject(RpcError.deserialize(error)) delete this.callbacks[id] } } diff --git a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js index e2659cb81d..03ffe09d5c 100644 --- a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js @@ -11,7 +11,11 @@ import Ae from '../../../ae' import verifyTransaction from '../../../tx/validator' import AccountMultiple from '../../../account/multiple' import RpcClient from './rpc-client' -import { ERRORS, METHODS, RPC_STATUS, VERSION } from '../schema' +import { + METHODS, RPC_STATUS, VERSION, + RpcBroadcastError, RpcConnectionDenyError, RpcInvalidTransactionError, + RpcNotAuthorizeError, RpcPermissionDenyError, RpcRejectedByUserError, RpcUnsupportedProtocolError +} from '../schema' import { ArgumentError, TypeError, UnknownRpcClientError } from '../../errors' import { isAccountBase } from '../../../account/base' import { filterObject, mapObject } from '../../other' @@ -37,8 +41,7 @@ const METHOD_HANDLERS = { instance, client, { name, version, icons, connectNode }) { - // Check if protocol and network is compatible with wallet - if (version !== VERSION) return { error: ERRORS.unsupportedProtocol() } + if (version !== VERSION) throw new RpcUnsupportedProtocolError() // Store new AEPP and wait for connection approve client.updateInfo({ status: RPC_STATUS.WAITING_FOR_CONNECTION_APPROVE, @@ -56,128 +59,93 @@ const METHOD_HANDLERS = { ({ shareNode } = {}) => { client.updateInfo({ status: shareNode ? RPC_STATUS.NODE_BINDED : RPC_STATUS.CONNECTED }) return { - result: { - ...instance.getWalletInfo(), - ...(shareNode && { node: instance.selectedNode }) - } + ...instance.getWalletInfo(), + ...(shareNode && { node: instance.selectedNode }) } }, (error) => { client.updateInfo({ status: RPC_STATUS.CONNECTION_REJECTED }) - return { error: ERRORS.connectionDeny(error) } + throw new RpcConnectionDenyError(error) } ) }, [METHODS.subscribeAddress] (callInstance, instance, client, { type, value }) { - // Authorization check - if (!client.isConnected()) return { error: ERRORS.notAuthorize() } + if (!client.isConnected()) throw new RpcNotAuthorizeError() return callInstance( 'onSubscription', { type, value }, async ({ accounts } = {}) => { - try { - const clientAccounts = accounts || instance.getAccounts() - const subscription = client.updateSubscription(type, value) - client.setAccounts(clientAccounts, { forceNotification: true }) - return { - result: { - subscription, - address: clientAccounts - } - } - } catch (e) { - if (instance.debug) console.error(e) - return { error: ERRORS.internalError(e.message) } + const clientAccounts = accounts || instance.getAccounts() + const subscription = client.updateSubscription(type, value) + client.setAccounts(clientAccounts, { forceNotification: true }) + return { + subscription, + address: clientAccounts } }, - (error) => ({ error: ERRORS.rejectedByUser(error) }) + (error) => { throw new RpcRejectedByUserError(error) } ) }, [METHODS.address] (callInstance, instance, client) { - // Authorization check - if (!client.isConnected()) return { error: ERRORS.notAuthorize() } - if (!client.isSubscribed()) return { error: ERRORS.notAuthorize() } + if (!client.isConnected() || !client.isSubscribed()) throw new RpcNotAuthorizeError() return callInstance( 'onAskAccounts', {}, - ({ accounts } = {}) => ({ - result: accounts || - [...Object.keys(client.accounts.current), ...Object.keys(client.accounts.connected)] - }), - (error) => ({ error: ERRORS.rejectedByUser(error) }) + ({ accounts } = {}) => accounts || + [...Object.keys(client.accounts.current), ...Object.keys(client.accounts.connected)], + (error) => { throw new RpcRejectedByUserError(error) } ) }, [METHODS.sign] (callInstance, instance, client, message) { const { tx, onAccount, returnSigned = false } = message const address = onAccount || client.currentAccount - // Authorization check - if (!client.isConnected()) return { error: ERRORS.notAuthorize() } - // Account permission check - if (!client.hasAccessToAccount(address)) { - return { error: ERRORS.permissionDeny(address) } - } + if (!client.isConnected()) throw new RpcNotAuthorizeError() + if (!client.hasAccessToAccount(address)) throw new RpcPermissionDenyError(address) return callInstance( 'onSign', { tx, returnSigned, onAccount: address, txObject: unpackTx(tx) }, async (rawTx, opt = {}) => { - let onAcc - try { - onAcc = resolveOnAccount(instance.addresses(), address, opt) - } catch (e) { - if (instance.debug) console.error(e) - return { error: ERRORS.internalError(e.message) } - } + const onAcc = resolveOnAccount(instance.addresses(), address, opt) try { const t = rawTx || tx - const result = returnSigned + return returnSigned ? { signedTransaction: await instance.signTransaction(t, { onAccount: onAcc }) } : { transactionHash: await instance.send(t, { onAccount: onAcc, verify: false }) } - return { result } } catch (e) { if (!returnSigned) { // Validate transaction const validation = await verifyTransaction(rawTx || tx, instance.selectedNode.instance) - if (validation.length) return { error: ERRORS.invalidTransaction(validation) } + if (validation.length) throw new RpcInvalidTransactionError(validation) // Send broadcast failed error to aepp - return { error: ERRORS.broadcastFailed(e.message) } + throw new RpcBroadcastError(e.message) } throw e } }, - (error) => ({ error: ERRORS.rejectedByUser(error) }) + (error) => { throw new RpcRejectedByUserError(error) } ) }, [METHODS.signMessage] (callInstance, instance, client, { message, onAccount }) { - // Authorization check - if (!client.isConnected()) return { error: ERRORS.notAuthorize() } + if (!client.isConnected()) throw new RpcNotAuthorizeError() const address = onAccount || client.currentAccount - if (!client.hasAccessToAccount(address)) { - return { error: ERRORS.permissionDeny(address) } - } + if (!client.hasAccessToAccount(address)) throw new RpcPermissionDenyError(address) return callInstance( 'onMessageSign', { message, onAccount: address }, async (opt = {}) => { - try { - const onAcc = resolveOnAccount(instance.addresses(), address, opt) - return { - result: { - signature: await instance.signMessage(message, { - onAccount: onAcc, - returnHex: true - }) - } - } - } catch (e) { - if (instance.debug) console.error(e) - return { error: ERRORS.internalError(e.message) } + const onAcc = resolveOnAccount(instance.addresses(), address, opt) + return { + signature: await instance.signMessage(message, { + onAccount: onAcc, + returnHex: true + }) } }, - (error) => ({ error: ERRORS.rejectedByUser(error) }) + (error) => { throw new RpcRejectedByUserError(error) } ) } } diff --git a/src/utils/aepp-wallet-communication/schema.ts b/src/utils/aepp-wallet-communication/schema.ts index d5529cdbe2..117f25ccfd 100644 --- a/src/utils/aepp-wallet-communication/schema.ts +++ b/src/utils/aepp-wallet-communication/schema.ts @@ -1,3 +1,6 @@ +import { EncodedData } from '../encoder' +import { BaseError, InternalError } from '../errors' + export const VERSION = 1 export const enum MESSAGE_DIRECTION { @@ -36,41 +39,109 @@ export const enum RPC_STATUS { WAITING_FOR_CONNECTION_REQUEST = 'WAITING_FOR_CONNECTION_REQUEST' } -export const ERRORS = { - broadcastFailed: (error = {}) => ({ - code: 3, - data: error, - message: 'Broadcast failed' - }), - invalidTransaction: (error = {}) => ({ - code: 2, - data: error, - message: 'Invalid transaction' - }), - rejectedByUser: (error = {}) => ({ - code: 4, - data: error, - message: 'Operation rejected by user' - }), - connectionDeny: (error = {}) => ({ - code: 9, - data: error, - message: 'Wallet deny your connection request' - }), - permissionDeny: (address: string) => ({ - code: 11, - message: `You are not subscribed for account ${address}` - }), - internalError: (message: string) => ({ - code: 12, - message - }), - notAuthorize: () => ({ - code: 10, - message: 'You are not connected to the wallet' - }), - unsupportedProtocol: () => ({ - code: 5, - message: 'Unsupported Protocol Version' - }) +interface RpcErrorAsJson { + code: number + message: string + data?: any +} + +export abstract class RpcError extends BaseError { + static code: number + code: number + data?: any + + toJSON (): RpcErrorAsJson { + return { + code: this.code, + message: this.message, + data: this.data + } + } + + static deserialize (json: RpcErrorAsJson): RpcError { + const RpcErr = [ + RpcInvalidTransactionError, RpcBroadcastError, RpcRejectedByUserError, + RpcUnsupportedProtocolError, RpcConnectionDenyError, RpcNotAuthorizeError, + RpcPermissionDenyError, RpcInternalError + ].find(cl => cl.code === json.code) + if (RpcErr == null) throw new InternalError(`Can't find RpcError with code: ${json.code}`) + return new RpcErr(json.data) + } +} + +export class RpcInvalidTransactionError extends RpcError { + static code = 2 + code = 2 + constructor (data?: any) { + super('Invalid transaction') + this.data = data + this.name = 'RpcInvalidTransactionError' + } +} + +export class RpcBroadcastError extends RpcError { + static code = 3 + code = 3 + constructor (data?: any) { + super('Broadcast failed') + this.data = data + this.name = 'RpcBroadcastError' + } +} + +export class RpcRejectedByUserError extends RpcError { + static code = 4 + code = 4 + constructor (data?: any) { + super('Operation rejected by user') + this.data = data + this.name = 'RpcRejectedByUserError' + } +} + +export class RpcUnsupportedProtocolError extends RpcError { + static code = 5 + code = 5 + constructor () { + super('Unsupported Protocol Version') + this.name = 'RpcUnsupportedProtocolError' + } +} + +export class RpcConnectionDenyError extends RpcError { + static code = 9 + code = 9 + constructor (data?: any) { + super('Wallet deny your connection request') + this.data = data + this.name = 'RpcConnectionDenyError' + } +} + +export class RpcNotAuthorizeError extends RpcError { + static code = 10 + code = 10 + constructor () { + super('You are not connected to the wallet') + this.name = 'RpcNotAuthorizeError' + } +} + +export class RpcPermissionDenyError extends RpcError { + static code = 11 + code = 11 + constructor (address: EncodedData<'ak'>) { + super(`You are not subscribed for account ${address}`) + this.data = address + this.name = 'RpcPermissionDenyError' + } +} + +export class RpcInternalError extends RpcError { + static code = 12 + code = 12 + constructor () { + super('The peer failed to execute your request due to unknown error') + this.name = 'RpcInternalError' + } } diff --git a/test/integration/rpc.js b/test/integration/rpc.js index e5288fa524..4e676e2431 100644 --- a/test/integration/rpc.js +++ b/test/integration/rpc.js @@ -240,7 +240,7 @@ describe('Aepp<->Wallet', function () { payload: 'zerospend' }) await expect(aepp.signTransaction(tx, { onAccount: keypair.publicKey })) - .to.be.rejectedWith('Provided onAccount should be an AccountBase') + .to.be.rejectedWith('The peer failed to execute your request due to unknown error') }) it('Sign transaction: wallet allow', async () => { @@ -320,7 +320,7 @@ describe('Aepp<->Wallet', function () { } const onAccount = aepp.addresses()[1] await expect(aepp.signMessage('test', { onAccount })).to.be.eventually - .rejectedWith('Provided onAccount should be an AccountBase') + .rejectedWith('The peer failed to execute your request due to unknown error') .with.property('code', 12) }) From 4b41b8922a4b8e909f1c6ad646b6c088ad824149 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sat, 28 May 2022 21:03:41 +0300 Subject: [PATCH 06/11] refactor: remove extra brackets around object in spread --- src/ae/aens.js | 8 ++++---- src/utils/aepp-wallet-communication/rpc/wallet-rpc.js | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/ae/aens.js b/src/ae/aens.js index 003574c0a9..b71387a1c2 100644 --- a/src/ae/aens.js +++ b/src/ae/aens.js @@ -182,14 +182,14 @@ async function query (name, opt = {}) { pointers: o.pointers || [], update: async (pointers, options = {}) => { return { - ...(await this.aensUpdate(name, pointers, { ...opt, ...options })), - ...(await this.aensQuery(name)) + ...await this.aensUpdate(name, pointers, { ...opt, ...options }), + ...await this.aensQuery(name) } }, transfer: async (account, options = {}) => { return { - ...(await this.aensTransfer(name, account, { ...opt, ...options })), - ...(await this.aensQuery(name)) + ...await this.aensTransfer(name, account, { ...opt, ...options }), + ...await this.aensQuery(name) } }, revoke: async (options = {}) => this.aensRevoke(name, { ...opt, ...options }), diff --git a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js index 03ffe09d5c..9221638592 100644 --- a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js @@ -60,7 +60,7 @@ const METHOD_HANDLERS = { client.updateInfo({ status: shareNode ? RPC_STATUS.NODE_BINDED : RPC_STATUS.CONNECTED }) return { ...instance.getWalletInfo(), - ...(shareNode && { node: instance.selectedNode }) + ...shareNode && { node: instance.selectedNode } } }, (error) => { From 9e97a1a97716066ec6d87c845186c8815e409ac3 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sat, 28 May 2022 21:46:32 +0300 Subject: [PATCH 07/11] refactor(rpc-client)!: add notify method BREAKING CHANGE: `sendMessage` of RpcClient is a private method Use `request` or `notify` instead. BREAKING CHANGE: `shareWalletInfo` of WalletRpc accepts rpcClientId instead of callback For example, rewrite ``` const connection = new BrowserRuntimeConnection({ port }) aeSdk.addRpcClient(connection) aeSdk.shareWalletInfo(port.postMessage.bind(port)) ``` to ``` const connection = new BrowserRuntimeConnection({ port }) const rpcClientId = aeSdk.addRpcClient(connection) aeSdk.shareWalletInfo(rpcClientId) ``` --- docs/guides/build-wallet.md | 12 ++--- examples/browser/wallet-iframe/src/App.vue | 14 +++--- .../wallet-web-extension/src/background.js | 6 +-- .../aepp-wallet-communication/rpc/aepp-rpc.js | 2 +- .../rpc/rpc-client.js | 19 +++++--- .../rpc/wallet-rpc.js | 21 +++------ test/integration/rpc.js | 47 +++++++------------ 7 files changed, 53 insertions(+), 68 deletions(-) diff --git a/docs/guides/build-wallet.md b/docs/guides/build-wallet.md index e8dfce1f43..73efd57939 100644 --- a/docs/guides/build-wallet.md +++ b/docs/guides/build-wallet.md @@ -106,10 +106,10 @@ async function init () { // create connection const connection = new BrowserRuntimeConnection({ port }) // add new aepp to wallet - wallet.addRpcClient(connection) + const clientId = aeSdk.addRpcClient(connection) // share wallet details - wallet.shareWalletInfo(port.postMessage.bind(port)) - setTimeout(() => wallet.shareWalletInfo(port.postMessage.bind(port)), 3000) + aeSdk.shareWalletInfo(clientId) + setInterval(() => aeSdk.shareWalletInfo(clientId), 3000) }) }).catch(err => { console.error(err) @@ -190,10 +190,10 @@ async function init () { // create connection const connection = new BrowserRuntimeConnection({ port }) // add new aepp to wallet - wallet.addRpcClient(connection) + const clientId = aeSdk.addRpcClient(connection) // share wallet details - wallet.shareWalletInfo(port.postMessage.bind(port)) - setTimeout(() => wallet.shareWalletInfo(port.postMessage.bind(port)), 3000) + aeSdk.shareWalletInfo(clientId) + setInterval(() => aeSdk.shareWalletInfo(clientId), 3000) }) }).catch(err => { console.error(err) diff --git a/examples/browser/wallet-iframe/src/App.vue b/examples/browser/wallet-iframe/src/App.vue index baa1938e50..602de2a177 100644 --- a/examples/browser/wallet-iframe/src/App.vue +++ b/examples/browser/wallet-iframe/src/App.vue @@ -45,17 +45,17 @@ export default { } }, methods: { - async shareWalletInfo (postFn, { interval = 5000, attemps = 5 } = {}) { - this.aeSdk.shareWalletInfo(postFn) + async shareWalletInfo (clientId, { interval = 5000, attemps = 5 } = {}) { + this.aeSdk.shareWalletInfo(clientId) while (attemps -= 1) { await new Promise(resolve => setTimeout(resolve, interval)) - this.aeSdk.shareWalletInfo(postFn) + this.aeSdk.shareWalletInfo(clientId) } console.log('Finish sharing wallet info') }, disconnect () { Object.values(this.aeSdk.rpcClients).forEach(client => { - client.sendMessage({ method: METHODS.closeConnection }, true) + client.notify(METHODS.closeConnection) client.disconnect() }) }, @@ -99,7 +99,7 @@ export default { onMessageSign: genConfirmCallback(() => 'message sign'), onAskAccounts: genConfirmCallback(() => 'get accounts'), onDisconnect (message, client) { - this.shareWalletInfo(connection.sendMessage.bind(connection)) + this.shareWalletInfo(clientId) } }) this.nodeName = this.aeSdk.selectedNode.name @@ -107,8 +107,8 @@ export default { const target = this.runningInFrame ? window.parent : this.$refs.aepp.contentWindow const connection = new BrowserWindowMessageConnection({ target }) - this.aeSdk.addRpcClient(connection) - this.shareWalletInfo(connection.sendMessage.bind(connection)) + const clientId = this.aeSdk.addRpcClient(connection) + this.shareWalletInfo(clientId) this.$watch( ({ address, nodeName }) => [address, nodeName], diff --git a/examples/browser/wallet-web-extension/src/background.js b/examples/browser/wallet-web-extension/src/background.js index 8eb43cf8fa..fbe07b5a2b 100644 --- a/examples/browser/wallet-web-extension/src/background.js +++ b/examples/browser/wallet-web-extension/src/background.js @@ -69,10 +69,10 @@ import { // create connection const connection = new BrowserRuntimeConnection({ port }) // add new aepp to wallet - aeSdk.addRpcClient(connection) + const clientId = aeSdk.addRpcClient(connection) // share wallet details - aeSdk.shareWalletInfo(port.postMessage.bind(port)) - setInterval(() => aeSdk.shareWalletInfo(port.postMessage.bind(port)), 3000) + aeSdk.shareWalletInfo(clientId) + setInterval(() => aeSdk.shareWalletInfo(clientId), 3000) }) console.log('Wallet initialized!') diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index 5bfc247936..9d6a4b1470 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -129,7 +129,7 @@ export default AccountResolver.compose({ async disconnectWallet (sendDisconnect = true) { this._ensureConnected() if (sendDisconnect) { - this.rpcClient.sendMessage({ method: METHODS.closeConnection, params: { reason: 'bye' } }, true) + this.rpcClient.notify(METHODS.closeConnection, { reason: 'bye' }) } this.rpcClient.disconnect() this.rpcClient = null diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index e9f956a0f0..fdfe031e6d 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -58,7 +58,7 @@ export default stampit({ } catch (error) { response.error = error instanceof RpcError ? error : new RpcInternalError() } - if (response.id) this.sendMessage(response, true) + if (response.id) this._sendMessage(response, true) } const disconnect = (aepp, connection) => { @@ -96,7 +96,7 @@ export default stampit({ } }, methods: { - sendMessage ({ id, method, params, result, error }, isNotificationOrResponse = false) { + _sendMessage ({ id, method, params, result, error }, isNotificationOrResponse = false) { if (!isNotificationOrResponse) this._messageId += 1 id = isNotificationOrResponse ? (id ?? null) : this._messageId const msgData = params @@ -185,10 +185,7 @@ export default stampit({ */ setAccounts (accounts, { forceNotification } = {}) { this.accounts = accounts - if (!forceNotification) { - // Sent notification about account updates - this.sendMessage({ method: METHODS.updateAddress, params: this.accounts }, true) - } + if (!forceNotification) this.notify(METHODS.updateAddress, this.accounts) }, /** * Update subscription @@ -218,7 +215,7 @@ export default stampit({ * @return {Promise} Promise which will be resolved after receiving response message */ request (name, params) { - const msgId = this.sendMessage({ method: name, params }) + const msgId = this._sendMessage({ method: name, params }) if (this.callbacks[msgId] != null) { throw new DuplicateCallbackError() } @@ -226,6 +223,14 @@ export default stampit({ this.callbacks[msgId] = { resolve, reject } }) }, + /** + * Make a notification + * @param {String} name Method name + * @param {Object} params Method params + */ + notify (name, params) { + this._sendMessage({ method: name, params }, true) + }, /** * Process response message * @function processResponse diff --git a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js index 9221638592..14deb82b9c 100644 --- a/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/wallet-rpc.js @@ -226,13 +226,10 @@ export default Ae.compose(AccountMultiple, { Object.values(this.rpcClients) .filter(client => client.isConnected()) .forEach(client => { - client.sendMessage({ - method: METHODS.updateNetwork, - params: { - networkId: this.getNetworkId(), - ...client.info.status === RPC_STATUS.NODE_BINDED && { node: this.selectedNode } - } - }, true) + client.notify(METHODS.updateNetwork, { + networkId: this.getNetworkId(), + ...client.info.status === RPC_STATUS.NODE_BINDED && { node: this.selectedNode } + }) }) } }, @@ -294,15 +291,11 @@ export default Ae.compose(AccountMultiple, { * @function shareWalletInfo * @instance * @rtype (postFn: Function) => void - * @param {Function} postFn Send message function like `(msg) => void` + * @param {Function} clientId ID of RPC client send message to * @return {void} */ - shareWalletInfo (postFn) { - postFn({ - jsonrpc: '2.0', - method: METHODS.readyToConnect, - params: this.getWalletInfo() - }) + shareWalletInfo (clientId) { + this.rpcClients[clientId].notify(METHODS.readyToConnect, this.getWalletInfo()) }, /** * Get Wallet info object diff --git a/test/integration/rpc.js b/test/integration/rpc.js index 4e676e2431..bf7dd836fa 100644 --- a/test/integration/rpc.js +++ b/test/integration/rpc.js @@ -68,11 +68,7 @@ describe('Aepp<->Wallet', function () { onSign () {}, onAskAccounts () {}, onMessageSign () {}, - onDisconnect () { - this.shareWalletInfo( - connectionFromWalletToAepp.sendMessage.bind(connectionFromWalletToAepp) - ) - } + onDisconnect () {} }) aepp = await RpcAepp({ name: 'AEPP', @@ -101,10 +97,8 @@ describe('Aepp<->Wallet', function () { }) }) - wallet.addRpcClient(connectionFromWalletToAepp) - await wallet.shareWalletInfo( - connectionFromWalletToAepp.sendMessage.bind(connectionFromWalletToAepp) - ) + const clientId = wallet.addRpcClient(connectionFromWalletToAepp) + await wallet.shareWalletInfo(clientId) const is = await isReceived is.should.be.equal(true) }) @@ -404,23 +398,20 @@ describe('Aepp<->Wallet', function () { }) it('Disconnect from wallet', async () => { - const received = await new Promise((resolve) => { - let received = false - wallet.onDisconnect = (msg, from) => { - msg.reason.should.be.equal('bye') - from.info.status.should.be.equal('DISCONNECTED') - if (received) resolve(true) - received = true - } - aepp.onDisconnect = () => { - if (received) resolve(true) - received = true - } - connectionFromWalletToAepp.sendMessage({ - method: METHODS.closeConnection, params: { reason: 'bye' }, jsonrpc: '2.0' - }) + const walletDisconnect = new Promise((resolve) => { + wallet.onDisconnect = (...args) => resolve(args) }) - received.should.be.equal(true) + const aeppDisconnect = new Promise((resolve) => { + aepp.onDisconnect = (...args) => resolve(args) + }) + connectionFromWalletToAepp.sendMessage({ + method: METHODS.closeConnection, params: { reason: 'bye' }, jsonrpc: '2.0' + }) + const [[walletMessage, rpcClient], [aeppMessage]] = + await Promise.all([walletDisconnect, aeppDisconnect]) + walletMessage.reason.should.be.equal('bye') + rpcClient.info.status.should.be.equal('DISCONNECTED') + aeppMessage.reason.should.be.equal('bye') }) it('Remove rpc client', async () => { @@ -486,11 +477,7 @@ describe('Aepp<->Wallet', function () { onSign () {}, onAskAccounts () {}, onMessageSign () {}, - onDisconnect () { - this.shareWalletInfo( - connectionFromWalletToAepp.sendMessage.bind(connectionFromWalletToAepp) - ) - } + onDisconnect () {} }) aepp = await RpcAepp({ name: 'AEPP', From f329549da3362cb4f216127bbbce41eb49eb7ab7 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sun, 29 May 2022 09:32:03 +0300 Subject: [PATCH 08/11] refactor(aepp-rpc): depend on simplified version of RpcClient BREAKING CHANGE: `connectToWallet` accepts wallet connection as the first argument See connect-aepp-to-wallet.md for details. BREAKING CHANGE: `disconnectWallet` runs in sync and `sendDisconnect` arg removed So, aepp would always send `closeConnection` notification. BREAKING CHANGE: `sendConnectRequest` removed Use `connectToWallet` instead. --- docs/guides/connect-aepp-to-wallet.md | 4 +- examples/browser/aepp/src/Connect.vue | 7 +- .../connection/BrowserWindowMessage.ts | 8 +- .../rpc/RpcClient.js | 112 ++++++++++++++++++ .../aepp-wallet-communication/rpc/aepp-rpc.js | 84 +++++-------- test/integration/rpc.js | 50 ++++---- 6 files changed, 176 insertions(+), 89 deletions(-) create mode 100644 src/utils/aepp-wallet-communication/rpc/RpcClient.js diff --git a/docs/guides/connect-aepp-to-wallet.md b/docs/guides/connect-aepp-to-wallet.md index 365abaccbf..a22d479fe9 100644 --- a/docs/guides/connect-aepp-to-wallet.md +++ b/docs/guides/connect-aepp-to-wallet.md @@ -79,7 +79,7 @@ Append method for wallet connection ```js async connect(wallet) { - await this.aeSdk.connectToWallet(wallet.info, wallet.getConnection()) + await this.aeSdk.connectToWallet(wallet.getConnection()) this.connectedAccounts = await this.aeSdk.subscribeAddress('subscribe', 'connected') this.address = await this.aeSdk.address() this.balance = await this.aeSdk.getBalance(this.address).catch(() => '0') @@ -93,7 +93,7 @@ Aepp can request the wallet to share its connected node URLs if any to interact ```js async connect (wallet) { - await this.aeSdk.connectToWallet(wallet.info, wallet.getConnection(), { connectNode: true, name: 'wallet-node', select: true }) + await this.aeSdk.connectToWallet(wallet.getConnection(), { connectNode: true, name: 'wallet-node', select: true }) } ``` diff --git a/examples/browser/aepp/src/Connect.vue b/examples/browser/aepp/src/Connect.vue index efa4229cd9..2d6e33e249 100644 --- a/examples/browser/aepp/src/Connect.vue +++ b/examples/browser/aepp/src/Connect.vue @@ -51,14 +51,15 @@ export default { walletConnected: false, connectPromise: null, reverseIframe: null, - reverseIframeWalletUrl: 'http://localhost:9000' + reverseIframeWalletUrl: 'http://localhost:9000', + walletInfo: null }), computed: { ...mapGetters('aeSdk', ['aeSdk']), walletName () { if (!this.aeSdk) return 'SDK is not ready' if (!this.walletConnected) return 'Wallet is not connected' - return this.aeSdk.rpcClient.info.name + return this.walletInfo.name } }, methods: { @@ -69,7 +70,7 @@ export default { console.log('newWallet', newWallet) stopScan() - await this.aeSdk.connectToWallet(newWallet.info, newWallet.getConnection()) + this.walletInfo = await this.aeSdk.connectToWallet(newWallet.getConnection()) this.walletConnected = true const { address: { current } } = await this.aeSdk.subscribeAddress('subscribe', 'connected') this.$store.commit('aeSdk/setAddress', Object.keys(current)[0]) diff --git a/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts b/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts index a947123f85..e71bc29ac6 100644 --- a/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts +++ b/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts @@ -35,6 +35,7 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { sendDirection?: MESSAGE_DIRECTION receiveDirection: MESSAGE_DIRECTION listener?: (this: Window, ev: MessageEvent) => void + _onDisconnect?: () => void _target?: Window _self: Window @@ -83,13 +84,18 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { onMessage(data, message.origin, message.source) } this._self.addEventListener('message', this.listener) + this._onDisconnect = onDisconnect } disconnect (): void { super.disconnect() - if (this.listener == null) throw new InternalError('Expected to not happen, required for TS') + if (this.listener == null || this._onDisconnect == null) { + throw new InternalError('Expected to not happen, required for TS') + } this._self.removeEventListener('message', this.listener) delete this.listener + this._onDisconnect() + delete this._onDisconnect } sendMessage (msg: MessageEvent): void { diff --git a/src/utils/aepp-wallet-communication/rpc/RpcClient.js b/src/utils/aepp-wallet-communication/rpc/RpcClient.js new file mode 100644 index 0000000000..da555029bf --- /dev/null +++ b/src/utils/aepp-wallet-communication/rpc/RpcClient.js @@ -0,0 +1,112 @@ +/** + * RpcClient module + * + * @module @aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client + * @export { RpcClient, RpcClients } + * @example + * import RpcClient from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client' + */ +import stampit from '@stamp/it' + +import { RpcError, RpcInternalError } from '../schema' +import { InvalidRpcMessageError, DuplicateCallbackError, MissingCallbackError } from '../../errors' + +/** + * Contain functionality for using RPC conection + * @alias module:@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client + * @function + * @rtype Stamp + * @param {Object} param Init params object + * @param {String} param.name Client name + * @param {Object} param.connection Connection object + * @param {Function} param.onDisconnect Disconnect callback + * @param {Object} param.methods Object containing handlers for each request by name + * @return {Object} + */ +export default stampit({ + init ({ connection, onDisconnect, methods }) { + this.connection = connection + this._callbacks = {} + this._messageId = 0 + this._methods = methods + connection.connect(this._handleMessage.bind(this), onDisconnect) + }, + methods: { + async _handleMessage (msg, origin) { + if (msg?.jsonrpc !== '2.0') throw new InvalidRpcMessageError(msg) + if ((msg.result ?? msg.error) != null) { + this._processResponse(msg) + return + } + + // TODO: remove methods as far it is not required in JSON RPC + const response = { id: msg.id, method: msg.method } + try { + response.result = await this._methods[msg.method](msg.params, origin) + } catch (error) { + response.error = error instanceof RpcError ? error : new RpcInternalError() + } + if (response.id) this._sendMessage(response, true) + }, + _sendMessage ({ id, method, params, result, error }, isNotificationOrResponse = false) { + if (!isNotificationOrResponse) this._messageId += 1 + id = isNotificationOrResponse ? (id ?? null) : this._messageId + const msgData = params + ? { params } + : result + ? { result } + : { error } + this.connection.sendMessage({ + jsonrpc: '2.0', + ...id ? { id } : {}, + method, + ...msgData + }) + return id + }, + /** + * Make a request + * @function request + * @instance + * @rtype (name: String, params: Object) => Promise + * @param {String} name Method name + * @param {Object} params Method params + * @return {Promise} Promise which will be resolved after receiving response message + */ + request (name, params) { + const msgId = this._sendMessage({ method: name, params }) + if (this._callbacks[msgId] != null) { + throw new DuplicateCallbackError() + } + return new Promise((resolve, reject) => { + this._callbacks[msgId] = { resolve, reject } + }) + }, + /** + * Make a notification + * @function request + * @instance + * @rtype (name: String, params: Object) => Promise + * @param {String} name Method name + * @param {Object} params Method params + * @return {Promise} Promise which will be resolved after receiving response message + */ + notify (name, params) { + this.sendMessage({ method: name, params }, true) + }, + /** + * Process response message + * @function processResponse + * @instance + * @rtype (msg: Object, transformResult: Function) => void + * @param {Object} msg Message object + * @return {void} + */ + _processResponse ({ id, error, result }) { + if (!this._callbacks[id]) throw new MissingCallbackError(id) + if (result) this._callbacks[id].resolve(result) + else this._callbacks[id].reject(RpcError.deserialize(error)) + delete this._callbacks[id] + } + } +}) diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js index 9d6a4b1470..26f4108031 100644 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js @@ -7,12 +7,11 @@ * import AeppRpc * from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc' */ -import { v4 as uuid } from '@aeternity/uuid' import AccountResolver from '../../../account/resolver' import AccountRpc from '../../../account/rpc' import { decode } from '../../encoder' import { mapObject } from '../../other' -import RpcClient from './rpc-client' +import RpcClient from './RpcClient' import { METHODS, RPC_STATUS, VERSION } from '../schema' import { AlreadyConnectedError, @@ -26,7 +25,7 @@ import Node from '../../../node' const METHOD_HANDLERS = { [METHODS.updateAddress]: (instance, params) => { - instance.rpcClient.accounts = params + instance._accounts = params instance.onAddressChange(params) }, [METHODS.updateNetwork]: async (instance, params) => { @@ -35,8 +34,8 @@ const METHOD_HANDLERS = { instance.onNetworkChange(params) }, [METHODS.closeConnection]: (instance, params) => { - instance.disconnectWallet() - instance.onDisconnect(params) + instance._disconnectParams = params + instance.rpcClient.connection.disconnect() }, [METHODS.readyToConnect]: () => {} } @@ -57,7 +56,6 @@ const METHOD_HANDLERS = { export default AccountResolver.compose({ init ({ name, - debug = false, ...other }) { ['onAddressChange', 'onDisconnect', 'onNetworkChange'].forEach(event => { @@ -67,15 +65,13 @@ export default AccountResolver.compose({ }) this.name = name - this.debug = debug + this._status = RPC_STATUS.DISCONNECTED const resolveAccountBase = this._resolveAccount - this._resolveAccount = (account = this.rpcClient?.currentAccount) => { + this._resolveAccount = (account = this.addresses()[0]) => { if (typeof account === 'string') { decode(account, 'ak') - if (!this.rpcClient?.hasAccessToAccount(account)) { - throw new UnAuthorizedAccountError(account) - } + if (!this.addresses().includes(account)) throw new UnAuthorizedAccountError(account) account = AccountRpc({ rpcClient: this.rpcClient, address: account, networkId: this.getNetworkId() }) @@ -86,8 +82,9 @@ export default AccountResolver.compose({ }, methods: { addresses () { - this._ensureAccountAccess() - return [this.rpcClient.currentAccount, ...Object.keys(this.rpcClient.accounts.connected)] + if (this._accounts == null) return [] + const current = Object.keys(this._accounts.current)[0] + return [...current ? [current] : [], ...Object.keys(this._accounts.connected)] }, /** * Connect to wallet @@ -102,37 +99,40 @@ export default AccountResolver.compose({ * @param {Boolean} [options.select=false] - Select this node as current * @return {Object} */ - async connectToWallet (walletInfo, connection, { connectNode = false, name = 'wallet-node', select = false } = {}) { - if (this.rpcClient?.isConnected()) throw new AlreadyConnectedError('You are already connected to wallet ' + this.rpcClient) + async connectToWallet (connection, { connectNode = false, name = 'wallet-node', select = false } = {}) { + if (this._status === RPC_STATUS.CONNECTED) throw new AlreadyConnectedError('You are already connected to wallet') this.rpcClient = RpcClient({ - ...walletInfo, connection, - id: uuid(), - onDisconnect: this.onDisconnect, + onDisconnect: () => { + this._status = RPC_STATUS.DISCONNECTED + delete this.rpcClient + delete this._accounts + this.onDisconnect(this._disconnectParams) + delete this._disconnectParams + }, methods: mapObject(METHOD_HANDLERS, ([key, value]) => [key, value.bind(null, this)]) }) - const { node } = await this.sendConnectRequest(connectNode) + const { node, ...walletInfo } = await this.rpcClient.request( + METHODS.connect, { name: this.name, version: VERSION, connectNode } + ) if (connectNode) { if (node == null) throw new RpcConnectionError('Missing URLs of the Node') this.addNode(name, await Node(node), select) } + this._status = RPC_STATUS.CONNECTED return walletInfo }, /** * Disconnect from wallet * @function disconnectWallet * @instance - * @rtype (force: Boolean = false) => void - * @param {Boolean} sendDisconnect=false Force sending close connection message + * @rtype () => void * @return {void} */ - async disconnectWallet (sendDisconnect = true) { + disconnectWallet () { this._ensureConnected() - if (sendDisconnect) { - this.rpcClient.notify(METHODS.closeConnection, { reason: 'bye' }) - } - this.rpcClient.disconnect() - this.rpcClient = null + this.rpcClient.notify(METHODS.closeConnection, { reason: 'bye' }) + this.rpcClient.connection.disconnect() }, /** * Ask address from wallet @@ -157,40 +157,16 @@ export default AccountResolver.compose({ async subscribeAddress (type, value) { this._ensureConnected() const result = await this.rpcClient.request(METHODS.subscribeAddress, { type, value }) - if (result.address) { - this.rpcClient.accounts = result.address - } - if (result.subscription) { - this.rpcClient.addressSubscription = result.subscription - } + this._accounts = result.address return result }, - /** - * Send connection request to wallet - * @function sendConnectRequest - * @instance - * @param {Boolean} connectNode - Request wallet to bind node - * @rtype () => Promise - * @return {Promise} Connection response - */ - async sendConnectRequest (connectNode) { - const walletInfo = this.rpcClient.request( - METHODS.connect, { - name: this.name, - version: VERSION, - connectNode - } - ) - this.rpcClient.info.status = RPC_STATUS.CONNECTED - return walletInfo - }, _ensureConnected () { - if (this.rpcClient?.isConnected()) return + if (this._status === RPC_STATUS.CONNECTED) return throw new NoWalletConnectedError('You are not connected to Wallet') }, _ensureAccountAccess () { this._ensureConnected() - if (this.rpcClient?.currentAccount) return + if (this.addresses().length) return throw new UnsubscribedAccountError() } } diff --git a/test/integration/rpc.js b/test/integration/rpc.js index bf7dd836fa..c111be9525 100644 --- a/test/integration/rpc.js +++ b/test/integration/rpc.js @@ -85,9 +85,10 @@ describe('Aepp<->Wallet', function () { it('Fail on not connected', async () => { await Promise.all( - ['send', 'subscribeAddress', 'askAddresses', 'address', 'disconnectWallet'] + ['send', 'subscribeAddress', 'askAddresses', 'address'] .map(method => expect(aepp[method]()).to.be.rejectedWith(NoWalletConnectedError, 'You are not connected to Wallet')) ) + expect(() => aepp.disconnectWallet()).to.throw(NoWalletConnectedError, 'You are not connected to Wallet') }) it('Should receive `announcePresence` message from wallet', async () => { @@ -107,9 +108,7 @@ describe('Aepp<->Wallet', function () { wallet.onConnection = (aepp, actions) => { actions.deny() } - await expect( - aepp.connectToWallet(wallet.getWalletInfo(), connectionFromAeppToWallet) - ).to.be.eventually + await expect(aepp.connectToWallet(connectionFromAeppToWallet)).to.be.eventually .rejectedWith('Wallet deny your connection request') .with.property('code', 9) }) @@ -119,9 +118,7 @@ describe('Aepp<->Wallet', function () { actions.accept() } connectionFromAeppToWallet.disconnect() - const connected = await aepp.connectToWallet( - wallet.getWalletInfo(), connectionFromAeppToWallet - ) + const connected = await aepp.connectToWallet(connectionFromAeppToWallet) connected.name.should.be.equal('Wallet') }) @@ -349,23 +346,17 @@ describe('Aepp<->Wallet', function () { }) it('Add new account to wallet: receive notification for update accounts', async () => { - const keypair = generateKeyPair() - const connectedLength = Object.keys(aepp.rpcClient.accounts.connected).length - const received = await new Promise((resolve) => { - aepp.onAddressChange = (accounts) => { - resolve(Object.keys(accounts.connected).length === connectedLength + 1) - } - wallet.addAccount(MemoryAccount({ keypair })) - }) - received.should.be.equal(true) + const connectedLength = Object.keys(aepp._accounts.connected).length + const accountsPromise = new Promise((resolve) => { aepp.onAddressChange = resolve }) + await wallet.addAccount(MemoryAccount({ keypair: generateKeyPair() })) + expect(Object.keys((await accountsPromise).connected).length).to.equal(connectedLength + 1) }) it('Receive update for wallet select account', async () => { - const connectedAccount = Object.keys(aepp.rpcClient.accounts.connected)[0] - const { connected, current } = await new Promise((resolve) => { - aepp.onAddressChange = resolve - wallet.selectAccount(connectedAccount) - }) + const connectedAccount = Object.keys(aepp._accounts.connected)[0] + const accountsPromise = new Promise((resolve) => { aepp.onAddressChange = resolve }) + wallet.selectAccount(connectedAccount) + const { connected, current } = await accountsPromise expect(current[connectedAccount]).to.be.eql({}) expect(Object.keys(connected).includes(connectedAccount)).to.be.equal(false) }) @@ -382,7 +373,7 @@ describe('Aepp<->Wallet', function () { }) it('Resolve/Reject callback for undefined message', async () => { - expect(() => aepp.rpcClient.processResponse({ id: 0 })) + expect(() => aepp.rpcClient._processResponse({ id: 0 })) .to.throw('Can\'t find callback for this messageId 0') }) @@ -393,7 +384,7 @@ describe('Aepp<->Wallet', function () { }) it('Process response ', () => { - expect(() => aepp.rpcClient.processResponse({ id: 11, error: {} })) + expect(() => aepp.rpcClient._processResponse({ id: 11, error: {} })) .to.throw('Can\'t find callback for this messageId ' + 11) }) @@ -407,11 +398,14 @@ describe('Aepp<->Wallet', function () { connectionFromWalletToAepp.sendMessage({ method: METHODS.closeConnection, params: { reason: 'bye' }, jsonrpc: '2.0' }) - const [[walletMessage, rpcClient], [aeppMessage]] = - await Promise.all([walletDisconnect, aeppDisconnect]) + const [aeppMessage] = await aeppDisconnect + aeppMessage.reason.should.be.equal('bye') + connectionFromAeppToWallet.sendMessage({ + method: METHODS.closeConnection, params: { reason: 'bye' }, jsonrpc: '2.0' + }) + const [walletMessage, rpcClient] = await walletDisconnect walletMessage.reason.should.be.equal('bye') rpcClient.info.status.should.be.equal('DISCONNECTED') - aeppMessage.reason.should.be.equal('bye') }) it('Remove rpc client', async () => { @@ -423,7 +417,6 @@ describe('Aepp<->Wallet', function () { target: connections.aeppWindow })) await aepp.connectToWallet( - wallet.getWalletInfo(), new BrowserWindowMessageConnection({ self: connections.aeppWindow, target: connections.walletWindow @@ -450,7 +443,7 @@ describe('Aepp<->Wallet', function () { connectionFromAeppToWallet.connect((msg) => { msg.method.should.be.equal('hey') resolve(true) - }) + }, () => {}) connectionFromWalletToAepp.sendMessage({ method: 'hey' }) }) ok.should.be.equal(true) @@ -487,7 +480,6 @@ describe('Aepp<->Wallet', function () { }) wallet.addRpcClient(connectionFromWalletToAepp) await aepp.connectToWallet( - wallet.getWalletInfo(), connectionFromAeppToWallet, { connectNode: true, name: 'wallet-node', select: true } ) From b3a2af6ebe8370c3a1071986af21b14e89a61d67 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sun, 29 May 2022 13:18:01 +0300 Subject: [PATCH 09/11] refactor: drop DuplicateCallbackError error Message counted is maintained locally, so this shouldn't be a case --- docs/guides/error-handling.md | 1 - src/utils/aepp-wallet-communication/rpc/RpcClient.js | 5 +---- src/utils/aepp-wallet-communication/rpc/rpc-client.js | 5 +---- src/utils/errors.ts | 7 ------- 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/docs/guides/error-handling.md b/docs/guides/error-handling.md index e42eea66a1..ee739c1ef1 100644 --- a/docs/guides/error-handling.md +++ b/docs/guides/error-handling.md @@ -31,7 +31,6 @@ BaseError │ │ InvalidAensNameError │ └───AeppError -│ │ DuplicateCallbackError │ │ InvalidRpcMessageError │ │ MissingCallbackError │ │ UnAuthorizedAccountError diff --git a/src/utils/aepp-wallet-communication/rpc/RpcClient.js b/src/utils/aepp-wallet-communication/rpc/RpcClient.js index da555029bf..33270b95da 100644 --- a/src/utils/aepp-wallet-communication/rpc/RpcClient.js +++ b/src/utils/aepp-wallet-communication/rpc/RpcClient.js @@ -9,7 +9,7 @@ import stampit from '@stamp/it' import { RpcError, RpcInternalError } from '../schema' -import { InvalidRpcMessageError, DuplicateCallbackError, MissingCallbackError } from '../../errors' +import { InvalidRpcMessageError, MissingCallbackError } from '../../errors' /** * Contain functionality for using RPC conection @@ -75,9 +75,6 @@ export default stampit({ */ request (name, params) { const msgId = this._sendMessage({ method: name, params }) - if (this._callbacks[msgId] != null) { - throw new DuplicateCallbackError() - } return new Promise((resolve, reject) => { this._callbacks[msgId] = { resolve, reject } }) diff --git a/src/utils/aepp-wallet-communication/rpc/rpc-client.js b/src/utils/aepp-wallet-communication/rpc/rpc-client.js index fdfe031e6d..80e782366a 100644 --- a/src/utils/aepp-wallet-communication/rpc/rpc-client.js +++ b/src/utils/aepp-wallet-communication/rpc/rpc-client.js @@ -9,7 +9,7 @@ import stampit from '@stamp/it' import { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES, RpcError, RpcInternalError } from '../schema' -import { InvalidRpcMessageError, DuplicateCallbackError, MissingCallbackError } from '../../errors' +import { InvalidRpcMessageError, MissingCallbackError } from '../../errors' /** * Contain functionality for using RPC conection @@ -216,9 +216,6 @@ export default stampit({ */ request (name, params) { const msgId = this._sendMessage({ method: name, params }) - if (this.callbacks[msgId] != null) { - throw new DuplicateCallbackError() - } return new Promise((resolve, reject) => { this.callbacks[msgId] = { resolve, reject } }) diff --git a/src/utils/errors.ts b/src/utils/errors.ts index c0172455b8..b41b2ec56b 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -228,13 +228,6 @@ export class InvalidAensNameError extends AensError { } /* Aepp Errors */ -export class DuplicateCallbackError extends AeppError { - constructor () { - super('Callback Already exist') - this.name = 'DuplicateCallbackError' - } -} - export class InvalidRpcMessageError extends AeppError { constructor (message: string) { super(`Received invalid message: ${message}`) From b8f76ca0397df9b012af64d6b64afe452da5f696 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sun, 29 May 2022 13:56:32 +0300 Subject: [PATCH 10/11] refactor: convert aepp-rpc and RpcClient to TypeScript --- .../rpc/RpcClient.js | 109 ---------- .../rpc/RpcClient.ts | 145 +++++++++++++ .../aepp-wallet-communication/rpc/aepp-rpc.js | 173 ---------------- .../aepp-wallet-communication/rpc/aepp-rpc.ts | 195 ++++++++++++++++++ .../aepp-wallet-communication/rpc/types.ts | 58 ++++++ src/utils/aepp-wallet-communication/schema.ts | 9 + src/utils/errors.ts | 2 +- test/integration/rpc.js | 10 - 8 files changed, 408 insertions(+), 293 deletions(-) delete mode 100644 src/utils/aepp-wallet-communication/rpc/RpcClient.js create mode 100644 src/utils/aepp-wallet-communication/rpc/RpcClient.ts delete mode 100644 src/utils/aepp-wallet-communication/rpc/aepp-rpc.js create mode 100644 src/utils/aepp-wallet-communication/rpc/aepp-rpc.ts create mode 100644 src/utils/aepp-wallet-communication/rpc/types.ts diff --git a/src/utils/aepp-wallet-communication/rpc/RpcClient.js b/src/utils/aepp-wallet-communication/rpc/RpcClient.js deleted file mode 100644 index 33270b95da..0000000000 --- a/src/utils/aepp-wallet-communication/rpc/RpcClient.js +++ /dev/null @@ -1,109 +0,0 @@ -/** - * RpcClient module - * - * @module @aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client - * @export { RpcClient, RpcClients } - * @example - * import RpcClient from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client' - */ -import stampit from '@stamp/it' - -import { RpcError, RpcInternalError } from '../schema' -import { InvalidRpcMessageError, MissingCallbackError } from '../../errors' - -/** - * Contain functionality for using RPC conection - * @alias module:@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/rpc-client - * @function - * @rtype Stamp - * @param {Object} param Init params object - * @param {String} param.name Client name - * @param {Object} param.connection Connection object - * @param {Function} param.onDisconnect Disconnect callback - * @param {Object} param.methods Object containing handlers for each request by name - * @return {Object} - */ -export default stampit({ - init ({ connection, onDisconnect, methods }) { - this.connection = connection - this._callbacks = {} - this._messageId = 0 - this._methods = methods - connection.connect(this._handleMessage.bind(this), onDisconnect) - }, - methods: { - async _handleMessage (msg, origin) { - if (msg?.jsonrpc !== '2.0') throw new InvalidRpcMessageError(msg) - if ((msg.result ?? msg.error) != null) { - this._processResponse(msg) - return - } - - // TODO: remove methods as far it is not required in JSON RPC - const response = { id: msg.id, method: msg.method } - try { - response.result = await this._methods[msg.method](msg.params, origin) - } catch (error) { - response.error = error instanceof RpcError ? error : new RpcInternalError() - } - if (response.id) this._sendMessage(response, true) - }, - _sendMessage ({ id, method, params, result, error }, isNotificationOrResponse = false) { - if (!isNotificationOrResponse) this._messageId += 1 - id = isNotificationOrResponse ? (id ?? null) : this._messageId - const msgData = params - ? { params } - : result - ? { result } - : { error } - this.connection.sendMessage({ - jsonrpc: '2.0', - ...id ? { id } : {}, - method, - ...msgData - }) - return id - }, - /** - * Make a request - * @function request - * @instance - * @rtype (name: String, params: Object) => Promise - * @param {String} name Method name - * @param {Object} params Method params - * @return {Promise} Promise which will be resolved after receiving response message - */ - request (name, params) { - const msgId = this._sendMessage({ method: name, params }) - return new Promise((resolve, reject) => { - this._callbacks[msgId] = { resolve, reject } - }) - }, - /** - * Make a notification - * @function request - * @instance - * @rtype (name: String, params: Object) => Promise - * @param {String} name Method name - * @param {Object} params Method params - * @return {Promise} Promise which will be resolved after receiving response message - */ - notify (name, params) { - this.sendMessage({ method: name, params }, true) - }, - /** - * Process response message - * @function processResponse - * @instance - * @rtype (msg: Object, transformResult: Function) => void - * @param {Object} msg Message object - * @return {void} - */ - _processResponse ({ id, error, result }) { - if (!this._callbacks[id]) throw new MissingCallbackError(id) - if (result) this._callbacks[id].resolve(result) - else this._callbacks[id].reject(RpcError.deserialize(error)) - delete this._callbacks[id] - } - } -}) diff --git a/src/utils/aepp-wallet-communication/rpc/RpcClient.ts b/src/utils/aepp-wallet-communication/rpc/RpcClient.ts new file mode 100644 index 0000000000..d77f476bb5 --- /dev/null +++ b/src/utils/aepp-wallet-communication/rpc/RpcClient.ts @@ -0,0 +1,145 @@ +import { RpcError, RpcInternalError, RpcMethodNotFoundError } from '../schema' +import BrowserConnection from '../connection/Browser' +import { InvalidRpcMessageError, MissingCallbackError } from '../../errors' + +interface JsonRpcRequest { + jsonrpc: '2.0' + id: number + method: string + params?: any +} + +interface JsonRpcResponse { + jsonrpc: '2.0' + id: number + method: string + result?: any + error?: { + code: number + message: string + data?: any + } +} + +type RpcApiHandler = (p?: any, origin?: string) => any | undefined +type RpcApi = { [k in keyof Keys]: RpcApiHandler } + +/** + * Contain functionality for using RPC conection + * @param connection Connection object + * @param onDisconnect Disconnect callback + * @param methods Object containing handlers for each request by name + */ +export default class RpcClient < + RemoteApi extends RpcApi, LocalApi extends RpcApi +> { + connection: BrowserConnection + #callbacks = new Map void, reject: (e: Error) => void }>() + #messageId = 0 + #methods: LocalApi + + constructor ( + connection: BrowserConnection, + onDisconnect: () => void, + methods: LocalApi + ) { + this.connection = connection + this.#methods = methods + connection.connect(this.#handleMessage.bind(this), onDisconnect) + } + + async #handleMessage (msg: JsonRpcRequest | JsonRpcResponse, origin: string): Promise { + if (msg?.jsonrpc !== '2.0') throw new InvalidRpcMessageError(JSON.stringify(msg)) + if ('result' in msg || 'error' in msg) { + this.#processResponse(msg) + return + } + + const request = msg as JsonRpcRequest + let result, error + try { + if (!(request.method in this.#methods)) throw new RpcMethodNotFoundError() + const methodName = request.method as keyof LocalApi + result = await this.#methods[methodName](request.params, origin) + } catch (e) { + error = e instanceof RpcError ? e : new RpcInternalError() + } + if (request.id != null) { + this.#sendResponse(request.id, request.method as keyof LocalApi, result, error) + } + } + + #sendRequest ( + id: number | undefined, + method: keyof RemoteApi | keyof LocalApi, + params?: any + ): void { + this.connection.sendMessage({ + jsonrpc: '2.0', + ...id != null ? { id } : {}, + method, + ...params != null ? { params } : {} + }) + } + + #sendResponse ( + id: number, + method: keyof RemoteApi | keyof LocalApi, // TODO: remove as far it is not required in JSON RPC + result?: any, + error?: any + ): void { + this.connection.sendMessage({ + jsonrpc: '2.0', + id, + method, + ...error != null ? { error } : { result } + }) + } + + /** + * Make a request + * @function request + * @instance + * @rtype (name: String, params: Object) => Promise + * @param {String} name Method name + * @param {Object} params Method params + * @return {Promise} Promise which will be resolved after receiving response message + */ + async request ( + name: Name, params: Parameters[0] + ): Promise> { + this.#sendRequest(++this.#messageId, name, params) + return await new Promise((resolve, reject) => { + this.#callbacks.set(this.#messageId, { resolve, reject }) + }) + } + + /** + * Make a notification + * @function request + * @instance + * @rtype (name: String, params: Object) => Promise + * @param {String} name Method name + * @param {Object} params Method params + * @return {Promise} Promise which will be resolved after receiving response message + */ + notify (name: Name, params: Parameters[0]): void { + this.#sendRequest(undefined, name, params) + } + + /** + * Process response message + * @function processResponse + * @instance + * @rtype (msg: Object, transformResult: Function) => void + * @param {Object} msg Message object + * @return {void} + */ + #processResponse ({ id, error, result }: { id: number, error?: any, result?: any }): void { + const callbacks = this.#callbacks.get(id) + if (callbacks == null) throw new MissingCallbackError(id) + if (error != null) callbacks.reject(RpcError.deserialize(error)) + else callbacks.resolve(result) + this.#callbacks.delete(id) + } +} diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js deleted file mode 100644 index 26f4108031..0000000000 --- a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.js +++ /dev/null @@ -1,173 +0,0 @@ -/** - * RPC handler for AEPP side - * - * @module @aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc - * @export AeppRpc - * @example - * import AeppRpc - * from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc' - */ -import AccountResolver from '../../../account/resolver' -import AccountRpc from '../../../account/rpc' -import { decode } from '../../encoder' -import { mapObject } from '../../other' -import RpcClient from './RpcClient' -import { METHODS, RPC_STATUS, VERSION } from '../schema' -import { - AlreadyConnectedError, - NoWalletConnectedError, - UnsubscribedAccountError, - UnAuthorizedAccountError, - ArgumentError, - RpcConnectionError -} from '../../errors' -import Node from '../../../node' - -const METHOD_HANDLERS = { - [METHODS.updateAddress]: (instance, params) => { - instance._accounts = params - instance.onAddressChange(params) - }, - [METHODS.updateNetwork]: async (instance, params) => { - const { node } = params - if (node) instance.addNode(node.name, await Node(node), true) - instance.onNetworkChange(params) - }, - [METHODS.closeConnection]: (instance, params) => { - instance._disconnectParams = params - instance.rpcClient.connection.disconnect() - }, - [METHODS.readyToConnect]: () => {} -} - -/** - * Contain functionality for wallet interaction and connect it to sdk - * @alias module:@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc - * @function - * @rtype Stamp - * @param {Object} param Init params object - * @param {String=} [param.name] Aepp name - * @param {Function} onAddressChange Call-back function for update address event - * @param {Function} onDisconnect Call-back function for disconnect event - * @param {Function} onNetworkChange Call-back function for update network event - * @param {Object} connection Wallet connection object - * @return {Object} - */ -export default AccountResolver.compose({ - init ({ - name, - ...other - }) { - ['onAddressChange', 'onDisconnect', 'onNetworkChange'].forEach(event => { - const handler = other[event] ?? (() => {}) - if (typeof handler !== 'function') throw new ArgumentError(event, 'a function', handler) - this[event] = handler - }) - - this.name = name - this._status = RPC_STATUS.DISCONNECTED - - const resolveAccountBase = this._resolveAccount - this._resolveAccount = (account = this.addresses()[0]) => { - if (typeof account === 'string') { - decode(account, 'ak') - if (!this.addresses().includes(account)) throw new UnAuthorizedAccountError(account) - account = AccountRpc({ - rpcClient: this.rpcClient, address: account, networkId: this.getNetworkId() - }) - } - if (!account) this._ensureAccountAccess() - return resolveAccountBase(account) - } - }, - methods: { - addresses () { - if (this._accounts == null) return [] - const current = Object.keys(this._accounts.current)[0] - return [...current ? [current] : [], ...Object.keys(this._accounts.connected)] - }, - /** - * Connect to wallet - * @function connectToWallet - * @instance - * @rtype (connection: Object) => void - * @param {Object} walletInfo Wallet info object - * @param {Object} connection Wallet connection object - * @param {Object} [options={}] - * @param {Boolean} [options.connectNode=true] - Request wallet to bind node - * @param {String} [options.name=wallet-node] - Node name - * @param {Boolean} [options.select=false] - Select this node as current - * @return {Object} - */ - async connectToWallet (connection, { connectNode = false, name = 'wallet-node', select = false } = {}) { - if (this._status === RPC_STATUS.CONNECTED) throw new AlreadyConnectedError('You are already connected to wallet') - this.rpcClient = RpcClient({ - connection, - onDisconnect: () => { - this._status = RPC_STATUS.DISCONNECTED - delete this.rpcClient - delete this._accounts - this.onDisconnect(this._disconnectParams) - delete this._disconnectParams - }, - methods: mapObject(METHOD_HANDLERS, ([key, value]) => [key, value.bind(null, this)]) - }) - const { node, ...walletInfo } = await this.rpcClient.request( - METHODS.connect, { name: this.name, version: VERSION, connectNode } - ) - if (connectNode) { - if (node == null) throw new RpcConnectionError('Missing URLs of the Node') - this.addNode(name, await Node(node), select) - } - this._status = RPC_STATUS.CONNECTED - return walletInfo - }, - /** - * Disconnect from wallet - * @function disconnectWallet - * @instance - * @rtype () => void - * @return {void} - */ - disconnectWallet () { - this._ensureConnected() - this.rpcClient.notify(METHODS.closeConnection, { reason: 'bye' }) - this.rpcClient.connection.disconnect() - }, - /** - * Ask address from wallet - * @function askAddresses - * @instance - * @rtype () => Promise - * @return {Promise} Address from wallet - */ - async askAddresses () { - this._ensureAccountAccess() - return this.rpcClient.request(METHODS.address) - }, - /** - * Subscribe for addresses from wallet - * @function subscribeAddress - * @instance - * @rtype (type: String, value: String) => Promise - * @param {String} type Should be one of 'current' (the selected account), 'connected' (all) - * @param {String} value Subscription action('subscribe'|'unsubscribe') - * @return {Promise} Address from wallet - */ - async subscribeAddress (type, value) { - this._ensureConnected() - const result = await this.rpcClient.request(METHODS.subscribeAddress, { type, value }) - this._accounts = result.address - return result - }, - _ensureConnected () { - if (this._status === RPC_STATUS.CONNECTED) return - throw new NoWalletConnectedError('You are not connected to Wallet') - }, - _ensureAccountAccess () { - this._ensureConnected() - if (this.addresses().length) return - throw new UnsubscribedAccountError() - } - } -}) diff --git a/src/utils/aepp-wallet-communication/rpc/aepp-rpc.ts b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.ts new file mode 100644 index 0000000000..49aba8bd4f --- /dev/null +++ b/src/utils/aepp-wallet-communication/rpc/aepp-rpc.ts @@ -0,0 +1,195 @@ +/** + * RPC handler for AEPP side + * + * @module @aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc + * @export AeppRpc + * @example + * import AeppRpc + * from '@aeternity/aepp-sdk/es/utils/aepp-wallet-communication/rpc/aepp-rpc' + */ +import AccountResolver, { _AccountResolver, Account } from '../../../account/resolver' +import { _AccountBase } from '../../../account/base' +import AccountRpc from '../../../account/rpc' +import { decode, EncodedData } from '../../encoder' +import { Accounts, WalletInfo, Network, WalletApi, AeppApi } from './types' +import RpcClient from './RpcClient' +import { METHODS, VERSION } from '../schema' +import { + AlreadyConnectedError, + NoWalletConnectedError, + UnsubscribedAccountError, + UnAuthorizedAccountError, + RpcConnectionError +} from '../../errors' +// @ts-expect-error TODO remove +import Node from '../../../node' +import BrowserConnection from '../connection/Browser' +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import stampit from '@stamp/it' + +/** + * RPC handler for AEPP side + * Contain functionality for wallet interaction and connect it to sdk + * @param param Init params object + * @param param.name Aepp name + * @param param.onAddressChange Call-back function for update address event + * @param param.onDisconnect Call-back function for disconnect event + * @param param.onNetworkChange Call-back function for update network event + */ +abstract class _AeppRpc extends _AccountResolver { + name: string + onAddressChange: (a: Accounts) => void + onDisconnect: (p: any) => void + onNetworkChange: (a: { networkId: string }) => void + rpcClient?: RpcClient + _accounts?: Accounts + + init ({ + name, + onAddressChange = () => {}, + onDisconnect = () => {}, + onNetworkChange = () => {}, + ...other + }: { + name: string + onAddressChange: (a: Accounts) => void + onDisconnect: (p: any) => void + onNetworkChange: (a: Network) => void + } & Parameters<_AccountResolver['init']>[0]): void { + super.init(other) + this.onAddressChange = onAddressChange + this.onDisconnect = onDisconnect + this.onNetworkChange = onNetworkChange + this.name = name + } + + _resolveAccount (account: Account = this.addresses()[0]): _AccountBase { + if (typeof account === 'string') { + const address = account as EncodedData<'ak'> + decode(address) + if (!this.addresses().includes(address)) throw new UnAuthorizedAccountError(address) + account = AccountRpc({ + rpcClient: this.rpcClient, address, networkId: this.getNetworkId() + }) + } + if (account == null) this._ensureAccountAccess() + return super._resolveAccount(account) + } + + addresses (): Array> { + if (this._accounts == null) return [] + const current = Object.keys(this._accounts.current)[0] + return [ + ...current != null ? [current] : [], ...Object.keys(this._accounts.connected) + ] as Array> + } + + abstract addNode (name: string, node: any, select: boolean): void + + /** + * Connect to wallet + * @param connection Wallet connection object + * @param [options={}] + * @param [options.connectNode=true] - Request wallet to bind node + * @param [options.name=wallet-node] - Node name + * @param [options.select=false] - Select this node as current + */ + async connectToWallet ( + connection: BrowserConnection, + { connectNode = false, name = 'wallet-node', select = false }: + { connectNode?: boolean, name?: string, select?: boolean } = {} + ): Promise { + if (this.rpcClient != null) throw new AlreadyConnectedError('You are already connected to wallet') + let disconnectParams: any + const client = new RpcClient( + connection, + () => { + delete this.rpcClient + delete this._accounts + this.onDisconnect(disconnectParams) + }, { + [METHODS.updateAddress]: (params: Accounts) => { + this._accounts = params + this.onAddressChange(params) + }, + [METHODS.updateNetwork]: async (params: Network) => { + const { node } = params + if (node != null) this.addNode(node.name, await Node(node), true) + this.onNetworkChange(params) + }, + [METHODS.closeConnection]: (params: any) => { + disconnectParams = params + client.connection.disconnect() + }, + [METHODS.readyToConnect]: () => {} + } + ) + const { node, ...walletInfo } = await client.request( + METHODS.connect, { name: this.name, version: VERSION, connectNode } + ) + if (connectNode) { + if (node == null) throw new RpcConnectionError('Missing URLs of the Node') + this.addNode(name, await Node(node), select) + } + this.rpcClient = client + return walletInfo + } + + /** + * Disconnect from wallet + */ + disconnectWallet (): void { + this._ensureConnected() + this.rpcClient.notify(METHODS.closeConnection, { reason: 'bye' }) + this.rpcClient.connection.disconnect() + } + + /** + * Ask addresses from wallet + * @return Addresses from wallet + */ + async askAddresses (): Promise>> { + this._ensureAccountAccess() + return await this.rpcClient.request(METHODS.address, undefined) + } + + /** + * Subscribe for addresses from wallet + * @param type Should be one of 'current' (the selected account), 'connected' (all) + * @param value Subscription action + * @return Accounts from wallet + */ + async subscribeAddress ( + type: 'current' | 'connected', value: 'subscribe' | 'unsubscribe' + ): Promise> { + this._ensureConnected() + const result = await this.rpcClient.request(METHODS.subscribeAddress, { type, value }) + this._accounts = result.address + return result + } + + _ensureConnected (): asserts this is _AeppRpc & { rpcClient: NonNullable<_AeppRpc['rpcClient']> } { + if (this.rpcClient != null) return + throw new NoWalletConnectedError('You are not connected to Wallet') + } + + _ensureAccountAccess (): asserts this is _AeppRpc & { rpcClient: NonNullable<_AeppRpc['rpcClient']> } { + this._ensureConnected() + if (this.addresses().length !== 0) return + throw new UnsubscribedAccountError() + } +} + +export default AccountResolver.compose<_AeppRpc>({ + init: _AeppRpc.prototype.init, + methods: { + _resolveAccount: _AeppRpc.prototype._resolveAccount, + addresses: _AeppRpc.prototype.addresses, + connectToWallet: _AeppRpc.prototype.connectToWallet, + disconnectWallet: _AeppRpc.prototype.disconnectWallet, + askAddresses: _AeppRpc.prototype.askAddresses, + subscribeAddress: _AeppRpc.prototype.subscribeAddress, + _ensureConnected: _AeppRpc.prototype._ensureConnected, + _ensureAccountAccess: _AeppRpc.prototype._ensureAccountAccess + } +}) diff --git a/src/utils/aepp-wallet-communication/rpc/types.ts b/src/utils/aepp-wallet-communication/rpc/types.ts new file mode 100644 index 0000000000..cc30187dcd --- /dev/null +++ b/src/utils/aepp-wallet-communication/rpc/types.ts @@ -0,0 +1,58 @@ +import { EncodedData } from '../../encoder' +import { METHODS, WALLET_TYPE } from '../schema' + +export interface WalletInfo { + id: string + name: string + networkId: string + origin: string + type: WALLET_TYPE +} + +export interface Accounts { + connected: { [pub: EncodedData<'ak'>]: {} } + current: { [pub: EncodedData<'ak'>]: {} } +} + +export interface Node { + name: string + url: string +} + +export interface Network { + networkId: string + node?: Node +} + +type Icons = Array<{ src: string, sizes?: string, type?: string, purpose?: string }> + +export interface WalletApi { + [METHODS.connect]: ( + p: { name: string, icons?: Icons, version: 1, connectNode: boolean } + ) => WalletInfo & { node?: Node } + + [METHODS.closeConnection]: (p: any) => void + + [METHODS.subscribeAddress]: ( + p: { type: 'connected' | 'current', value: 'subscribe' | 'unsubscribe' } + ) => { subscription: Array<'subscribe' | 'unsubscribe'>, address: Accounts } + + [METHODS.address]: () => Array> + + [METHODS.sign]: (( + p: { tx: EncodedData<'tx'>, onAccount: EncodedData<'ak'>, returnSigned: false } + ) => { transactionHash: EncodedData<'th'> }) & (( + p: { tx: EncodedData<'tx'>, onAccount: EncodedData<'ak'>, returnSigned: true } + ) => { signedTransaction: EncodedData<'tx'> }) + + [METHODS.signMessage]: ( + p: { message: string, onAccount: EncodedData<'ak'> } + ) => { signature: string } +} + +export interface AeppApi { + [METHODS.updateAddress]: (a: Accounts) => void + [METHODS.updateNetwork]: (a: Network) => void + [METHODS.readyToConnect]: (w: WalletInfo) => void + [METHODS.closeConnection]: (p: any) => void +} diff --git a/src/utils/aepp-wallet-communication/schema.ts b/src/utils/aepp-wallet-communication/schema.ts index 117f25ccfd..a7b3d027af 100644 --- a/src/utils/aepp-wallet-communication/schema.ts +++ b/src/utils/aepp-wallet-communication/schema.ts @@ -145,3 +145,12 @@ export class RpcInternalError extends RpcError { this.name = 'RpcInternalError' } } + +export class RpcMethodNotFoundError extends RpcError { + static code = -32601 + code = -32601 + constructor () { + super('Method not found') + this.name = 'RpcMethodNotFoundError' + } +} diff --git a/src/utils/errors.ts b/src/utils/errors.ts index b41b2ec56b..0abe8b70d9 100644 --- a/src/utils/errors.ts +++ b/src/utils/errors.ts @@ -236,7 +236,7 @@ export class InvalidRpcMessageError extends AeppError { } export class MissingCallbackError extends AeppError { - constructor (id: string) { + constructor (id: number) { super(`Can't find callback for this messageId ${id}`) this.name = 'MissingCallbackError' } diff --git a/test/integration/rpc.js b/test/integration/rpc.js index c111be9525..ee890690a2 100644 --- a/test/integration/rpc.js +++ b/test/integration/rpc.js @@ -372,22 +372,12 @@ describe('Aepp<->Wallet', function () { received.should.be.equal(true) }) - it('Resolve/Reject callback for undefined message', async () => { - expect(() => aepp.rpcClient._processResponse({ id: 0 })) - .to.throw('Can\'t find callback for this messageId 0') - }) - it('Try to connect unsupported protocol', async () => { await expect(aepp.rpcClient.request( METHODS.connect, { name: 'test-aepp', version: 2 } )).to.be.eventually.rejectedWith('Unsupported Protocol Version').with.property('code', 5) }) - it('Process response ', () => { - expect(() => aepp.rpcClient._processResponse({ id: 11, error: {} })) - .to.throw('Can\'t find callback for this messageId ' + 11) - }) - it('Disconnect from wallet', async () => { const walletDisconnect = new Promise((resolve) => { wallet.onDisconnect = (...args) => resolve(args) From 1a8e830126bb2644de39ec4b3c712b0ab230a3e9 Mon Sep 17 00:00:00 2001 From: Denis Davidyuk Date: Sun, 29 May 2022 14:00:38 +0300 Subject: [PATCH 11/11] refactor(BrowserWindowMessage): make fields starting with `_` private --- .../connection/BrowserWindowMessage.ts | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts b/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts index e71bc29ac6..566596df3e 100644 --- a/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts +++ b/src/utils/aepp-wallet-communication/connection/BrowserWindowMessage.ts @@ -35,9 +35,9 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { sendDirection?: MESSAGE_DIRECTION receiveDirection: MESSAGE_DIRECTION listener?: (this: Window, ev: MessageEvent) => void - _onDisconnect?: () => void - _target?: Window - _self: Window + #onDisconnect?: () => void + #target?: Window + #self: Window constructor ({ target, @@ -55,8 +55,8 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { debug?: boolean } = {}) { super(options) - this._target = target - this._self = self + this.#target = target + this.#self = self this.origin = origin this.sendDirection = sendDirection this.receiveDirection = receiveDirection @@ -74,7 +74,7 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { this.listener = (message: MessageEvent) => { if (typeof message.data !== 'object') return if (this.origin != null && this.origin !== message.origin) return - if (this._target != null && this._target !== message.source) return + if (this.#target != null && this.#target !== message.source) return this.receiveMessage(message) let { data } = message if (data.type != null) { @@ -83,25 +83,25 @@ export default class BrowserWindowMessageConnection extends BrowserConnection { } onMessage(data, message.origin, message.source) } - this._self.addEventListener('message', this.listener) - this._onDisconnect = onDisconnect + this.#self.addEventListener('message', this.listener) + this.#onDisconnect = onDisconnect } disconnect (): void { super.disconnect() - if (this.listener == null || this._onDisconnect == null) { + if (this.listener == null || this.#onDisconnect == null) { throw new InternalError('Expected to not happen, required for TS') } - this._self.removeEventListener('message', this.listener) + this.#self.removeEventListener('message', this.listener) delete this.listener - this._onDisconnect() - delete this._onDisconnect + this.#onDisconnect() + this.#onDisconnect = undefined } sendMessage (msg: MessageEvent): void { - if (this._target == null) throw new RpcConnectionError('Can\'t send messages without target') + if (this.#target == null) throw new RpcConnectionError('Can\'t send messages without target') const message = this.sendDirection != null ? { type: this.sendDirection, data: msg } : msg super.sendMessage(message) - this._target.postMessage(message, this.origin ?? '*') + this.#target.postMessage(message, this.origin ?? '*') } }