@shapeshiftoss/hdwallet-gridplus 1.62.10-alpha.3 → 1.62.10

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/src/bitcoin.ts CHANGED
@@ -1,193 +1,711 @@
1
+ import { pointCompress } from "@bitcoinerlab/secp256k1";
1
2
  import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib";
2
3
  import * as core from "@shapeshiftoss/hdwallet-core";
3
- import { toCashAddress, toLegacyAddress } from "bchaddrjs";
4
+ import * as bchAddr from "bchaddrjs";
5
+ import * as bech32 from "bech32";
6
+ import { decode as bs58Decode } from "bs58check";
7
+ import CryptoJS from "crypto-js";
4
8
  import { Client, Constants } from "gridplus-sdk";
5
9
 
6
- import { getCompressedPubkey } from "./utils";
10
+ import { UTXO_NETWORK_PARAMS } from "./constants";
11
+ import { deriveAddressFromPubkey } from "./utils";
7
12
 
8
- export function deriveAddressFromPubkey(
9
- pubkey: Buffer,
10
- coin: string,
11
- scriptType: core.BTCScriptType = core.BTCInputScriptType.SpendAddress
12
- ): string | undefined {
13
- const network = core.getNetwork(coin, scriptType);
14
- return core.createPayment(pubkey, network, scriptType).address;
15
- }
13
+ const u32le = (n: number): Buffer => {
14
+ const b = Buffer.alloc(4);
15
+ b.writeUInt32LE(n >>> 0, 0);
16
+ return b;
17
+ };
16
18
 
17
- const getPublicKey = async (client: Client, addressNList: core.BIP32Path): Promise<Buffer> => {
18
- const pubkey = (
19
- await client.getAddresses({ startPath: addressNList, n: 1, flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB })
20
- )[0];
19
+ const scriptTypeToPurpose = (scriptType: core.BTCInputScriptType): number => {
20
+ switch (scriptType) {
21
+ case core.BTCInputScriptType.SpendAddress:
22
+ return 44;
23
+ case core.BTCInputScriptType.SpendP2SHWitness:
24
+ return 49;
25
+ case core.BTCInputScriptType.SpendWitness:
26
+ return 84;
27
+ default:
28
+ return 44;
29
+ }
30
+ };
21
31
 
22
- if (!pubkey) throw new Error("No public key returned from device");
23
- if (!Buffer.isBuffer(pubkey)) throw new Error("Invalid public key returned from device");
32
+ const encodeDerInteger = (x: Buffer): Buffer => {
33
+ if (x[0] & 0x80) {
34
+ return Buffer.concat([Buffer.from([0x00]), x]);
35
+ }
36
+ return x;
37
+ };
24
38
 
25
- return getCompressedPubkey(pubkey);
39
+ const encodeDerSignature = (rBuf: Buffer, sBuf: Buffer, sigHashType: number): Buffer => {
40
+ const rEncoded = encodeDerInteger(rBuf);
41
+ const sEncoded = encodeDerInteger(sBuf);
42
+
43
+ const derSignature = Buffer.concat([
44
+ Buffer.from([0x30]),
45
+ Buffer.from([rEncoded.length + sEncoded.length + 4]),
46
+ Buffer.from([0x02]),
47
+ Buffer.from([rEncoded.length]),
48
+ rEncoded,
49
+ Buffer.from([0x02]),
50
+ Buffer.from([sEncoded.length]),
51
+ sEncoded,
52
+ Buffer.from([sigHashType]),
53
+ ]);
54
+
55
+ return derSignature;
26
56
  };
27
57
 
28
- export async function btcGetAddress(client: Client, msg: core.BTCGetAddress): Promise<string | null> {
29
- const pubkey = await getPublicKey(client, msg.addressNList);
58
+ export const btcGetAccountPaths = (msg: core.BTCGetAccountPaths): Array<core.BTCAccountPath> => {
59
+ const slip44 = core.slip44ByCoin(msg.coin);
60
+ if (!slip44) throw new Error(`Unsupported coin: ${msg.coin}`);
61
+
62
+ const scriptTypes: core.BTCInputScriptType[] = (() => {
63
+ if (msg.coin === "Dogecoin" || msg.coin === "BitcoinCash") {
64
+ return [core.BTCInputScriptType.SpendAddress];
65
+ } else {
66
+ return [
67
+ core.BTCInputScriptType.SpendAddress,
68
+ core.BTCInputScriptType.SpendP2SHWitness,
69
+ core.BTCInputScriptType.SpendWitness,
70
+ ];
71
+ }
72
+ })();
73
+
74
+ return scriptTypes.map((scriptType) => {
75
+ const purpose = scriptTypeToPurpose(scriptType);
76
+ return {
77
+ coin: msg.coin,
78
+ scriptType,
79
+ addressNList: [0x80000000 + purpose, 0x80000000 + slip44, 0x80000000 + (msg.accountIdx || 0), 0, 0],
80
+ };
81
+ });
82
+ };
30
83
 
31
- const address = deriveAddressFromPubkey(pubkey, msg.coin, msg.scriptType);
32
- if (!address) return null;
84
+ export async function btcGetAddress(client: Client, msg: core.BTCGetAddress): Promise<string | null> {
85
+ // Get compressed public key from device (works for all UTXOs)
86
+ // Using SECP256K1_PUB flag bypasses Lattice's address formatting,
87
+ // which only supports Bitcoin/EVM chains/Solana
88
+ const pubkeys = await client.getAddresses({
89
+ startPath: msg.addressNList,
90
+ n: 1,
91
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
92
+ });
93
+
94
+ if (!pubkeys || !pubkeys.length) {
95
+ throw new Error("No public key returned from device");
96
+ }
33
97
 
34
- return msg.coin.toLowerCase() === "bitcoincash" ? toCashAddress(address) : address;
35
- }
98
+ // pubkeys[0] may be uncompressed (65 bytes) or compressed (33 bytes)
99
+ const pubkeyBuffer = Buffer.isBuffer(pubkeys[0]) ? pubkeys[0] : Buffer.from(pubkeys[0], "hex");
36
100
 
37
- export async function btcSignTx(client: Client, msg: core.BTCSignTx): Promise<core.BTCSignedTx | null> {
38
- const spendOutputs = msg.outputs.filter((o) => !o.isChange);
101
+ // Compress if needed (65 bytes = uncompressed, 33 bytes = already compressed)
102
+ const pubkeyHex =
103
+ pubkeyBuffer.length === 65
104
+ ? Buffer.from(pointCompress(pubkeyBuffer, true)).toString("hex")
105
+ : pubkeyBuffer.toString("hex");
39
106
 
40
- // GridPlus native BTC signing only supports single-recipient transactions without op_return data (Bitcoin only).
41
- // Use generic signing fallback for all other scenarios.
42
- if (msg.coin === "Bitcoin" && spendOutputs.length === 1 && !msg.opReturnData) {
43
- const spendOutput = spendOutputs[0];
107
+ // Derive address client-side using the coin's network parameters
108
+ const scriptType = msg.scriptType || core.BTCInputScriptType.SpendAddress;
109
+ const address = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
44
110
 
45
- const recipient = await (async () => {
46
- if (spendOutput.address) return spendOutput.address;
111
+ return address;
112
+ }
47
113
 
48
- if (spendOutput.addressNList) {
49
- const address = await btcGetAddress(client, {
50
- addressNList: spendOutput.addressNList,
51
- coin: msg.coin,
52
- scriptType: spendOutput.scriptType as unknown as core.BTCInputScriptType,
53
- });
114
+ export async function btcSignTx(client: Client, msg: core.BTCSignTx): Promise<core.BTCSignedTx | null> {
115
+ // All UTXOs (Bitcoin, Dogecoin, Litecoin, Bitcoin Cash) use Bitcoin-compatible transaction formats.
116
+ // The 'BTC' currency parameter is just SDK routing - the device signs Bitcoin-formatted transactions.
117
+ // Address derivation already handles all coins via client-side derivation with proper network parameters.
118
+
119
+ // Calculate fee: total inputs - total outputs
120
+ const totalInputValue = msg.inputs.reduce((sum, input) => sum + parseInt(input.amount || "0"), 0);
121
+ const totalOutputValue = msg.outputs.reduce((sum, output) => sum + parseInt(output.amount || "0"), 0);
122
+ const fee = totalInputValue - totalOutputValue;
123
+
124
+ // Find change output and its path
125
+ const changeOutput = msg.outputs.find((o) => o.isChange);
126
+ const changePath = changeOutput?.addressNList;
127
+
128
+ // SDK requires changePath even when there's no change output
129
+ // Use actual change path if available, otherwise use dummy path (first change address)
130
+ // The dummy path satisfies SDK validation but won't be used since there's no change output
131
+ const finalChangePath =
132
+ changePath && changePath.length === 5
133
+ ? changePath
134
+ : [
135
+ msg.inputs[0].addressNList[0], // purpose (44', 49', or 84')
136
+ msg.inputs[0].addressNList[1], // coin type
137
+ msg.inputs[0].addressNList[2], // account
138
+ 1, // change chain (1 = change, 0 = receive)
139
+ 0, // address index
140
+ ];
141
+
142
+ // Find the spend output (first non-change output)
143
+ // This ensures we don't accidentally use a change output as recipient
144
+ const spendOutput = msg.outputs.find((o) => !o.isChange) || msg.outputs[0];
145
+
146
+ if (!spendOutput.amount) {
147
+ throw new Error("missing amount for spend output");
148
+ }
54
149
 
55
- if (!address) throw new Error("No address returned from device");
150
+ // Determine spend address and value
151
+ let toAddress: string;
152
+ if (spendOutput.address) {
153
+ // Output already has address
154
+ toAddress = spendOutput.address;
155
+ } else if (spendOutput.addressNList) {
156
+ // Output only has addressNList - derive address from device
157
+ const pubkey = await client.getAddresses({
158
+ startPath: spendOutput.addressNList,
159
+ n: 1,
160
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
161
+ });
56
162
 
57
- return address;
58
- }
163
+ if (!pubkey || !pubkey.length) {
164
+ throw new Error("No public key for spend output");
165
+ }
59
166
 
60
- throw new Error("Invalid output (no address or addressNList specified).");
61
- })();
167
+ const pubkeyBuffer = Buffer.isBuffer(pubkey[0]) ? pubkey[0] : Buffer.from(pubkey[0], "hex");
168
+ const pubkeyHex =
169
+ pubkeyBuffer.length === 65
170
+ ? Buffer.from(pointCompress(pubkeyBuffer, true)).toString("hex")
171
+ : pubkeyBuffer.toString("hex");
62
172
 
63
- const fee =
64
- msg.inputs.reduce((sum, input) => sum + Number(input.amount), 0) -
65
- msg.outputs.reduce((sum, output) => sum + Number(output.amount), 0);
173
+ const scriptType =
174
+ (spendOutput.scriptType as unknown as core.BTCInputScriptType) || core.BTCInputScriptType.SpendAddress;
175
+ toAddress = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
176
+ } else {
177
+ throw new Error("Spend output must have either address or addressNList");
178
+ }
66
179
 
67
- // GridPlus SDK requires changePath even when there's no change output.
68
- // Derive a valid dummy change path from the first input for validation.
69
- const changePath = msg.outputs.find((o) => o.isChange && o.addressNList)?.addressNList ?? [
70
- ...msg.inputs[0].addressNList.slice(0, 3),
71
- 1,
72
- 0,
73
- ];
180
+ // Using parseInt instead of BigInt because GridPlus SDK payload requires `value: number`
181
+ // This is safe for Bitcoin amounts: max UTXO is 21M BTC (2.1 trillion satoshis) which is
182
+ // well under JavaScript's MAX_SAFE_INTEGER (9,007,199,254,740,991). SDK's internal
183
+ // writeUInt64LE converts to string for hex encoding, supporting the full 64-bit range.
184
+ const toValue = parseInt(spendOutput.amount, 10);
185
+ if (isNaN(toValue)) {
186
+ throw new Error(`Invalid amount for spend output: ${spendOutput.amount}`);
187
+ }
74
188
 
75
- const { sigs, tx } = await client.sign({
189
+ // Build base payload for GridPlus SDK
190
+ // Note: Input values also use parseInt for same reason as toValue above (SDK constraint + safe range)
191
+ const payload: {
192
+ prevOuts: Array<{ txHash: string; value: number; index: number; signerPath: number[] }>;
193
+ recipient: string;
194
+ value: number;
195
+ fee: number;
196
+ changePath: number[];
197
+ } = {
198
+ prevOuts: msg.inputs.map((input) => ({
199
+ txHash: input.txid,
200
+ value: parseInt(input.amount || "0", 10),
201
+ index: input.vout,
202
+ signerPath: input.addressNList,
203
+ })),
204
+ recipient: toAddress,
205
+ value: toValue,
206
+ fee: fee,
207
+ changePath: finalChangePath,
208
+ };
209
+
210
+ if (msg.coin === "Bitcoin") {
211
+ const signData = await client.sign({
76
212
  currency: "BTC",
77
- data: {
78
- prevOuts: msg.inputs.map((input) => ({
79
- txHash: input.txid,
80
- value: Number(input.amount),
81
- index: input.vout,
82
- signerPath: input.addressNList,
83
- })),
84
- recipient,
85
- value: Number(spendOutput.amount),
86
- fee,
87
- changePath,
88
- },
213
+ data: payload,
89
214
  });
90
215
 
91
- if (!tx) throw new Error("Failed to sign transaction - missing serialized tx");
92
- if (!sigs) throw new Error("Failed to sign transaction - missing signatures");
216
+ if (!signData || !signData.tx) {
217
+ throw new Error("No signed transaction returned from device");
218
+ }
219
+
220
+ const signatures = signData.sigs ? signData.sigs.map((s: Buffer) => s.toString("hex")) : [];
93
221
 
94
- return { signatures: sigs.map((s) => s.toString("hex")), serializedTx: tx };
222
+ return {
223
+ signatures,
224
+ serializedTx: signData.tx,
225
+ };
95
226
  } else {
96
- const psbt = new bitcoin.Psbt({
97
- network: core.getNetwork(msg.coin, core.BTCOutputScriptType.PayToMultisig),
98
- forkCoin: msg.coin.toLowerCase() === "bitcoincash" ? "bch" : "none",
99
- });
227
+ const network = UTXO_NETWORK_PARAMS[msg.coin];
228
+ if (!network) {
229
+ throw new Error(`Unsupported UTXO coin: ${msg.coin}`);
230
+ }
100
231
 
101
- psbt.setVersion(msg.version ?? 2);
102
- msg.locktime && psbt.setLocktime(msg.locktime);
232
+ const tx = new bitcoin.Transaction();
103
233
 
104
234
  for (const input of msg.inputs) {
105
- if (!input.hex) throw new Error("Invalid input (missing hex)");
235
+ const txHashBuffer = Buffer.from(input.txid, "hex").reverse();
236
+ tx.addInput(txHashBuffer, input.vout);
237
+ }
106
238
 
107
- const pubkey = await getPublicKey(client, input.addressNList);
108
- const network = core.getNetwork(msg.coin, input.scriptType);
109
- const redeemScript = core.createPayment(pubkey, network, input.scriptType)?.redeem?.output;
239
+ for (let outputIdx = 0; outputIdx < msg.outputs.length; outputIdx++) {
240
+ const output = msg.outputs[outputIdx];
241
+ let address: string;
242
+
243
+ if (output.address) {
244
+ address = output.address;
245
+ } else if (output.addressNList) {
246
+ // Derive address for change output
247
+ const pubkey = await client.getAddresses({
248
+ startPath: output.addressNList,
249
+ n: 1,
250
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
251
+ });
110
252
 
111
- psbt.addInput({
112
- hash: input.txid,
113
- index: input.vout,
114
- nonWitnessUtxo: Buffer.from(input.hex, "hex"),
115
- ...(redeemScript && { redeemScript }),
116
- });
117
- }
253
+ if (!pubkey || !pubkey.length) {
254
+ throw new Error(`No public key for output`);
255
+ }
118
256
 
119
- for (const output of msg.outputs) {
120
- if (!output.amount) throw new Error("Invalid output (missing amount)");
257
+ const pubkeyBuffer = Buffer.isBuffer(pubkey[0]) ? pubkey[0] : Buffer.from(pubkey[0], "hex");
121
258
 
122
- const address = await (async () => {
123
- if (!output.address && !output.addressNList) {
124
- throw new Error("Invalid output (missing address or addressNList)");
259
+ const pubkeyHex =
260
+ pubkeyBuffer.length === 65
261
+ ? Buffer.from(pointCompress(pubkeyBuffer, true)).toString("hex")
262
+ : pubkeyBuffer.toString("hex");
263
+
264
+ const scriptType =
265
+ (output.scriptType as unknown as core.BTCInputScriptType) || core.BTCInputScriptType.SpendAddress;
266
+ address = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
267
+ } else {
268
+ throw new Error("Output must have either address or addressNList");
269
+ }
270
+
271
+ const { scriptPubKey: outputScriptPubKey } = (() => {
272
+ // Native SegWit (bech32): ltc1 for Litecoin, bc1 for Bitcoin
273
+ if (address.startsWith("ltc1") || address.startsWith("bc1")) {
274
+ const decoded = bech32.decode(address);
275
+ const hash160 = Buffer.from(bech32.fromWords(decoded.words.slice(1)));
276
+
277
+ const scriptPubKey = bitcoin.script.compile([bitcoin.opcodes.OP_0, hash160]);
278
+ return { scriptPubKey };
125
279
  }
126
280
 
127
- const _address =
128
- output.address ??
129
- (await btcGetAddress(client, {
130
- addressNList: output.addressNList,
131
- coin: msg.coin,
132
- scriptType: output.scriptType as any,
133
- }));
281
+ // Bitcoin Cash CashAddr format: bitcoincash: prefix or q for mainnet
282
+ if (address.startsWith("bitcoincash:") || address.startsWith("q")) {
283
+ const legacyAddress = bchAddr.toLegacyAddress(address);
284
+ const decoded = bs58Decode(legacyAddress);
285
+ const versionByte = decoded[0];
286
+ const hash160 = decoded.slice(1);
287
+
288
+ // Check if P2SH (Bitcoin Cash uses 0x05 for P2SH)
289
+ if (versionByte === network.scriptHash) {
290
+ const scriptPubKey = bitcoin.script.compile([
291
+ bitcoin.opcodes.OP_HASH160,
292
+ hash160,
293
+ bitcoin.opcodes.OP_EQUAL,
294
+ ]);
295
+ return { scriptPubKey };
296
+ }
297
+
298
+ // P2PKH
299
+ const scriptPubKey = bitcoin.script.compile([
300
+ bitcoin.opcodes.OP_DUP,
301
+ bitcoin.opcodes.OP_HASH160,
302
+ hash160,
303
+ bitcoin.opcodes.OP_EQUALVERIFY,
304
+ bitcoin.opcodes.OP_CHECKSIG,
305
+ ]);
306
+ return { scriptPubKey };
307
+ }
134
308
 
135
- if (!_address) throw new Error("No public key for spend output");
309
+ // Other Base58 addresses (P2PKH or P2SH)
310
+ const decoded = bs58Decode(address);
311
+ const versionByte = decoded[0];
312
+ const hash160 = decoded.slice(1);
136
313
 
137
- return msg.coin.toLowerCase() === "bitcoincash" ? toLegacyAddress(_address) : _address;
314
+ // Check if P2SH by comparing version byte with network's scriptHash
315
+ if (versionByte === network.scriptHash) {
316
+ const scriptPubKey = bitcoin.script.compile([bitcoin.opcodes.OP_HASH160, hash160, bitcoin.opcodes.OP_EQUAL]);
317
+ return { scriptPubKey };
318
+ }
319
+
320
+ // P2PKH (Legacy)
321
+ const scriptPubKey = bitcoin.script.compile([
322
+ bitcoin.opcodes.OP_DUP,
323
+ bitcoin.opcodes.OP_HASH160,
324
+ hash160,
325
+ bitcoin.opcodes.OP_EQUALVERIFY,
326
+ bitcoin.opcodes.OP_CHECKSIG,
327
+ ]);
328
+ return { scriptPubKey };
138
329
  })();
139
330
 
140
- psbt.addOutput({ address, value: BigInt(output.amount) });
331
+ tx.addOutput(outputScriptPubKey, BigInt(output.amount));
141
332
  }
142
333
 
143
- if (msg.opReturnData) {
144
- const data = Buffer.from(msg.opReturnData, "utf-8");
145
- const script = bitcoin.payments.embed({ data: [data] }).output;
146
- if (!script) throw new Error("unable to build OP_RETURN script");
147
- // OP_RETURN_DATA outputs always have a value of 0
148
- psbt.addOutput({ script, value: BigInt(0) });
149
- }
334
+ const signatures: string[] = [];
150
335
 
151
336
  for (let i = 0; i < msg.inputs.length; i++) {
152
337
  const input = msg.inputs[i];
153
- const pubkey = await getPublicKey(client, input.addressNList);
154
-
155
- const signer: bitcoin.SignerAsync = {
156
- publicKey: pubkey,
157
- sign: async (hash) => {
158
- const { sig } = await client.sign({
159
- data: {
160
- payload: hash,
161
- curveType: Constants.SIGNING.CURVES.SECP256K1,
162
- hashType: Constants.SIGNING.HASHES.SHA256,
163
- encodingType: Constants.SIGNING.ENCODINGS.NONE,
164
- signerPath: input.addressNList,
165
- },
338
+
339
+ if (!input.hex) {
340
+ throw new Error(`Input ${i} missing hex field (raw previous transaction)`);
341
+ }
342
+
343
+ const prevTx = bitcoin.Transaction.fromHex(input.hex);
344
+ const prevOutput = prevTx.outs[input.vout];
345
+ const scriptPubKey = prevOutput.script;
346
+
347
+ // Detect input type:
348
+ // - Segwit Native (P2WPKH): 0x00 0x14 <20-byte-hash> (22 bytes)
349
+ // - Segwit (P2SH-P2WPKH): OP_HASH160 <20-byte-hash> OP_EQUAL (23 bytes), detected via BIP49 path
350
+ // - Legacy (P2PKH): OP_DUP OP_HASH160 <20-byte-hash> OP_EQUALVERIFY OP_CHECKSIG (25 bytes)
351
+ const isSegwitNative = scriptPubKey.length === 22 && scriptPubKey[0] === 0x00 && scriptPubKey[1] === 0x14;
352
+
353
+ // Detect Segwit (wrapped SegWit) from BIP49 derivation path (m/49'/...)
354
+ const purpose = input.addressNList[0] & ~0x80000000; // Remove hardening bit
355
+ const isSegwit = purpose === 49;
356
+
357
+ const isAnySegwit = isSegwitNative || isSegwit;
358
+
359
+ // Build signature preimage based on input type
360
+ let signaturePreimage: Buffer;
361
+ const hashType =
362
+ msg.coin === "BitcoinCash"
363
+ ? bitcoin.Transaction.SIGHASH_ALL | 0x40 // SIGHASH_FORKID for Bitcoin Cash
364
+ : bitcoin.Transaction.SIGHASH_ALL;
365
+
366
+ // BIP143 signing for SegWit inputs (Segwit Native + Segwit) and Bitcoin Cash P2PKH
367
+ // See: https://github.com/bitcoin/bips/blob/master/bip-0143.mediawiki
368
+ const useBIP143 = isAnySegwit || msg.coin === "BitcoinCash";
369
+
370
+ if (useBIP143) {
371
+ // BIP143 signing (used for SegWit and Bitcoin Cash)
372
+ const hashPrevouts = CryptoJS.SHA256(
373
+ CryptoJS.SHA256(
374
+ CryptoJS.lib.WordArray.create(
375
+ Buffer.concat(
376
+ msg.inputs.map((inp) => Buffer.concat([Buffer.from(inp.txid, "hex").reverse(), u32le(inp.vout)]))
377
+ )
378
+ )
379
+ )
380
+ );
381
+
382
+ const hashSequence = CryptoJS.SHA256(
383
+ CryptoJS.SHA256(
384
+ CryptoJS.lib.WordArray.create(Buffer.concat(msg.inputs.map(() => Buffer.from([0xff, 0xff, 0xff, 0xff]))))
385
+ )
386
+ );
387
+
388
+ const hashOutputs = CryptoJS.SHA256(
389
+ CryptoJS.SHA256(
390
+ CryptoJS.lib.WordArray.create(
391
+ Buffer.concat(
392
+ tx.outs.map((out) => {
393
+ const valueBuffer = Buffer.alloc(8);
394
+ const value = typeof out.value === "bigint" ? out.value : BigInt(out.value);
395
+ valueBuffer.writeBigUInt64LE(value);
396
+ return Buffer.concat([valueBuffer, Buffer.from([out.script.length]), out.script]);
397
+ })
398
+ )
399
+ )
400
+ )
401
+ );
402
+
403
+ // scriptCode depends on input type
404
+ let scriptCode: Buffer;
405
+ if (isSegwitNative) {
406
+ // Segwit Native (P2WPKH): Build scriptCode from hash extracted from witness program
407
+ scriptCode = Buffer.from(
408
+ bitcoin.script.compile([
409
+ bitcoin.opcodes.OP_DUP,
410
+ bitcoin.opcodes.OP_HASH160,
411
+ scriptPubKey.slice(2), // Remove OP_0 and length byte to get hash
412
+ bitcoin.opcodes.OP_EQUALVERIFY,
413
+ bitcoin.opcodes.OP_CHECKSIG,
414
+ ])
415
+ );
416
+ } else if (isSegwit) {
417
+ // Segwit (P2SH-P2WPKH): Build scriptCode from pubkey hash (same format as Segwit Native)
418
+ // Need to derive pubkey hash from the input's public key
419
+ const pubkey = await client.getAddresses({
420
+ startPath: input.addressNList,
421
+ n: 1,
422
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
166
423
  });
167
424
 
168
- if (!sig) throw new Error(`No signature returned from device for input ${i}`);
425
+ if (!pubkey || !pubkey.length) {
426
+ throw new Error(`No public key for input ${i}`);
427
+ }
428
+
429
+ const pubkeyBuffer = Buffer.isBuffer(pubkey[0]) ? pubkey[0] : Buffer.from(pubkey[0], "hex");
430
+ const compressedPubkey =
431
+ pubkeyBuffer.length === 65 ? Buffer.from(pointCompress(pubkeyBuffer, true)) : pubkeyBuffer;
432
+
433
+ // Hash160 = RIPEMD160(SHA256(pubkey))
434
+ const pubkeyHash = bitcoin.crypto.hash160(compressedPubkey);
435
+
436
+ scriptCode = Buffer.from(
437
+ bitcoin.script.compile([
438
+ bitcoin.opcodes.OP_DUP,
439
+ bitcoin.opcodes.OP_HASH160,
440
+ pubkeyHash,
441
+ bitcoin.opcodes.OP_EQUALVERIFY,
442
+ bitcoin.opcodes.OP_CHECKSIG,
443
+ ])
444
+ );
445
+ } else {
446
+ // P2PKH (Bitcoin Cash): scriptCode IS the scriptPubKey
447
+ scriptCode = Buffer.from(scriptPubKey);
448
+ }
449
+
450
+ if (!input.amount) {
451
+ throw new Error(`Input ${i} missing amount field (required for BIP143 signing)`);
452
+ }
453
+ const valueBuffer = Buffer.alloc(8);
454
+ const value = BigInt(input.amount);
455
+ valueBuffer.writeBigUInt64LE(value);
456
+
457
+ signaturePreimage = Buffer.concat([
458
+ u32le(tx.version),
459
+ Buffer.from(hashPrevouts.toString(CryptoJS.enc.Hex), "hex"),
460
+ Buffer.from(hashSequence.toString(CryptoJS.enc.Hex), "hex"),
461
+ Buffer.from(input.txid, "hex").reverse(),
462
+ u32le(input.vout),
463
+ Buffer.from([scriptCode.length]),
464
+ scriptCode,
465
+ valueBuffer,
466
+ Buffer.from([0xff, 0xff, 0xff, 0xff]), // sequence
467
+ Buffer.from(hashOutputs.toString(CryptoJS.enc.Hex), "hex"),
468
+ u32le(tx.locktime),
469
+ u32le(hashType),
470
+ ]);
471
+ } else {
472
+ // Legacy signing
473
+ const txTmp = tx.clone();
474
+
475
+ // Remove OP_CODESEPARATOR from scriptPubKey (Bitcoin standard)
476
+ const decompiled = bitcoin.script.decompile(scriptPubKey);
477
+ if (!decompiled) {
478
+ throw new Error(`Failed to decompile scriptPubKey for input ${i}`);
479
+ }
480
+ const scriptPubKeyForSigning = bitcoin.script.compile(
481
+ decompiled.filter((x) => x !== bitcoin.opcodes.OP_CODESEPARATOR)
482
+ );
169
483
 
170
- const { r, s } = sig;
484
+ // For SIGHASH_ALL: blank all input scripts except the one being signed
485
+ txTmp.ins.forEach((txInput, idx) => {
486
+ txInput.script = idx === i ? scriptPubKeyForSigning : Buffer.alloc(0);
487
+ });
171
488
 
172
- if (!Buffer.isBuffer(r)) throw new Error("Invalid signature (r)");
173
- if (!Buffer.isBuffer(s)) throw new Error("Invalid signature (s)");
489
+ // Serialize transaction + append hashType (4 bytes)
490
+ const txBuffer = txTmp.toBuffer();
491
+ const hashTypeBuffer = Buffer.alloc(4);
492
+ hashTypeBuffer.writeUInt32LE(hashType, 0);
493
+ signaturePreimage = Buffer.concat([txBuffer, hashTypeBuffer]);
494
+ }
174
495
 
175
- return Buffer.concat([r, s]);
496
+ // UTXOs require double SHA256 for transaction signatures.
497
+ // Strategy: Hash once ourselves, then let device hash again (SHA256 + SHA256 = double SHA256)
498
+ // This avoids using hashType.NONE which causes "Invalid Request" errors.
499
+ const hash1 = CryptoJS.SHA256(CryptoJS.lib.WordArray.create(signaturePreimage));
500
+ const singleHashedBuffer = Buffer.from(hash1.toString(CryptoJS.enc.Hex), "hex");
501
+
502
+ const signData = {
503
+ data: {
504
+ payload: singleHashedBuffer,
505
+ curveType: Constants.SIGNING.CURVES.SECP256K1,
506
+ hashType: Constants.SIGNING.HASHES.SHA256, // Device will hash again → double SHA256
507
+ encodingType: Constants.SIGNING.ENCODINGS.NONE,
508
+ signerPath: input.addressNList,
176
509
  },
177
510
  };
178
511
 
179
- // single sha256 hash and allow device to perform second sha256 hash when signing (pending gridplus fix for hashType.NONE)
180
- await psbt.signInputAsync(i, signer, undefined, true);
512
+ let signedResult;
513
+ try {
514
+ signedResult = await client.sign(signData);
515
+ } catch (error) {
516
+ throw new Error(`Device signing failed for input ${i}: ${(error as Error).message}`);
517
+ }
518
+
519
+ if (!signedResult?.sig) {
520
+ throw new Error(`No signature returned from device for input ${i}`);
521
+ }
522
+
523
+ const { r, s } = signedResult.sig;
524
+ const rBuf = Buffer.isBuffer(r) ? r : Buffer.from(r);
525
+ const sBuf = Buffer.isBuffer(s) ? s : Buffer.from(s);
526
+
527
+ // Use the same hashType that was used for the signature preimage
528
+ const sigHashType =
529
+ msg.coin === "BitcoinCash"
530
+ ? bitcoin.Transaction.SIGHASH_ALL | 0x40 // SIGHASH_FORKID for Bitcoin Cash
531
+ : bitcoin.Transaction.SIGHASH_ALL;
532
+ const derSig = encodeDerSignature(rBuf, sBuf, sigHashType);
533
+ signatures.push(derSig.toString("hex"));
534
+ }
535
+
536
+ // Reconstruct a clean transaction from scratch with all signatures
537
+ // This ensures proper scriptSig encoding using bitcoinjs-lib
538
+ const finalTx = new bitcoin.Transaction();
539
+ finalTx.version = tx.version;
540
+ finalTx.locktime = tx.locktime;
541
+
542
+ // Add inputs with proper scriptSigs
543
+ for (let i = 0; i < msg.inputs.length; i++) {
544
+ const input = msg.inputs[i];
545
+ const txHashBuffer = Buffer.from(input.txid, "hex").reverse();
546
+ finalTx.addInput(txHashBuffer, input.vout);
547
+
548
+ // Get the signature we collected earlier
549
+ const derSig = Buffer.from(signatures[i], "hex");
550
+
551
+ // Get pubkey for this input
552
+ const pubkey = await client.getAddresses({
553
+ startPath: input.addressNList,
554
+ n: 1,
555
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
556
+ });
557
+
558
+ if (!pubkey || !pubkey.length) {
559
+ throw new Error(`No public key for input ${i}`);
560
+ }
561
+
562
+ const pubkeyBuffer = Buffer.isBuffer(pubkey[0]) ? pubkey[0] : Buffer.from(pubkey[0], "hex");
563
+ const compressedPubkey =
564
+ pubkeyBuffer.length === 65 ? Buffer.from(pointCompress(pubkeyBuffer, true)) : pubkeyBuffer;
565
+
566
+ // Detect input type to determine if we need SegWit or legacy encoding
567
+ const prevTx = bitcoin.Transaction.fromHex(input.hex);
568
+ const prevOutput = prevTx.outs[input.vout];
569
+ const isSegwitNative =
570
+ prevOutput.script.length === 22 && prevOutput.script[0] === 0x00 && prevOutput.script[1] === 0x14;
571
+
572
+ // Detect Segwit (wrapped SegWit) from BIP49 derivation path (m/49'/...)
573
+ const purpose = input.addressNList[0] & ~0x80000000; // Remove hardening bit
574
+ const isSegwit = purpose === 49;
575
+
576
+ if (isSegwitNative) {
577
+ // Segwit Native (P2WPKH): empty scriptSig, signature + pubkey in witness
578
+ finalTx.ins[i].script = Buffer.alloc(0);
579
+ finalTx.ins[i].witness = [derSig, compressedPubkey];
580
+ } else if (isSegwit) {
581
+ // Segwit (P2SH-P2WPKH): redeemScript in scriptSig, signature + pubkey in witness
582
+ // redeemScript format: OP_0 OP_PUSH20 <hash160(pubkey)>
583
+ const pubkeyHash = bitcoin.crypto.hash160(compressedPubkey);
584
+ const redeemScript = bitcoin.script.compile([bitcoin.opcodes.OP_0, pubkeyHash]);
585
+
586
+ // scriptSig contains the redeemScript
587
+ finalTx.ins[i].script = bitcoin.script.compile([redeemScript]);
588
+ // Witness contains signature + pubkey
589
+ finalTx.ins[i].witness = [derSig, compressedPubkey];
590
+ } else {
591
+ // Legacy: signature + pubkey in scriptSig
592
+ const scriptSig = bitcoin.script.compile([derSig, compressedPubkey]);
593
+ finalTx.ins[i].script = scriptSig;
594
+ }
181
595
  }
182
596
 
183
- psbt.finalizeAllInputs();
597
+ // Add outputs - handle both address and addressNList
598
+ for (let outputIdx = 0; outputIdx < msg.outputs.length; outputIdx++) {
599
+ const output = msg.outputs[outputIdx];
600
+ let address: string;
601
+
602
+ if (output.address) {
603
+ // Output already has address
604
+ address = output.address;
605
+ } else if (output.addressNList) {
606
+ // Derive address from addressNList (for change outputs)
607
+ const pubkey = await client.getAddresses({
608
+ startPath: output.addressNList,
609
+ n: 1,
610
+ flag: Constants.GET_ADDR_FLAGS.SECP256K1_PUB,
611
+ });
612
+
613
+ if (!pubkey || !pubkey.length) {
614
+ throw new Error(`No public key for output`);
615
+ }
616
+
617
+ const pubkeyBuffer = Buffer.isBuffer(pubkey[0]) ? pubkey[0] : Buffer.from(pubkey[0], "hex");
618
+
619
+ const pubkeyHex =
620
+ pubkeyBuffer.length === 65
621
+ ? Buffer.from(pointCompress(pubkeyBuffer, true)).toString("hex")
622
+ : pubkeyBuffer.toString("hex");
623
+
624
+ const scriptType =
625
+ (output.scriptType as unknown as core.BTCInputScriptType) || core.BTCInputScriptType.SpendAddress;
626
+ address = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
627
+ } else {
628
+ throw new Error("Output must have either address or addressNList");
629
+ }
630
+
631
+ const { scriptPubKey: finalScriptPubKey } = (() => {
632
+ // Native SegWit (bech32): ltc1 for Litecoin, bc1 for Bitcoin
633
+ if (address.startsWith("ltc1") || address.startsWith("bc1")) {
634
+ const decoded = bech32.decode(address);
635
+ const hash160 = Buffer.from(bech32.fromWords(decoded.words.slice(1)));
636
+
637
+ const scriptPubKey = bitcoin.script.compile([bitcoin.opcodes.OP_0, hash160]);
638
+ return { scriptPubKey };
639
+ }
640
+
641
+ // Bitcoin Cash CashAddr format: bitcoincash: prefix or q for mainnet
642
+ if (address.startsWith("bitcoincash:") || address.startsWith("q")) {
643
+ const legacyAddress = bchAddr.toLegacyAddress(address);
644
+ const decoded = bs58Decode(legacyAddress);
645
+ const versionByte = decoded[0];
646
+ const hash160 = decoded.slice(1);
647
+
648
+ // Check if P2SH (Bitcoin Cash uses 0x05 for P2SH)
649
+ if (versionByte === network.scriptHash) {
650
+ const scriptPubKey = bitcoin.script.compile([
651
+ bitcoin.opcodes.OP_HASH160,
652
+ hash160,
653
+ bitcoin.opcodes.OP_EQUAL,
654
+ ]);
655
+ return { scriptPubKey };
656
+ }
657
+
658
+ // P2PKH
659
+ const scriptPubKey = bitcoin.script.compile([
660
+ bitcoin.opcodes.OP_DUP,
661
+ bitcoin.opcodes.OP_HASH160,
662
+ hash160,
663
+ bitcoin.opcodes.OP_EQUALVERIFY,
664
+ bitcoin.opcodes.OP_CHECKSIG,
665
+ ]);
666
+ return { scriptPubKey };
667
+ }
668
+
669
+ // Other Base58 addresses (P2PKH or P2SH)
670
+ const decoded = bs58Decode(address);
671
+ const versionByte = decoded[0];
672
+ const hash160 = decoded.slice(1);
184
673
 
185
- const tx = psbt.extractTransaction(true);
674
+ // Check if P2SH by comparing version byte with network's scriptHash
675
+ if (versionByte === network.scriptHash) {
676
+ const scriptPubKey = bitcoin.script.compile([bitcoin.opcodes.OP_HASH160, hash160, bitcoin.opcodes.OP_EQUAL]);
677
+ return { scriptPubKey };
678
+ }
186
679
 
187
- const signatures = psbt.data.inputs.map((input) =>
188
- input.partialSig ? Buffer.from(input.partialSig[0].signature).toString("hex") : ""
189
- );
680
+ // P2PKH (Legacy)
681
+ const scriptPubKey = bitcoin.script.compile([
682
+ bitcoin.opcodes.OP_DUP,
683
+ bitcoin.opcodes.OP_HASH160,
684
+ hash160,
685
+ bitcoin.opcodes.OP_EQUALVERIFY,
686
+ bitcoin.opcodes.OP_CHECKSIG,
687
+ ]);
688
+ return { scriptPubKey };
689
+ })();
190
690
 
191
- return { signatures, serializedTx: tx.toHex() };
691
+ finalTx.addOutput(finalScriptPubKey, BigInt(output.amount));
692
+ }
693
+
694
+ const serializedTx = finalTx.toHex();
695
+
696
+ return {
697
+ signatures,
698
+ serializedTx,
699
+ };
192
700
  }
193
701
  }
702
+
703
+ export const btcNextAccountPath = (msg: core.BTCAccountPath): core.BTCAccountPath | undefined => {
704
+ const newAddressNList = [...msg.addressNList];
705
+ newAddressNList[2] += 1;
706
+
707
+ return {
708
+ ...msg,
709
+ addressNList: newAddressNList,
710
+ };
711
+ };