UNPKG

@coinbase/wallet-sdk

Version:
210 lines 9.6 kB
import { SCWKeyManager } from './SCWKeyManager.js'; import { standardErrors } from '../../core/error/errors.js'; import { ScopedLocalStorage } from '../../core/storage/ScopedLocalStorage.js'; import { ensureIntNumber, hexStringFromNumber } from '../../core/type/util.js'; import { decryptContent, encryptContent, exportKeyToHexString, importKeyFromHexString, } from '../../util/cipher.js'; import { fetchRPCRequest } from '../../util/provider.js'; const ACCOUNTS_KEY = 'accounts'; const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain'; const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains'; const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities'; export class SCWSigner { constructor(params) { var _a, _b, _c; this.metadata = params.metadata; this.communicator = params.communicator; this.callback = params.callback; this.keyManager = new SCWKeyManager(); this.storage = new ScopedLocalStorage('CBWSDK', 'SCWStateManager'); this.accounts = (_a = this.storage.loadObject(ACCOUNTS_KEY)) !== null && _a !== void 0 ? _a : []; this.chain = this.storage.loadObject(ACTIVE_CHAIN_STORAGE_KEY) || { id: (_c = (_b = params.metadata.appChainIds) === null || _b === void 0 ? void 0 : _b[0]) !== null && _c !== void 0 ? _c : 1, }; this.handshake = this.handshake.bind(this); this.request = this.request.bind(this); this.createRequestMessage = this.createRequestMessage.bind(this); this.decryptResponseMessage = this.decryptResponseMessage.bind(this); } async handshake(args) { var _a, _b, _c, _d; // Open the popup before constructing the request message. // This is to ensure that the popup is not blocked by some browsers (i.e. Safari) await ((_b = (_a = this.communicator).waitForPopupLoaded) === null || _b === void 0 ? void 0 : _b.call(_a)); const handshakeMessage = await this.createRequestMessage({ handshake: { method: args.method, params: Object.assign({}, this.metadata, (_c = args.params) !== null && _c !== void 0 ? _c : {}), }, }); const response = await this.communicator.postRequestAndWaitForResponse(handshakeMessage); // store peer's public key if ('failure' in response.content) throw response.content.failure; const peerPublicKey = await importKeyFromHexString('public', response.sender); await this.keyManager.setPeerPublicKey(peerPublicKey); const decrypted = await this.decryptResponseMessage(response); const result = decrypted.result; if ('error' in result) throw result.error; switch (args.method) { case 'eth_requestAccounts': { const accounts = result.value; this.accounts = accounts; this.storage.storeObject(ACCOUNTS_KEY, accounts); (_d = this.callback) === null || _d === void 0 ? void 0 : _d.call(this, 'accountsChanged', accounts); break; } } } async request(request) { var _a; if (this.accounts.length === 0) { switch (request.method) { case 'wallet_sendCalls': return this.sendRequestToPopup(request); default: throw standardErrors.provider.unauthorized(); } } switch (request.method) { case 'eth_requestAccounts': (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'connect', { chainId: hexStringFromNumber(this.chain.id) }); return this.accounts; case 'eth_accounts': return this.accounts; case 'eth_coinbase': return this.accounts[0]; case 'net_version': return this.chain.id; case 'eth_chainId': return hexStringFromNumber(this.chain.id); case 'wallet_getCapabilities': return this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY); case 'wallet_switchEthereumChain': return this.handleSwitchChainRequest(request); case 'eth_ecRecover': case 'personal_sign': case 'wallet_sign': case 'personal_ecRecover': case 'eth_signTransaction': case 'eth_sendTransaction': case 'eth_signTypedData_v1': case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': case 'eth_signTypedData': case 'wallet_addEthereumChain': case 'wallet_watchAsset': case 'wallet_sendCalls': case 'wallet_showCallsStatus': case 'wallet_grantPermissions': return this.sendRequestToPopup(request); default: if (!this.chain.rpcUrl) throw standardErrors.rpc.internal('No RPC URL set for chain'); return fetchRPCRequest(request, this.chain.rpcUrl); } } async sendRequestToPopup(request) { var _a, _b; // Open the popup before constructing the request message. // This is to ensure that the popup is not blocked by some browsers (i.e. Safari) await ((_b = (_a = this.communicator).waitForPopupLoaded) === null || _b === void 0 ? void 0 : _b.call(_a)); const response = await this.sendEncryptedRequest(request); const decrypted = await this.decryptResponseMessage(response); const result = decrypted.result; if ('error' in result) throw result.error; return result.value; } async cleanup() { var _a, _b; this.storage.clear(); await this.keyManager.clear(); this.accounts = []; this.chain = { id: (_b = (_a = this.metadata.appChainIds) === null || _a === void 0 ? void 0 : _a[0]) !== null && _b !== void 0 ? _b : 1, }; } /** * @returns `null` if the request was successful. * https://eips.ethereum.org/EIPS/eip-3326#wallet_switchethereumchain */ async handleSwitchChainRequest(request) { var _a; const params = request.params; if (!params || !((_a = params[0]) === null || _a === void 0 ? void 0 : _a.chainId)) { throw standardErrors.rpc.invalidParams(); } const chainId = ensureIntNumber(params[0].chainId); const localResult = this.updateChain(chainId); if (localResult) return null; const popupResult = await this.sendRequestToPopup(request); if (popupResult === null) { this.updateChain(chainId); } return popupResult; } async sendEncryptedRequest(request) { const sharedSecret = await this.keyManager.getSharedSecret(); if (!sharedSecret) { throw standardErrors.provider.unauthorized('No valid session found, try requestAccounts before other methods'); } const encrypted = await encryptContent({ action: request, chainId: this.chain.id, }, sharedSecret); const message = await this.createRequestMessage({ encrypted }); return this.communicator.postRequestAndWaitForResponse(message); } async createRequestMessage(content) { const publicKey = await exportKeyToHexString('public', await this.keyManager.getOwnPublicKey()); return { id: crypto.randomUUID(), sender: publicKey, content, timestamp: new Date(), }; } async decryptResponseMessage(message) { var _a, _b; const content = message.content; // throw protocol level error if ('failure' in content) { throw content.failure; } const sharedSecret = await this.keyManager.getSharedSecret(); if (!sharedSecret) { throw standardErrors.provider.unauthorized('Invalid session'); } const response = await decryptContent(content.encrypted, sharedSecret); const availableChains = (_a = response.data) === null || _a === void 0 ? void 0 : _a.chains; if (availableChains) { const chains = Object.entries(availableChains).map(([id, rpcUrl]) => ({ id: Number(id), rpcUrl, })); this.storage.storeObject(AVAILABLE_CHAINS_STORAGE_KEY, chains); this.updateChain(this.chain.id, chains); } const walletCapabilities = (_b = response.data) === null || _b === void 0 ? void 0 : _b.capabilities; if (walletCapabilities) { this.storage.storeObject(WALLET_CAPABILITIES_STORAGE_KEY, walletCapabilities); } return response; } updateChain(chainId, newAvailableChains) { var _a; const chains = newAvailableChains !== null && newAvailableChains !== void 0 ? newAvailableChains : this.storage.loadObject(AVAILABLE_CHAINS_STORAGE_KEY); const chain = chains === null || chains === void 0 ? void 0 : chains.find((chain) => chain.id === chainId); if (!chain) return false; if (chain !== this.chain) { this.chain = chain; this.storage.storeObject(ACTIVE_CHAIN_STORAGE_KEY, chain); (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'chainChanged', hexStringFromNumber(chain.id)); } return true; } } //# sourceMappingURL=SCWSigner.js.map