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