UNPKG

@aeternity/aepp-sdk

Version:

SDK for the æternity blockchain

347 lines (342 loc) 11.9 kB
import { Buffer as _Buffer } from "buffer"; import _defineProperty from "@babel/runtime-corejs3/helpers/defineProperty"; import nacl from 'tweetnacl'; import AeSdk from './AeSdk.js'; import verifyTransaction from './tx/validator.js'; import RpcClient from './aepp-wallet-communication/rpc/RpcClient.js'; import { METHODS, RPC_STATUS, SUBSCRIPTION_TYPES, RpcInvalidTransactionError, RpcNotAuthorizeError, RpcPermissionDenyError, RpcUnsupportedProtocolError } from './aepp-wallet-communication/schema.js'; import { InternalError, UnknownRpcClientError } from './utils/errors.js'; import { RPC_VERSION } from './aepp-wallet-communication/rpc/types.js'; import { Encoding, encode, decode } from './utils/encoder.js'; import jsonBig from './utils/json-big.js'; /** * Contain functionality for aepp interaction and managing multiple aepps * @category aepp wallet communication */ export default class AeSdkWallet extends AeSdk { /** * @param options - Options * @param options.name - Wallet name * @param options.id - Wallet id * @param options.type - Wallet type * @param options.onConnection - Call-back function for incoming AEPP connection * @param options.onSubscription - Call-back function for incoming AEPP account subscription * @param options.onAskAccounts - Call-back function for incoming AEPP get address request * @param options.onAskToSelectNetwork - Call-back function for incoming AEPP select network * request. If the request is fine then this function should change the current network. * @param options.onDisconnect - Call-back function for disconnect event */ constructor({ name, id, type, onConnection, onSubscription, onDisconnect, onAskAccounts, onAskToSelectNetwork, ...options }) { super(options); _defineProperty(this, "_clients", new Map()); this.onConnection = onConnection; this.onSubscription = onSubscription; this.onDisconnect = onDisconnect; this.onAskAccounts = onAskAccounts; this.onAskToSelectNetwork = onAskToSelectNetwork; this.name = name; this.id = id; this._type = type; } _getAccountsForClient({ addressSubscription }) { const { current, connected } = this.getAccounts(); return { current: addressSubscription.has('current') || addressSubscription.has('connected') ? current : {}, connected: addressSubscription.has('connected') ? connected : {} }; } _pushAccountsToApps() { if (this._clients == null) return; Array.from(this._clients.keys()).filter(clientId => this._isRpcClientConnected(clientId)).map(clientId => this._getClient(clientId)).filter(client => client.addressSubscription.size !== 0).forEach(client => client.rpc.notify(METHODS.updateAddress, this._getAccountsForClient(client))); } selectAccount(address) { super.selectAccount(address); this._pushAccountsToApps(); } addAccount(account, options) { super.addAccount(account, options); this._pushAccountsToApps(); } _getNode() { this.ensureNodeConnected(); return { node: { url: this.api.$host, name: this.selectedNodeName } }; } async selectNode(name) { super.selectNode(name); const networkId = await this.api.getNetworkId(); Array.from(this._clients.keys()).filter(clientId => this._isRpcClientConnected(clientId)).map(clientId => this._getClient(clientId)).forEach(client => { client.rpc.notify(METHODS.updateNetwork, { networkId, ...(client.connectNode && this._getNode()) }); }); } _getClient(clientId) { const client = this._clients.get(clientId); if (client == null) throw new UnknownRpcClientError(clientId); return client; } _isRpcClientConnected(clientId) { return RPC_STATUS.CONNECTED === this._getClient(clientId).status && this._getClient(clientId).rpc.connection.isConnected(); } _disconnectRpcClient(clientId) { const client = this._getClient(clientId); client.rpc.connection.disconnect(); client.status = RPC_STATUS.DISCONNECTED; client.addressSubscription = new Set(); } /** * Remove specific RpcClient by ID * @param id - Client ID */ removeRpcClient(id) { this._disconnectRpcClient(id); this._clients.delete(id); } /** * Add new client by AEPP connection * @param clientConnection - AEPP connection object * @returns Client ID */ addRpcClient(clientConnection) { // @TODO detect if aepp has some history based on origin???? // if yes use this instance for connection const id = _Buffer.from(nacl.randomBytes(8)).toString('base64'); let disconnectParams; const client = { id, status: RPC_STATUS.WAITING_FOR_CONNECTION_REQUEST, addressSubscription: new Set(), connectNode: false, rpc: new RpcClient(clientConnection, () => { this._clients.delete(id); this.onDisconnect(id, disconnectParams); // also related info }, { [METHODS.closeConnection]: params => { disconnectParams = params; this._disconnectRpcClient(id); }, // Store client info and prepare two fn for each client `connect` and `denyConnection` // which automatically prepare and send response for that client [METHODS.connect]: async ({ name, version, icons, connectNode }, origin) => { if (version !== RPC_VERSION) throw new RpcUnsupportedProtocolError(); await this.onConnection(id, { name, icons, connectNode }, origin); client.status = RPC_STATUS.CONNECTED; client.connectNode = connectNode; return { ...(await this.getWalletInfo()), ...(connectNode && this._getNode()) }; }, [METHODS.subscribeAddress]: async ({ type, value }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); switch (type) { case SUBSCRIPTION_TYPES.subscribe: // TODO: remove `type` as it always subscribe await this.onSubscription(id, { type, value }, origin); client.addressSubscription.add(value); break; case SUBSCRIPTION_TYPES.unsubscribe: client.addressSubscription.delete(value); break; default: throw new InternalError(`Unknown subscription type: ${type}`); } return { subscription: Array.from(client.addressSubscription), address: this._getAccountsForClient(client) }; }, [METHODS.address]: async (params, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); await this.onAskAccounts(id, params, origin); return this.addresses(); }, [METHODS.sign]: async ({ tx, onAccount = this.address, returnSigned, innerTx }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); if (!this.addresses().includes(onAccount)) { throw new RpcPermissionDenyError(onAccount); } const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id, innerTx }; if (returnSigned || innerTx === true) { return { signedTransaction: await this.signTransaction(tx, parameters) }; } try { return jsonBig.parse(jsonBig.stringify({ transactionHash: await this.sendTransaction(tx, { ...parameters, verify: false }) })); } catch (error) { const validation = await verifyTransaction(tx, this.api); if (validation.length > 0) throw new RpcInvalidTransactionError(validation); throw error; } }, [METHODS.signMessage]: async ({ message, onAccount = this.address }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); if (!this.addresses().includes(onAccount)) { throw new RpcPermissionDenyError(onAccount); } const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id }; return { signature: _Buffer.from(await this.signMessage(message, parameters)).toString('hex') }; }, [METHODS.signTypedData]: async ({ domain, aci, data, onAccount = this.address }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); if (!this.addresses().includes(onAccount)) { throw new RpcPermissionDenyError(onAccount); } const parameters = { ...domain, onAccount, aeppOrigin: origin, aeppRpcClientId: id }; return { signature: await this.signTypedData(data, aci, parameters) }; }, [METHODS.unsafeSign]: async ({ data, onAccount = this.address }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); if (!this.addresses().includes(onAccount)) throw new RpcPermissionDenyError(onAccount); const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id }; const signature = encode(await this.unsafeSign(decode(data), parameters), Encoding.Signature); return { signature }; }, [METHODS.signDelegation]: async ({ delegation, onAccount = this.address }, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); if (!this.addresses().includes(onAccount)) throw new RpcPermissionDenyError(onAccount); const parameters = { onAccount, aeppOrigin: origin, aeppRpcClientId: id }; const signature = await this.signDelegation(delegation, parameters); return { signature }; }, [METHODS.updateNetwork]: async (params, origin) => { if (!this._isRpcClientConnected(id)) throw new RpcNotAuthorizeError(); await this.onAskToSelectNetwork(id, params, origin); return null; } }) }; this._clients.set(id, client); return id; } /** * Send shareWalletInfo message to notify AEPP about wallet * @param clientId - ID of RPC client send message to */ async shareWalletInfo(clientId) { this._getClient(clientId).rpc.notify(METHODS.readyToConnect, await this.getWalletInfo()); } /** * Get Wallet info object * @returns Object with wallet information */ async getWalletInfo() { const { origin } = window.location; return { id: this.id, name: this.name, networkId: await this.api.getNetworkId(), origin: origin === 'file://' ? '*' : origin, type: this._type }; } /** * Get Wallet accounts * @returns Object with accounts information (\{ connected: Object, current: Object \}) */ getAccounts() { return { current: this.selectedAddress != null ? { [this.selectedAddress]: {} } : {}, connected: this.addresses().filter(a => a !== this.selectedAddress).reduce((acc, a) => ({ ...acc, [a]: {} }), {}) }; } } //# sourceMappingURL=AeSdkWallet.js.map