@meshconnect/uwc-bitcoin 0.4.3-snapshot.c8ab084 → 0.5.0
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/bitcoin-namespace.d.ts +11 -4
- package/dist/bitcoin-namespace.d.ts.map +1 -1
- package/dist/bitcoin-namespace.js +18 -4
- package/dist/bitcoin-namespace.js.map +1 -1
- package/dist/broadcast.d.ts +24 -0
- package/dist/broadcast.d.ts.map +1 -0
- package/dist/broadcast.js +68 -0
- package/dist/broadcast.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/psbt.d.ts +31 -0
- package/dist/psbt.d.ts.map +1 -0
- package/dist/psbt.js +299 -0
- package/dist/psbt.js.map +1 -0
- package/package.json +3 -3
- package/src/bitcoin-namespace.test.ts +65 -1
- package/src/bitcoin-namespace.ts +22 -4
- package/src/broadcast.test.ts +137 -0
- package/src/broadcast.ts +75 -0
- package/src/index.ts +11 -1
- package/src/psbt.test.ts +113 -0
- package/src/psbt.ts +373 -0
package/src/psbt.ts
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { WalletConnectorError } from '@meshconnect/uwc-types'
|
|
2
|
+
import { bytesToHex, hexToBytes } from './encoding'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Finalize a wallet-signed PSBT (BIP-174 v0) and extract the network-ready
|
|
6
|
+
* raw transaction as hex.
|
|
7
|
+
*
|
|
8
|
+
* Hand-rolled rather than pulling in a Bitcoin library so `uwc-bitcoin` stays
|
|
9
|
+
* dependency-free. That is only safe because the scope is deliberately narrow:
|
|
10
|
+
* single-key spends, where finalizing is pure re-arrangement of what the wallet
|
|
11
|
+
* already produced — no script interpretation, no signature checks. Anything
|
|
12
|
+
* else (multisig, Taproot script-path, bare scripts) throws instead of guessing.
|
|
13
|
+
*
|
|
14
|
+
* Per input, in order:
|
|
15
|
+
* - Already final (`final_scriptsig` / `final_scriptwitness` present) → used
|
|
16
|
+
* as-is. Required, not optional: OKX finalizes every input it signs even when
|
|
17
|
+
* asked not to (`autoFinalized: false`), so a finalizer that only accepted
|
|
18
|
+
* partial signatures would reject every OKX transaction.
|
|
19
|
+
* - P2WPKH, P2TR key-path, P2PKH, P2SH-P2WPKH → finalized from the partial
|
|
20
|
+
* signature (or `tap_key_sig`).
|
|
21
|
+
*
|
|
22
|
+
* The caller owns the trust decision about the PSBT's contents — this never
|
|
23
|
+
* inspects outputs or amounts.
|
|
24
|
+
*/
|
|
25
|
+
export function finalizePsbt(signedPsbt: Uint8Array): string {
|
|
26
|
+
const psbt = parsePsbt(signedPsbt)
|
|
27
|
+
const finals = psbt.inputs.map((input, index) =>
|
|
28
|
+
finalizeInput(input, index, psbt.tx.inputs[index]!.prevIndex)
|
|
29
|
+
)
|
|
30
|
+
return bytesToHex(serializeTransaction(psbt.tx, finals))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The txid of a raw transaction (hex, legacy or segwit serialization): the
|
|
35
|
+
* double SHA-256 of its witness-stripped serialization, byte-reversed.
|
|
36
|
+
*
|
|
37
|
+
* Lets a broadcaster know the txid before the network answers, so a lost
|
|
38
|
+
* response or an "already known" rejection can still be tied to the payment.
|
|
39
|
+
*/
|
|
40
|
+
export async function computeTxid(rawTxHex: string): Promise<string> {
|
|
41
|
+
const tx = parseTransaction(hexToBytes(rawTxHex))
|
|
42
|
+
const stripped = serializeTransaction(
|
|
43
|
+
tx,
|
|
44
|
+
tx.inputs.map(input => ({ scriptSig: input.scriptSig, witness: [] }))
|
|
45
|
+
)
|
|
46
|
+
const once = await crypto.subtle.digest('SHA-256', stripped)
|
|
47
|
+
const twice = new Uint8Array(await crypto.subtle.digest('SHA-256', once))
|
|
48
|
+
return bytesToHex(twice.reverse())
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// BIP-174 key types used here.
|
|
52
|
+
const GLOBAL_UNSIGNED_TX = 0x00
|
|
53
|
+
const GLOBAL_VERSION = 0xfb
|
|
54
|
+
const IN_NON_WITNESS_UTXO = 0x00
|
|
55
|
+
const IN_WITNESS_UTXO = 0x01
|
|
56
|
+
const IN_PARTIAL_SIG = 0x02
|
|
57
|
+
const IN_REDEEM_SCRIPT = 0x04
|
|
58
|
+
const IN_FINAL_SCRIPTSIG = 0x07
|
|
59
|
+
const IN_FINAL_SCRIPTWITNESS = 0x08
|
|
60
|
+
const IN_TAP_KEY_SIG = 0x13
|
|
61
|
+
|
|
62
|
+
const PSBT_MAGIC = [0x70, 0x73, 0x62, 0x74, 0xff]
|
|
63
|
+
|
|
64
|
+
interface TxInput {
|
|
65
|
+
/** Serialized outpoint (32-byte txid LE + 4-byte index LE). */
|
|
66
|
+
outpoint: Uint8Array
|
|
67
|
+
prevIndex: number
|
|
68
|
+
scriptSig: Uint8Array
|
|
69
|
+
sequence: Uint8Array
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Transaction {
|
|
73
|
+
version: Uint8Array
|
|
74
|
+
inputs: TxInput[]
|
|
75
|
+
outputs: Uint8Array[]
|
|
76
|
+
locktime: Uint8Array
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface KeyValue {
|
|
80
|
+
type: number
|
|
81
|
+
keyData: Uint8Array
|
|
82
|
+
value: Uint8Array
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ParsedPsbt {
|
|
86
|
+
tx: Transaction
|
|
87
|
+
inputs: KeyValue[][]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface FinalInput {
|
|
91
|
+
scriptSig: Uint8Array
|
|
92
|
+
witness: Uint8Array[]
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function fail(message: string): never {
|
|
96
|
+
throw new WalletConnectorError({
|
|
97
|
+
type: 'unknown',
|
|
98
|
+
message: `finalizePsbt: ${message}`
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class Reader {
|
|
103
|
+
private offset = 0
|
|
104
|
+
|
|
105
|
+
constructor(private readonly bytes: Uint8Array) {}
|
|
106
|
+
|
|
107
|
+
take(length: number): Uint8Array {
|
|
108
|
+
if (this.offset + length > this.bytes.length) fail('truncated data')
|
|
109
|
+
const slice = this.bytes.subarray(this.offset, this.offset + length)
|
|
110
|
+
this.offset += length
|
|
111
|
+
return slice
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
byte(): number {
|
|
115
|
+
return this.take(1)[0]!
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
uint32(): number {
|
|
119
|
+
const b = this.take(4)
|
|
120
|
+
return (b[0]! | (b[1]! << 8) | (b[2]! << 16) | (b[3]! << 24)) >>> 0
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
compactSize(): number {
|
|
124
|
+
const first = this.byte()
|
|
125
|
+
if (first < 0xfd) return first
|
|
126
|
+
if (first === 0xfd) {
|
|
127
|
+
const b = this.take(2)
|
|
128
|
+
return b[0]! | (b[1]! << 8)
|
|
129
|
+
}
|
|
130
|
+
if (first === 0xfe) return this.uint32()
|
|
131
|
+
const low = this.uint32()
|
|
132
|
+
const high = this.uint32()
|
|
133
|
+
// No PSBT field legitimately approaches 2^53; anything that large is corrupt.
|
|
134
|
+
if (high > 0x1fffff) fail('length out of range')
|
|
135
|
+
return high * 0x100000000 + low
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
varBytes(): Uint8Array {
|
|
139
|
+
return this.take(this.compactSize())
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function parsePsbt(bytes: Uint8Array): ParsedPsbt {
|
|
144
|
+
const reader = new Reader(bytes)
|
|
145
|
+
if (!PSBT_MAGIC.every(b => reader.byte() === b)) fail('not a PSBT')
|
|
146
|
+
|
|
147
|
+
const global = readMap(reader)
|
|
148
|
+
const version = global.find(kv => kv.type === GLOBAL_VERSION)
|
|
149
|
+
if (version && new Reader(version.value).uint32() !== 0) {
|
|
150
|
+
fail('only PSBT version 0 is supported')
|
|
151
|
+
}
|
|
152
|
+
const unsignedTx = global.find(kv => kv.type === GLOBAL_UNSIGNED_TX)
|
|
153
|
+
if (!unsignedTx) fail('missing unsigned transaction')
|
|
154
|
+
const tx = parseTransaction(unsignedTx.value)
|
|
155
|
+
|
|
156
|
+
const inputs = tx.inputs.map(() => readMap(reader))
|
|
157
|
+
// Output maps carry nothing extraction needs, but must be present — a PSBT
|
|
158
|
+
// cut short there is malformed and should not be trusted.
|
|
159
|
+
for (let i = 0; i < tx.outputs.length; i++) readMap(reader)
|
|
160
|
+
return { tx, inputs }
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function readMap(reader: Reader): KeyValue[] {
|
|
164
|
+
const map: KeyValue[] = []
|
|
165
|
+
for (;;) {
|
|
166
|
+
const key = reader.varBytes()
|
|
167
|
+
if (key.length === 0) return map
|
|
168
|
+
map.push({
|
|
169
|
+
type: key[0]!,
|
|
170
|
+
keyData: key.subarray(1),
|
|
171
|
+
value: reader.varBytes()
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Parse a transaction in either legacy or segwit serialization. */
|
|
177
|
+
function parseTransaction(bytes: Uint8Array): Transaction {
|
|
178
|
+
const reader = new Reader(bytes)
|
|
179
|
+
const version = reader.take(4)
|
|
180
|
+
let inputCount = reader.compactSize()
|
|
181
|
+
const segwit = inputCount === 0
|
|
182
|
+
if (segwit) {
|
|
183
|
+
if (reader.byte() !== 0x01) fail('invalid segwit flag')
|
|
184
|
+
inputCount = reader.compactSize()
|
|
185
|
+
}
|
|
186
|
+
const inputs: TxInput[] = []
|
|
187
|
+
for (let i = 0; i < inputCount; i++) {
|
|
188
|
+
const outpoint = reader.take(36)
|
|
189
|
+
inputs.push({
|
|
190
|
+
outpoint,
|
|
191
|
+
prevIndex: new Reader(outpoint.subarray(32)).uint32(),
|
|
192
|
+
scriptSig: reader.varBytes(),
|
|
193
|
+
sequence: reader.take(4)
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
const outputCount = reader.compactSize()
|
|
197
|
+
const outputs: Uint8Array[] = []
|
|
198
|
+
for (let i = 0; i < outputCount; i++) {
|
|
199
|
+
const value = reader.take(8)
|
|
200
|
+
const script = reader.varBytes()
|
|
201
|
+
outputs.push(concat([value, compactSize(script.length), script]))
|
|
202
|
+
}
|
|
203
|
+
if (segwit) {
|
|
204
|
+
for (let i = 0; i < inputCount; i++) {
|
|
205
|
+
const items = reader.compactSize()
|
|
206
|
+
for (let j = 0; j < items; j++) reader.varBytes()
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return { version, inputs, outputs, locktime: reader.take(4) }
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function finalizeInput(
|
|
213
|
+
input: KeyValue[],
|
|
214
|
+
index: number,
|
|
215
|
+
prevIndex: number
|
|
216
|
+
): FinalInput {
|
|
217
|
+
const get = (type: number) => input.find(kv => kv.type === type)?.value
|
|
218
|
+
|
|
219
|
+
const finalScriptSig = get(IN_FINAL_SCRIPTSIG)
|
|
220
|
+
const finalWitness = get(IN_FINAL_SCRIPTWITNESS)
|
|
221
|
+
if (finalScriptSig || finalWitness) {
|
|
222
|
+
return {
|
|
223
|
+
scriptSig: finalScriptSig ?? new Uint8Array(),
|
|
224
|
+
witness: finalWitness ? parseWitness(finalWitness) : []
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const script = prevoutScript(input, index, prevIndex)
|
|
229
|
+
const partialSigs = input.filter(kv => kv.type === IN_PARTIAL_SIG)
|
|
230
|
+
const singleSig = (): [Uint8Array, Uint8Array] => {
|
|
231
|
+
if (partialSigs.length === 0) fail(`input ${index} is not signed`)
|
|
232
|
+
if (partialSigs.length > 1) {
|
|
233
|
+
fail(
|
|
234
|
+
`input ${index} has ${partialSigs.length} signatures; only single-key spends are supported`
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
const { value: signature, keyData: pubkey } = partialSigs[0]!
|
|
238
|
+
return [signature, pubkey]
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (isP2wpkh(script)) {
|
|
242
|
+
return { scriptSig: new Uint8Array(), witness: singleSig() }
|
|
243
|
+
}
|
|
244
|
+
if (isP2tr(script)) {
|
|
245
|
+
const tapKeySig = get(IN_TAP_KEY_SIG)
|
|
246
|
+
if (!tapKeySig) {
|
|
247
|
+
fail(
|
|
248
|
+
`input ${index} has no Taproot key-path signature; script-path spends are not supported`
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
return { scriptSig: new Uint8Array(), witness: [tapKeySig] }
|
|
252
|
+
}
|
|
253
|
+
if (isP2pkh(script)) {
|
|
254
|
+
const [signature, pubkey] = singleSig()
|
|
255
|
+
return {
|
|
256
|
+
scriptSig: concat([pushData(signature), pushData(pubkey)]),
|
|
257
|
+
witness: []
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (isP2sh(script)) {
|
|
261
|
+
const redeemScript = get(IN_REDEEM_SCRIPT)
|
|
262
|
+
if (!redeemScript || !isP2wpkh(redeemScript)) {
|
|
263
|
+
fail(`input ${index} is P2SH but not P2SH-P2WPKH; unsupported`)
|
|
264
|
+
}
|
|
265
|
+
return { scriptSig: pushData(redeemScript), witness: singleSig() }
|
|
266
|
+
}
|
|
267
|
+
fail(`input ${index} has an unsupported script type`)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** The scriptPubKey this input spends, from whichever UTXO field is present. */
|
|
271
|
+
function prevoutScript(
|
|
272
|
+
input: KeyValue[],
|
|
273
|
+
index: number,
|
|
274
|
+
prevIndex: number
|
|
275
|
+
): Uint8Array {
|
|
276
|
+
const witnessUtxo = input.find(kv => kv.type === IN_WITNESS_UTXO)?.value
|
|
277
|
+
if (witnessUtxo) {
|
|
278
|
+
const reader = new Reader(witnessUtxo)
|
|
279
|
+
reader.take(8) // amount
|
|
280
|
+
return reader.varBytes()
|
|
281
|
+
}
|
|
282
|
+
const nonWitnessUtxo = input.find(
|
|
283
|
+
kv => kv.type === IN_NON_WITNESS_UTXO
|
|
284
|
+
)?.value
|
|
285
|
+
if (nonWitnessUtxo) {
|
|
286
|
+
const output = parseTransaction(nonWitnessUtxo).outputs[prevIndex]
|
|
287
|
+
if (!output) fail(`input ${index} spends an output its UTXO does not have`)
|
|
288
|
+
const reader = new Reader(output)
|
|
289
|
+
reader.take(8)
|
|
290
|
+
return reader.varBytes()
|
|
291
|
+
}
|
|
292
|
+
fail(`input ${index} has no UTXO information`)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function parseWitness(bytes: Uint8Array): Uint8Array[] {
|
|
296
|
+
const reader = new Reader(bytes)
|
|
297
|
+
const items: Uint8Array[] = []
|
|
298
|
+
const count = reader.compactSize()
|
|
299
|
+
for (let i = 0; i < count; i++) items.push(reader.varBytes())
|
|
300
|
+
return items
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const isP2wpkh = (s: Uint8Array) =>
|
|
304
|
+
s.length === 22 && s[0] === 0x00 && s[1] === 0x14
|
|
305
|
+
const isP2tr = (s: Uint8Array) =>
|
|
306
|
+
s.length === 34 && s[0] === 0x51 && s[1] === 0x20
|
|
307
|
+
const isP2pkh = (s: Uint8Array) =>
|
|
308
|
+
s.length === 25 &&
|
|
309
|
+
s[0] === 0x76 &&
|
|
310
|
+
s[1] === 0xa9 &&
|
|
311
|
+
s[2] === 0x14 &&
|
|
312
|
+
s[23] === 0x88 &&
|
|
313
|
+
s[24] === 0xac
|
|
314
|
+
const isP2sh = (s: Uint8Array) =>
|
|
315
|
+
s.length === 23 && s[0] === 0xa9 && s[1] === 0x14 && s[22] === 0x87
|
|
316
|
+
|
|
317
|
+
function serializeTransaction(
|
|
318
|
+
tx: Transaction,
|
|
319
|
+
finals: FinalInput[]
|
|
320
|
+
): Uint8Array<ArrayBuffer> {
|
|
321
|
+
const segwit = finals.some(f => f.witness.length > 0)
|
|
322
|
+
const parts: Uint8Array[] = [tx.version]
|
|
323
|
+
if (segwit) parts.push(Uint8Array.of(0x00, 0x01))
|
|
324
|
+
parts.push(compactSize(tx.inputs.length))
|
|
325
|
+
tx.inputs.forEach((input, i) => {
|
|
326
|
+
const { scriptSig } = finals[i]!
|
|
327
|
+
parts.push(
|
|
328
|
+
input.outpoint,
|
|
329
|
+
compactSize(scriptSig.length),
|
|
330
|
+
scriptSig,
|
|
331
|
+
input.sequence
|
|
332
|
+
)
|
|
333
|
+
})
|
|
334
|
+
parts.push(compactSize(tx.outputs.length), ...tx.outputs)
|
|
335
|
+
if (segwit) {
|
|
336
|
+
for (const { witness } of finals) {
|
|
337
|
+
parts.push(compactSize(witness.length))
|
|
338
|
+
for (const item of witness) parts.push(compactSize(item.length), item)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
parts.push(tx.locktime)
|
|
342
|
+
return concat(parts)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** Minimal script push for data up to 255 bytes (signatures, keys, redeem scripts). */
|
|
346
|
+
function pushData(data: Uint8Array): Uint8Array {
|
|
347
|
+
if (data.length <= 75) return concat([Uint8Array.of(data.length), data])
|
|
348
|
+
if (data.length <= 0xff)
|
|
349
|
+
return concat([Uint8Array.of(0x4c, data.length), data])
|
|
350
|
+
fail('script push too large')
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function compactSize(n: number): Uint8Array {
|
|
354
|
+
if (n < 0xfd) return Uint8Array.of(n)
|
|
355
|
+
if (n <= 0xffff) return Uint8Array.of(0xfd, n & 0xff, n >> 8)
|
|
356
|
+
return Uint8Array.of(
|
|
357
|
+
0xfe,
|
|
358
|
+
n & 0xff,
|
|
359
|
+
(n >> 8) & 0xff,
|
|
360
|
+
(n >> 16) & 0xff,
|
|
361
|
+
n >>> 24
|
|
362
|
+
)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function concat(parts: Uint8Array[]): Uint8Array<ArrayBuffer> {
|
|
366
|
+
const out = new Uint8Array(parts.reduce((sum, p) => sum + p.length, 0))
|
|
367
|
+
let offset = 0
|
|
368
|
+
for (const part of parts) {
|
|
369
|
+
out.set(part, offset)
|
|
370
|
+
offset += part.length
|
|
371
|
+
}
|
|
372
|
+
return out
|
|
373
|
+
}
|