Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/wallet-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@coinbase/wallet-sdk",
"version": "4.3.2",
"version": "4.3.5",
"description": "Coinbase Wallet JavaScript SDK",
"keywords": [
"coinbase",
Expand All @@ -23,7 +23,7 @@
"test": "vitest",
"test:coverage": "yarn test:unit && open coverage/lcov-report/index.html",
"prebuild": "rm -rf ./dist && node -p \"'export const VERSION = \\'' + require('./package.json').version + '\\';\\nexport const NAME = \\'' + require('./package.json').name + '\\';'\" > src/sdk-info.ts",
"build": "node compile-assets.cjs && tsc -p ./tsconfig.build.json && tsc-alias && cp -a src/vendor-js dist",
"build": "node compile-assets.cjs && tsc -p ./tsconfig.build.json && tsc-alias && cp -a src/vendor-js dist && cp src/sign/walletlink/relay/connection/HeartbeatWorker.js dist/sign/walletlink/relay/connection/",
"dev": "yarn build && tsc --watch & nodemon --watch dist --delay 1 --exec tsc-alias",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx --fix",
Expand All @@ -33,7 +33,8 @@
"@noble/hashes": "^1.4.0",
"clsx": "^1.2.1",
"eventemitter3": "^5.0.1",
"preact": "^10.24.2"
"preact": "^10.24.2",
"viem": "^2.27.2"
},
"devDependencies": {
"@size-limit/preset-big-lib": "^11.1.6",
Expand All @@ -42,12 +43,15 @@
"@types/node": "^14.18.54",
"@typescript-eslint/eslint-plugin": "^6.2.0",
"@typescript-eslint/parser": "^6.2.0",
"@vitest/coverage-v8": "2.1.2",
"@vitest/web-worker": "3.2.1",
"eslint": "^8.45.0",
"eslint-config-preact": "^1.3.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-simple-import-sort": "^10.0.0",
"eslint-plugin-unused-imports": "^3.0.0",
"fake-indexeddb": "^6.0.0",
"glob": "^11.0.0",
"jest-websocket-mock": "^2.4.0",
"jsdom": "^25.0.1",
Expand Down
2 changes: 1 addition & 1 deletion packages/wallet-sdk/src/sdk-info.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export const VERSION = '4.3.0';
export const VERSION = '4.3.5';
export const NAME = '@coinbase/wallet-sdk';
50 changes: 49 additions & 1 deletion packages/wallet-sdk/src/sign/scw/SCWSigner.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { hexToNumber, isAddressEqual } from 'viem';

import { Signer } from '../interface.js';
import { SCWKeyManager } from './SCWKeyManager.js';
import { assertGetCapabilitiesParams } from './utils.js';
import { Communicator } from ':core/communicator/Communicator.js';
import { standardErrors } from ':core/error/errors.js';
import { RPCRequestMessage, RPCResponseMessage } from ':core/message/RPCMessage.js';
Expand Down Expand Up @@ -118,7 +121,7 @@ export class SCWSigner implements Signer {
case 'eth_chainId':
return hexStringFromNumber(this.chain.id);
case 'wallet_getCapabilities':
return this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY);
return this.handleGetCapabilitiesRequest(request);
case 'wallet_switchEthereumChain':
return this.handleSwitchChainRequest(request);
case 'eth_ecRecover':
Expand Down Expand Up @@ -191,6 +194,51 @@ export class SCWSigner implements Signer {
return popupResult;
}

private async handleGetCapabilitiesRequest(request: RequestArguments) {
assertGetCapabilitiesParams(request.params);

const requestedAccount = request.params[0];
const filterChainIds = request.params[1]; // Optional second parameter

if (
!this.accounts.some((account: AddressString) =>
isAddressEqual(account as `0x${string}`, requestedAccount)
)
) {
throw standardErrors.provider.unauthorized('no active account found');
}

const capabilities = this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY);

// 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 as `0x${string}`);
return filterChainNumbers.has(capabilityChainNumber);
} catch {
// If capabilityKey is not a valid hex string, exclude it
return false;
}
})
);

return filteredCapabilities;
}

private async sendEncryptedRequest(request: RequestArguments): Promise<RPCResponseMessage> {
const sharedSecret = await this.keyManager.getSharedSecret();
if (!sharedSecret) {
Expand Down
27 changes: 27 additions & 0 deletions packages/wallet-sdk/src/sign/scw/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { isAddress } from 'viem';

import { standardErrors } from ':core/error/errors.js';

export function assertGetCapabilitiesParams(
params: unknown
): asserts params is [`0x${string}`, `0x${string}`[]?] {
if (!params || !Array.isArray(params) || (params.length !== 1 && params.length !== 2)) {
throw standardErrors.rpc.invalidParams();
}

if (typeof params[0] !== 'string' || !isAddress(params[0])) {
throw standardErrors.rpc.invalidParams();
}

if (params.length === 2) {
if (!Array.isArray(params[1])) {
throw standardErrors.rpc.invalidParams();
}

for (const param of params[1]) {
if (typeof param !== 'string' || !param.startsWith('0x')) {
throw standardErrors.rpc.invalidParams();
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2018-2025 Coinbase, Inc. <https://www.coinbase.com/>

/**
* This worker is used to send heartbeat messages to the main thread.
* It is used to keep the websocket connection alive when the webpage is backgrounded.
*
*/

// Define the heartbeat interval constant directly in the worker to avoid import issues
const HEARTBEAT_INTERVAL = 10000;

let heartbeatInterval;

// Listen for messages from the main thread
self.addEventListener('message', (event) => {
const { type } = event.data;

switch (type) {
case 'start':
startHeartbeat();
break;
case 'stop':
stopHeartbeat();
break;
default:
console.warn('Unknown message type received by HeartbeatWorker:', type);
}
});

function startHeartbeat() {
// Clear any existing interval
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
}

// Start the heartbeat interval
heartbeatInterval = setInterval(() => {
// Send heartbeat message to main thread
const response = { type: 'heartbeat' };
self.postMessage(response);
}, HEARTBEAT_INTERVAL);

// Send confirmation that heartbeat started
const response = { type: 'started' };
self.postMessage(response);
}

function stopHeartbeat() {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = undefined;
}

// Send confirmation that heartbeat stopped
const response = { type: 'stopped' };
self.postMessage(response);
}

// Handle worker termination
self.addEventListener('beforeunload', () => {
stopHeartbeat();
});
Loading
Loading