UNPKG

@coinbase/wallet-sdk

Version:
346 lines 15.2 kB
// Copyright (c) 2018-2024 Coinbase, Inc. <https://www.coinbase.com/> // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-nocheck import eip712 from '../../vendor-js/eth-eip712-util/index.cjs'; import { LOCAL_STORAGE_ADDRESSES_KEY } from './relay/constants.js'; import { isErrorResponse } from './relay/type/Web3Response.js'; import { WalletLinkRelay } from './relay/WalletLinkRelay.js'; import { WALLETLINK_URL } from '../../core/constants.js'; import { standardErrors } from '../../core/error/errors.js'; import { ScopedLocalStorage } from '../../core/storage/ScopedLocalStorage.js'; import { encodeToHexString, ensureAddressString, ensureBigInt, ensureBuffer, ensureIntNumber, ensureParsedJSONObject, hexStringFromBuffer, hexStringFromNumber, } from '../../core/type/util.js'; import { fetchRPCRequest } from '../../util/provider.js'; const DEFAULT_CHAIN_ID_KEY = 'DefaultChainId'; const DEFAULT_JSON_RPC_URL = 'DefaultJsonRpcUrl'; // original source: https://github.com/coinbase/coinbase-wallet-sdk/blob/v3.7.1/packages/wallet-sdk/src/provider/CoinbaseWalletProvider.ts export class WalletLinkSigner { constructor(options) { this._relay = null; this._addresses = []; this.metadata = options.metadata; this._storage = new ScopedLocalStorage('walletlink', WALLETLINK_URL); this.callback = options.callback || null; const cachedAddresses = this._storage.getItem(LOCAL_STORAGE_ADDRESSES_KEY); if (cachedAddresses) { const addresses = cachedAddresses.split(' '); if (addresses[0] !== '') { this._addresses = addresses.map((address) => ensureAddressString(address)); } } this.initializeRelay(); } getSession() { const relay = this.initializeRelay(); const { id, secret } = relay.getWalletLinkSession(); return { id, secret }; } async handshake() { await this._eth_requestAccounts(); } get selectedAddress() { return this._addresses[0] || undefined; } get jsonRpcUrl() { var _a; return (_a = this._storage.getItem(DEFAULT_JSON_RPC_URL)) !== null && _a !== void 0 ? _a : undefined; } set jsonRpcUrl(value) { this._storage.setItem(DEFAULT_JSON_RPC_URL, value); } updateProviderInfo(jsonRpcUrl, chainId) { var _a; this.jsonRpcUrl = jsonRpcUrl; // emit chainChanged event if necessary const originalChainId = this.getChainId(); this._storage.setItem(DEFAULT_CHAIN_ID_KEY, chainId.toString(10)); const chainChanged = ensureIntNumber(chainId) !== originalChainId; if (chainChanged) { (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'chainChanged', hexStringFromNumber(chainId)); } } async watchAsset(params) { const request = (Array.isArray(params) ? params[0] : params); if (!request.type) { throw standardErrors.rpc.invalidParams('Type is required'); } if ((request === null || request === void 0 ? void 0 : request.type) !== 'ERC20') { throw standardErrors.rpc.invalidParams(`Asset of type '${request.type}' is not supported`); } if (!(request === null || request === void 0 ? void 0 : request.options)) { throw standardErrors.rpc.invalidParams('Options are required'); } if (!(request === null || request === void 0 ? void 0 : request.options.address)) { throw standardErrors.rpc.invalidParams('Address is required'); } const chainId = this.getChainId(); const { address, symbol, image, decimals } = request.options; const relay = this.initializeRelay(); const result = await relay.watchAsset(request.type, address, symbol, decimals, image, chainId === null || chainId === void 0 ? void 0 : chainId.toString()); if (isErrorResponse(result)) return false; return !!result.result; } async addEthereumChain(params) { var _a, _b; const request = params[0]; if (((_a = request.rpcUrls) === null || _a === void 0 ? void 0 : _a.length) === 0) { throw standardErrors.rpc.invalidParams('please pass in at least 1 rpcUrl'); } if (!request.chainName || request.chainName.trim() === '') { throw standardErrors.rpc.invalidParams('chainName is a required field'); } if (!request.nativeCurrency) { throw standardErrors.rpc.invalidParams('nativeCurrency is a required field'); } const chainIdNumber = Number.parseInt(request.chainId, 16); if (chainIdNumber === this.getChainId()) { return false; } const relay = this.initializeRelay(); const { rpcUrls = [], blockExplorerUrls = [], chainName, iconUrls = [], nativeCurrency, } = request; const res = await relay.addEthereumChain(chainIdNumber.toString(), rpcUrls, iconUrls, blockExplorerUrls, chainName, nativeCurrency); if (isErrorResponse(res)) return false; if (((_b = res.result) === null || _b === void 0 ? void 0 : _b.isApproved) === true) { this.updateProviderInfo(rpcUrls[0], chainIdNumber); return null; } throw standardErrors.rpc.internal('unable to add ethereum chain'); } async switchEthereumChain(params) { const request = params[0]; const chainId = Number.parseInt(request.chainId, 16); const relay = this.initializeRelay(); const res = await relay.switchEthereumChain(chainId.toString(10), this.selectedAddress || undefined); if (isErrorResponse(res)) throw res; const switchResponse = res.result; if (switchResponse.isApproved && switchResponse.rpcUrl.length > 0) { this.updateProviderInfo(switchResponse.rpcUrl, chainId); } return null; } async cleanup() { this.callback = null; if (this._relay) { this._relay.resetAndReload(); } this._storage.clear(); } _setAddresses(addresses, _) { var _a; if (!Array.isArray(addresses)) { throw new Error('addresses is not an array'); } const newAddresses = addresses.map((address) => ensureAddressString(address)); if (JSON.stringify(newAddresses) === JSON.stringify(this._addresses)) { return; } this._addresses = newAddresses; (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'accountsChanged', newAddresses); this._storage.setItem(LOCAL_STORAGE_ADDRESSES_KEY, newAddresses.join(' ')); } async request(request) { const params = request.params || []; switch (request.method) { case 'eth_accounts': return [...this._addresses]; case 'eth_coinbase': return this.selectedAddress || null; case 'net_version': return this.getChainId().toString(10); case 'eth_chainId': return hexStringFromNumber(this.getChainId()); case 'eth_requestAccounts': return this._eth_requestAccounts(); case 'eth_ecRecover': case 'personal_ecRecover': return this.ecRecover(request); case 'personal_sign': return this.personalSign(request); case 'eth_signTransaction': return this._eth_signTransaction(params); case 'eth_sendRawTransaction': return this._eth_sendRawTransaction(params); case 'eth_sendTransaction': return this._eth_sendTransaction(params); case 'eth_signTypedData_v1': case 'eth_signTypedData_v3': case 'eth_signTypedData_v4': case 'eth_signTypedData': return this.signTypedData(request); case 'wallet_addEthereumChain': return this.addEthereumChain(params); case 'wallet_switchEthereumChain': return this.switchEthereumChain(params); case 'wallet_watchAsset': return this.watchAsset(params); default: if (!this.jsonRpcUrl) throw standardErrors.rpc.internal('No RPC URL set for chain'); return fetchRPCRequest(request, this.jsonRpcUrl); } } _ensureKnownAddress(addressString) { const addressStr = ensureAddressString(addressString); const lowercaseAddresses = this._addresses.map((address) => ensureAddressString(address)); if (!lowercaseAddresses.includes(addressStr)) { throw new Error('Unknown Ethereum address'); } } _prepareTransactionParams(tx) { const fromAddress = tx.from ? ensureAddressString(tx.from) : this.selectedAddress; if (!fromAddress) { throw new Error('Ethereum address is unavailable'); } this._ensureKnownAddress(fromAddress); const toAddress = tx.to ? ensureAddressString(tx.to) : null; const weiValue = tx.value != null ? ensureBigInt(tx.value) : BigInt(0); const data = tx.data ? ensureBuffer(tx.data) : Buffer.alloc(0); const nonce = tx.nonce != null ? ensureIntNumber(tx.nonce) : null; const gasPriceInWei = tx.gasPrice != null ? ensureBigInt(tx.gasPrice) : null; const maxFeePerGas = tx.maxFeePerGas != null ? ensureBigInt(tx.maxFeePerGas) : null; const maxPriorityFeePerGas = tx.maxPriorityFeePerGas != null ? ensureBigInt(tx.maxPriorityFeePerGas) : null; const gasLimit = tx.gas != null ? ensureBigInt(tx.gas) : null; const chainId = tx.chainId ? ensureIntNumber(tx.chainId) : this.getChainId(); return { fromAddress, toAddress, weiValue, data, nonce, gasPriceInWei, maxFeePerGas, maxPriorityFeePerGas, gasLimit, chainId, }; } async ecRecover(request) { const { method, params } = request; if (!Array.isArray(params)) throw standardErrors.rpc.invalidParams(); const relay = this.initializeRelay(); const res = await relay.sendRequest({ method: 'ethereumAddressFromSignedMessage', params: { message: encodeToHexString(params[0]), signature: encodeToHexString(params[1]), addPrefix: method === 'personal_ecRecover', }, }); if (isErrorResponse(res)) throw res; return res.result; } getChainId() { var _a; return Number.parseInt((_a = this._storage.getItem(DEFAULT_CHAIN_ID_KEY)) !== null && _a !== void 0 ? _a : '1', 10); } async _eth_requestAccounts() { var _a, _b; if (this._addresses.length > 0) { (_a = this.callback) === null || _a === void 0 ? void 0 : _a.call(this, 'connect', { chainId: hexStringFromNumber(this.getChainId()) }); return this._addresses; } const relay = this.initializeRelay(); const res = await relay.requestEthereumAccounts(); if (isErrorResponse(res)) throw res; if (!res.result) { throw new Error('accounts received is empty'); } this._setAddresses(res.result); (_b = this.callback) === null || _b === void 0 ? void 0 : _b.call(this, 'connect', { chainId: hexStringFromNumber(this.getChainId()) }); return this._addresses; } async personalSign({ params }) { if (!Array.isArray(params)) throw standardErrors.rpc.invalidParams(); const address = params[1]; const rawData = params[0]; this._ensureKnownAddress(address); const relay = this.initializeRelay(); const res = await relay.sendRequest({ method: 'signEthereumMessage', params: { address: ensureAddressString(address), message: encodeToHexString(rawData), addPrefix: true, typedDataJson: null, }, }); if (isErrorResponse(res)) throw res; return res.result; } async _eth_signTransaction(params) { const tx = this._prepareTransactionParams(params[0] || {}); const relay = this.initializeRelay(); const res = await relay.signEthereumTransaction(tx); if (isErrorResponse(res)) throw res; return res.result; } async _eth_sendRawTransaction(params) { const signedTransaction = ensureBuffer(params[0]); const relay = this.initializeRelay(); const res = await relay.submitEthereumTransaction(signedTransaction, this.getChainId()); if (isErrorResponse(res)) throw res; return res.result; } async _eth_sendTransaction(params) { const tx = this._prepareTransactionParams(params[0] || {}); const relay = this.initializeRelay(); const res = await relay.signAndSubmitEthereumTransaction(tx); if (isErrorResponse(res)) throw res; return res.result; } async signTypedData(request) { const { method, params } = request; if (!Array.isArray(params)) throw standardErrors.rpc.invalidParams(); const encode = (input) => { const hashFuncMap = { eth_signTypedData_v1: eip712.hashForSignTypedDataLegacy, eth_signTypedData_v3: eip712.hashForSignTypedData_v3, eth_signTypedData_v4: eip712.hashForSignTypedData_v4, eth_signTypedData: eip712.hashForSignTypedData_v4, }; return hexStringFromBuffer(hashFuncMap[method]({ data: ensureParsedJSONObject(input), }), true); }; const address = params[method === 'eth_signTypedData_v1' ? 1 : 0]; const rawData = params[method === 'eth_signTypedData_v1' ? 0 : 1]; this._ensureKnownAddress(address); const relay = this.initializeRelay(); const res = await relay.sendRequest({ method: 'signEthereumMessage', params: { address: ensureAddressString(address), message: encode(rawData), typedDataJson: JSON.stringify(rawData, null, 2), addPrefix: false, }, }); if (isErrorResponse(res)) throw res; return res.result; } initializeRelay() { if (!this._relay) { this._relay = new WalletLinkRelay({ linkAPIUrl: WALLETLINK_URL, storage: this._storage, metadata: this.metadata, accountsCallback: this._setAddresses.bind(this), chainCallback: this.updateProviderInfo.bind(this), }); } return this._relay; } } //# sourceMappingURL=WalletLinkSigner.js.map