@aeternity/aepp-sdk
Version:
SDK for the æternity blockchain
220 lines (213 loc) • 7.16 kB
JavaScript
/**
* 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 { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES } from '../schema'
import { sendMessage, message, isValidAccounts } from '../helpers'
/**
* 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.handlers Arrays with two function for handling messages ([ onMessage: Function, onDisconnect: Function])
* @return {Object}
*/
export default stampit({
init ({ id, name, networkId, icons, connection, handlers: [onMessage, onDisconnect] }) {
this.id = id
this.connection = connection
this.info = { name, networkId, icons }
// {
// [msg.id]: { resolve, reject }
// }
this.callbacks = {}
// ['connected', 'current']
this.addressSubscription = []
// {
// connected: { [pub]: {...meta} },
// current: { [pub]: {...meta} }
// }
this.accounts = {}
this.sendMessage = sendMessage(this.connection)
const handleMessage = (msg, origin) => {
if (!msg || !msg.jsonrpc || msg.jsonrpc !== '2.0' || !msg.method) {
throw new Error(`Received invalid message: ${msg}`)
}
onMessage(msg, origin)
}
const disconnect = (aepp, connection) => {
this.disconnect(true)
typeof onDisconnect === 'function' && onDisconnect(connection, this)
}
connection.connect(handleMessage, disconnect)
},
propertyDescriptors: {
currentAccount: {
enumerable: true,
configurable: false,
get () {
return this.isHasAccounts()
? Object.keys(this.accounts.current)[0]
: undefined
}
},
addresses: {
enumerable: true,
configurable: false,
get () {
return this.isHasAccounts()
? [...Object.keys(this.accounts.current), ...Object.keys(this.accounts.connected)]
: []
}
},
origin: {
enumerable: true,
configurable: false,
get () {
return this.connection
}
}
},
methods: {
/**
* Update info
* @function updateInfo
* @instance
* @rtype (info: Object) => void
* @param {Object} info Info to update (will be merged with current info object)
* @return {void}
*/
updateInfo (info) {
Object.assign(this.info, info)
},
isHasAccounts () {
return typeof this.accounts === 'object' &&
typeof this.accounts.connected === 'object' &&
typeof this.accounts.current === 'object'
},
isSubscribed () {
return this.addressSubscription.length && this.isHasAccounts()
},
/**
* Check if aepp has access to account
* @function hasAccessToAccount
* @instance
* @rtype (address: String) => Boolean
* @param {String} address Account address
* @return {Boolean} is connected
*/
hasAccessToAccount (address) {
return !!address && this.addresses.find(a => a === address)
},
/**
* Check if is connected
* @function isConnected
* @instance
* @rtype () => Boolean
* @return {Boolean} is connected
*/
isConnected () {
return this.connection.isConnected() && this.info.status === RPC_STATUS.CONNECTED
},
/**
* Get selected account
* @function getCurrentAccount
* @instance
* @rtype ({ onAccount } = {}) => String
* @param {Object} options Options
* @return {String}
*/
getCurrentAccount ({ onAccount } = {}) {
return onAccount || Object.keys(this.accounts.current)[0]
},
/**
* Disconnect
* @function disconnect
* @instance
* @rtype () => void
* @return {void}
*/
disconnect (forceConnectionClose = false) {
this.info.status = RPC_STATUS.DISCONNECTED
this.addressSubscription = []
this.accounts = {}
forceConnectionClose || this.connection.disconnect()
},
/**
* Update accounts and sent `update.address` notification to AEPP
* @param {{ current: Object, connected: Object }} accounts Object with current and connected accounts
* @param {{ forceNotification: Boolean }} [options={ forceNotification: false }] Don not sent update notification to AEPP
*/
setAccounts (accounts, { forceNotification = false } = {}) {
if (!isValidAccounts(accounts)) {
throw new Error('Invalid accounts object. Should be object like: `{ connected: {}, selected: {} }`')
}
this.accounts = accounts
if (!forceNotification) {
// Sent notification about account updates
this.sendMessage(message(METHODS.wallet.updateAddress, this.accounts), true)
}
},
/**
* Update subscription
* @function updateSubscription
* @instance
* @rtype (type: String, value: String) => void
* @param {String} type Subscription type
* @param {String} value Subscription value
* @return {String[]}
*/
updateSubscription (type, value) {
if (type === SUBSCRIPTION_TYPES.subscribe && !this.addressSubscription.includes(value)) {
this.addressSubscription.push(value)
}
if (type === SUBSCRIPTION_TYPES.unsubscribe && this.addressSubscription.includes(value)) {
this.addressSubscription = this.addressSubscription.filter(s => s !== value)
}
return this.addressSubscription
},
/**
* 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(message(name, params))
if (Object.prototype.hasOwnProperty.call(this.callbacks, msgId)) throw new Error('Callback Already exist')
return new Promise((resolve, reject) => {
this.callbacks[msgId] = { resolve, reject }
})
},
/**
* Process response message
* @function processResponse
* @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) {
if (!this.callbacks[id]) throw new Error(`Can't find callback for this messageId ${id}`)
if (result) {
this.callbacks[id].resolve(...typeof transformResult === 'function'
? transformResult({ id, result })
: [result])
} else {
this.callbacks[id].reject(error)
}
delete this.callbacks[id]
}
}
})