UNPKG

@base-org/account

Version:
611 lines 31.7 kB
import { CB_WALLET_RPC_URL } from '../../core/constants.js'; import { hexToNumber, isAddressEqual, numberToHex } from 'viem'; import { isActionableHttpRequestError, isViemError, standardErrors } from '../../core/error/errors.js'; import { logHandshakeCompleted, logHandshakeError, logHandshakeStarted, logRequestCompleted, logRequestError, logRequestStarted, } from '../../core/telemetry/events/scw-signer.js'; import { logAddOwnerCompleted, logAddOwnerError, logAddOwnerStarted, logInsufficientBalanceErrorHandlingCompleted, logInsufficientBalanceErrorHandlingError, logInsufficientBalanceErrorHandlingStarted, logSubAccountRequestCompleted, logSubAccountRequestError, logSubAccountRequestStarted, } from '../../core/telemetry/events/scw-sub-account.js'; import { parseErrorMessageFromAny } from '../../core/telemetry/utils.js'; import { ensureIntNumber, hexStringFromNumber } from '../../core/type/util.js'; import { createClients, getClient } from '../../store/chain-clients/utils.js'; import { correlationIds } from '../../store/correlation-ids/store.js'; import { store } from '../../store/store.js'; import { assertArrayPresence, assertPresence } from '../../util/assertPresence.js'; import { assertSubAccount } from '../../util/assertSubAccount.js'; import { decryptContent, encryptContent, exportKeyToHexString, importKeyFromHexString, } from '../../util/cipher.js'; import { fetchRPCRequest } from '../../util/provider.js'; import { getCryptoKeyAccount } from '../../kms/crypto-key/index.js'; import { SCWKeyManager } from './SCWKeyManager.js'; import { addSenderToRequest, appendWithoutDuplicates, assertFetchPermissionsRequest, assertGetCapabilitiesParams, assertParamsChainId, fillMissingParamsForFetchPermissions, getCachedWalletConnectResponse, getSenderFromRequest, initSubAccountConfig, injectRequestCapabilities, makeDataSuffix, prependWithoutDuplicates, requestHasCapability, } from './utils.js'; import { createSubAccountSigner } from './utils/createSubAccountSigner.js'; import { findOwnerIndex } from './utils/findOwnerIndex.js'; import { handleAddSubAccountOwner } from './utils/handleAddSubAccountOwner.js'; import { handleInsufficientBalanceError } from './utils/handleInsufficientBalance.js'; export class Signer { constructor(params) { var _a, _b, _c, _d; this.communicator = params.communicator; this.callback = params.callback; this.keyManager = new SCWKeyManager(); const { account, chains } = store.getState(); this.accounts = (_a = account.accounts) !== null && _a !== void 0 ? _a : []; this.chain = (_b = account.chain) !== null && _b !== void 0 ? _b : { id: (_d = (_c = params.metadata.appChainIds) === null || _c === void 0 ? void 0 : _c[0]) !== null && _d !== void 0 ? _d : 1, }; if (chains) { createClients(chains); } } get isConnected() { return this.accounts.length > 0; } async handshake(args) { var _a, _b, _c; const correlationId = correlationIds.get(args); logHandshakeStarted({ method: args.method, correlationId }); try { // 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: (_c = args.params) !== null && _c !== void 0 ? _c : [], }, }, correlationId); 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); this.handleResponse(args, decrypted); logHandshakeCompleted({ method: args.method, correlationId }); } catch (error) { logHandshakeError({ method: args.method, correlationId, errorMessage: parseErrorMessageFromAny(error), }); throw error; } } async request(request) { const correlationId = correlationIds.get(request); logRequestStarted({ method: request.method, correlationId }); try { const result = await this._request(request); logRequestCompleted({ method: request.method, correlationId }); return result; } catch (error) { logRequestError({ method: request.method, correlationId, errorMessage: parseErrorMessageFromAny(error), }); throw error; } } async _request(request) { var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; if (this.accounts.length === 0) { switch (request.method) { case 'wallet_switchEthereumChain': { assertParamsChainId(request.params); this.chain.id = Number(request.params[0].chainId); return; } case 'wallet_connect': { // Wait for the popup to be loaded before making async calls await ((_b = (_a = this.communicator).waitForPopupLoaded) === null || _b === void 0 ? void 0 : _b.call(_a)); await initSubAccountConfig(); // Check if addSubAccount capability is present and if so, inject the the sub account capabilities let capabilitiesToInject = {}; if (requestHasCapability(request, 'addSubAccount')) { capabilitiesToInject = (_d = (_c = store.subAccountsConfig.get()) === null || _c === void 0 ? void 0 : _c.capabilities) !== null && _d !== void 0 ? _d : {}; } const modifiedRequest = injectRequestCapabilities(request, capabilitiesToInject); return this.sendRequestToPopup(modifiedRequest); } case 'wallet_sendCalls': case 'wallet_sign': { return this.sendRequestToPopup(request); } default: throw standardErrors.provider.unauthorized(); } } if (this.shouldRequestUseSubAccountSigner(request)) { const correlationId = correlationIds.get(request); logSubAccountRequestStarted({ method: request.method, correlationId }); try { const result = await this.sendRequestToSubAccountSigner(request); logSubAccountRequestCompleted({ method: request.method, correlationId }); return result; } catch (error) { logSubAccountRequestError({ method: request.method, correlationId, errorMessage: parseErrorMessageFromAny(error), }); throw error; } } switch (request.method) { case 'eth_requestAccounts': case 'eth_accounts': { const subAccount = store.subAccounts.get(); const subAccountsConfig = store.subAccountsConfig.get(); if (subAccount === null || subAccount === void 0 ? void 0 : subAccount.address) { // if auto sub accounts are enabled and we have a sub account, we need to return it as a top level account // otherwise, we just append it to the accounts array this.accounts = (subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.enableAutoSubAccounts) ? prependWithoutDuplicates(this.accounts, subAccount.address) : appendWithoutDuplicates(this.accounts, subAccount.address); } (_e = this.callback) === null || _e === void 0 ? void 0 : _e.call(this, 'connect', { chainId: numberToHex(this.chain.id) }); return this.accounts; } case 'eth_coinbase': return this.accounts[0]; case 'net_version': return this.chain.id; case 'eth_chainId': return numberToHex(this.chain.id); case 'wallet_getCapabilities': return this.handleGetCapabilitiesRequest(request); 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); case 'wallet_connect': { // Return cached wallet connect response if available, unless signInWithEthereum capability is present // SIWE requires fresh signatures/nonces so we should not use cached responses const hasSiweCapability = requestHasCapability(request, 'signInWithEthereum'); if (!hasSiweCapability) { const cachedResponse = await getCachedWalletConnectResponse(); if (cachedResponse) { return cachedResponse; } } // Wait for the popup to be loaded before making async calls await ((_g = (_f = this.communicator).waitForPopupLoaded) === null || _g === void 0 ? void 0 : _g.call(_f)); await initSubAccountConfig(); const subAccountsConfig = store.subAccountsConfig.get(); const modifiedRequest = injectRequestCapabilities(request, (_h = subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.capabilities) !== null && _h !== void 0 ? _h : {}); const result = await this.sendRequestToPopup(modifiedRequest); (_j = this.callback) === null || _j === void 0 ? void 0 : _j.call(this, 'connect', { chainId: numberToHex(this.chain.id) }); return result; } // Sub Account Support case 'wallet_getSubAccounts': { const subAccount = store.subAccounts.get(); if (subAccount === null || subAccount === void 0 ? void 0 : subAccount.address) { return { subAccounts: [subAccount], }; } if (!this.chain.rpcUrl) { throw standardErrors.rpc.internal('No RPC URL set for chain'); } const response = (await fetchRPCRequest(request, this.chain.rpcUrl)); assertArrayPresence(response.subAccounts, 'subAccounts'); if (response.subAccounts.length > 0) { // cache the sub account assertSubAccount(response.subAccounts[0]); const subAccount = response.subAccounts[0]; store.subAccounts.set({ address: subAccount.address, factory: subAccount.factory, factoryData: subAccount.factoryData, }); } return response; } case 'wallet_addSubAccount': return this.addSubAccount(request); case 'coinbase_fetchPermissions': { assertFetchPermissionsRequest(request); const completeRequest = fillMissingParamsForFetchPermissions(request); const permissions = (await fetchRPCRequest(completeRequest, CB_WALLET_RPC_URL)); const requestedChainId = hexToNumber((_k = completeRequest.params) === null || _k === void 0 ? void 0 : _k[0].chainId); store.spendPermissions.set(permissions.permissions.map((permission) => (Object.assign(Object.assign({}, permission), { chainId: requestedChainId })))); return permissions; } 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); return this.handleResponse(request, decrypted); } async handleResponse(request, decrypted) { var _a, _b, _c, _d, _e; const result = decrypted.result; if ('error' in result) throw result.error; switch (request.method) { case 'eth_requestAccounts': { const accounts = result.value; this.accounts = accounts; store.account.set({ accounts, chain: this.chain, }); (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'accountsChanged', accounts); break; } case 'wallet_connect': { const response = result.value; const accounts = response.accounts.map((account) => account.address); this.accounts = accounts; store.account.set({ accounts, }); const account = response.accounts.at(0); const capabilities = account === null || account === void 0 ? void 0 : account.capabilities; if (capabilities === null || capabilities === void 0 ? void 0 : capabilities.subAccounts) { const capabilityResponse = capabilities === null || capabilities === void 0 ? void 0 : capabilities.subAccounts; assertArrayPresence(capabilityResponse, 'subAccounts'); assertSubAccount(capabilityResponse[0]); store.subAccounts.set({ address: capabilityResponse[0].address, factory: capabilityResponse[0].factory, factoryData: capabilityResponse[0].factoryData, }); } let accounts_ = [this.accounts[0]]; const subAccount = store.subAccounts.get(); const subAccountsConfig = store.subAccountsConfig.get(); if (subAccount === null || subAccount === void 0 ? void 0 : subAccount.address) { // Sub account should be returned as a top level account if auto sub accounts are enabled this.accounts = (subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.enableAutoSubAccounts) ? prependWithoutDuplicates(this.accounts, subAccount.address) : appendWithoutDuplicates(this.accounts, subAccount.address); } const spendPermissions = (_c = (_b = response === null || response === void 0 ? void 0 : response.accounts) === null || _b === void 0 ? void 0 : _b[0].capabilities) === null || _c === void 0 ? void 0 : _c.spendPermissions; if (spendPermissions && 'permissions' in spendPermissions) { store.spendPermissions.set(spendPermissions === null || spendPermissions === void 0 ? void 0 : spendPermissions.permissions); } (_d = this.callback) === null || _d === void 0 ? void 0 : _d.call(this, 'accountsChanged', accounts_); break; } case 'wallet_addSubAccount': { assertSubAccount(result.value); const subAccount = result.value; store.subAccounts.set(subAccount); const subAccountsConfig = store.subAccountsConfig.get(); this.accounts = (subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.enableAutoSubAccounts) ? prependWithoutDuplicates(this.accounts, subAccount.address) : appendWithoutDuplicates(this.accounts, subAccount.address); (_e = this.callback) === null || _e === void 0 ? void 0 : _e.call(this, 'accountsChanged', this.accounts); break; } default: break; } return result.value; } async cleanup() { var _a, _b; const metadata = store.config.get().metadata; await this.keyManager.clear(); // clear the store store.account.clear(); store.subAccounts.clear(); store.spendPermissions.clear(); store.chains.clear(); // reset the signer this.accounts = []; this.chain = { id: (_b = (_a = metadata === null || metadata === void 0 ? void 0 : 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) { assertParamsChainId(request.params); const chainId = ensureIntNumber(request.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 handleGetCapabilitiesRequest(request) { assertGetCapabilitiesParams(request.params); const requestedAccount = request.params[0]; const filterChainIds = request.params[1]; // Optional second parameter if (!this.accounts.some((account) => isAddressEqual(account, requestedAccount))) { throw standardErrors.provider.unauthorized('no active account found when getting capabilities'); } const capabilities = store.getState().account.capabilities; // Return empty object if capabilities is undefined if (!capabilities) { return {}; } // If no filter is provided, return all capabilities if (!filterChainIds || filterChainIds.length === 0) { return capabilities; } // Convert filter chain IDs to numbers once for efficient lookup const filterChainNumbers = new Set(filterChainIds.map((chainId) => hexToNumber(chainId))); // Filter capabilities const filteredCapabilities = Object.fromEntries(Object.entries(capabilities).filter(([capabilityKey]) => { try { const capabilityChainNumber = hexToNumber(capabilityKey); return filterChainNumbers.has(capabilityChainNumber); } catch (_a) { // If capabilityKey is not a valid hex string, exclude it return false; } })); return filteredCapabilities; } async sendEncryptedRequest(request) { const sharedSecret = await this.keyManager.getSharedSecret(); if (!sharedSecret) { throw standardErrors.provider.unauthorized('No shared secret found when encrypting request'); } const encrypted = await encryptContent({ action: request, chainId: this.chain.id, }, sharedSecret); const correlationId = correlationIds.get(request); const message = await this.createRequestMessage({ encrypted }, correlationId); return this.communicator.postRequestAndWaitForResponse(message); } async createRequestMessage(content, correlationId) { const publicKey = await exportKeyToHexString('public', await this.keyManager.getOwnPublicKey()); return { id: crypto.randomUUID(), correlationId, sender: publicKey, content, timestamp: new Date(), }; } async decryptResponseMessage(message) { var _a, _b, _c; 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: no shared secret found when decrypting response'); } const response = await decryptContent(content.encrypted, sharedSecret); const availableChains = (_a = response.data) === null || _a === void 0 ? void 0 : _a.chains; if (availableChains) { const nativeCurrencies = (_b = response.data) === null || _b === void 0 ? void 0 : _b.nativeCurrencies; const chains = Object.entries(availableChains).map(([id, rpcUrl]) => { const nativeCurrency = nativeCurrencies === null || nativeCurrencies === void 0 ? void 0 : nativeCurrencies[Number(id)]; return Object.assign({ id: Number(id), rpcUrl }, (nativeCurrency ? { nativeCurrency } : {})); }); store.chains.set(chains); this.updateChain(this.chain.id, chains); createClients(chains); } const walletCapabilities = (_c = response.data) === null || _c === void 0 ? void 0 : _c.capabilities; if (walletCapabilities) { store.account.set({ capabilities: walletCapabilities, }); } return response; } updateChain(chainId, newAvailableChains) { var _a; const state = store.getState(); const chains = newAvailableChains !== null && newAvailableChains !== void 0 ? newAvailableChains : state.chains; 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; store.account.set({ chain, }); (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'chainChanged', hexStringFromNumber(chain.id)); } return true; } async addSubAccount(request) { var _a, _b, _c, _d; const state = store.getState(); const subAccount = state.subAccount; const subAccountsConfig = store.subAccountsConfig.get(); if (subAccount === null || subAccount === void 0 ? void 0 : subAccount.address) { this.accounts = (subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.enableAutoSubAccounts) ? prependWithoutDuplicates(this.accounts, subAccount.address) : appendWithoutDuplicates(this.accounts, subAccount.address); (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'accountsChanged', this.accounts); return subAccount; } // Wait for the popup to be loaded before sending the request await ((_c = (_b = this.communicator).waitForPopupLoaded) === null || _c === void 0 ? void 0 : _c.call(_b)); if (Array.isArray(request.params) && request.params.length > 0 && request.params[0].account && request.params[0].account.type === 'create') { let keys; if (request.params[0].account.keys && request.params[0].account.keys.length > 0) { keys = request.params[0].account.keys; } else { const config = (_d = store.subAccountsConfig.get()) !== null && _d !== void 0 ? _d : {}; const { account: ownerAccount } = config.toOwnerAccount ? await config.toOwnerAccount() : await getCryptoKeyAccount(); if (!ownerAccount) { throw standardErrors.provider.unauthorized('could not get subaccount owner account when adding sub account'); } keys = [ { type: ownerAccount.address ? 'address' : 'webauthn-p256', publicKey: ownerAccount.address || ownerAccount.publicKey, }, ]; } request.params[0].account.keys = keys; } const response = await this.sendRequestToPopup(request); assertSubAccount(response); return response; } shouldRequestUseSubAccountSigner(request) { const sender = getSenderFromRequest(request); const subAccount = store.subAccounts.get(); if (sender) { return sender.toLowerCase() === (subAccount === null || subAccount === void 0 ? void 0 : subAccount.address.toLowerCase()); } return false; } async sendRequestToSubAccountSigner(request) { var _a; const subAccount = store.subAccounts.get(); const subAccountsConfig = store.subAccountsConfig.get(); const config = store.config.get(); assertPresence(subAccount === null || subAccount === void 0 ? void 0 : subAccount.address, standardErrors.provider.unauthorized('no active sub account when sending request to sub account signer')); // Get the owner account from the config const ownerAccount = (subAccountsConfig === null || subAccountsConfig === void 0 ? void 0 : subAccountsConfig.toOwnerAccount) ? await subAccountsConfig.toOwnerAccount() : await getCryptoKeyAccount(); assertPresence(ownerAccount === null || ownerAccount === void 0 ? void 0 : ownerAccount.account, standardErrors.provider.unauthorized('no active sub account owner when sending request to sub account signer')); const sender = getSenderFromRequest(request); // if sender is undefined, we inject the active sub account // address into the params for the supported request methods if (sender === undefined) { request = addSenderToRequest(request, subAccount.address); } const client = getClient(this.chain.id); assertPresence(client, standardErrors.rpc.internal(`client not found for chainId ${this.chain.id} when sending request to sub account signer`)); const globalAccountAddress = this.accounts.find((account) => account.toLowerCase() !== subAccount.address.toLowerCase()); assertPresence(globalAccountAddress, standardErrors.provider.unauthorized('no global account found when sending request to sub account signer')); const dataSuffix = makeDataSuffix({ attribution: (_a = config.preference) === null || _a === void 0 ? void 0 : _a.attribution, dappOrigin: window.location.origin, }); const publicKey = ownerAccount.account.type === 'local' ? ownerAccount.account.address : ownerAccount.account.publicKey; let ownerIndex = await findOwnerIndex({ address: subAccount.address, factory: subAccount.factory, factoryData: subAccount.factoryData, publicKey, client, }); if (ownerIndex === -1) { const correlationId = correlationIds.get(request); logAddOwnerStarted({ method: request.method, correlationId }); try { ownerIndex = await handleAddSubAccountOwner({ ownerAccount: ownerAccount.account, globalAccountRequest: this.sendRequestToPopup.bind(this), chainId: this.chain.id, }); logAddOwnerCompleted({ method: request.method, correlationId }); } catch (error) { logAddOwnerError({ method: request.method, correlationId, errorMessage: parseErrorMessageFromAny(error), }); return standardErrors.provider.unauthorized('failed to add sub account owner when sending request to sub account signer'); } } const { request: subAccountRequest } = await createSubAccountSigner({ address: subAccount.address, owner: ownerAccount.account, client: client, factory: subAccount.factory, factoryData: subAccount.factoryData, parentAddress: globalAccountAddress, attribution: dataSuffix ? { suffix: dataSuffix } : undefined, ownerIndex, }); try { const result = await subAccountRequest(request); return result; } catch (error) { let errorObject; if (isViemError(error)) { errorObject = JSON.parse(error.details); } else if (isActionableHttpRequestError(error)) { errorObject = error; } else { throw error; } if (!(isActionableHttpRequestError(errorObject) && errorObject.data)) { throw error; } if (!errorObject.data) { throw error; } const correlationId = correlationIds.get(request); logInsufficientBalanceErrorHandlingStarted({ method: request.method, correlationId }); try { const result = await handleInsufficientBalanceError({ errorData: errorObject.data, globalAccountAddress, subAccountAddress: subAccount.address, client, request, subAccountRequest, globalAccountRequest: this.request.bind(this), }); logInsufficientBalanceErrorHandlingCompleted({ method: request.method, correlationId }); return result; } catch (handlingError) { console.error(handlingError); logInsufficientBalanceErrorHandlingError({ method: request.method, correlationId, errorMessage: parseErrorMessageFromAny(handlingError), }); throw error; } } } } //# sourceMappingURL=Signer.js.map