@shapeshiftoss/hdwallet-gridplus 1.62.10 → 1.62.12
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 +3 -11
- package/dist/adapter.d.ts.map +1 -1
- package/dist/adapter.js +26 -98
- package/dist/adapter.js.map +1 -1
- package/dist/bitcoin.d.ts +1 -2
- package/dist/bitcoin.d.ts.map +1 -1
- package/dist/bitcoin.js +130 -555
- package/dist/bitcoin.js.map +1 -1
- package/dist/ethereum.d.ts.map +1 -1
- package/dist/ethereum.js +18 -34
- package/dist/ethereum.js.map +1 -1
- package/dist/gridplus.d.ts +17 -22
- package/dist/gridplus.d.ts.map +1 -1
- package/dist/gridplus.js +74 -101
- package/dist/gridplus.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -1
- package/dist/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/utils.d.ts +0 -16
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -87
- package/dist/utils.js.map +1 -1
- package/package.json +6 -5
- package/src/adapter.ts +26 -84
- package/src/bitcoin.ts +133 -651
- package/src/ethereum.ts +11 -47
- package/src/gridplus.ts +83 -130
- package/src/index.ts +0 -1
- package/src/utils.ts +2 -100
- package/tsconfig.json +2 -1
- package/dist/transport.d.ts +0 -28
- package/dist/transport.d.ts.map +0 -1
- package/dist/transport.js +0 -148
- package/dist/transport.js.map +0 -1
- package/src/transport.ts +0 -131
- package/tsconfig.tsbuildinfo +0 -1
package/src/bitcoin.ts
CHANGED
|
@@ -1,711 +1,193 @@
|
|
|
1
|
-
import { pointCompress } from "@bitcoinerlab/secp256k1";
|
|
2
1
|
import * as bitcoin from "@shapeshiftoss/bitcoinjs-lib";
|
|
3
2
|
import * as core from "@shapeshiftoss/hdwallet-core";
|
|
4
|
-
import
|
|
5
|
-
import * as bech32 from "bech32";
|
|
6
|
-
import { decode as bs58Decode } from "bs58check";
|
|
7
|
-
import CryptoJS from "crypto-js";
|
|
3
|
+
import { toCashAddress, toLegacyAddress } from "bchaddrjs";
|
|
8
4
|
import { Client, Constants } from "gridplus-sdk";
|
|
9
5
|
|
|
10
|
-
import {
|
|
11
|
-
import { deriveAddressFromPubkey } from "./utils";
|
|
6
|
+
import { getCompressedPubkey } from "./utils";
|
|
12
7
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
+
}
|
|
18
16
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
case core.BTCInputScriptType.SpendP2SHWitness:
|
|
24
|
-
return 49;
|
|
25
|
-
case core.BTCInputScriptType.SpendWitness:
|
|
26
|
-
return 84;
|
|
27
|
-
default:
|
|
28
|
-
return 44;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
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];
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
if (
|
|
34
|
-
return Buffer.concat([Buffer.from([0x00]), x]);
|
|
35
|
-
}
|
|
36
|
-
return x;
|
|
37
|
-
};
|
|
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");
|
|
38
24
|
|
|
39
|
-
|
|
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;
|
|
56
|
-
};
|
|
57
|
-
|
|
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
|
-
});
|
|
25
|
+
return getCompressedPubkey(pubkey);
|
|
82
26
|
};
|
|
83
27
|
|
|
84
28
|
export async function btcGetAddress(client: Client, msg: core.BTCGetAddress): Promise<string | null> {
|
|
85
|
-
|
|
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
|
-
}
|
|
97
|
-
|
|
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");
|
|
29
|
+
const pubkey = await getPublicKey(client, msg.addressNList);
|
|
100
30
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
pubkeyBuffer.length === 65
|
|
104
|
-
? Buffer.from(pointCompress(pubkeyBuffer, true)).toString("hex")
|
|
105
|
-
: pubkeyBuffer.toString("hex");
|
|
31
|
+
const address = deriveAddressFromPubkey(pubkey, msg.coin, msg.scriptType);
|
|
32
|
+
if (!address) return null;
|
|
106
33
|
|
|
107
|
-
|
|
108
|
-
const scriptType = msg.scriptType || core.BTCInputScriptType.SpendAddress;
|
|
109
|
-
const address = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
|
|
110
|
-
|
|
111
|
-
return address;
|
|
34
|
+
return msg.coin.toLowerCase() === "bitcoincash" ? toCashAddress(address) : address;
|
|
112
35
|
}
|
|
113
36
|
|
|
114
37
|
export async function btcSignTx(client: Client, msg: core.BTCSignTx): Promise<core.BTCSignedTx | null> {
|
|
115
|
-
|
|
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
|
-
}
|
|
38
|
+
const spendOutputs = msg.outputs.filter((o) => !o.isChange);
|
|
149
39
|
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
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
|
-
});
|
|
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];
|
|
162
44
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
}
|
|
45
|
+
const recipient = await (async () => {
|
|
46
|
+
if (spendOutput.address) return spendOutput.address;
|
|
166
47
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
+
});
|
|
172
54
|
|
|
173
|
-
|
|
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
|
-
}
|
|
55
|
+
if (!address) throw new Error("No address returned from device");
|
|
179
56
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
57
|
+
return address;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
throw new Error("Invalid output (no address or addressNList specified).");
|
|
61
|
+
})();
|
|
62
|
+
|
|
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);
|
|
188
66
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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({
|
|
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
|
+
];
|
|
74
|
+
|
|
75
|
+
const { sigs, tx } = await client.sign({
|
|
212
76
|
currency: "BTC",
|
|
213
|
-
data:
|
|
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
|
+
},
|
|
214
89
|
});
|
|
215
90
|
|
|
216
|
-
if (!
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
const signatures = signData.sigs ? signData.sigs.map((s: Buffer) => s.toString("hex")) : [];
|
|
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");
|
|
221
93
|
|
|
222
|
-
return {
|
|
223
|
-
signatures,
|
|
224
|
-
serializedTx: signData.tx,
|
|
225
|
-
};
|
|
94
|
+
return { signatures: sigs.map((s) => s.toString("hex")), serializedTx: tx };
|
|
226
95
|
} else {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
96
|
+
const psbt = new bitcoin.Psbt({
|
|
97
|
+
network: core.getNetwork(msg.coin, core.BTCOutputScriptType.PayToMultisig),
|
|
98
|
+
forkCoin: msg.coin.toLowerCase() === "bitcoincash" ? "bch" : "none",
|
|
99
|
+
});
|
|
231
100
|
|
|
232
|
-
|
|
101
|
+
psbt.setVersion(msg.version ?? 2);
|
|
102
|
+
msg.locktime && psbt.setLocktime(msg.locktime);
|
|
233
103
|
|
|
234
104
|
for (const input of msg.inputs) {
|
|
235
|
-
|
|
236
|
-
tx.addInput(txHashBuffer, input.vout);
|
|
237
|
-
}
|
|
238
|
-
|
|
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
|
-
});
|
|
252
|
-
|
|
253
|
-
if (!pubkey || !pubkey.length) {
|
|
254
|
-
throw new Error(`No public key for output`);
|
|
255
|
-
}
|
|
105
|
+
if (!input.hex) throw new Error("Invalid input (missing hex)");
|
|
256
106
|
|
|
257
|
-
|
|
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;
|
|
258
110
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
address = deriveAddressFromPubkey(pubkeyHex, msg.coin, scriptType);
|
|
267
|
-
} else {
|
|
268
|
-
throw new Error("Output must have either address or addressNList");
|
|
269
|
-
}
|
|
111
|
+
psbt.addInput({
|
|
112
|
+
hash: input.txid,
|
|
113
|
+
index: input.vout,
|
|
114
|
+
nonWitnessUtxo: Buffer.from(input.hex, "hex"),
|
|
115
|
+
...(redeemScript && { redeemScript }),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
270
118
|
|
|
271
|
-
|
|
272
|
-
|
|
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)));
|
|
119
|
+
for (const output of msg.outputs) {
|
|
120
|
+
if (!output.amount) throw new Error("Invalid output (missing amount)");
|
|
276
121
|
|
|
277
|
-
|
|
278
|
-
|
|
122
|
+
const address = await (async () => {
|
|
123
|
+
if (!output.address && !output.addressNList) {
|
|
124
|
+
throw new Error("Invalid output (missing address or addressNList)");
|
|
279
125
|
}
|
|
280
126
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
}
|
|
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
|
+
}));
|
|
308
134
|
|
|
309
|
-
|
|
310
|
-
const decoded = bs58Decode(address);
|
|
311
|
-
const versionByte = decoded[0];
|
|
312
|
-
const hash160 = decoded.slice(1);
|
|
135
|
+
if (!_address) throw new Error("No public key for spend output");
|
|
313
136
|
|
|
314
|
-
|
|
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 };
|
|
137
|
+
return msg.coin.toLowerCase() === "bitcoincash" ? toLegacyAddress(_address) : _address;
|
|
329
138
|
})();
|
|
330
139
|
|
|
331
|
-
|
|
140
|
+
psbt.addOutput({ address, value: BigInt(output.amount) });
|
|
332
141
|
}
|
|
333
142
|
|
|
334
|
-
|
|
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
|
+
}
|
|
335
150
|
|
|
336
151
|
for (let i = 0; i < msg.inputs.length; i++) {
|
|
337
152
|
const input = msg.inputs[i];
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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,
|
|
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
|
+
},
|
|
423
166
|
});
|
|
424
167
|
|
|
425
|
-
if (!
|
|
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
|
-
);
|
|
168
|
+
if (!sig) throw new Error(`No signature returned from device for input ${i}`);
|
|
483
169
|
|
|
484
|
-
|
|
485
|
-
txTmp.ins.forEach((txInput, idx) => {
|
|
486
|
-
txInput.script = idx === i ? scriptPubKeyForSigning : Buffer.alloc(0);
|
|
487
|
-
});
|
|
170
|
+
const { r, s } = sig;
|
|
488
171
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const hashTypeBuffer = Buffer.alloc(4);
|
|
492
|
-
hashTypeBuffer.writeUInt32LE(hashType, 0);
|
|
493
|
-
signaturePreimage = Buffer.concat([txBuffer, hashTypeBuffer]);
|
|
494
|
-
}
|
|
172
|
+
if (!Buffer.isBuffer(r)) throw new Error("Invalid signature (r)");
|
|
173
|
+
if (!Buffer.isBuffer(s)) throw new Error("Invalid signature (s)");
|
|
495
174
|
|
|
496
|
-
|
|
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,
|
|
175
|
+
return Buffer.concat([r, s]);
|
|
509
176
|
},
|
|
510
177
|
};
|
|
511
178
|
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
}
|
|
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);
|
|
595
181
|
}
|
|
596
182
|
|
|
597
|
-
|
|
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);
|
|
183
|
+
psbt.finalizeAllInputs();
|
|
673
184
|
|
|
674
|
-
|
|
675
|
-
if (versionByte === network.scriptHash) {
|
|
676
|
-
const scriptPubKey = bitcoin.script.compile([bitcoin.opcodes.OP_HASH160, hash160, bitcoin.opcodes.OP_EQUAL]);
|
|
677
|
-
return { scriptPubKey };
|
|
678
|
-
}
|
|
185
|
+
const tx = psbt.extractTransaction(true);
|
|
679
186
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
bitcoin.opcodes.OP_HASH160,
|
|
684
|
-
hash160,
|
|
685
|
-
bitcoin.opcodes.OP_EQUALVERIFY,
|
|
686
|
-
bitcoin.opcodes.OP_CHECKSIG,
|
|
687
|
-
]);
|
|
688
|
-
return { scriptPubKey };
|
|
689
|
-
})();
|
|
187
|
+
const signatures = psbt.data.inputs.map((input) =>
|
|
188
|
+
input.partialSig ? Buffer.from(input.partialSig[0].signature).toString("hex") : ""
|
|
189
|
+
);
|
|
690
190
|
|
|
691
|
-
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
const serializedTx = finalTx.toHex();
|
|
695
|
-
|
|
696
|
-
return {
|
|
697
|
-
signatures,
|
|
698
|
-
serializedTx,
|
|
699
|
-
};
|
|
191
|
+
return { signatures, serializedTx: tx.toHex() };
|
|
700
192
|
}
|
|
701
193
|
}
|
|
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
|
-
};
|