From b1cdfe01f1fe9f1e95ad5763e27978083014b544 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Thu, 26 Jun 2025 11:25:00 -0700 Subject: [PATCH 1/8] key generation script for well known file - domain verification key generation script for well known file - yarn generate-key-script - readme doc on how to use it --- docs/domain-verification-key-generation.md | 101 ++++++++++++++++++ package.json | 5 +- packages/client/package.json | 3 +- packages/client/scripts/README.md | 25 +++++ .../client/scripts/generate-domain-keys.ts | 96 +++++++++++++++++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 docs/domain-verification-key-generation.md create mode 100644 packages/client/scripts/README.md create mode 100644 packages/client/scripts/generate-domain-keys.ts diff --git a/docs/domain-verification-key-generation.md b/docs/domain-verification-key-generation.md new file mode 100644 index 0000000..55aa9d2 --- /dev/null +++ b/docs/domain-verification-key-generation.md @@ -0,0 +1,101 @@ +# Domain Verification Key Generation + +This script generates a secp256k1 key pair and outputs a JWKS (JSON Web Key Set) file for Coinbase domain verification, as well as a private key for signing. + +## What is JWKS? + +JWKS (JSON Web Key Set) is an industry standard format (RFC 7517) for representing cryptographic keys in JSON. It's widely used in OAuth 2.0, OpenID Connect, and other security protocols. + +## Where is the Public Key? + +Your public key is stored as **x and y coordinates** in the JWKS: + +```json +{ + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate" +} +``` + +### How to Reconstruct the Full Public Key: + +The full public key is: `04 + [x-coordinate] + [y-coordinate]` + +- `04` = uncompressed point prefix +- `x` = 32-byte x-coordinate (base64url decoded) +- `y` = 32-byte y-coordinate (base64url decoded) + +**Total: 65 bytes** (1 + 32 + 32) + +## What does it do? +- Generates a secp256k1 key pair +- Outputs a `.well-known/jwks.json` file in the current directory (for hosting at `https://yourdomain.com/.well-known/jwks.json`) +- Outputs a `domain-verification-private-key.txt` file (for secure storage and use with the SDK) + +## Usage + +From the root directory of the repo, run: + +```sh +yarn generate-key-script +``` + +## Output +- `.well-known/jwks.json`: Public key in JWKS format for domain verification +- `domain-verification-private-key.txt`: Private key (keep this secure!) + +## Getting Started with Origin Verification + +### Step 1: Generate Your Key Pair + +First, generate your domain verification keys: + +```sh +yarn generate-key-script +``` + +This will create: +- `.well-known/jwks.json` - Your public key (host this on your domain) +- `domain-verification-private-key.txt` - Your private key (keep secure!) + +### Step 2: Host Your Public Key + +Host the `jwks.json` file at: +``` +https://yourdomain.com/.well-known/jwks.json +``` + +This allows Coinbase to verify signatures from your domain. + +## Example JWKS Output +```json +{ + "version": "1.0", + "keys": [ + { + "kty": "EC", + "crv": "secp256k1", + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate", + "use": "sig", + "kid": "coinbase-domain-verification", + "alg": "ES256K" + } + ] +} +``` + +### JWKS Field Descriptions: +- `version`: Format version for tracking changes +- `kty`: Key type ("EC" for Elliptic Curve) +- `crv`: Curve name ("secp256k1" for Bitcoin/Ethereum curve) +- `x`, `y`: Base64URL-encoded x and y coordinates of the public key +- `use`: Key usage ("sig" for signing) +- `kid`: Key ID for identification ("coinbase-domain-verification") +- `alg`: Algorithm ("ES256K" for ECDSA with secp256k1 and SHA-256) + +## Why? +This enables secure domain verification for Coinbase Wallet SDK integrations. The SDK will use the private key to sign requests, and Coinbase will verify signatures using the public key hosted at your domain. + +--- +**Keep your private key safe!** Never share it or commit it to source control. \ No newline at end of file diff --git a/package.json b/package.json index 911781e..eadd431 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,8 @@ "workspaces": [ "packages/*" ], - "packageManager": "yarn@4.4.0" + "packageManager": "yarn@4.4.0", + "scripts": { + "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"" + } } diff --git a/packages/client/package.json b/packages/client/package.json index 3963a5c..0dfd05d 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -29,7 +29,8 @@ "build": "tsc -p ./tsconfig.build.json && tsc-alias", "dev": "tsc --watch & nodemon --watch dist --delay 1 --exec tsc-alias", "typecheck": "tsc --noEmit", - "lint": "eslint . --ext .ts,.tsx --fix" + "lint": "eslint . --ext .ts,.tsx --fix", + "generate-key-script": "ts-node scripts/generate-domain-keys.ts" }, "dependencies": { "@noble/ciphers": "^0.5.3", diff --git a/packages/client/scripts/README.md b/packages/client/scripts/README.md new file mode 100644 index 0000000..d388485 --- /dev/null +++ b/packages/client/scripts/README.md @@ -0,0 +1,25 @@ +# Scripts + +This directory contains utility scripts for the Mobile Wallet Protocol client. + +## Available Scripts + +### `generate-domain-keys.ts` + +Generates secp256k1 key pairs for Coinbase domain verification. + +**Usage:** +```bash +yarn generate-key-script +``` + +**What it does:** +- Generates a secp256k1 key pair +- Creates a `.well-known/jwks.json` file in JWKS format +- Creates a `domain-verification-private-key.txt` file with the private key + +**Output:** +- `.well-known/jwks.json` - Public key in JWKS format for domain verification +- `domain-verification-private-key.txt` - Private key (keep secure!) + +For detailed documentation, see [Domain Verification Key Generation](../../docs/domain-verification-key-generation.md). \ No newline at end of file diff --git a/packages/client/scripts/generate-domain-keys.ts b/packages/client/scripts/generate-domain-keys.ts new file mode 100644 index 0000000..b97a7ee --- /dev/null +++ b/packages/client/scripts/generate-domain-keys.ts @@ -0,0 +1,96 @@ +#!/usr/bin/env node + +import { secp256k1 } from '@noble/curves/secp256k1'; +import { randomBytes } from '@noble/hashes/utils'; +import { mkdirSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +// Base64URL encoding function +function base64url(buffer: Uint8Array): string { + return Buffer.from(buffer) + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); +} + +function getOutputDir(): string { + // Check for --cwd argument + const cwdArgIndex = process.argv.indexOf('--cwd'); + if (cwdArgIndex !== -1 && process.argv[cwdArgIndex + 1]) { + return process.argv[cwdArgIndex + 1]; + } + // Fallbacks + return process.env.INIT_CWD || process.cwd(); +} + +function generateDomainVerificationKeys() { + // Generate a random private key + const privateKeyBytes = randomBytes(32); + const privateKey = base64url(privateKeyBytes); + + // Get the uncompressed public key from the private key + const publicKey = secp256k1.getPublicKey(privateKeyBytes, false); // false = uncompressed + + // Extract x and y coordinates from the public key + // secp256k1 uncompressed public key is 65 bytes: 1 byte prefix + 32 bytes x + 32 bytes y + const x = base64url(publicKey.slice(1, 33)); + const y = base64url(publicKey.slice(33)); + + // Create the JWK + const jwk = { + kty: 'EC', + crv: 'secp256k1', + x, + y, + use: 'sig', + kid: 'coinbase-domain-verification', + alg: 'ES256K' + }; + + // Create the JWKS + const jwks = { + version: "1.0", + keys: [jwk] + }; + + return { jwks, privateKey }; +} + +function main() { + try { + const outputDir = getOutputDir(); + + console.log('🔑 Generating Coinbase domain verification keys...\n'); + + const { jwks, privateKey } = generateDomainVerificationKeys(); + + // Create .well-known directory if it doesn't exist + const wellKnownDir = join(outputDir, '.well-known'); + mkdirSync(wellKnownDir, { recursive: true }); + + // Write the JWKS file + const jwksPath = join(wellKnownDir, 'jwks.json'); + writeFileSync(jwksPath, JSON.stringify(jwks, null, 2)); + + // Write the private key to a separate file + const privateKeyPath = join(outputDir, 'domain-verification-private-key.txt'); + writeFileSync(privateKeyPath, privateKey); + + console.log('✅ Successfully generated domain verification keys!\n'); + console.log('📁 Files created:'); + console.log(` • ${jwksPath} - JWKS file for your domain`); + console.log(` • ${privateKeyPath} - Private key (keep this secure!)\n`); + + console.log('🌐 Next steps:'); + console.log(' 1. Host the jwks.json file at: https://yourdomain.com/.well-known/jwks.json'); + console.log(' 2. Store the private key securely for use with the SDK'); + console.log(' 3. Use the private key with the SDK\'s generateSignature function\n'); + + } catch (error) { + console.error('❌ Error generating domain verification keys:', error); + process.exit(1); + } +} + +main(); \ No newline at end of file From d733c46a997bd3af05557503406c4fa0b4f2fcb9 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Fri, 27 Jun 2025 00:08:05 -0700 Subject: [PATCH 2/8] init commit for domain verification in sdk constructor --- docs/domain-verification-key-generation.md | 173 ++++++++++++++++++ packages/client/src/MWPClient.test.ts | 35 ++++ packages/client/src/MWPClient.ts | 60 +++++- .../client/src/core/provider/interface.ts | 25 +++ .../src/interfaces/eip1193/EIP1193Provider.ts | 2 + 5 files changed, 294 insertions(+), 1 deletion(-) diff --git a/docs/domain-verification-key-generation.md b/docs/domain-verification-key-generation.md index 55aa9d2..ebf574f 100644 --- a/docs/domain-verification-key-generation.md +++ b/docs/domain-verification-key-generation.md @@ -6,6 +6,14 @@ This script generates a secp256k1 key pair and outputs a JWKS (JSON Web Key Set) JWKS (JSON Web Key Set) is an industry standard format (RFC 7517) for representing cryptographic keys in JSON. It's widely used in OAuth 2.0, OpenID Connect, and other security protocols. +### Why JWKS instead of simple JSON? + +- **Industry Standard**: IETF standard format used by major platforms +- **Rich Metadata**: Includes algorithm, usage, and key identification +- **Key Rotation**: Can contain multiple keys for seamless rotation +- **Interoperability**: Works with standard JWT libraries and tools +- **Base64URL Encoding**: URL-safe encoding for key components + ## Where is the Public Key? Your public key is stored as **x and y coordinates** in the JWKS: @@ -67,6 +75,171 @@ https://yourdomain.com/.well-known/jwks.json This allows Coinbase to verify signatures from your domain. +### Step 3: Initialize the SDK with Origin Verification + +```typescript +import { EIP1193Provider } from '@mobile-wallet-protocol/client'; + +// Read your private key (store this securely!) +const privateKeyHex = 'your-private-key-from-domain-verification-private-key.txt'; + +// Create a signing function using your private key +const generateSignature = async (nonce: string, payload: unknown): Promise => { + // Import your private key (you'll need a crypto library like ethers.js or viem) + const privateKey = new ethers.SigningKey(privateKeyHex); + + // Create the data to sign + const dataToSign = JSON.stringify({ + domain: 'www.example.com', // Your domain + nonce, // The SDK will provide a nonce for you to use + requestData: payload + }); + + // Sign the data + const signature = await privateKey.sign(dataToSign); + return signature; +}; + +// Initialize the provider with origin verification +const provider = new EIP1193Provider({ + metadata: { + name: 'My Dapp', + logoUrl: 'https://example.com/logo.png', + chainIds: [1, 137], // Ethereum mainnet and Polygon + customScheme: 'myapp://', + originVerification: { + domain: 'www.example.com', // Your domain + generateSignature + } + }, + wallet: { + type: 'web', + scheme: 'https://wallet.coinbase.com' + } +}); + +// Get a provider +const provider = sdk.getProvider(); +``` + +### Step 4: Use the SDK with Origin Verification + +The SDK automatically handles nonce requests and signature generation behind the scenes. Simply make your RPC requests as usual: + +```typescript +// The SDK automatically requests a nonce and generates a signature for you +const accounts = await provider.request({ + method: 'eth_requestAccounts' +}); + +// All subsequent RPC requests will also include origin verification +const balance = await provider.request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'] +}); +``` + +### Complete Example + +Here's a complete example showing how to set up origin verification: + +```typescript +import { EIP1193Provider } from '@mobile-wallet-protocol/client'; +import { ethers } from 'ethers'; + +class DomainVerificationManager { + private privateKey: ethers.SigningKey; + private domain: string; + + constructor(privateKeyHex: string, domain: string) { + this.privateKey = new ethers.SigningKey(privateKeyHex); + this.domain = domain; + } + + async generateSignature(nonce: string, payload: unknown): Promise { + const dataToSign = JSON.stringify({ + domain: this.domain, + nonce, // The SDK will provide a nonce for you to use + requestData: payload + }); + + const signature = await this.privateKey.sign(dataToSign); + return signature; + } +} + +// Initialize with your private key and domain +const privateKeyHex = 'your-private-key-here'; +const domain = 'www.example.com'; + +const verificationManager = new DomainVerificationManager(privateKeyHex, domain); + +const provider = new EIP1193Provider({ + metadata: { + name: 'My Secure Dapp', + logoUrl: 'https://example.com/logo.png', + chainIds: [1, 137], + customScheme: 'myapp://', + originVerification: { + domain, + generateSignature: verificationManager.generateSignature.bind(verificationManager) + } + }, + wallet: { + type: 'web', + scheme: 'https://wallet.coinbase.com' + } +}); + +// Usage example - no manual nonce requests needed! +async function connectWallet() { + try { + // The SDK automatically handles nonce requests and signature generation + const accounts = await provider.request({ + method: 'eth_requestAccounts' + }); + + console.log('Connected accounts:', accounts); + + // All subsequent requests will also include origin verification automatically + const balance = await provider.request({ + method: 'eth_getBalance', + params: [accounts[0], 'latest'] + }); + + console.log('Account balance:', balance); + } catch (error) { + console.error('Connection failed:', error); + } +} +``` + +## Security Best Practices + +1. **Store Private Keys Securely**: Never commit private keys to source control +2. **Use Environment Variables**: Store private keys in environment variables +3. **Rotate Keys Regularly**: Generate new keys periodically +4. **Validate Domain**: Ensure your domain matches exactly +5. **Handle Errors Gracefully**: Implement proper error handling for signature failures + +### Environment Setup Example + +```bash +# .env file +DOMAIN_VERIFICATION_PRIVATE_KEY=your-private-key-here +DOMAIN=www.example.com +``` + +```typescript +// Load from environment +const privateKey = process.env.DOMAIN_VERIFICATION_PRIVATE_KEY; +const domain = process.env.DOMAIN; + +if (!privateKey || !domain) { + throw new Error('Missing domain verification configuration'); +} +``` + ## Example JWKS Output ```json { diff --git a/packages/client/src/MWPClient.test.ts b/packages/client/src/MWPClient.test.ts index 036aa75..95c9102 100644 --- a/packages/client/src/MWPClient.test.ts +++ b/packages/client/src/MWPClient.test.ts @@ -285,6 +285,18 @@ describe('MWPClient', () => { ]); expect(storageStoreSpy).toHaveBeenCalledWith('walletCapabilities', mockCapabilities); }); + + it('should generate UUID for wallet_requestNonce', async () => { + const mockRequest: RequestArguments = { + method: 'wallet_requestNonce', + params: { domain: 'example.com' }, + }; + + const result = await client.request(mockRequest); + + // Verify it returns a UUID (36 characters with hyphens) + expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + }); }); describe('reset', () => { @@ -297,4 +309,27 @@ describe('MWPClient', () => { expect(client['chain']).toEqual({ id: 1 }); }); }); + + describe('requestNonce', () => { + it('should generate UUID when origin verification is configured', async () => { + // Create a client with origin verification + const clientWithVerification = await MWPClient.createInstance({ + metadata: mockMetadata, + wallet: mockWallet, + originVerification: { + domain: 'example.com', + generateSignature: jest.fn().mockResolvedValue('signature'), + }, + }); + + const nonce = await clientWithVerification.requestNonce(); + + // Verify it returns a UUID (36 characters with hyphens) + expect(nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); + }); + + it('should throw error when origin verification is not configured', async () => { + await expect(client.requestNonce()).rejects.toThrow('Origin verification not configured'); + }); + }); }); diff --git a/packages/client/src/MWPClient.ts b/packages/client/src/MWPClient.ts index 3d8d980..e7bcbf0 100644 --- a/packages/client/src/MWPClient.ts +++ b/packages/client/src/MWPClient.ts @@ -24,6 +24,7 @@ import { fetchRPCRequest, } from ':core/util/utils'; import { Wallet } from ':core/wallet'; +import { OriginVerification } from ':core/provider/interface'; type Chain = { id: number; @@ -33,6 +34,7 @@ type Chain = { type MWPClientOptions = { metadata: AppMetadata; wallet: Wallet; + originVerification?: OriginVerification; }; export class MWPClient { @@ -40,11 +42,12 @@ export class MWPClient { private readonly wallet: Wallet; private readonly keyManager: KeyManager; private readonly storage: ScopedAsyncStorage; + private readonly originVerification?: OriginVerification; private accounts: AddressString[]; private chain: Chain; - private constructor({ metadata, wallet }: MWPClientOptions) { + private constructor({ metadata, wallet, originVerification }: MWPClientOptions) { this.metadata = { ...metadata, name: metadata.name || 'Dapp', @@ -52,6 +55,7 @@ export class MWPClient { }; this.wallet = wallet; + this.originVerification = originVerification; this.keyManager = new KeyManager({ wallet: this.wallet }); this.storage = new ScopedAsyncStorage(this.wallet.name, 'MWPClient'); @@ -119,6 +123,20 @@ export class MWPClient { return accounts; } + /** + * Request a nonce for origin verification + * @returns Promise - The nonce provided by the wallet + */ + async requestNonce(): Promise { + if (!this.originVerification) { + throw standardErrors.rpc.internal('Origin verification not configured'); + } + + // For now, generate a UUID locally instead of calling the wallet API + // In production, this would call: this.request({ method: 'wallet_requestNonce', params: { domain: this.originVerification.domain } }) + return crypto.randomUUID(); + } + async request(request: RequestArguments) { if (this.accounts.length === 0) { throw standardErrors.provider.unauthorized(); @@ -139,6 +157,10 @@ export class MWPClient { return hexStringFromNumber(this.chain.id); case 'wallet_getCapabilities': return this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY); + case 'wallet_requestNonce': + // For now, generate a UUID locally instead of calling the wallet API + // In production, this would return this.sendRequestToPopup(request); + return crypto.randomUUID(); case 'wallet_switchEthereumChain': return this.handleSwitchChainRequest(request); case 'eth_ecRecover': @@ -163,6 +185,42 @@ export class MWPClient { } private async sendRequestToPopup(request: RequestArguments) { + // If origin verification is configured and accounts are available, automatically handle nonce and signature + if (this.originVerification && this.accounts.length > 0) { + try { + // Request nonce from wallet + const nonce = await this.requestNonce(); + + // Generate signature using the developer's function + const signature = await this.originVerification.generateSignature(nonce, request); + + // Add signature to the request + const requestWithSignature = { + ...request, + params: { + ...request.params, + originVerification: { + domain: this.originVerification.domain, + nonce, + signature + } + } + }; + + const response = await this.sendEncryptedRequest(requestWithSignature); + const decrypted = await this.decryptResponseMessage(response); + + const result = decrypted.result; + if ('error' in result) throw result.error; + + return result.value; + } catch (error) { + // If nonce request fails, fall back to regular request + console.warn('Origin verification failed, falling back to regular request:', error); + } + } + + // Regular request flow (no origin verification or fallback) const response = await this.sendEncryptedRequest(request); const decrypted = await this.decryptResponseMessage(response); diff --git a/packages/client/src/core/provider/interface.ts b/packages/client/src/core/provider/interface.ts index 7d528da..a1165a0 100644 --- a/packages/client/src/core/provider/interface.ts +++ b/packages/client/src/core/provider/interface.ts @@ -33,6 +33,25 @@ export interface ProviderInterface extends ProviderEventEmitter { export type ProviderEventCallback = ProviderInterface['emit']; +export interface OriginVerification { + /** + * @param domain + * @type string + * @description The domain for which origin verification is enabled + * @example "www.example.com" + */ + domain: string; + /** + * @param generateSignature + * @type function + * @description Async function that generates a signature for domain verification + * @param nonce - The nonce provided by the wallet for this request + * @param payload - The request payload to be signed + * @returns Promise - The signature as a string + */ + generateSignature: (nonce: string, payload: unknown) => Promise; +} + export interface AppMetadata { /** * @param name @@ -59,4 +78,10 @@ export interface AppMetadata { * @example 'myapp://' */ customScheme: string; + /** + * @param originVerification + * @type {OriginVerification} + * @description Optional origin verification configuration for domain verification + */ + originVerification?: OriginVerification; } diff --git a/packages/client/src/interfaces/eip1193/EIP1193Provider.ts b/packages/client/src/interfaces/eip1193/EIP1193Provider.ts index 01c95f9..0d2e9d0 100644 --- a/packages/client/src/interfaces/eip1193/EIP1193Provider.ts +++ b/packages/client/src/interfaces/eip1193/EIP1193Provider.ts @@ -3,6 +3,7 @@ import { standardErrorCodes, standardErrors } from ':core/error'; import { serializeError } from ':core/error/serialize'; import { AppMetadata, + OriginVerification, ProviderEventEmitter, ProviderInterface, RequestArguments, @@ -12,6 +13,7 @@ import { Wallet } from ':core/wallet'; type EIP1193ProviderOptions = { metadata: AppMetadata; wallet: Wallet; + originVerification?: OriginVerification; }; export class EIP1193Provider extends ProviderEventEmitter implements ProviderInterface { From 96dabf4045b202c50d27323e6fcfd1c363299686 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Tue, 1 Jul 2025 00:42:44 -0700 Subject: [PATCH 3/8] added originVerification to constructor --- docs/domain-verification-key-generation.md | 274 ------------------ docs/domain-verification.md | 174 +++++++++++ package.json | 3 +- packages/client/scripts/README.md | 10 +- .../client/scripts/generate-domain-keys.ts | 35 +-- packages/client/src/MWPClient.ts | 6 +- .../client/src/core/provider/interface.ts | 10 +- .../src/createConnectorFromWallet.ts | 4 +- 8 files changed, 201 insertions(+), 315 deletions(-) delete mode 100644 docs/domain-verification-key-generation.md create mode 100644 docs/domain-verification.md diff --git a/docs/domain-verification-key-generation.md b/docs/domain-verification-key-generation.md deleted file mode 100644 index ebf574f..0000000 --- a/docs/domain-verification-key-generation.md +++ /dev/null @@ -1,274 +0,0 @@ -# Domain Verification Key Generation - -This script generates a secp256k1 key pair and outputs a JWKS (JSON Web Key Set) file for Coinbase domain verification, as well as a private key for signing. - -## What is JWKS? - -JWKS (JSON Web Key Set) is an industry standard format (RFC 7517) for representing cryptographic keys in JSON. It's widely used in OAuth 2.0, OpenID Connect, and other security protocols. - -### Why JWKS instead of simple JSON? - -- **Industry Standard**: IETF standard format used by major platforms -- **Rich Metadata**: Includes algorithm, usage, and key identification -- **Key Rotation**: Can contain multiple keys for seamless rotation -- **Interoperability**: Works with standard JWT libraries and tools -- **Base64URL Encoding**: URL-safe encoding for key components - -## Where is the Public Key? - -Your public key is stored as **x and y coordinates** in the JWKS: - -```json -{ - "x": "base64url-encoded-x-coordinate", - "y": "base64url-encoded-y-coordinate" -} -``` - -### How to Reconstruct the Full Public Key: - -The full public key is: `04 + [x-coordinate] + [y-coordinate]` - -- `04` = uncompressed point prefix -- `x` = 32-byte x-coordinate (base64url decoded) -- `y` = 32-byte y-coordinate (base64url decoded) - -**Total: 65 bytes** (1 + 32 + 32) - -## What does it do? -- Generates a secp256k1 key pair -- Outputs a `.well-known/jwks.json` file in the current directory (for hosting at `https://yourdomain.com/.well-known/jwks.json`) -- Outputs a `domain-verification-private-key.txt` file (for secure storage and use with the SDK) - -## Usage - -From the root directory of the repo, run: - -```sh -yarn generate-key-script -``` - -## Output -- `.well-known/jwks.json`: Public key in JWKS format for domain verification -- `domain-verification-private-key.txt`: Private key (keep this secure!) - -## Getting Started with Origin Verification - -### Step 1: Generate Your Key Pair - -First, generate your domain verification keys: - -```sh -yarn generate-key-script -``` - -This will create: -- `.well-known/jwks.json` - Your public key (host this on your domain) -- `domain-verification-private-key.txt` - Your private key (keep secure!) - -### Step 2: Host Your Public Key - -Host the `jwks.json` file at: -``` -https://yourdomain.com/.well-known/jwks.json -``` - -This allows Coinbase to verify signatures from your domain. - -### Step 3: Initialize the SDK with Origin Verification - -```typescript -import { EIP1193Provider } from '@mobile-wallet-protocol/client'; - -// Read your private key (store this securely!) -const privateKeyHex = 'your-private-key-from-domain-verification-private-key.txt'; - -// Create a signing function using your private key -const generateSignature = async (nonce: string, payload: unknown): Promise => { - // Import your private key (you'll need a crypto library like ethers.js or viem) - const privateKey = new ethers.SigningKey(privateKeyHex); - - // Create the data to sign - const dataToSign = JSON.stringify({ - domain: 'www.example.com', // Your domain - nonce, // The SDK will provide a nonce for you to use - requestData: payload - }); - - // Sign the data - const signature = await privateKey.sign(dataToSign); - return signature; -}; - -// Initialize the provider with origin verification -const provider = new EIP1193Provider({ - metadata: { - name: 'My Dapp', - logoUrl: 'https://example.com/logo.png', - chainIds: [1, 137], // Ethereum mainnet and Polygon - customScheme: 'myapp://', - originVerification: { - domain: 'www.example.com', // Your domain - generateSignature - } - }, - wallet: { - type: 'web', - scheme: 'https://wallet.coinbase.com' - } -}); - -// Get a provider -const provider = sdk.getProvider(); -``` - -### Step 4: Use the SDK with Origin Verification - -The SDK automatically handles nonce requests and signature generation behind the scenes. Simply make your RPC requests as usual: - -```typescript -// The SDK automatically requests a nonce and generates a signature for you -const accounts = await provider.request({ - method: 'eth_requestAccounts' -}); - -// All subsequent RPC requests will also include origin verification -const balance = await provider.request({ - method: 'eth_getBalance', - params: [accounts[0], 'latest'] -}); -``` - -### Complete Example - -Here's a complete example showing how to set up origin verification: - -```typescript -import { EIP1193Provider } from '@mobile-wallet-protocol/client'; -import { ethers } from 'ethers'; - -class DomainVerificationManager { - private privateKey: ethers.SigningKey; - private domain: string; - - constructor(privateKeyHex: string, domain: string) { - this.privateKey = new ethers.SigningKey(privateKeyHex); - this.domain = domain; - } - - async generateSignature(nonce: string, payload: unknown): Promise { - const dataToSign = JSON.stringify({ - domain: this.domain, - nonce, // The SDK will provide a nonce for you to use - requestData: payload - }); - - const signature = await this.privateKey.sign(dataToSign); - return signature; - } -} - -// Initialize with your private key and domain -const privateKeyHex = 'your-private-key-here'; -const domain = 'www.example.com'; - -const verificationManager = new DomainVerificationManager(privateKeyHex, domain); - -const provider = new EIP1193Provider({ - metadata: { - name: 'My Secure Dapp', - logoUrl: 'https://example.com/logo.png', - chainIds: [1, 137], - customScheme: 'myapp://', - originVerification: { - domain, - generateSignature: verificationManager.generateSignature.bind(verificationManager) - } - }, - wallet: { - type: 'web', - scheme: 'https://wallet.coinbase.com' - } -}); - -// Usage example - no manual nonce requests needed! -async function connectWallet() { - try { - // The SDK automatically handles nonce requests and signature generation - const accounts = await provider.request({ - method: 'eth_requestAccounts' - }); - - console.log('Connected accounts:', accounts); - - // All subsequent requests will also include origin verification automatically - const balance = await provider.request({ - method: 'eth_getBalance', - params: [accounts[0], 'latest'] - }); - - console.log('Account balance:', balance); - } catch (error) { - console.error('Connection failed:', error); - } -} -``` - -## Security Best Practices - -1. **Store Private Keys Securely**: Never commit private keys to source control -2. **Use Environment Variables**: Store private keys in environment variables -3. **Rotate Keys Regularly**: Generate new keys periodically -4. **Validate Domain**: Ensure your domain matches exactly -5. **Handle Errors Gracefully**: Implement proper error handling for signature failures - -### Environment Setup Example - -```bash -# .env file -DOMAIN_VERIFICATION_PRIVATE_KEY=your-private-key-here -DOMAIN=www.example.com -``` - -```typescript -// Load from environment -const privateKey = process.env.DOMAIN_VERIFICATION_PRIVATE_KEY; -const domain = process.env.DOMAIN; - -if (!privateKey || !domain) { - throw new Error('Missing domain verification configuration'); -} -``` - -## Example JWKS Output -```json -{ - "version": "1.0", - "keys": [ - { - "kty": "EC", - "crv": "secp256k1", - "x": "base64url-encoded-x-coordinate", - "y": "base64url-encoded-y-coordinate", - "use": "sig", - "kid": "coinbase-domain-verification", - "alg": "ES256K" - } - ] -} -``` - -### JWKS Field Descriptions: -- `version`: Format version for tracking changes -- `kty`: Key type ("EC" for Elliptic Curve) -- `crv`: Curve name ("secp256k1" for Bitcoin/Ethereum curve) -- `x`, `y`: Base64URL-encoded x and y coordinates of the public key -- `use`: Key usage ("sig" for signing) -- `kid`: Key ID for identification ("coinbase-domain-verification") -- `alg`: Algorithm ("ES256K" for ECDSA with secp256k1 and SHA-256) - -## Why? -This enables secure domain verification for Coinbase Wallet SDK integrations. The SDK will use the private key to sign requests, and Coinbase will verify signatures using the public key hosted at your domain. - ---- -**Keep your private key safe!** Never share it or commit it to source control. \ No newline at end of file diff --git a/docs/domain-verification.md b/docs/domain-verification.md new file mode 100644 index 0000000..60da8a6 --- /dev/null +++ b/docs/domain-verification.md @@ -0,0 +1,174 @@ +# Domain Verification + +Domain verification enables secure authentication for Coinbase Smart Wallet RPC requests across all platforms (web, mobile, server-side). This system uses cryptographic signatures to verify domain ownership, allowing applications to authenticate with CBSW without relying on browser APIs. + +## Key Generation + +### What is JWKS? + +JWKS (JSON Web Key Set) is an industry standard format (RFC 7517) for representing cryptographic keys in JSON. It's widely used in OAuth 2.0, OpenID Connect, and other security protocols. + +### Generate Your Key Pair + +From the root directory of the repo, run: + +```sh +yarn generate-key-script +``` + +This will create: +- `.well-known/base-jwks.json` - Your public key (host this on your domain) +- `domain-verification-private-key.txt` - Your private key (keep secure!) + +### Host Your Public Key + +Host the `base-jwks.json` file at: +``` +https://yourdomain.com/.well-known/base-jwks.json +``` + +This allows Base to verify signatures from your domain. + +### Example JWKS Output +```json +{ + "version": "1.0", + "keys": [ + { + "kty": "EC", + "crv": "secp256k1", + "x": "base64url-encoded-x-coordinate", + "y": "base64url-encoded-y-coordinate", + "use": "sig", + "kid": "base-domain-verification", + "alg": "ES256K" + } + ] +} +``` + +### JWKS Field Descriptions: +- `version`: Format version for tracking changes +- `kty`: Key type ("EC" for Elliptic Curve) +- `crv`: Curve name ("secp256k1" for Bitcoin/Ethereum curve) +- `x`, `y`: Base64URL-encoded x and y coordinates of the public key +- `use`: Key usage ("sig" for signing) +- `kid`: Key ID for identification ("base-domain-verification") +- `alg`: Algorithm ("ES256K" for ECDSA with secp256k1 and SHA-256) + +## SDK Integration + +### Basic Setup + +Add the `originVerification` parameter to your provider configuration: + +```typescript +import { EIP1193Provider, Wallets } from '@mobile-wallet-protocol/client'; + +const provider = new EIP1193Provider({ + metadata: { + name: 'My App', + customScheme: 'myapp://', + }, + wallet: Wallets.CoinbaseSmartWallet, + originVerification: { + domain: "www.example.com", + generateSignature: async (nonce, requestData) => { + // Developer implements signing with their private key + const dataToSign = JSON.stringify({ + domain: "www.example.com", + nonce, + requestData + }); + return await privateKey.sign(dataToSign); + } + } +}); +``` + +### Signature Implementation + +The `generateSignature` function receives: +- `nonce`: A unique identifier provided by the wallet to prevent replay attacks +- `requestData`: The RPC request being made + +You must sign the payload: `JSON.stringify({ domain, nonce, requestData })` + +### Example with secp256k1 + +```typescript +import { EIP1193Provider, Wallets } from '@mobile-wallet-protocol/client'; +import { secp256k1 } from '@noble/curves/secp256k1'; + +// Load your private key (keep this secure!) +const privateKeyBase64 = 'your-private-key-here'; +const privateKeyBytes = Buffer.from(privateKeyBase64, 'base64'); + +const provider = new EIP1193Provider({ + metadata: { + name: 'My App', + customScheme: 'myapp://', + }, + wallet: Wallets.CoinbaseSmartWallet, + originVerification: { + domain: "www.example.com", + generateSignature: async (nonce, requestData) => { + const payload = JSON.stringify({ + domain: "www.example.com", + nonce, + requestData + }); + + // Create message hash + const messageHash = new TextEncoder().encode(payload); + + // Sign with secp256k1 + const signature = secp256k1.sign(messageHash, privateKeyBytes); + + // Return signature as hex string + return `0x${Buffer.from(signature.toCompactRawBytes()).toString('hex')}`; + } + } +}); +``` + +### Wagmi Integration + +```typescript +import { createConnectorFromWallet, Wallets } from '@mobile-wallet-protocol/wagmi-connectors'; + +const connector = createConnectorFromWallet({ + metadata: { + name: 'My App', + customScheme: 'myapp://', + }, + wallet: Wallets.CoinbaseSmartWallet, + originVerification: { + domain: "www.example.com", + generateSignature: async (nonce, requestData) => { + // Your signature implementation + return await signWithPrivateKey(nonce, requestData); + } + } +}); +``` + +## Security Best Practices + +**Keep your private key safe!** Never share it or commit it to source control. + +- Store private keys in secure environment variables or key management systems +- Generate new key pairs periodically and update well-known files +- Limit access to private keys to authorized personnel only +- Monitor well-known file accessibility and alert on unauthorized changes +- Use HTTPS enforcement for well-known file fetches + +## How It Works + +1. **Nonce Request**: Before each RPC request, the SDK requests a nonce from the wallet +2. **Signature Generation**: Your `generateSignature` function creates a cryptographic signature +3. **Request Enhancement**: The signature is added to the RPC request +4. **Verification**: Base verifies the signature against your published public key +5. **Processing**: If verification passes, the RPC request is processed normally + +This enables secure domain verification for Base Wallet SDK integrations across all platforms. \ No newline at end of file diff --git a/package.json b/package.json index eadd431..4e39491 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ ], "packageManager": "yarn@4.4.0", "scripts": { - "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"" + "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"", + "test:domain-verification": "yarn workspace @mobile-wallet-protocol/client test:domain-verification --cwd \"$PWD\"" } } diff --git a/packages/client/scripts/README.md b/packages/client/scripts/README.md index d388485..9691cad 100644 --- a/packages/client/scripts/README.md +++ b/packages/client/scripts/README.md @@ -6,7 +6,7 @@ This directory contains utility scripts for the Mobile Wallet Protocol client. ### `generate-domain-keys.ts` -Generates secp256k1 key pairs for Coinbase domain verification. +Generates secp256k1 key pairs for Base domain verification. **Usage:** ```bash @@ -15,11 +15,9 @@ yarn generate-key-script **What it does:** - Generates a secp256k1 key pair -- Creates a `.well-known/jwks.json` file in JWKS format +- Creates a `.well-known/base-jwks.json` file in JWKS format - Creates a `domain-verification-private-key.txt` file with the private key **Output:** -- `.well-known/jwks.json` - Public key in JWKS format for domain verification -- `domain-verification-private-key.txt` - Private key (keep secure!) - -For detailed documentation, see [Domain Verification Key Generation](../../docs/domain-verification-key-generation.md). \ No newline at end of file +- `.well-known/base-jwks.json` - Public key in JWKS format for domain verification +- `domain-verification-private-key.txt` - Private key (keep secure!) \ No newline at end of file diff --git a/packages/client/scripts/generate-domain-keys.ts b/packages/client/scripts/generate-domain-keys.ts index b97a7ee..3af600f 100644 --- a/packages/client/scripts/generate-domain-keys.ts +++ b/packages/client/scripts/generate-domain-keys.ts @@ -2,7 +2,7 @@ import { secp256k1 } from '@noble/curves/secp256k1'; import { randomBytes } from '@noble/hashes/utils'; -import { mkdirSync, writeFileSync } from 'fs'; +import { writeFileSync } from 'fs'; import { join } from 'path'; // Base64URL encoding function @@ -14,16 +14,6 @@ function base64url(buffer: Uint8Array): string { .replace(/=/g, ''); } -function getOutputDir(): string { - // Check for --cwd argument - const cwdArgIndex = process.argv.indexOf('--cwd'); - if (cwdArgIndex !== -1 && process.argv[cwdArgIndex + 1]) { - return process.argv[cwdArgIndex + 1]; - } - // Fallbacks - return process.env.INIT_CWD || process.cwd(); -} - function generateDomainVerificationKeys() { // Generate a random private key const privateKeyBytes = randomBytes(32); @@ -44,7 +34,7 @@ function generateDomainVerificationKeys() { x, y, use: 'sig', - kid: 'coinbase-domain-verification', + kid: 'base-domain-verification', alg: 'ES256K' }; @@ -59,22 +49,23 @@ function generateDomainVerificationKeys() { function main() { try { - const outputDir = getOutputDir(); - console.log('🔑 Generating Coinbase domain verification keys...\n'); const { jwks, privateKey } = generateDomainVerificationKeys(); - // Create .well-known directory if it doesn't exist - const wellKnownDir = join(outputDir, '.well-known'); - mkdirSync(wellKnownDir, { recursive: true }); + // Try to get the project root by going up from the current directory + let projectRoot = process.cwd(); + if (projectRoot.includes('packages/client')) { + // If we're in packages/client, go up to the project root + projectRoot = projectRoot.replace('/packages/client', ''); + } - // Write the JWKS file - const jwksPath = join(wellKnownDir, 'jwks.json'); + // Write the JWKS file in the project root directory + const jwksPath = join(projectRoot, 'base-jwks.json'); writeFileSync(jwksPath, JSON.stringify(jwks, null, 2)); - // Write the private key to a separate file - const privateKeyPath = join(outputDir, 'domain-verification-private-key.txt'); + // Write the private key to a separate file in the project root directory + const privateKeyPath = join(projectRoot, 'domain-verification-private-key.txt'); writeFileSync(privateKeyPath, privateKey); console.log('✅ Successfully generated domain verification keys!\n'); @@ -83,7 +74,7 @@ function main() { console.log(` • ${privateKeyPath} - Private key (keep this secure!)\n`); console.log('🌐 Next steps:'); - console.log(' 1. Host the jwks.json file at: https://yourdomain.com/.well-known/jwks.json'); + console.log(' 1. Host the base-jwks.json file at: https://yourdomain.com/.well-known/base-jwks.json'); console.log(' 2. Store the private key securely for use with the SDK'); console.log(' 3. Use the private key with the SDK\'s generateSignature function\n'); diff --git a/packages/client/src/MWPClient.ts b/packages/client/src/MWPClient.ts index e7bcbf0..884e98d 100644 --- a/packages/client/src/MWPClient.ts +++ b/packages/client/src/MWPClient.ts @@ -194,8 +194,8 @@ export class MWPClient { // Generate signature using the developer's function const signature = await this.originVerification.generateSignature(nonce, request); - // Add signature to the request - const requestWithSignature = { + // Add domain verification to the request params + const requestWithVerification = { ...request, params: { ...request.params, @@ -207,7 +207,7 @@ export class MWPClient { } }; - const response = await this.sendEncryptedRequest(requestWithSignature); + const response = await this.sendEncryptedRequest(requestWithVerification); const decrypted = await this.decryptResponseMessage(response); const result = decrypted.result; diff --git a/packages/client/src/core/provider/interface.ts b/packages/client/src/core/provider/interface.ts index a1165a0..8208544 100644 --- a/packages/client/src/core/provider/interface.ts +++ b/packages/client/src/core/provider/interface.ts @@ -46,10 +46,10 @@ export interface OriginVerification { * @type function * @description Async function that generates a signature for domain verification * @param nonce - The nonce provided by the wallet for this request - * @param payload - The request payload to be signed + * @param requestData - The request data to be signed * @returns Promise - The signature as a string */ - generateSignature: (nonce: string, payload: unknown) => Promise; + generateSignature: (nonce: string, requestData: unknown) => Promise; } export interface AppMetadata { @@ -78,10 +78,4 @@ export interface AppMetadata { * @example 'myapp://' */ customScheme: string; - /** - * @param originVerification - * @type {OriginVerification} - * @description Optional origin verification configuration for domain verification - */ - originVerification?: OriginVerification; } diff --git a/packages/wagmi-connectors/src/createConnectorFromWallet.ts b/packages/wagmi-connectors/src/createConnectorFromWallet.ts index 947d5e2..211c343 100644 --- a/packages/wagmi-connectors/src/createConnectorFromWallet.ts +++ b/packages/wagmi-connectors/src/createConnectorFromWallet.ts @@ -1,4 +1,4 @@ -import type { AppMetadata, EIP1193Provider, Wallet } from '@mobile-wallet-protocol/client'; +import type { AppMetadata, EIP1193Provider, Wallet, OriginVerification } from '@mobile-wallet-protocol/client'; import { ChainNotConfiguredError, type Connector, createConnector } from '@wagmi/core'; import type { Omit } from '@wagmi/core/internal'; import { @@ -21,6 +21,7 @@ type WagmiWallet = Wallet & { export type CreateConnectorParameters = { metadata: Omit; wallet: WagmiWallet; + originVerification?: OriginVerification; }; export function createConnectorFromWallet(parameters: CreateConnectorParameters) { @@ -128,6 +129,7 @@ export function createConnectorFromWallet(parameters: CreateConnectorParameters) chainIds: config.chains.map((x) => x.id), }, wallet: parameters.wallet, + originVerification: parameters.originVerification, }); } From 00f3c37e9963610a056dbbc0d05fdb11a1b315bc Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Wed, 2 Jul 2025 01:02:42 -0700 Subject: [PATCH 4/8] minor fixes --- package.json | 3 +-- packages/client/scripts/generate-domain-keys.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4e39491..eadd431 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ ], "packageManager": "yarn@4.4.0", "scripts": { - "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"", - "test:domain-verification": "yarn workspace @mobile-wallet-protocol/client test:domain-verification --cwd \"$PWD\"" + "generate-key-script": "yarn workspace @mobile-wallet-protocol/client generate-key-script --cwd \"$PWD\"" } } diff --git a/packages/client/scripts/generate-domain-keys.ts b/packages/client/scripts/generate-domain-keys.ts index 3af600f..f45681a 100644 --- a/packages/client/scripts/generate-domain-keys.ts +++ b/packages/client/scripts/generate-domain-keys.ts @@ -49,7 +49,7 @@ function generateDomainVerificationKeys() { function main() { try { - console.log('🔑 Generating Coinbase domain verification keys...\n'); + console.log('🔑 Generating Base domain verification keys...\n'); const { jwks, privateKey } = generateDomainVerificationKeys(); From 6ac8c5273d88b7b1b3cd5eb8cfe82976be455b69 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Wed, 2 Jul 2025 09:58:24 -0700 Subject: [PATCH 5/8] changed 'requestNonce' to 'getNonce' & --- packages/client/src/MWPClient.test.ts | 11 ++++--- packages/client/src/MWPClient.ts | 43 ++------------------------- 2 files changed, 8 insertions(+), 46 deletions(-) diff --git a/packages/client/src/MWPClient.test.ts b/packages/client/src/MWPClient.test.ts index 95c9102..518ca18 100644 --- a/packages/client/src/MWPClient.test.ts +++ b/packages/client/src/MWPClient.test.ts @@ -286,10 +286,9 @@ describe('MWPClient', () => { expect(storageStoreSpy).toHaveBeenCalledWith('walletCapabilities', mockCapabilities); }); - it('should generate UUID for wallet_requestNonce', async () => { + it('should generate UUID for wallet_getNonce', async () => { const mockRequest: RequestArguments = { - method: 'wallet_requestNonce', - params: { domain: 'example.com' }, + method: 'wallet_getNonce', }; const result = await client.request(mockRequest); @@ -310,7 +309,7 @@ describe('MWPClient', () => { }); }); - describe('requestNonce', () => { + describe('getNonce', () => { it('should generate UUID when origin verification is configured', async () => { // Create a client with origin verification const clientWithVerification = await MWPClient.createInstance({ @@ -322,14 +321,14 @@ describe('MWPClient', () => { }, }); - const nonce = await clientWithVerification.requestNonce(); + const nonce = await clientWithVerification.getNonce(); // Verify it returns a UUID (36 characters with hyphens) expect(nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); }); it('should throw error when origin verification is not configured', async () => { - await expect(client.requestNonce()).rejects.toThrow('Origin verification not configured'); + await expect(client.getNonce()).rejects.toThrow('Origin verification not configured'); }); }); }); diff --git a/packages/client/src/MWPClient.ts b/packages/client/src/MWPClient.ts index 884e98d..7afdb6c 100644 --- a/packages/client/src/MWPClient.ts +++ b/packages/client/src/MWPClient.ts @@ -127,13 +127,13 @@ export class MWPClient { * Request a nonce for origin verification * @returns Promise - The nonce provided by the wallet */ - async requestNonce(): Promise { + async getNonce(): Promise { if (!this.originVerification) { throw standardErrors.rpc.internal('Origin verification not configured'); } // For now, generate a UUID locally instead of calling the wallet API - // In production, this would call: this.request({ method: 'wallet_requestNonce', params: { domain: this.originVerification.domain } }) + // In the future, this would call: this.request({ method: 'wallet_getNonce' }) return crypto.randomUUID(); } @@ -157,9 +157,8 @@ export class MWPClient { return hexStringFromNumber(this.chain.id); case 'wallet_getCapabilities': return this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY); - case 'wallet_requestNonce': + case 'wallet_getNonce': // For now, generate a UUID locally instead of calling the wallet API - // In production, this would return this.sendRequestToPopup(request); return crypto.randomUUID(); case 'wallet_switchEthereumChain': return this.handleSwitchChainRequest(request); @@ -185,42 +184,6 @@ export class MWPClient { } private async sendRequestToPopup(request: RequestArguments) { - // If origin verification is configured and accounts are available, automatically handle nonce and signature - if (this.originVerification && this.accounts.length > 0) { - try { - // Request nonce from wallet - const nonce = await this.requestNonce(); - - // Generate signature using the developer's function - const signature = await this.originVerification.generateSignature(nonce, request); - - // Add domain verification to the request params - const requestWithVerification = { - ...request, - params: { - ...request.params, - originVerification: { - domain: this.originVerification.domain, - nonce, - signature - } - } - }; - - const response = await this.sendEncryptedRequest(requestWithVerification); - const decrypted = await this.decryptResponseMessage(response); - - const result = decrypted.result; - if ('error' in result) throw result.error; - - return result.value; - } catch (error) { - // If nonce request fails, fall back to regular request - console.warn('Origin verification failed, falling back to regular request:', error); - } - } - - // Regular request flow (no origin verification or fallback) const response = await this.sendEncryptedRequest(request); const decrypted = await this.decryptResponseMessage(response); From 8f6c3df785a96275004238a952b005b2f26f8180 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Thu, 10 Jul 2025 09:05:08 -0700 Subject: [PATCH 6/8] switched 'originVerification to domainVerification' & removed 'wallet_getNonce' case --- docs/domain-verification.md | 8 ++-- packages/client/src/MWPClient.test.ts | 34 --------------- packages/client/src/MWPClient.ts | 42 ++++++------------- .../client/src/core/provider/interface.ts | 4 +- .../src/interfaces/eip1193/EIP1193Provider.ts | 4 +- .../src/createConnectorFromWallet.ts | 6 +-- 6 files changed, 23 insertions(+), 75 deletions(-) diff --git a/docs/domain-verification.md b/docs/domain-verification.md index 60da8a6..3cf1df3 100644 --- a/docs/domain-verification.md +++ b/docs/domain-verification.md @@ -60,7 +60,7 @@ This allows Base to verify signatures from your domain. ### Basic Setup -Add the `originVerification` parameter to your provider configuration: +Add the `domainVerification` parameter to your provider configuration: ```typescript import { EIP1193Provider, Wallets } from '@mobile-wallet-protocol/client'; @@ -71,7 +71,7 @@ const provider = new EIP1193Provider({ customScheme: 'myapp://', }, wallet: Wallets.CoinbaseSmartWallet, - originVerification: { + domainVerification: { domain: "www.example.com", generateSignature: async (nonce, requestData) => { // Developer implements signing with their private key @@ -110,7 +110,7 @@ const provider = new EIP1193Provider({ customScheme: 'myapp://', }, wallet: Wallets.CoinbaseSmartWallet, - originVerification: { + domainVerification: { domain: "www.example.com", generateSignature: async (nonce, requestData) => { const payload = JSON.stringify({ @@ -143,7 +143,7 @@ const connector = createConnectorFromWallet({ customScheme: 'myapp://', }, wallet: Wallets.CoinbaseSmartWallet, - originVerification: { + domainVerification: { domain: "www.example.com", generateSignature: async (nonce, requestData) => { // Your signature implementation diff --git a/packages/client/src/MWPClient.test.ts b/packages/client/src/MWPClient.test.ts index 518ca18..036aa75 100644 --- a/packages/client/src/MWPClient.test.ts +++ b/packages/client/src/MWPClient.test.ts @@ -285,17 +285,6 @@ describe('MWPClient', () => { ]); expect(storageStoreSpy).toHaveBeenCalledWith('walletCapabilities', mockCapabilities); }); - - it('should generate UUID for wallet_getNonce', async () => { - const mockRequest: RequestArguments = { - method: 'wallet_getNonce', - }; - - const result = await client.request(mockRequest); - - // Verify it returns a UUID (36 characters with hyphens) - expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); - }); }); describe('reset', () => { @@ -308,27 +297,4 @@ describe('MWPClient', () => { expect(client['chain']).toEqual({ id: 1 }); }); }); - - describe('getNonce', () => { - it('should generate UUID when origin verification is configured', async () => { - // Create a client with origin verification - const clientWithVerification = await MWPClient.createInstance({ - metadata: mockMetadata, - wallet: mockWallet, - originVerification: { - domain: 'example.com', - generateSignature: jest.fn().mockResolvedValue('signature'), - }, - }); - - const nonce = await clientWithVerification.getNonce(); - - // Verify it returns a UUID (36 characters with hyphens) - expect(nonce).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i); - }); - - it('should throw error when origin verification is not configured', async () => { - await expect(client.getNonce()).rejects.toThrow('Origin verification not configured'); - }); - }); }); diff --git a/packages/client/src/MWPClient.ts b/packages/client/src/MWPClient.ts index 7afdb6c..d08c25b 100644 --- a/packages/client/src/MWPClient.ts +++ b/packages/client/src/MWPClient.ts @@ -1,4 +1,6 @@ +import { postRequestToWallet } from './components/communication/postRequestToWallet'; import { KeyManager } from './components/key/KeyManager'; +import { LIB_VERSION } from './version'; import { decryptContent, encryptContent, @@ -7,24 +9,21 @@ import { } from ':core/cipher/cipher'; import { standardErrors } from ':core/error'; import { RPCRequestMessage, RPCResponse, RPCResponseMessage } from ':core/message'; -import { AppMetadata, RequestArguments } from ':core/provider/interface'; +import { AppMetadata, DomainVerification, RequestArguments } from ':core/provider/interface'; import { ScopedAsyncStorage } from ':core/storage/ScopedAsyncStorage'; import { AddressString } from ':core/type'; import { ensureIntNumber, hexStringFromNumber } from ':core/type/util'; - -const ACCOUNTS_KEY = 'accounts'; -const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain'; -const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains'; -const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities'; -import { postRequestToWallet } from './components/communication/postRequestToWallet'; -import { LIB_VERSION } from './version'; import { appendMWPResponsePath, checkErrorForInvalidRequestArgs, fetchRPCRequest, } from ':core/util/utils'; import { Wallet } from ':core/wallet'; -import { OriginVerification } from ':core/provider/interface'; + +const ACCOUNTS_KEY = 'accounts'; +const ACTIVE_CHAIN_STORAGE_KEY = 'activeChain'; +const AVAILABLE_CHAINS_STORAGE_KEY = 'availableChains'; +const WALLET_CAPABILITIES_STORAGE_KEY = 'walletCapabilities'; type Chain = { id: number; @@ -34,7 +33,7 @@ type Chain = { type MWPClientOptions = { metadata: AppMetadata; wallet: Wallet; - originVerification?: OriginVerification; + domainVerification?: DomainVerification; }; export class MWPClient { @@ -42,12 +41,12 @@ export class MWPClient { private readonly wallet: Wallet; private readonly keyManager: KeyManager; private readonly storage: ScopedAsyncStorage; - private readonly originVerification?: OriginVerification; + private readonly domainVerification?: DomainVerification; private accounts: AddressString[]; private chain: Chain; - private constructor({ metadata, wallet, originVerification }: MWPClientOptions) { + private constructor({ metadata, wallet, domainVerification }: MWPClientOptions) { this.metadata = { ...metadata, name: metadata.name || 'Dapp', @@ -55,7 +54,7 @@ export class MWPClient { }; this.wallet = wallet; - this.originVerification = originVerification; + this.domainVerification = domainVerification; this.keyManager = new KeyManager({ wallet: this.wallet }); this.storage = new ScopedAsyncStorage(this.wallet.name, 'MWPClient'); @@ -123,20 +122,6 @@ export class MWPClient { return accounts; } - /** - * Request a nonce for origin verification - * @returns Promise - The nonce provided by the wallet - */ - async getNonce(): Promise { - if (!this.originVerification) { - throw standardErrors.rpc.internal('Origin verification not configured'); - } - - // For now, generate a UUID locally instead of calling the wallet API - // In the future, this would call: this.request({ method: 'wallet_getNonce' }) - return crypto.randomUUID(); - } - async request(request: RequestArguments) { if (this.accounts.length === 0) { throw standardErrors.provider.unauthorized(); @@ -157,9 +142,6 @@ export class MWPClient { return hexStringFromNumber(this.chain.id); case 'wallet_getCapabilities': return this.storage.loadObject(WALLET_CAPABILITIES_STORAGE_KEY); - case 'wallet_getNonce': - // For now, generate a UUID locally instead of calling the wallet API - return crypto.randomUUID(); case 'wallet_switchEthereumChain': return this.handleSwitchChainRequest(request); case 'eth_ecRecover': diff --git a/packages/client/src/core/provider/interface.ts b/packages/client/src/core/provider/interface.ts index 8208544..d610fc8 100644 --- a/packages/client/src/core/provider/interface.ts +++ b/packages/client/src/core/provider/interface.ts @@ -33,11 +33,11 @@ export interface ProviderInterface extends ProviderEventEmitter { export type ProviderEventCallback = ProviderInterface['emit']; -export interface OriginVerification { +export interface DomainVerification { /** * @param domain * @type string - * @description The domain for which origin verification is enabled + * @description The domain for which domain verification is enabled * @example "www.example.com" */ domain: string; diff --git a/packages/client/src/interfaces/eip1193/EIP1193Provider.ts b/packages/client/src/interfaces/eip1193/EIP1193Provider.ts index 0d2e9d0..6126cee 100644 --- a/packages/client/src/interfaces/eip1193/EIP1193Provider.ts +++ b/packages/client/src/interfaces/eip1193/EIP1193Provider.ts @@ -3,7 +3,7 @@ import { standardErrorCodes, standardErrors } from ':core/error'; import { serializeError } from ':core/error/serialize'; import { AppMetadata, - OriginVerification, + DomainVerification, ProviderEventEmitter, ProviderInterface, RequestArguments, @@ -13,7 +13,7 @@ import { Wallet } from ':core/wallet'; type EIP1193ProviderOptions = { metadata: AppMetadata; wallet: Wallet; - originVerification?: OriginVerification; + domainVerification?: DomainVerification; }; export class EIP1193Provider extends ProviderEventEmitter implements ProviderInterface { diff --git a/packages/wagmi-connectors/src/createConnectorFromWallet.ts b/packages/wagmi-connectors/src/createConnectorFromWallet.ts index 211c343..ecacdd4 100644 --- a/packages/wagmi-connectors/src/createConnectorFromWallet.ts +++ b/packages/wagmi-connectors/src/createConnectorFromWallet.ts @@ -1,4 +1,4 @@ -import type { AppMetadata, EIP1193Provider, Wallet, OriginVerification } from '@mobile-wallet-protocol/client'; +import type { AppMetadata, EIP1193Provider, Wallet, DomainVerification } from '@mobile-wallet-protocol/client'; import { ChainNotConfiguredError, type Connector, createConnector } from '@wagmi/core'; import type { Omit } from '@wagmi/core/internal'; import { @@ -21,7 +21,7 @@ type WagmiWallet = Wallet & { export type CreateConnectorParameters = { metadata: Omit; wallet: WagmiWallet; - originVerification?: OriginVerification; + domainVerification?: DomainVerification; }; export function createConnectorFromWallet(parameters: CreateConnectorParameters) { @@ -129,7 +129,7 @@ export function createConnectorFromWallet(parameters: CreateConnectorParameters) chainIds: config.chains.map((x) => x.id), }, wallet: parameters.wallet, - originVerification: parameters.originVerification, + domainVerification: parameters.domainVerification, }); } From 01d1a82aea237f3e4645b8bc83e5bf41f8870d57 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Thu, 10 Jul 2025 17:20:54 -0700 Subject: [PATCH 7/8] removed console.logs for lint --- packages/client/scripts/generate-domain-keys.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/client/scripts/generate-domain-keys.ts b/packages/client/scripts/generate-domain-keys.ts index f45681a..e4b8395 100644 --- a/packages/client/scripts/generate-domain-keys.ts +++ b/packages/client/scripts/generate-domain-keys.ts @@ -49,7 +49,6 @@ function generateDomainVerificationKeys() { function main() { try { - console.log('🔑 Generating Base domain verification keys...\n'); const { jwks, privateKey } = generateDomainVerificationKeys(); @@ -67,19 +66,7 @@ function main() { // Write the private key to a separate file in the project root directory const privateKeyPath = join(projectRoot, 'domain-verification-private-key.txt'); writeFileSync(privateKeyPath, privateKey); - - console.log('✅ Successfully generated domain verification keys!\n'); - console.log('📁 Files created:'); - console.log(` • ${jwksPath} - JWKS file for your domain`); - console.log(` • ${privateKeyPath} - Private key (keep this secure!)\n`); - - console.log('🌐 Next steps:'); - console.log(' 1. Host the base-jwks.json file at: https://yourdomain.com/.well-known/base-jwks.json'); - console.log(' 2. Store the private key securely for use with the SDK'); - console.log(' 3. Use the private key with the SDK\'s generateSignature function\n'); - } catch (error) { - console.error('❌ Error generating domain verification keys:', error); process.exit(1); } } From cef8b11bee30764866538e246aef7aee3cbd8142 Mon Sep 17 00:00:00 2001 From: Saketh Kotamraju Date: Thu, 10 Jul 2025 18:21:12 -0700 Subject: [PATCH 8/8] fixes to prevent CI error --- packages/client/src/MWPClient.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/client/src/MWPClient.ts b/packages/client/src/MWPClient.ts index d08c25b..cbacb42 100644 --- a/packages/client/src/MWPClient.ts +++ b/packages/client/src/MWPClient.ts @@ -41,12 +41,15 @@ export class MWPClient { private readonly wallet: Wallet; private readonly keyManager: KeyManager; private readonly storage: ScopedAsyncStorage; - private readonly domainVerification?: DomainVerification; private accounts: AddressString[]; private chain: Chain; - private constructor({ metadata, wallet, domainVerification }: MWPClientOptions) { + private constructor({ + metadata, + wallet, + domainVerification: _domainVerification, + }: MWPClientOptions) { this.metadata = { ...metadata, name: metadata.name || 'Dapp', @@ -54,7 +57,8 @@ export class MWPClient { }; this.wallet = wallet; - this.domainVerification = domainVerification; + // Domain verification will be implemented in a future update + // this.domainVerification = domainVerification; this.keyManager = new KeyManager({ wallet: this.wallet }); this.storage = new ScopedAsyncStorage(this.wallet.name, 'MWPClient');