@coinbase/wallet-sdk
Version:
Coinbase Wallet JavaScript SDK
210 lines • 9.6 kB
JavaScript
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