Created
August 11, 2026 12:48
-
-
Save danielsimao/f4ce1b8d03153739b5a0ee2f3bc6447e to your computer and use it in GitHub Desktop.
Minimal repro: @reown/appkit-adapter-bitcoin WalletStandardConnector mishandles publicKey / drops signInput params
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Minimal reproduction against @reown/appkit-adapter-bitcoin@1.8.21's WalletStandardConnector. | |
| // No app, no dependencies beyond the package itself. Run with: node reown-repro.mjs | |
| // | |
| // Simulates exactly what a real MetaMask BTC connection looks like on the page (verified via a | |
| // live wallet-standard `register-wallet` probe against a real MetaMask account), then calls the | |
| // connector's own public API the way a consuming app is expected to. | |
| // WalletStandardConnector isn't in the package's public `exports` map, so a bare subpath import | |
| // is rejected — resolving through the package root and stepping in by relative path instead. | |
| import { createRequire } from 'node:module'; | |
| import { dirname, join } from 'node:path'; | |
| const require = createRequire(import.meta.url); | |
| const pkgRoot = dirname(require.resolve('@reown/appkit-adapter-bitcoin')); | |
| const { WalletStandardConnector } = await import( | |
| join(pkgRoot, '..', 'src', 'connectors', 'WalletStandardConnector.js') | |
| ); | |
| const ADDRESS = 'bc1qxj43gq5jgx843h3s4nmk2lqme825jsps3nl824'; | |
| // This is MetaMask's real registered wallet-standard account for a p2wpkh (bc1q) address. | |
| // `publicKey` should be a 33-byte compressed secp256k1 key. It is not. | |
| const fakeMetaMaskWallet = { | |
| name: 'MetaMask', | |
| icon: 'data:image/png;base64,', | |
| chains: ['bitcoin:mainnet'], | |
| accounts: [ | |
| { | |
| address: ADDRESS, | |
| publicKey: new TextEncoder().encode(ADDRESS), // <- 42 bytes: the address string itself | |
| chains: ['bitcoin:mainnet'], | |
| features: ['bitcoin:signTransaction'], | |
| }, | |
| ], | |
| features: { | |
| 'bitcoin:connect': { | |
| connect: async () => ({ accounts: [{ address: ADDRESS }] }), | |
| }, | |
| 'bitcoin:signTransaction': { | |
| signTransaction: async (params) => { | |
| console.log('\n--- What MetaMask\'s wallet-standard feature actually receives ---'); | |
| console.log(JSON.stringify(params.inputsToSign, null, 2)); | |
| return [{ signedPsbt: params.psbt }]; // stand-in signature, irrelevant to the bug | |
| }, | |
| }, | |
| }, | |
| }; | |
| const connector = new WalletStandardConnector({ | |
| wallet: fakeMetaMaskWallet, | |
| requestedChains: [{ caipNetworkId: 'bitcoin:mainnet' }], | |
| }); | |
| console.log('--- Bug 1: getAccountAddresses() exposes the malformed publicKey verbatim ---'); | |
| const accounts = await connector.getAccountAddresses(); | |
| console.log(accounts); | |
| const decoded = Buffer.from(accounts[0].publicKey, 'hex').toString('utf8'); | |
| console.log(`Decoded "publicKey" hex back to UTF-8: "${decoded}"`); | |
| console.log(`Byte length: ${accounts[0].publicKey.length / 2} (not 33/65/32 — not a valid key of any kind)\n`); | |
| console.log('--- Bug 2: signPSBT() silently drops publicKey, sighashTypes, disableTweakSigner ---'); | |
| // A minimal valid PSBT (single input, no signature) — its content is irrelevant to this bug. | |
| const emptyPsbtBase64 = 'cHNidP8BAAoAAAAAAAAAAAAAAA=='; | |
| await connector.signPSBT({ | |
| psbt: emptyPsbtBase64, | |
| broadcast: false, | |
| signInputs: [ | |
| { | |
| index: 0, | |
| address: ADDRESS, | |
| sighashTypes: [0x01, 0x03], // SIGHASH_ALL, SIGHASH_SINGLE — caller's actual request | |
| publicKey: accounts[0].publicKey, // caller's public-key hint, whatever it is | |
| disableTweakSigner: true, | |
| }, | |
| ], | |
| }); | |
| console.log('Notice: no `publicKey`, `sighashTypes`, or `disableTweakSigner` field above.'); | |
| console.log('The connector rebuilt the request from `{ account, signingIndexes, sigHash: undefined }`,'); | |
| console.log('discarding everything the caller supplied.'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment