|
| 1 | +# XCP Wallet Provider API |
| 2 | + |
| 3 | +The XCP Wallet browser extension injects a provider at `window.xcpwallet` on all HTTPS pages. Websites use this provider to connect wallets, sign messages, compose transactions, and broadcast to the Bitcoin network. |
| 4 | + |
| 5 | +## Detection |
| 6 | + |
| 7 | +The provider is injected at `document_start` — before page scripts run. Two detection methods: |
| 8 | + |
| 9 | +```js |
| 10 | +// Synchronous (provider already injected) |
| 11 | +if (window.xcpwallet) { |
| 12 | + // ready |
| 13 | +} |
| 14 | + |
| 15 | +// Asynchronous (wait for injection) |
| 16 | +window.addEventListener('xcp-wallet#initialized', () => { |
| 17 | + // window.xcpwallet is now available |
| 18 | +}); |
| 19 | + |
| 20 | +// Request re-announcement (for SPAs that mount after injection) |
| 21 | +window.dispatchEvent(new Event('xcp-wallet#discover')); |
| 22 | +``` |
| 23 | + |
| 24 | +## Provider Interface |
| 25 | + |
| 26 | +```ts |
| 27 | +interface XcpProvider { |
| 28 | + request(args: { method: string; params?: unknown[] }): Promise<unknown> |
| 29 | + on(event: string, handler: (...args: any[]) => void): void |
| 30 | + removeListener(event: string, handler: (...args: any[]) => void): void |
| 31 | +} |
| 32 | +``` |
| 33 | + |
| 34 | +## Methods |
| 35 | + |
| 36 | +### Connection |
| 37 | + |
| 38 | +#### `xcp_requestAccounts` |
| 39 | + |
| 40 | +Connect to the wallet. Opens a popup for user approval on first connection. |
| 41 | + |
| 42 | +Returns a connection proof — a BIP-322 signature proving the user controls the address. The proof message is generated by the extension (not the website) and includes the requesting origin, a random nonce, and a timestamp. |
| 43 | + |
| 44 | +```js |
| 45 | +const result = await xcpwallet.request({ method: 'xcp_requestAccounts' }); |
| 46 | +// { |
| 47 | +// accounts: ['bc1q...'], |
| 48 | +// proof: { |
| 49 | +// address: 'bc1q...', |
| 50 | +// message: 'xcp-wallet\norigin:https://example.com\nnonce:a1b2c3d4\nissued:1711130400', |
| 51 | +// signature: '<BIP-322 signature>', |
| 52 | +// verification: { |
| 53 | +// method: 'BIP-322', |
| 54 | +// format: 'p2wpkh' // address format used for signing |
| 55 | +// } |
| 56 | +// } |
| 57 | +// } |
| 58 | +``` |
| 59 | + |
| 60 | +**Proof verification (server-side):** |
| 61 | + |
| 62 | +1. Parse the `message` — verify `origin` matches your domain and `issued` is recent (< 5 minutes) |
| 63 | +2. Verify the BIP-322 signature against the `address` using the `verification.format` hint |
| 64 | +3. Optionally store the `nonce` to prevent replay |
| 65 | + |
| 66 | +The proof is auto-signed during connection — no additional user prompt beyond the connect approval. |
| 67 | + |
| 68 | +#### `xcp_accounts` |
| 69 | + |
| 70 | +Get currently connected accounts. No popup — returns empty array if not connected or wallet is locked. |
| 71 | + |
| 72 | +```js |
| 73 | +const accounts = await xcpwallet.request({ method: 'xcp_accounts' }); |
| 74 | +// ['bc1q...'] or [] |
| 75 | +``` |
| 76 | + |
| 77 | +#### `xcp_disconnect` |
| 78 | + |
| 79 | +Disconnect the current site. |
| 80 | + |
| 81 | +```js |
| 82 | +await xcpwallet.request({ method: 'xcp_disconnect' }); |
| 83 | +// true |
| 84 | +``` |
| 85 | + |
| 86 | +### Signing |
| 87 | + |
| 88 | +All signing methods require an active connection and open a popup for user approval. |
| 89 | + |
| 90 | +#### `xcp_signMessage` |
| 91 | + |
| 92 | +Sign a message with the active address using BIP-322. |
| 93 | + |
| 94 | +```js |
| 95 | +const result = await xcpwallet.request({ |
| 96 | + method: 'xcp_signMessage', |
| 97 | + params: ['Hello, world!'] |
| 98 | +}); |
| 99 | +// { signature: '<base64 BIP-322 signature>' } |
| 100 | +``` |
| 101 | + |
| 102 | +An optional second parameter can specify the address (must match the active address): |
| 103 | + |
| 104 | +```js |
| 105 | +params: ['Hello, world!', 'bc1q...'] |
| 106 | +``` |
| 107 | + |
| 108 | +#### `xcp_signTransaction` |
| 109 | + |
| 110 | +Sign a raw transaction hex. |
| 111 | + |
| 112 | +```js |
| 113 | +const result = await xcpwallet.request({ |
| 114 | + method: 'xcp_signTransaction', |
| 115 | + params: [{ hex: '0200000001...' }] |
| 116 | +}); |
| 117 | +// { hex: '<signed transaction hex>' } |
| 118 | + |
| 119 | +// Also accepts a plain string: |
| 120 | +params: ['0200000001...'] |
| 121 | +``` |
| 122 | + |
| 123 | +#### `xcp_signPsbt` |
| 124 | + |
| 125 | +Sign a PSBT (Partially Signed Bitcoin Transaction). |
| 126 | + |
| 127 | +```js |
| 128 | +const result = await xcpwallet.request({ |
| 129 | + method: 'xcp_signPsbt', |
| 130 | + params: [{ |
| 131 | + hex: '<PSBT hex>', |
| 132 | + signInputs: { 'bc1q...': [0, 1] }, // optional: which inputs to sign |
| 133 | + sighashTypes: [0x01] // optional: sighash types |
| 134 | + }] |
| 135 | +}); |
| 136 | +// { hex: '<signed PSBT hex>' } |
| 137 | +``` |
| 138 | + |
| 139 | +### Broadcasting |
| 140 | + |
| 141 | +#### `xcp_broadcastTransaction` |
| 142 | + |
| 143 | +Broadcast a signed transaction to the Bitcoin network. Includes replay protection — the same transaction cannot be broadcast twice. |
| 144 | + |
| 145 | +```js |
| 146 | +const result = await xcpwallet.request({ |
| 147 | + method: 'xcp_broadcastTransaction', |
| 148 | + params: ['<signed transaction hex>'] |
| 149 | +}); |
| 150 | +// { txid: '<64-char transaction hash>' } |
| 151 | +``` |
| 152 | + |
| 153 | +### Read-Only |
| 154 | + |
| 155 | +#### `xcp_getBalances` |
| 156 | + |
| 157 | +Get BTC and token balances for the connected address. |
| 158 | + |
| 159 | +```js |
| 160 | +const result = await xcpwallet.request({ method: 'xcp_getBalances' }); |
| 161 | +// { address: 'bc1q...', btc: { ... }, xcp: { ... }, tokens: [...] } |
| 162 | +``` |
| 163 | + |
| 164 | +#### `xcp_chainId` |
| 165 | + |
| 166 | +```js |
| 167 | +await xcpwallet.request({ method: 'xcp_chainId' }); |
| 168 | +// '0x0' (Bitcoin mainnet) |
| 169 | +``` |
| 170 | + |
| 171 | +#### `xcp_getNetwork` |
| 172 | + |
| 173 | +```js |
| 174 | +await xcpwallet.request({ method: 'xcp_getNetwork' }); |
| 175 | +// 'mainnet' |
| 176 | +``` |
| 177 | + |
| 178 | +## Events |
| 179 | + |
| 180 | +```js |
| 181 | +// Account changed (address switch or connect/disconnect) |
| 182 | +xcpwallet.on('accountsChanged', (accounts) => { |
| 183 | + // accounts: string[] — empty on disconnect |
| 184 | +}); |
| 185 | + |
| 186 | +// Wallet disconnected this site |
| 187 | +xcpwallet.on('disconnect', () => { |
| 188 | + // Connection revoked |
| 189 | +}); |
| 190 | +``` |
| 191 | + |
| 192 | +## SDK |
| 193 | + |
| 194 | +An SDK is available for React/Next.js applications at [`lib/wallet/sdk`](https://github.com/XCP/exchange/tree/master/apps/web/src/lib/wallet/sdk). Copy the `sdk/` folder into your project: |
| 195 | + |
| 196 | +``` |
| 197 | +lib/wallet/ |
| 198 | +├── sdk/ |
| 199 | +│ ├── constants.ts — validation patterns, error codes |
| 200 | +│ ├── detect.ts — provider detection with race condition handling |
| 201 | +│ ├── errors.ts — user-friendly error messages |
| 202 | +│ ├── index.ts — public exports |
| 203 | +│ ├── provider.ts — typed XcpWallet wrapper class |
| 204 | +│ ├── types.ts — TypeScript interfaces |
| 205 | +│ └── verify.ts — connection proof validation |
| 206 | +├── wallet-context.tsx — React context provider (useWallet hook) |
| 207 | +└── useCompose.ts — compose → sign → broadcast pipeline |
| 208 | +``` |
| 209 | + |
| 210 | +### React usage |
| 211 | + |
| 212 | +```tsx |
| 213 | +import { WalletProvider, useWallet } from '@/lib/wallet/wallet-context'; |
| 214 | + |
| 215 | +// Wrap your app |
| 216 | +<WalletProvider>{children}</WalletProvider> |
| 217 | + |
| 218 | +// In components |
| 219 | +const { status, address, connectionProof, connect, disconnect } = useWallet(); |
| 220 | +// status: 'not_detected' | 'disconnected' | 'connected' |
| 221 | +``` |
| 222 | + |
| 223 | +### Direct usage (non-React) |
| 224 | + |
| 225 | +```ts |
| 226 | +import { detectProvider, XcpWallet } from '@/lib/wallet/sdk'; |
| 227 | + |
| 228 | +const provider = await detectProvider(); |
| 229 | +const wallet = new XcpWallet(provider); |
| 230 | +const { accounts, proof } = await wallet.connect(); |
| 231 | +``` |
| 232 | + |
| 233 | +### Proof verification |
| 234 | + |
| 235 | +```ts |
| 236 | +import { validateProof } from '@/lib/wallet/sdk'; |
| 237 | + |
| 238 | +// Client-side (structural checks only) |
| 239 | +const result = await validateProof(proof, 'https://example.com', address); |
| 240 | + |
| 241 | +// Server-side (with cryptographic verification) |
| 242 | +const result = await validateProof(proof, origin, address, { |
| 243 | + verifySignature: async (message, signature, addr) => { |
| 244 | + // Use your BIP-322 verification library |
| 245 | + return await verifyBIP322(message, signature, addr); |
| 246 | + } |
| 247 | +}); |
| 248 | +``` |
| 249 | + |
| 250 | +## Security |
| 251 | + |
| 252 | +- **Origin validation**: The content script provides the origin — page JavaScript cannot spoof it |
| 253 | +- **Connection proof**: BIP-322 signature proving address ownership, message format controlled by extension |
| 254 | +- **Rate limiting**: Connection, transaction, and API requests are rate-limited per origin |
| 255 | +- **Replay protection**: Broadcast transactions are tracked to prevent double-submission |
| 256 | +- **Parameter validation**: All inputs are type-checked and size-limited (max 1MB) |
| 257 | +- **CSP analysis**: Sites without Content Security Policy generate console warnings |
0 commit comments