@meshconnect/uwc-bitcoin 0.2.2 → 0.2.4-snapshot.aa56682
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter.d.ts +38 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +2 -0
- package/dist/adapter.js.map +1 -0
- package/dist/adapters/wallet-standard.d.ts +79 -0
- package/dist/adapters/wallet-standard.d.ts.map +1 -0
- package/dist/adapters/wallet-standard.js +101 -0
- package/dist/adapters/wallet-standard.js.map +1 -0
- package/dist/adapters/walletconnect.d.ts +36 -0
- package/dist/adapters/walletconnect.d.ts.map +1 -0
- package/dist/adapters/walletconnect.js +90 -0
- package/dist/adapters/walletconnect.js.map +1 -0
- package/dist/bitcoin-namespace.d.ts +29 -9
- package/dist/bitcoin-namespace.d.ts.map +1 -1
- package/dist/bitcoin-namespace.js +48 -8
- package/dist/bitcoin-namespace.js.map +1 -1
- package/dist/encoding.d.ts +11 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +29 -0
- package/dist/encoding.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/adapter.ts +49 -0
- package/src/adapters/wallet-standard.test.ts +148 -0
- package/src/adapters/wallet-standard.ts +172 -0
- package/src/adapters/walletconnect.test.ts +97 -0
- package/src/adapters/walletconnect.ts +126 -0
- package/src/bitcoin-namespace.test.ts +91 -5
- package/src/bitcoin-namespace.ts +63 -9
- package/src/encoding.ts +29 -0
- package/src/index.ts +8 -0
package/src/adapter.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BitcoinAccount,
|
|
3
|
+
BitcoinPsbtSignRequest,
|
|
4
|
+
BitcoinSendTransferRequest,
|
|
5
|
+
BitcoinSignMessageParams,
|
|
6
|
+
SignatureType,
|
|
7
|
+
TransactionResult
|
|
8
|
+
} from '@meshconnect/uwc-types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The seam between the Bitcoin namespace module and a single transport.
|
|
12
|
+
*
|
|
13
|
+
* A concrete adapter is constructed by its transport bound to an
|
|
14
|
+
* already-connected provider/session — Wallet Standard `bitcoin:*` for the
|
|
15
|
+
* injected transport, WalletConnect bip122 for the WalletConnect transport —
|
|
16
|
+
* so the port carries no `connect`/provider argument: connecting is the
|
|
17
|
+
* transport's job. Each adapter converts between these transport-agnostic
|
|
18
|
+
* domain shapes and its wallet's native interface (e.g. hex/base64 strings ↔
|
|
19
|
+
* `Uint8Array`).
|
|
20
|
+
*
|
|
21
|
+
* `getAccounts` + `signMessage` are the always-present verify-flow methods.
|
|
22
|
+
* `signPsbt` and `sendTransfer` are optional because they map to distinct wallet
|
|
23
|
+
* paths — a Wallet Standard wallet signs a PSBT (Path B); a WalletConnect wallet
|
|
24
|
+
* does a high-level `sendTransfer` (Path A) — and not every adapter offers both.
|
|
25
|
+
*/
|
|
26
|
+
export interface BitcoinTransportAdapter {
|
|
27
|
+
/** Enumerate the bip122 accounts available on the connected session. */
|
|
28
|
+
getAccounts(): Promise<BitcoinAccount[]>
|
|
29
|
+
|
|
30
|
+
/** Sign a message with the given account. */
|
|
31
|
+
signMessage(params: BitcoinSignMessageParams): Promise<SignatureType>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Sign a PSBT (Path B). Returns the wallet-signed PSBT, base64-encoded — the
|
|
35
|
+
* wallet does NOT broadcast; finalizing + broadcasting is a separate step.
|
|
36
|
+
* Optional: only adapters whose wallet exposes PSBT signing implement it.
|
|
37
|
+
*/
|
|
38
|
+
signPsbt?(request: BitcoinPsbtSignRequest): Promise<string>
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* High-level transfer (Path A): the wallet does coin selection, fee, signing,
|
|
42
|
+
* and broadcast, returning a txid. Optional: only adapters whose transport
|
|
43
|
+
* offers it (e.g. WalletConnect `sendTransfer`) implement it.
|
|
44
|
+
*/
|
|
45
|
+
sendTransfer?(request: BitcoinSendTransferRequest): Promise<TransactionResult>
|
|
46
|
+
|
|
47
|
+
/** Release any session resources the adapter holds. Optional. */
|
|
48
|
+
disconnect?(): Promise<void>
|
|
49
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
3
|
+
import {
|
|
4
|
+
WalletStandardBitcoinAdapter,
|
|
5
|
+
type WalletStandardBitcoinAccount,
|
|
6
|
+
type WalletStandardBitcoinWallet
|
|
7
|
+
} from './wallet-standard'
|
|
8
|
+
import { base64ToBytes, bytesToBase64, utf8ToBytes } from '../encoding'
|
|
9
|
+
|
|
10
|
+
const wsAccount: WalletStandardBitcoinAccount = {
|
|
11
|
+
address: 'bc1qpayment',
|
|
12
|
+
publicKey: new Uint8Array([2, 0xaa, 0xbb, 0xcc]),
|
|
13
|
+
purpose: 'payment'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A fake `bitcoin:*` wallet with all three features wired via vi mocks. */
|
|
17
|
+
const fakeWallet = (
|
|
18
|
+
overrides: Partial<WalletStandardBitcoinWallet['features']> = {}
|
|
19
|
+
): WalletStandardBitcoinWallet => ({
|
|
20
|
+
name: 'Fake BTC Wallet',
|
|
21
|
+
accounts: [],
|
|
22
|
+
features: {
|
|
23
|
+
'bitcoin:connect': {
|
|
24
|
+
connect: vi.fn().mockResolvedValue({ accounts: [wsAccount] })
|
|
25
|
+
},
|
|
26
|
+
'bitcoin:signMessage': {
|
|
27
|
+
signMessage: vi.fn().mockResolvedValue([
|
|
28
|
+
{
|
|
29
|
+
signature: new Uint8Array([1, 2, 3]),
|
|
30
|
+
signedMessage: new Uint8Array()
|
|
31
|
+
}
|
|
32
|
+
])
|
|
33
|
+
},
|
|
34
|
+
'bitcoin:signTransaction': {
|
|
35
|
+
signTransaction: vi
|
|
36
|
+
.fn()
|
|
37
|
+
.mockResolvedValue([{ psbt: new Uint8Array([9, 8, 7]) }])
|
|
38
|
+
},
|
|
39
|
+
...overrides
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('WalletStandardBitcoinAdapter', () => {
|
|
44
|
+
it('getAccounts connects and maps accounts to hex publicKey', async () => {
|
|
45
|
+
const wallet = fakeWallet()
|
|
46
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
47
|
+
|
|
48
|
+
const accounts = await adapter.getAccounts()
|
|
49
|
+
|
|
50
|
+
expect(wallet.features['bitcoin:connect']!.connect).toHaveBeenCalledWith({
|
|
51
|
+
purposes: ['payment']
|
|
52
|
+
})
|
|
53
|
+
expect(accounts).toEqual([
|
|
54
|
+
{ address: 'bc1qpayment', publicKey: '02aabbcc', purpose: 'payment' }
|
|
55
|
+
])
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('signMessage encodes the message and returns a base64 standard signature', async () => {
|
|
59
|
+
const wallet = fakeWallet()
|
|
60
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
61
|
+
|
|
62
|
+
const sig = await adapter.signMessage({
|
|
63
|
+
account: { address: 'bc1qpayment', publicKey: '02aabbcc' },
|
|
64
|
+
message: 'verify me'
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const call = (
|
|
68
|
+
wallet.features['bitcoin:signMessage']!.signMessage as ReturnType<
|
|
69
|
+
typeof vi.fn
|
|
70
|
+
>
|
|
71
|
+
).mock.calls[0][0]
|
|
72
|
+
expect(call.account).toBe(wsAccount)
|
|
73
|
+
expect(call.message).toEqual(utf8ToBytes('verify me'))
|
|
74
|
+
expect(sig).toEqual({
|
|
75
|
+
type: 'standard',
|
|
76
|
+
signature: bytesToBase64(new Uint8Array([1, 2, 3]))
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('signPsbt round-trips base64 <-> bytes and returns the signed PSBT', async () => {
|
|
81
|
+
const wallet = fakeWallet()
|
|
82
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
83
|
+
const psbt = bytesToBase64(new Uint8Array([10, 20, 30]))
|
|
84
|
+
|
|
85
|
+
const signed = await adapter.signPsbt({ from: 'bc1qpayment', psbt })
|
|
86
|
+
|
|
87
|
+
const call = (
|
|
88
|
+
wallet.features['bitcoin:signTransaction']!.signTransaction as ReturnType<
|
|
89
|
+
typeof vi.fn
|
|
90
|
+
>
|
|
91
|
+
).mock.calls[0][0]
|
|
92
|
+
expect(call.account).toBe(wsAccount)
|
|
93
|
+
expect(call.psbt).toEqual(base64ToBytes(psbt))
|
|
94
|
+
expect(signed).toBe(bytesToBase64(new Uint8Array([9, 8, 7])))
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('signPsbt maps inputsToSign accounts by address', async () => {
|
|
98
|
+
const wallet = fakeWallet()
|
|
99
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
100
|
+
await adapter.getAccounts()
|
|
101
|
+
|
|
102
|
+
await adapter.signPsbt({
|
|
103
|
+
from: 'bc1qpayment',
|
|
104
|
+
psbt: bytesToBase64(new Uint8Array([1])),
|
|
105
|
+
inputsToSign: [
|
|
106
|
+
{ address: 'bc1qpayment', signingIndexes: [0], sigHash: 'ALL' }
|
|
107
|
+
]
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const call = (
|
|
111
|
+
wallet.features['bitcoin:signTransaction']!.signTransaction as ReturnType<
|
|
112
|
+
typeof vi.fn
|
|
113
|
+
>
|
|
114
|
+
).mock.calls[0][0]
|
|
115
|
+
expect(call.inputsToSign).toEqual([
|
|
116
|
+
{ account: wsAccount, signingIndexes: [0], sigHash: 'ALL' }
|
|
117
|
+
])
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('throws for an address with no connected account', async () => {
|
|
121
|
+
const wallet = fakeWallet()
|
|
122
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
123
|
+
|
|
124
|
+
await expect(
|
|
125
|
+
adapter.signMessage({
|
|
126
|
+
account: { address: 'bc1qunknown', publicKey: '00' },
|
|
127
|
+
message: 'x'
|
|
128
|
+
})
|
|
129
|
+
).rejects.toBeInstanceOf(WalletConnectorError)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('throws when the wallet lacks the signTransaction feature', async () => {
|
|
133
|
+
const wallet = fakeWallet({ 'bitcoin:signTransaction': undefined })
|
|
134
|
+
const adapter = new WalletStandardBitcoinAdapter(wallet)
|
|
135
|
+
|
|
136
|
+
await expect(
|
|
137
|
+
adapter.signPsbt({
|
|
138
|
+
from: 'bc1qpayment',
|
|
139
|
+
psbt: bytesToBase64(new Uint8Array([1]))
|
|
140
|
+
})
|
|
141
|
+
).rejects.toBeInstanceOf(WalletConnectorError)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('does not implement sendTransfer (Wallet Standard has no high-level transfer)', () => {
|
|
145
|
+
const adapter = new WalletStandardBitcoinAdapter(fakeWallet())
|
|
146
|
+
expect(adapter.sendTransfer).toBeUndefined()
|
|
147
|
+
})
|
|
148
|
+
})
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BitcoinAccount,
|
|
3
|
+
BitcoinPsbtSignRequest,
|
|
4
|
+
BitcoinSignMessageParams,
|
|
5
|
+
SignatureType
|
|
6
|
+
} from '@meshconnect/uwc-types'
|
|
7
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
8
|
+
import type { BitcoinTransportAdapter } from '../adapter'
|
|
9
|
+
import {
|
|
10
|
+
base64ToBytes,
|
|
11
|
+
bytesToBase64,
|
|
12
|
+
bytesToHex,
|
|
13
|
+
utf8ToBytes
|
|
14
|
+
} from '../encoding'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The subset of the Wallet Standard `bitcoin:*` interface this adapter drives.
|
|
18
|
+
* Structural (no `@wallet-standard/*` dependency); grounded on the community
|
|
19
|
+
* bitcoin-wallet-standard interface that MetaMask exposes. (OKX signs BTC too,
|
|
20
|
+
* but via a legacy injected provider, not Wallet Standard — it needs a separate
|
|
21
|
+
* adapter, not this one.)
|
|
22
|
+
* @see https://github.com/ExodusMovement/bitcoin-wallet-standard
|
|
23
|
+
*/
|
|
24
|
+
export interface WalletStandardBitcoinAccount {
|
|
25
|
+
address: string
|
|
26
|
+
publicKey: Uint8Array
|
|
27
|
+
purpose?: 'payment' | 'ordinals'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BitcoinConnectFeature {
|
|
31
|
+
connect(input: {
|
|
32
|
+
purposes: ('payment' | 'ordinals')[]
|
|
33
|
+
}): Promise<{ accounts: WalletStandardBitcoinAccount[] }>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface BitcoinSignMessageFeature {
|
|
37
|
+
signMessage(input: {
|
|
38
|
+
account: WalletStandardBitcoinAccount
|
|
39
|
+
message: Uint8Array
|
|
40
|
+
protocol?: 'ecdsa' | 'bip322'
|
|
41
|
+
}): Promise<{ signature: Uint8Array; signedMessage: Uint8Array }[]>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BitcoinSignTransactionFeature {
|
|
45
|
+
signTransaction(input: {
|
|
46
|
+
account: WalletStandardBitcoinAccount
|
|
47
|
+
psbt: Uint8Array
|
|
48
|
+
inputsToSign?: {
|
|
49
|
+
account: WalletStandardBitcoinAccount
|
|
50
|
+
signingIndexes: number[]
|
|
51
|
+
sigHash?: string
|
|
52
|
+
}[]
|
|
53
|
+
}): Promise<{ psbt: Uint8Array }[]>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The handle the injected transport passes in (a discovered `bitcoin:*` wallet). */
|
|
57
|
+
export interface WalletStandardBitcoinWallet {
|
|
58
|
+
name: string
|
|
59
|
+
accounts: readonly WalletStandardBitcoinAccount[]
|
|
60
|
+
features: Record<string, unknown> & {
|
|
61
|
+
'bitcoin:connect'?: BitcoinConnectFeature
|
|
62
|
+
'bitcoin:signMessage'?: BitcoinSignMessageFeature
|
|
63
|
+
'bitcoin:signTransaction'?: BitcoinSignTransactionFeature
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Wallet Standard (`bitcoin:*`) transport adapter — the injected path for
|
|
69
|
+
* wallets that speak Wallet Standard (verified: MetaMask). The injected
|
|
70
|
+
* transport discovers the wallet and hands it here; this adapter performs the
|
|
71
|
+
* bip122-specific operations against it, converting hex/base64 ↔ `Uint8Array`
|
|
72
|
+
* at the boundary.
|
|
73
|
+
*
|
|
74
|
+
* `sendTransfer` is intentionally not implemented — Wallet Standard has no
|
|
75
|
+
* high-level transfer; that is the WalletConnect adapter's path.
|
|
76
|
+
*/
|
|
77
|
+
export class WalletStandardBitcoinAdapter implements BitcoinTransportAdapter {
|
|
78
|
+
private accounts: readonly WalletStandardBitcoinAccount[] | null = null
|
|
79
|
+
|
|
80
|
+
constructor(private readonly wallet: WalletStandardBitcoinWallet) {}
|
|
81
|
+
|
|
82
|
+
async getAccounts(): Promise<BitcoinAccount[]> {
|
|
83
|
+
const connect = this.wallet.features['bitcoin:connect']
|
|
84
|
+
// The wallet exposes accounts only after `bitcoin:connect`; fall back to any
|
|
85
|
+
// it already lists (a wallet without the connect feature).
|
|
86
|
+
this.accounts = connect
|
|
87
|
+
? (await connect.connect({ purposes: ['payment'] })).accounts
|
|
88
|
+
: this.wallet.accounts
|
|
89
|
+
|
|
90
|
+
return this.accounts.map(account => ({
|
|
91
|
+
address: account.address,
|
|
92
|
+
publicKey: bytesToHex(account.publicKey),
|
|
93
|
+
...(account.purpose ? { purpose: account.purpose } : {})
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async signMessage(params: BitcoinSignMessageParams): Promise<SignatureType> {
|
|
98
|
+
const feature = this.wallet.features['bitcoin:signMessage']
|
|
99
|
+
if (!feature) {
|
|
100
|
+
throw this.unsupported('signMessage')
|
|
101
|
+
}
|
|
102
|
+
const account = await this.requireAccount(params.account.address)
|
|
103
|
+
const [result] = await feature.signMessage({
|
|
104
|
+
account,
|
|
105
|
+
message: utf8ToBytes(params.message),
|
|
106
|
+
...(params.protocol ? { protocol: params.protocol } : {})
|
|
107
|
+
})
|
|
108
|
+
if (!result) {
|
|
109
|
+
throw new WalletConnectorError({
|
|
110
|
+
type: 'unknown',
|
|
111
|
+
message: 'wallet-standard bitcoin: signMessage returned no signature'
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
return { type: 'standard', signature: bytesToBase64(result.signature) }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async signPsbt(request: BitcoinPsbtSignRequest): Promise<string> {
|
|
118
|
+
const feature = this.wallet.features['bitcoin:signTransaction']
|
|
119
|
+
if (!feature) {
|
|
120
|
+
throw this.unsupported('signPsbt')
|
|
121
|
+
}
|
|
122
|
+
const account = await this.requireAccount(request.from)
|
|
123
|
+
const inputsToSign = request.inputsToSign
|
|
124
|
+
? await Promise.all(
|
|
125
|
+
request.inputsToSign.map(async input => ({
|
|
126
|
+
account: await this.requireAccount(input.address),
|
|
127
|
+
signingIndexes: input.signingIndexes,
|
|
128
|
+
...(input.sigHash ? { sigHash: input.sigHash } : {})
|
|
129
|
+
}))
|
|
130
|
+
)
|
|
131
|
+
: undefined
|
|
132
|
+
const [result] = await feature.signTransaction({
|
|
133
|
+
account,
|
|
134
|
+
psbt: base64ToBytes(request.psbt),
|
|
135
|
+
...(inputsToSign ? { inputsToSign } : {})
|
|
136
|
+
})
|
|
137
|
+
if (!result) {
|
|
138
|
+
throw new WalletConnectorError({
|
|
139
|
+
type: 'unknown',
|
|
140
|
+
message: 'wallet-standard bitcoin: signTransaction returned no PSBT'
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
return bytesToBase64(result.psbt)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
disconnect(): Promise<void> {
|
|
147
|
+
this.accounts = null
|
|
148
|
+
return Promise.resolve()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Look up the wallet-native account for an address, connecting first if needed. */
|
|
152
|
+
private async requireAccount(
|
|
153
|
+
address: string
|
|
154
|
+
): Promise<WalletStandardBitcoinAccount> {
|
|
155
|
+
if (!this.accounts) await this.getAccounts()
|
|
156
|
+
const account = this.accounts?.find(a => a.address === address)
|
|
157
|
+
if (!account) {
|
|
158
|
+
throw new WalletConnectorError({
|
|
159
|
+
type: 'unknown',
|
|
160
|
+
message: `wallet-standard bitcoin: no connected account for "${address}"`
|
|
161
|
+
})
|
|
162
|
+
}
|
|
163
|
+
return account
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private unsupported(method: string): WalletConnectorError {
|
|
167
|
+
return new WalletConnectorError({
|
|
168
|
+
type: 'unknown',
|
|
169
|
+
message: `wallet-standard bitcoin: this wallet does not support ${method}`
|
|
170
|
+
})
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
3
|
+
import { WalletConnectBitcoinAdapter } from './walletconnect'
|
|
4
|
+
|
|
5
|
+
const ACCOUNT = 'bc1qsender'
|
|
6
|
+
|
|
7
|
+
describe('WalletConnectBitcoinAdapter', () => {
|
|
8
|
+
it('getAccounts calls getAccountAddresses and maps intention -> purpose', async () => {
|
|
9
|
+
const request = vi.fn().mockResolvedValue([
|
|
10
|
+
{ address: 'bc1qpay', publicKey: '02aa', intention: 'payment' },
|
|
11
|
+
{ address: 'bc1qord', intention: 'ordinal' }
|
|
12
|
+
])
|
|
13
|
+
const adapter = new WalletConnectBitcoinAdapter(ACCOUNT, request)
|
|
14
|
+
|
|
15
|
+
const accounts = await adapter.getAccounts()
|
|
16
|
+
|
|
17
|
+
expect(request).toHaveBeenCalledWith('getAccountAddresses', {
|
|
18
|
+
account: ACCOUNT
|
|
19
|
+
})
|
|
20
|
+
expect(accounts).toEqual([
|
|
21
|
+
{ address: 'bc1qpay', publicKey: '02aa', purpose: 'payment' },
|
|
22
|
+
{ address: 'bc1qord', publicKey: '', purpose: 'ordinals' }
|
|
23
|
+
])
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('signMessage passes account + address + raw message and wraps the signature', async () => {
|
|
27
|
+
const request = vi.fn().mockResolvedValue({ signature: 'sigbase64' })
|
|
28
|
+
const adapter = new WalletConnectBitcoinAdapter(ACCOUNT, request)
|
|
29
|
+
|
|
30
|
+
const sig = await adapter.signMessage({
|
|
31
|
+
account: { address: 'bc1qpay', publicKey: '02aa' },
|
|
32
|
+
message: 'verify me',
|
|
33
|
+
protocol: 'bip322'
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
expect(request).toHaveBeenCalledWith('signMessage', {
|
|
37
|
+
account: ACCOUNT,
|
|
38
|
+
message: 'verify me',
|
|
39
|
+
address: 'bc1qpay',
|
|
40
|
+
protocol: 'bip322'
|
|
41
|
+
})
|
|
42
|
+
expect(sig).toEqual({ type: 'standard', signature: 'sigbase64' })
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('signPsbt flattens inputsToSign per index and maps SIGHASH to numbers', async () => {
|
|
46
|
+
const request = vi.fn().mockResolvedValue({ psbt: 'signedpsbt' })
|
|
47
|
+
const adapter = new WalletConnectBitcoinAdapter(ACCOUNT, request)
|
|
48
|
+
|
|
49
|
+
const signed = await adapter.signPsbt({
|
|
50
|
+
from: 'bc1qpay',
|
|
51
|
+
psbt: 'cHNidP8=',
|
|
52
|
+
inputsToSign: [
|
|
53
|
+
{
|
|
54
|
+
address: 'bc1qpay',
|
|
55
|
+
signingIndexes: [0, 2],
|
|
56
|
+
sigHash: 'ALL|ANYONECANPAY'
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
expect(request).toHaveBeenCalledWith('signPsbt', {
|
|
62
|
+
account: ACCOUNT,
|
|
63
|
+
psbt: 'cHNidP8=',
|
|
64
|
+
signInputs: [
|
|
65
|
+
{ address: 'bc1qpay', index: 0, sighashTypes: [0x81] },
|
|
66
|
+
{ address: 'bc1qpay', index: 2, sighashTypes: [0x81] }
|
|
67
|
+
],
|
|
68
|
+
broadcast: false
|
|
69
|
+
})
|
|
70
|
+
expect(signed).toBe('signedpsbt')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('signPsbt rejects when inputsToSign is missing (WC has no sign-all mode)', async () => {
|
|
74
|
+
const adapter = new WalletConnectBitcoinAdapter(ACCOUNT, vi.fn())
|
|
75
|
+
await expect(
|
|
76
|
+
adapter.signPsbt({ from: 'bc1qpay', psbt: 'cHNidP8=' })
|
|
77
|
+
).rejects.toBeInstanceOf(WalletConnectorError)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('sendTransfer maps to recipientAddress + amount string and returns the txid', async () => {
|
|
81
|
+
const request = vi.fn().mockResolvedValue({ txid: 'deadbeef' })
|
|
82
|
+
const adapter = new WalletConnectBitcoinAdapter(ACCOUNT, request)
|
|
83
|
+
|
|
84
|
+
const txid = await adapter.sendTransfer({
|
|
85
|
+
from: 'bc1qpay',
|
|
86
|
+
to: 'bc1qdest',
|
|
87
|
+
amount: 100000n
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(request).toHaveBeenCalledWith('sendTransfer', {
|
|
91
|
+
account: ACCOUNT,
|
|
92
|
+
recipientAddress: 'bc1qdest',
|
|
93
|
+
amount: '100000'
|
|
94
|
+
})
|
|
95
|
+
expect(txid).toBe('deadbeef')
|
|
96
|
+
})
|
|
97
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BitcoinAccount,
|
|
3
|
+
BitcoinPsbtSignRequest,
|
|
4
|
+
BitcoinSendTransferRequest,
|
|
5
|
+
BitcoinSigHash,
|
|
6
|
+
BitcoinSignMessageParams,
|
|
7
|
+
SignatureType,
|
|
8
|
+
TransactionResult
|
|
9
|
+
} from '@meshconnect/uwc-types'
|
|
10
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
11
|
+
import type { BitcoinTransportAdapter } from '../adapter'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Issues a WalletConnect JSON-RPC request for a bip122 method. The WalletConnect
|
|
15
|
+
* transport binds this to its live `SignClient` + session topic + chainId, e.g.
|
|
16
|
+
* `(method, params) => signClient.request({ topic, chainId, request: { method, params } })`.
|
|
17
|
+
* Kept as a plain function so `uwc-bitcoin` needs no `@walletconnect/*` dependency.
|
|
18
|
+
*/
|
|
19
|
+
export type BitcoinWalletConnectRequest = (
|
|
20
|
+
method: string,
|
|
21
|
+
params: Record<string, unknown>
|
|
22
|
+
) => Promise<unknown>
|
|
23
|
+
|
|
24
|
+
/** Maps a SIGHASH flag to its numeric value (Bitcoin Core encoding). */
|
|
25
|
+
const SIGHASH_VALUES: Record<BitcoinSigHash, number> = {
|
|
26
|
+
ALL: 0x01,
|
|
27
|
+
NONE: 0x02,
|
|
28
|
+
SINGLE: 0x03,
|
|
29
|
+
'ALL|ANYONECANPAY': 0x81,
|
|
30
|
+
'NONE|ANYONECANPAY': 0x82,
|
|
31
|
+
'SINGLE|ANYONECANPAY': 0x83
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface WcAccountAddress {
|
|
35
|
+
address: string
|
|
36
|
+
publicKey?: string
|
|
37
|
+
path?: string
|
|
38
|
+
intention?: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* WalletConnect (bip122) transport adapter — the Path A / mobile path (Trust
|
|
43
|
+
* Wallet, …). The WalletConnect transport pairs the session and constructs this
|
|
44
|
+
* adapter bound to the connected account's first external address plus a
|
|
45
|
+
* `request` function; this adapter maps our domain shapes onto the Reown/CAIP-2
|
|
46
|
+
* bip122 JSON-RPC methods (`getAccountAddresses`, `signMessage`, `signPsbt`,
|
|
47
|
+
* `sendTransfer`).
|
|
48
|
+
*
|
|
49
|
+
* Wire format follows the Reown Bitcoin RPC reference (what wallets implement
|
|
50
|
+
* over WalletConnect) — all-JSON strings, `account` on every request. NOTE:
|
|
51
|
+
* untested end-to-end; grounded on the spec, not a live wallet.
|
|
52
|
+
*/
|
|
53
|
+
export class WalletConnectBitcoinAdapter implements BitcoinTransportAdapter {
|
|
54
|
+
/**
|
|
55
|
+
* @param account The connected account's first external address — the
|
|
56
|
+
* `account` parameter every bip122 method requires.
|
|
57
|
+
* @param request Issues the bound WalletConnect JSON-RPC request.
|
|
58
|
+
*/
|
|
59
|
+
constructor(
|
|
60
|
+
private readonly account: string,
|
|
61
|
+
private readonly request: BitcoinWalletConnectRequest
|
|
62
|
+
) {}
|
|
63
|
+
|
|
64
|
+
async getAccounts(): Promise<BitcoinAccount[]> {
|
|
65
|
+
const result = (await this.request('getAccountAddresses', {
|
|
66
|
+
account: this.account
|
|
67
|
+
})) as WcAccountAddress[]
|
|
68
|
+
return result.map(entry => ({
|
|
69
|
+
address: entry.address,
|
|
70
|
+
// WalletConnect wallets MAY omit the public key; empty when absent.
|
|
71
|
+
publicKey: entry.publicKey ?? '',
|
|
72
|
+
...(entry.intention === 'ordinal'
|
|
73
|
+
? { purpose: 'ordinals' as const }
|
|
74
|
+
: entry.intention === 'payment'
|
|
75
|
+
? { purpose: 'payment' as const }
|
|
76
|
+
: {})
|
|
77
|
+
}))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async signMessage(params: BitcoinSignMessageParams): Promise<SignatureType> {
|
|
81
|
+
const result = (await this.request('signMessage', {
|
|
82
|
+
account: this.account,
|
|
83
|
+
message: params.message,
|
|
84
|
+
address: params.account.address,
|
|
85
|
+
...(params.protocol ? { protocol: params.protocol } : {})
|
|
86
|
+
})) as { signature: string }
|
|
87
|
+
return { type: 'standard', signature: result.signature }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async signPsbt(request: BitcoinPsbtSignRequest): Promise<string> {
|
|
91
|
+
if (!request.inputsToSign?.length) {
|
|
92
|
+
throw new WalletConnectorError({
|
|
93
|
+
type: 'unknown',
|
|
94
|
+
message:
|
|
95
|
+
'walletconnect bitcoin: signPsbt requires inputsToSign (the WC signPsbt method has no "sign all" mode)'
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
const signInputs = request.inputsToSign.flatMap(input =>
|
|
99
|
+
input.signingIndexes.map(index => ({
|
|
100
|
+
address: input.address,
|
|
101
|
+
index,
|
|
102
|
+
...(input.sigHash
|
|
103
|
+
? { sighashTypes: [SIGHASH_VALUES[input.sigHash]] }
|
|
104
|
+
: {})
|
|
105
|
+
}))
|
|
106
|
+
)
|
|
107
|
+
const result = (await this.request('signPsbt', {
|
|
108
|
+
account: this.account,
|
|
109
|
+
psbt: request.psbt,
|
|
110
|
+
signInputs,
|
|
111
|
+
broadcast: false
|
|
112
|
+
})) as { psbt: string }
|
|
113
|
+
return result.psbt
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async sendTransfer(
|
|
117
|
+
request: BitcoinSendTransferRequest
|
|
118
|
+
): Promise<TransactionResult> {
|
|
119
|
+
const result = (await this.request('sendTransfer', {
|
|
120
|
+
account: this.account,
|
|
121
|
+
recipientAddress: request.to,
|
|
122
|
+
amount: request.amount.toString()
|
|
123
|
+
})) as { txid: string }
|
|
124
|
+
return result.txid
|
|
125
|
+
}
|
|
126
|
+
}
|