@swapkit/wallet-hardware 6.1.1 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/THIRD_PARTY_LICENSES.md +4 -4
- package/dist/{chunk-jzrytcrf.js → chunk-0k1b2ddn.js} +3 -3
- package/dist/{chunk-jzrytcrf.js.map → chunk-0k1b2ddn.js.map} +2 -2
- package/dist/{chunk-jewm2ag3.js → chunk-2tzsz8f3.js} +4 -4
- package/dist/{chunk-jewm2ag3.js.map → chunk-2tzsz8f3.js.map} +1 -1
- package/dist/{chunk-yrxe491r.js → chunk-410fpx61.js} +4 -4
- package/dist/{chunk-yrxe491r.js.map → chunk-410fpx61.js.map} +1 -1
- package/dist/chunk-8mkdjmw2.js +4 -0
- package/dist/chunk-8mkdjmw2.js.map +10 -0
- package/dist/{chunk-xmvg4cds.js → chunk-96p7njae.js} +3 -3
- package/dist/{chunk-xmvg4cds.js.map → chunk-96p7njae.js.map} +2 -2
- package/dist/chunk-97nsp4z6.js +5 -0
- package/dist/chunk-97nsp4z6.js.map +10 -0
- package/dist/{chunk-nhva847d.js → chunk-9a9gn4pb.js} +3 -3
- package/dist/{chunk-5p3ttw6y.js.map → chunk-9a9gn4pb.js.map} +1 -1
- package/dist/chunk-a39rm9nv.js +4 -0
- package/dist/{chunk-gr3eswf1.js.map → chunk-a39rm9nv.js.map} +5 -5
- package/dist/chunk-c8mwf9cn.js +4 -0
- package/dist/{chunk-k3thje6a.js.map → chunk-c8mwf9cn.js.map} +1 -1
- package/dist/{chunk-ds2m83jp.js → chunk-dhhmywmb.js} +4 -4
- package/dist/{chunk-ds2m83jp.js.map → chunk-dhhmywmb.js.map} +1 -1
- package/dist/{chunk-eenx3cv6.js → chunk-dsa641rq.js} +3 -3
- package/dist/{chunk-eenx3cv6.js.map → chunk-dsa641rq.js.map} +2 -2
- package/dist/{chunk-0arpsm5f.js → chunk-grj9nvte.js} +3 -3
- package/dist/{chunk-0arpsm5f.js.map → chunk-grj9nvte.js.map} +1 -1
- package/dist/{chunk-hg71hp4m.js → chunk-napm4102.js} +3 -3
- package/dist/{chunk-hg71hp4m.js.map → chunk-napm4102.js.map} +1 -1
- package/dist/{chunk-k3thje6a.js → chunk-nb08g045.js} +3 -3
- package/dist/{chunk-nhva847d.js.map → chunk-nb08g045.js.map} +1 -1
- package/dist/chunk-npe2r2ge.js +4 -0
- package/dist/{chunk-xx7cwpzq.js.map → chunk-npe2r2ge.js.map} +1 -1
- package/dist/chunk-p9mwkgye.js +4 -0
- package/dist/{chunk-09e51wma.js.map → chunk-p9mwkgye.js.map} +14 -12
- package/dist/{chunk-wfbg89d3.js → chunk-qjp8j8e5.js} +3 -3
- package/dist/{chunk-wfbg89d3.js.map → chunk-qjp8j8e5.js.map} +1 -1
- package/dist/{chunk-xn0fnag5.js → chunk-r1fk1sx9.js} +3 -3
- package/dist/{chunk-xn0fnag5.js.map → chunk-r1fk1sx9.js.map} +1 -1
- package/dist/{chunk-1amf58aq.js → chunk-ta7bqmy3.js} +3 -3
- package/dist/{chunk-1amf58aq.js.map → chunk-ta7bqmy3.js.map} +1 -1
- package/dist/{chunk-3t17wpjg.js → chunk-tgvgr630.js} +3 -3
- package/dist/{chunk-3t17wpjg.js.map → chunk-tgvgr630.js.map} +2 -2
- package/dist/chunk-v8x9ptxs.js +4 -0
- package/dist/chunk-v8x9ptxs.js.map +10 -0
- package/dist/{chunk-dptbnkzm.js → chunk-wm89desc.js} +20 -20
- package/dist/{chunk-dptbnkzm.js.map → chunk-wm89desc.js.map} +12 -10
- package/dist/{chunk-5c7b7z2d.js → chunk-wtyhjt5e.js} +3 -3
- package/dist/{chunk-5c7b7z2d.js.map → chunk-wtyhjt5e.js.map} +1 -1
- package/dist/{chunk-tm47brht.js → chunk-xskdbz15.js} +3 -3
- package/dist/{chunk-tm47brht.js.map → chunk-xskdbz15.js.map} +2 -2
- package/dist/{chunk-8m2sn76p.js → chunk-ytjqb1sd.js} +3 -3
- package/dist/{chunk-8m2sn76p.js.map → chunk-ytjqb1sd.js.map} +1 -1
- package/dist/{chunk-3h1f3hyp.js → chunk-zbesf7sy.js} +3 -3
- package/dist/{chunk-3h1f3hyp.js.map → chunk-zbesf7sy.js.map} +1 -1
- package/dist/keepkey/index.cjs +2 -2
- package/dist/keepkey/index.cjs.map +3 -3
- package/dist/keepkey/index.js +2 -2
- package/dist/keepkey/index.js.map +3 -3
- package/dist/ledger/index.cjs +30 -30
- package/dist/ledger/index.cjs.map +35 -27
- package/dist/ledger/index.js +2 -2
- package/dist/ledger/index.js.map +11 -8
- package/dist/trezor/index.cjs +2 -2
- package/dist/trezor/index.cjs.map +7 -5
- package/dist/trezor/index.js +2 -2
- package/dist/trezor/index.js.map +3 -3
- package/dist/types/helpers/cardano.d.ts +35 -0
- package/dist/types/helpers/cardano.d.ts.map +1 -0
- package/dist/types/keepkey/index.d.ts +1 -0
- package/dist/types/keepkey/index.d.ts.map +1 -1
- package/dist/types/ledger/clients/cardano.d.ts +24 -0
- package/dist/types/ledger/clients/cardano.d.ts.map +1 -0
- package/dist/types/ledger/clients/evm.d.ts +1 -0
- package/dist/types/ledger/clients/evm.d.ts.map +1 -1
- package/dist/types/ledger/clients/stellar.d.ts +13 -0
- package/dist/types/ledger/clients/stellar.d.ts.map +1 -0
- package/dist/types/ledger/clients/ton.d.ts +13 -0
- package/dist/types/ledger/clients/ton.d.ts.map +1 -0
- package/dist/types/ledger/clients/zcash.d.ts.map +1 -1
- package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
- package/dist/types/ledger/helpers/getLedgerClient.d.ts +8 -1
- package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
- package/dist/types/ledger/index.d.ts +3 -2
- package/dist/types/ledger/index.d.ts.map +1 -1
- package/dist/types/ledger/types.d.ts +2 -2
- package/dist/types/ledger/types.d.ts.map +1 -1
- package/dist/types/trezor/cardanoSigner.d.ts.map +1 -1
- package/dist/types/trezor/hypercoreSigner.d.ts +22 -0
- package/dist/types/trezor/hypercoreSigner.d.ts.map +1 -0
- package/dist/types/trezor/index.d.ts +3 -2
- package/dist/types/trezor/index.d.ts.map +1 -1
- package/dist/types/trezor/stellarSigner.d.ts +1 -1
- package/package.json +25 -13
- package/src/helpers/cardano.ts +112 -0
- package/src/keepkey/index.ts +14 -14
- package/src/ledger/clients/cardano.ts +204 -0
- package/src/ledger/clients/evm.ts +4 -0
- package/src/ledger/clients/stellar.ts +83 -0
- package/src/ledger/clients/ton.ts +282 -0
- package/src/ledger/clients/zcash.ts +16 -6
- package/src/ledger/helpers/getLedgerAddress.ts +16 -0
- package/src/ledger/helpers/getLedgerClient.ts +13 -0
- package/src/ledger/index.ts +102 -30
- package/src/ledger/types.ts +2 -0
- package/src/trezor/cardanoSigner.ts +24 -80
- package/src/trezor/hypercoreSigner.ts +96 -0
- package/src/trezor/index.ts +153 -75
- package/dist/chunk-09e51wma.js +0 -4
- package/dist/chunk-5p3ttw6y.js +0 -4
- package/dist/chunk-gr3eswf1.js +0 -4
- package/dist/chunk-qjq7gvyx.js +0 -4
- package/dist/chunk-qjq7gvyx.js.map +0 -10
- package/dist/chunk-xx7cwpzq.js +0 -4
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Ada,
|
|
3
|
+
AssetGroup,
|
|
4
|
+
BIP32Path,
|
|
5
|
+
ExtendedPublicKey,
|
|
6
|
+
SignedTransactionData,
|
|
7
|
+
SignTransactionRequest,
|
|
8
|
+
TxOutput,
|
|
9
|
+
TxOutputDestination,
|
|
10
|
+
} from "@cardano-foundation/ledgerjs-hw-app-cardano";
|
|
11
|
+
import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
|
|
12
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
13
|
+
import { hex } from "@scure/base";
|
|
14
|
+
import { Chain, type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers";
|
|
15
|
+
|
|
16
|
+
import { type CardanoOutputFields, readCardanoBody } from "../../helpers/cardano";
|
|
17
|
+
import {
|
|
18
|
+
LEDGER_USER_INTERACTION_REQUIRED,
|
|
19
|
+
type LedgerJsClientParams,
|
|
20
|
+
normalizeLedgerJsClientParams,
|
|
21
|
+
runLedgerJsOperation,
|
|
22
|
+
} from "../helpers/ledgerJsDmkBridge";
|
|
23
|
+
|
|
24
|
+
export type CardanoLedgerModule = typeof import("@cardano-foundation/ledgerjs-hw-app-cardano");
|
|
25
|
+
export type CardanoLedgerAddressParams = { address: string; paymentPath: BIP32Path; stakingPath: BIP32Path };
|
|
26
|
+
|
|
27
|
+
type CardanoLedgerParams = LedgerJsClientParams<DerivationPathArray>;
|
|
28
|
+
|
|
29
|
+
function notSupported(reason: string) {
|
|
30
|
+
return new SwapKitError("wallet_ledger_method_not_supported", { chain: Chain.Cardano, reason });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function toLedgerOutput(
|
|
34
|
+
{ address, amount, format, tokenGroups }: CardanoOutputFields,
|
|
35
|
+
own: CardanoLedgerAddressParams,
|
|
36
|
+
{ AddressType, TxOutputDestinationType, TxOutputFormat }: CardanoLedgerModule,
|
|
37
|
+
): Promise<TxOutput> {
|
|
38
|
+
const { getCardanoAddressFromBytes } = await import("@swapkit/toolboxes/cardano");
|
|
39
|
+
const outputAddress = await getCardanoAddressFromBytes(address);
|
|
40
|
+
const destination: TxOutputDestination =
|
|
41
|
+
outputAddress === own.address
|
|
42
|
+
? {
|
|
43
|
+
params: {
|
|
44
|
+
params: { spendingPath: own.paymentPath, stakingPath: own.stakingPath },
|
|
45
|
+
type: AddressType.BASE_PAYMENT_KEY_STAKE_KEY,
|
|
46
|
+
},
|
|
47
|
+
type: TxOutputDestinationType.DEVICE_OWNED,
|
|
48
|
+
}
|
|
49
|
+
: { params: { addressHex: hex.encode(address) }, type: TxOutputDestinationType.THIRD_PARTY };
|
|
50
|
+
const tokenBundle: AssetGroup[] | undefined = tokenGroups?.map(({ policyId, tokens }) => ({
|
|
51
|
+
policyIdHex: hex.encode(policyId),
|
|
52
|
+
tokens: tokens.map(({ amount, assetName }) => ({ amount, assetNameHex: hex.encode(assetName) })),
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
amount,
|
|
57
|
+
destination,
|
|
58
|
+
format: format === "legacy" ? TxOutputFormat.ARRAY_LEGACY : TxOutputFormat.MAP_BABBAGE,
|
|
59
|
+
...(tokenBundle === undefined ? {} : { tokenBundle }),
|
|
60
|
+
} as TxOutput;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function toLedgerCardanoTransaction(
|
|
64
|
+
txHex: string,
|
|
65
|
+
own: CardanoLedgerAddressParams,
|
|
66
|
+
ledgerModule: CardanoLedgerModule,
|
|
67
|
+
): Promise<SignTransactionRequest> {
|
|
68
|
+
const { decodeCardanoTransaction } = await import("@swapkit/toolboxes/cardano");
|
|
69
|
+
const { Networks, TransactionSigningMode, TxAuxiliaryDataType } = ledgerModule;
|
|
70
|
+
const { body } = await decodeCardanoTransaction(txHex);
|
|
71
|
+
const { auxiliaryDataHash, fee, inputs, outputs, tagCborSets, ttl, validityIntervalStart } = readCardanoBody(
|
|
72
|
+
body,
|
|
73
|
+
"Ledger",
|
|
74
|
+
notSupported,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
additionalWitnessPaths: [],
|
|
79
|
+
options: { tagCborSets },
|
|
80
|
+
signingMode: TransactionSigningMode.ORDINARY_TRANSACTION,
|
|
81
|
+
tx: {
|
|
82
|
+
fee,
|
|
83
|
+
inputs: inputs.map(({ index, txHash }) => ({
|
|
84
|
+
outputIndex: index,
|
|
85
|
+
path: own.paymentPath,
|
|
86
|
+
txHashHex: hex.encode(txHash),
|
|
87
|
+
})),
|
|
88
|
+
network: Networks.Mainnet,
|
|
89
|
+
outputs: await Promise.all(outputs.map((output) => toLedgerOutput(output, own, ledgerModule))),
|
|
90
|
+
...(auxiliaryDataHash
|
|
91
|
+
? {
|
|
92
|
+
auxiliaryData: {
|
|
93
|
+
params: { hashHex: hex.encode(auxiliaryDataHash) },
|
|
94
|
+
type: TxAuxiliaryDataType.ARBITRARY_HASH,
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
...(ttl === undefined ? {} : { ttl }),
|
|
99
|
+
...(validityIntervalStart === undefined ? {} : { validityIntervalStart }),
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function addLedgerWitnesses(
|
|
105
|
+
txHex: string,
|
|
106
|
+
{ txHashHex, witnesses }: Pick<SignedTransactionData, "txHashHex" | "witnesses">,
|
|
107
|
+
{ paymentPath, publicKey }: { paymentPath: BIP32Path; publicKey: Uint8Array },
|
|
108
|
+
) {
|
|
109
|
+
const { addCardanoVkeyWitnesses, decodeCardanoTransaction } = await import("@swapkit/toolboxes/cardano");
|
|
110
|
+
const { bodyHash } = await decodeCardanoTransaction(txHex);
|
|
111
|
+
|
|
112
|
+
if (txHashHex.toLowerCase() !== bodyHash) {
|
|
113
|
+
throw new SwapKitError("wallet_ledger_invalid_signature", {
|
|
114
|
+
chain: Chain.Cardano,
|
|
115
|
+
error: "Ledger signed a different transaction body",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (witnesses.length === 0) {
|
|
119
|
+
throw new SwapKitError("wallet_ledger_signing_error", {
|
|
120
|
+
chain: Chain.Cardano,
|
|
121
|
+
error: "Ledger returned no witness",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const vkeyWitnesses = witnesses.map(({ path, witnessSignatureHex }) => {
|
|
126
|
+
if (path.length !== paymentPath.length || path.some((level, i) => level !== paymentPath[i])) {
|
|
127
|
+
throw new SwapKitError("wallet_ledger_signing_error", {
|
|
128
|
+
chain: Chain.Cardano,
|
|
129
|
+
error: "Ledger returned a witness for an unexpected key",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { publicKey, signature: hex.decode(witnessSignatureHex) };
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return addCardanoVkeyWitnesses(txHex, vkeyWitnesses);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function CardanoLedger(paramsOrPath?: CardanoLedgerParams | DerivationPathArray, transport?: Transport) {
|
|
139
|
+
const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
|
|
140
|
+
const path = derivationPath || NetworkDerivationPath[Chain.Cardano];
|
|
141
|
+
const [purpose, coinType, account, role, index] = path;
|
|
142
|
+
if (path.length !== 5 || role !== 0 || index === undefined) {
|
|
143
|
+
throw new SwapKitError("wallet_ledger_derivation_path_error", { chain: Chain.Cardano, derivationPath: path });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const ledgerModule = await import("@cardano-foundation/ledgerjs-hw-app-cardano");
|
|
147
|
+
const { getCardanoAddressFromBytes, getCardanoPublicKeyHash } = await import("@swapkit/toolboxes/cardano");
|
|
148
|
+
const { HARDENED, Networks } = ledgerModule;
|
|
149
|
+
const accountPath = [purpose + HARDENED, coinType + HARDENED, account + HARDENED];
|
|
150
|
+
const paymentPath = [...accountPath, 0, index];
|
|
151
|
+
const stakingPath = [...accountPath, 2, 0];
|
|
152
|
+
|
|
153
|
+
function runCardanoOperation<Output>({
|
|
154
|
+
operation,
|
|
155
|
+
requiredUserInteraction,
|
|
156
|
+
}: {
|
|
157
|
+
operation: (app: Ada) => Promise<Output>;
|
|
158
|
+
requiredUserInteraction?: UserInteractionRequired;
|
|
159
|
+
}) {
|
|
160
|
+
return runLedgerJsOperation({
|
|
161
|
+
appName: "Cardano ADA",
|
|
162
|
+
connection,
|
|
163
|
+
// Named export: the package is CommonJS only, so `default` is `module.exports` under Node ESM.
|
|
164
|
+
createApp: (ledgerTransport) => new ledgerModule.Ada(ledgerTransport),
|
|
165
|
+
operation,
|
|
166
|
+
requiredUserInteraction,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function toPublicKey(key: ExtendedPublicKey | undefined) {
|
|
171
|
+
const publicKey = Buffer.from(key?.publicKeyHex ?? "", "hex");
|
|
172
|
+
if (publicKey.length !== 32) {
|
|
173
|
+
throw new SwapKitError("wallet_ledger_failed_to_get_address", { chain: Chain.Cardano, derivationPath: path });
|
|
174
|
+
}
|
|
175
|
+
return publicKey;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const keys = await runCardanoOperation({
|
|
179
|
+
operation: (app) => app.getExtendedPublicKeys({ paths: [paymentPath, stakingPath] }),
|
|
180
|
+
});
|
|
181
|
+
const paymentKey = toPublicKey(keys[0]);
|
|
182
|
+
const stakingKey = toPublicKey(keys[1]);
|
|
183
|
+
|
|
184
|
+
const publicKeyHash = await getCardanoPublicKeyHash(paymentKey);
|
|
185
|
+
const address = await getCardanoAddressFromBytes(
|
|
186
|
+
Buffer.concat([
|
|
187
|
+
Buffer.from([Networks.Mainnet.networkId]),
|
|
188
|
+
publicKeyHash,
|
|
189
|
+
await getCardanoPublicKeyHash(stakingKey),
|
|
190
|
+
]),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
async function signTransaction(txHex: string) {
|
|
194
|
+
const request = await toLedgerCardanoTransaction(txHex, { address, paymentPath, stakingPath }, ledgerModule);
|
|
195
|
+
const result = await runCardanoOperation({
|
|
196
|
+
operation: (app) => app.signTransaction(request),
|
|
197
|
+
requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return addLedgerWitnesses(txHex, result, { paymentPath, publicKey: paymentKey });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { getAddress: () => address, publicKey: paymentKey, publicKeyHash, signTransaction };
|
|
204
|
+
}
|
|
@@ -316,6 +316,10 @@ export function PolygonLedger(params: LedgerParams) {
|
|
|
316
316
|
return new EVMLedgerInterface({ ...params, chainId: ChainId.Polygon });
|
|
317
317
|
}
|
|
318
318
|
|
|
319
|
+
export function RobinhoodLedger(params: LedgerParams) {
|
|
320
|
+
return new EVMLedgerInterface({ ...params, chainId: ChainId.Robinhood });
|
|
321
|
+
}
|
|
322
|
+
|
|
319
323
|
export function BinanceSmartChainLedger(params: LedgerParams) {
|
|
320
324
|
return new EVMLedgerInterface({ ...params, chainId: ChainId.BinanceSmartChain });
|
|
321
325
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
|
|
2
|
+
import type Str from "@ledgerhq/hw-app-str";
|
|
3
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
4
|
+
import {
|
|
5
|
+
Chain,
|
|
6
|
+
type DerivationPathArray,
|
|
7
|
+
derivationPathToString,
|
|
8
|
+
NetworkDerivationPath,
|
|
9
|
+
SwapKitError,
|
|
10
|
+
} from "@swapkit/helpers";
|
|
11
|
+
import type { StellarSigner, StellarTransaction } from "@swapkit/toolboxes/stellar";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
LEDGER_USER_INTERACTION_REQUIRED,
|
|
15
|
+
type LedgerJsClientParams,
|
|
16
|
+
normalizeLedgerJsClientParams,
|
|
17
|
+
runLedgerJsOperation,
|
|
18
|
+
} from "../helpers/ledgerJsDmkBridge";
|
|
19
|
+
|
|
20
|
+
type StellarLedgerParams = LedgerJsClientParams<DerivationPathArray>;
|
|
21
|
+
|
|
22
|
+
// The Stellar app derives accounts from m/44'/148'/x' only, like Trezor and the SwapKit keystore.
|
|
23
|
+
export function getStellarLedgerPath(derivationPath: DerivationPathArray) {
|
|
24
|
+
const [purpose, coinType, account, change, index] = derivationPath;
|
|
25
|
+
if (purpose !== 44 || coinType !== 148 || account === undefined || change || index) {
|
|
26
|
+
throw new SwapKitError("wallet_ledger_derivation_path_error", {
|
|
27
|
+
chain: Chain.Stellar,
|
|
28
|
+
derivationPath,
|
|
29
|
+
reason: "Stellar only uses m/44'/148'/x'",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// hw-app-str hardens every segment, so a leading "m" would become "m'" and fail to parse.
|
|
34
|
+
return derivationPathToString([purpose, coinType, account]).replace(/^m\//, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function StellarLedger(paramsOrPath?: StellarLedgerParams | DerivationPathArray, transport?: Transport) {
|
|
38
|
+
const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
|
|
39
|
+
const path = getStellarLedgerPath(derivationPath || NetworkDerivationPath[Chain.Stellar]);
|
|
40
|
+
|
|
41
|
+
async function runStellarOperation<Output>({
|
|
42
|
+
operation,
|
|
43
|
+
requiredUserInteraction,
|
|
44
|
+
}: {
|
|
45
|
+
operation: (app: Str) => Promise<Output>;
|
|
46
|
+
requiredUserInteraction?: UserInteractionRequired;
|
|
47
|
+
}) {
|
|
48
|
+
const StrApp = (await import("@ledgerhq/hw-app-str")).default;
|
|
49
|
+
return runLedgerJsOperation({
|
|
50
|
+
appName: "Stellar",
|
|
51
|
+
connection,
|
|
52
|
+
createApp: (ledgerTransport) => new StrApp(ledgerTransport),
|
|
53
|
+
operation,
|
|
54
|
+
requiredUserInteraction,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const [{ rawPublicKey }, { StrKey }] = await Promise.all([
|
|
59
|
+
runStellarOperation({ operation: (app) => app.getPublicKey(path) }),
|
|
60
|
+
import("@stellar/stellar-sdk"),
|
|
61
|
+
]);
|
|
62
|
+
const address = StrKey.encodeEd25519PublicKey(rawPublicKey);
|
|
63
|
+
|
|
64
|
+
// The device parses the signature base itself, so it displays and signs the transaction exactly as received.
|
|
65
|
+
async function signTransaction(transaction: StellarTransaction) {
|
|
66
|
+
const { signature } = await runStellarOperation({
|
|
67
|
+
operation: (app) => app.signTransaction(path, transaction.signatureBase()),
|
|
68
|
+
requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
transaction.addSignature(address, signature.toString("base64"));
|
|
73
|
+
} catch (error) {
|
|
74
|
+
throw new SwapKitError("wallet_ledger_invalid_signature", { chain: Chain.Stellar, error });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return transaction;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { getAddress: () => address, publicKey: rawPublicKey, signTransaction } satisfies StellarSigner & {
|
|
81
|
+
publicKey: Buffer;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
|
|
2
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
3
|
+
import {
|
|
4
|
+
Chain,
|
|
5
|
+
type DerivationPathArray,
|
|
6
|
+
getRPCUrl,
|
|
7
|
+
NetworkDerivationPath,
|
|
8
|
+
SwapKitError,
|
|
9
|
+
WalletOption,
|
|
10
|
+
} from "@swapkit/helpers";
|
|
11
|
+
import type { TONTransactionInput, TONTransactionMessage } from "@swapkit/toolboxes/ton";
|
|
12
|
+
import type { Address, Cell, MessageRelaxed, StateInit } from "@ton/core";
|
|
13
|
+
import type { TonClient } from "@ton/ton";
|
|
14
|
+
import type { TonPayloadFormat, TonTransport } from "@ton-community/ton-ledger";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
LEDGER_USER_INTERACTION_REQUIRED,
|
|
18
|
+
type LedgerJsClientParams,
|
|
19
|
+
normalizeLedgerJsClientParams,
|
|
20
|
+
runLedgerJsOperation,
|
|
21
|
+
} from "../helpers/ledgerJsDmkBridge";
|
|
22
|
+
|
|
23
|
+
// SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, the TON toolbox default.
|
|
24
|
+
const DEFAULT_SEND_MODE = 3;
|
|
25
|
+
const TRANSACTION_VALID_SECONDS = 300;
|
|
26
|
+
const TON_WORKCHAIN = 0;
|
|
27
|
+
|
|
28
|
+
type TONLedgerParams = LedgerJsClientParams<DerivationPathArray>;
|
|
29
|
+
|
|
30
|
+
interface TONLedgerOrder {
|
|
31
|
+
amount: bigint;
|
|
32
|
+
bounce: boolean;
|
|
33
|
+
payload?: Cell;
|
|
34
|
+
payloadFormat?: TonPayloadFormat;
|
|
35
|
+
stateInit?: StateInit;
|
|
36
|
+
to: Address;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SignedTransferExpectation extends TONLedgerOrder {
|
|
40
|
+
sendMode: number;
|
|
41
|
+
seqno: number;
|
|
42
|
+
timeout: number;
|
|
43
|
+
walletId: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toLedgerPath(derivationPath: DerivationPathArray) {
|
|
47
|
+
const [purpose, coinType, network, workchain, account, ...rest] = derivationPath;
|
|
48
|
+
const isTonPath =
|
|
49
|
+
purpose === 44 &&
|
|
50
|
+
coinType === 607 &&
|
|
51
|
+
network === 0 &&
|
|
52
|
+
workchain === 0 &&
|
|
53
|
+
typeof account === "number" &&
|
|
54
|
+
Number.isInteger(account) &&
|
|
55
|
+
account >= 0 &&
|
|
56
|
+
rest.length === 0;
|
|
57
|
+
|
|
58
|
+
if (!isTonPath) {
|
|
59
|
+
throw new SwapKitError({
|
|
60
|
+
errorKey: "wallet_ledger_derivation_path_error",
|
|
61
|
+
info: { derivationPath, reason: "Ledger TON accounts derive from m/44'/607'/0'/0'/{account}'" },
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return [44, 607, 0, 0, account, 0];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function invalidParams(reason: string, info: Record<string, unknown> = {}) {
|
|
69
|
+
return new SwapKitError({
|
|
70
|
+
errorKey: "wallet_ledger_invalid_params",
|
|
71
|
+
info: { ...info, reason, wallet: WalletOption.LEDGER },
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveSingleMessage(input: TONTransactionInput) {
|
|
76
|
+
const messages = Array.isArray(input) ? input : input.messages;
|
|
77
|
+
const [message] = messages;
|
|
78
|
+
|
|
79
|
+
if (!message) throw invalidParams("A TON transaction needs one message");
|
|
80
|
+
if (messages.length > 1) {
|
|
81
|
+
throw new SwapKitError({
|
|
82
|
+
errorKey: "wallet_ledger_method_not_supported",
|
|
83
|
+
info: {
|
|
84
|
+
messageCount: messages.length,
|
|
85
|
+
method: "signTransaction",
|
|
86
|
+
reason: "Ledger signs one TON message per transaction",
|
|
87
|
+
wallet: WalletOption.LEDGER,
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const sendMode =
|
|
93
|
+
(Array.isArray(input) ? message.sendMode : (input.sendMode ?? message.sendMode)) ?? DEFAULT_SEND_MODE;
|
|
94
|
+
if (!Array.isArray(input) && message.sendMode !== undefined && message.sendMode !== sendMode) {
|
|
95
|
+
throw invalidParams("The message send mode differs from the transaction send mode", {
|
|
96
|
+
messageSendMode: message.sendMode,
|
|
97
|
+
sendMode,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!Number.isInteger(sendMode) || sendMode < 0 || sendMode > 255) {
|
|
102
|
+
throw invalidParams("The TON send mode must be an integer between 0 and 255", { sendMode });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { message, sendMode };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function toLedgerOrder(message: TONTransactionMessage): Promise<TONLedgerOrder> {
|
|
109
|
+
const [{ Address, Cell, loadStateInit }, { parseMessage }] = await Promise.all([
|
|
110
|
+
import("@ton/core"),
|
|
111
|
+
import("@ton-community/ton-ledger"),
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
if (!/^\d+$/.test(message.amount)) {
|
|
115
|
+
throw invalidParams("The TON amount must be an integer number of nanotons", { amount: message.amount });
|
|
116
|
+
}
|
|
117
|
+
const amount = BigInt(message.amount);
|
|
118
|
+
|
|
119
|
+
let to: Address;
|
|
120
|
+
try {
|
|
121
|
+
to = Address.parse(message.address);
|
|
122
|
+
} catch {
|
|
123
|
+
throw invalidParams("Invalid TON destination address", { address: message.address });
|
|
124
|
+
}
|
|
125
|
+
// Raw `0:<hex>` destinations default to bounceable, the toolbox convention; friendly ones carry their own flag.
|
|
126
|
+
const bounce = Address.isFriendly(message.address) ? Address.parseFriendly(message.address).isBounceable : true;
|
|
127
|
+
|
|
128
|
+
let stateInit: StateInit | undefined;
|
|
129
|
+
if (message.stateInit !== undefined) {
|
|
130
|
+
try {
|
|
131
|
+
stateInit = loadStateInit(Cell.fromBase64(message.stateInit).beginParse());
|
|
132
|
+
} catch {
|
|
133
|
+
throw invalidParams("The TON state init is not a valid BOC", { stateInit: message.stateInit });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let payload: Cell | undefined;
|
|
138
|
+
if (message.payload !== undefined) {
|
|
139
|
+
try {
|
|
140
|
+
payload = Cell.fromBase64(message.payload);
|
|
141
|
+
} catch {
|
|
142
|
+
throw invalidParams("The TON payload is not a valid BOC", { payload: message.payload });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// A payload the app cannot display is kept as the untouched `unsafe` cell, which the device signs blind or refuses.
|
|
147
|
+
const payloadFormat = payload ? parseMessage(payload, { disallowModification: true }) : undefined;
|
|
148
|
+
|
|
149
|
+
return { amount, bounce, payload, payloadFormat, stateInit, to };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function sameCell(a: Cell | null | undefined, b: Cell | null | undefined) {
|
|
153
|
+
return a && b ? a.hash().equals(b.hash()) : a == null && b == null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function sameStateInit(signed: StateInit | null | undefined, requested: StateInit | undefined) {
|
|
157
|
+
if (!(signed && requested)) return signed == null && requested == null;
|
|
158
|
+
return sameCell(signed.code, requested.code) && sameCell(signed.data, requested.data);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// The library re-encodes structured payloads before the device signs them, so the body it returns is only
|
|
162
|
+
// broadcast when the signed order is the requested one.
|
|
163
|
+
async function assertSignedTransfer(body: Cell, expected: SignedTransferExpectation) {
|
|
164
|
+
const { Cell, loadMessageRelaxed } = await import("@ton/core");
|
|
165
|
+
|
|
166
|
+
let signed: { walletId: number; timeout: number; seqno: number; sendMode: number; order: MessageRelaxed };
|
|
167
|
+
try {
|
|
168
|
+
const transfer = body.beginParse().skip(512);
|
|
169
|
+
const walletId = transfer.loadUint(32);
|
|
170
|
+
const timeout = transfer.loadUint(32);
|
|
171
|
+
const seqno = transfer.loadUint(32);
|
|
172
|
+
transfer.skip(8);
|
|
173
|
+
const sendMode = transfer.loadUint(8);
|
|
174
|
+
const order = loadMessageRelaxed(transfer.loadRef().beginParse());
|
|
175
|
+
transfer.endParse();
|
|
176
|
+
signed = { order, sendMode, seqno, timeout, walletId };
|
|
177
|
+
} catch (error) {
|
|
178
|
+
throw new SwapKitError(
|
|
179
|
+
{ errorKey: "wallet_ledger_signing_error", info: { reason: "Unreadable signed TON transfer" } },
|
|
180
|
+
error,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const { order } = signed;
|
|
185
|
+
const matches =
|
|
186
|
+
signed.walletId === expected.walletId &&
|
|
187
|
+
signed.timeout === expected.timeout &&
|
|
188
|
+
signed.seqno === expected.seqno &&
|
|
189
|
+
signed.sendMode === expected.sendMode &&
|
|
190
|
+
order.info.type === "internal" &&
|
|
191
|
+
order.info.dest.equals(expected.to) &&
|
|
192
|
+
order.info.value.coins === expected.amount &&
|
|
193
|
+
order.info.bounce === expected.bounce &&
|
|
194
|
+
order.body.hash().equals((expected.payload ?? Cell.EMPTY).hash()) &&
|
|
195
|
+
sameStateInit(order.init, expected.stateInit);
|
|
196
|
+
if (!matches) {
|
|
197
|
+
throw new SwapKitError({
|
|
198
|
+
errorKey: "wallet_ledger_signing_error",
|
|
199
|
+
info: { reason: "The signed TON transfer does not match the requested message" },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function TONLedger(paramsOrPath?: TONLedgerParams | DerivationPathArray, transport?: Transport) {
|
|
205
|
+
const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
|
|
206
|
+
const path = toLedgerPath(derivationPath || NetworkDerivationPath[Chain.Ton]);
|
|
207
|
+
|
|
208
|
+
async function runTonOperation<Output>({
|
|
209
|
+
operation,
|
|
210
|
+
requiredUserInteraction,
|
|
211
|
+
}: {
|
|
212
|
+
operation: (app: TonTransport) => Promise<Output>;
|
|
213
|
+
requiredUserInteraction?: UserInteractionRequired;
|
|
214
|
+
}) {
|
|
215
|
+
const { TonTransport } = await import("@ton-community/ton-ledger");
|
|
216
|
+
return runLedgerJsOperation({
|
|
217
|
+
appName: "TON",
|
|
218
|
+
connection,
|
|
219
|
+
createApp: (ledgerTransport) => new TonTransport(ledgerTransport),
|
|
220
|
+
operation,
|
|
221
|
+
requiredUserInteraction,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const [{ Address }, { WalletContractV4 }] = await Promise.all([import("@ton/core"), import("@ton/ton")]);
|
|
226
|
+
const { address: deviceAddress, publicKey } = await runTonOperation({
|
|
227
|
+
operation: (app) =>
|
|
228
|
+
app.getAddress(path, { bounceable: false, chain: TON_WORKCHAIN, testOnly: false, walletVersion: "v4" }),
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
const wallet = WalletContractV4.create({ publicKey, workchain: TON_WORKCHAIN });
|
|
232
|
+
if (!wallet.address.equals(Address.parse(deviceAddress))) {
|
|
233
|
+
throw new SwapKitError({
|
|
234
|
+
errorKey: "wallet_ledger_failed_to_get_address",
|
|
235
|
+
info: { deviceAddress, reason: "The Ledger TON address is not the wallet v4 address of its public key" },
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const address = wallet.address.toString({ bounceable: false, urlSafe: true });
|
|
239
|
+
|
|
240
|
+
let client: TonClient | undefined;
|
|
241
|
+
async function getWalletState() {
|
|
242
|
+
if (!client) {
|
|
243
|
+
const { TonClient } = await import("@ton/ton");
|
|
244
|
+
client = new TonClient({ endpoint: await getRPCUrl(Chain.Ton) });
|
|
245
|
+
}
|
|
246
|
+
const { state } = await client.getContractState(wallet.address);
|
|
247
|
+
const deployed = state === "active";
|
|
248
|
+
const seqno = deployed ? await client.open(wallet).getSeqno() : 0;
|
|
249
|
+
|
|
250
|
+
return { deployed, seqno };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function signTransaction(input: TONTransactionInput): Promise<Cell> {
|
|
254
|
+
const { message, sendMode } = resolveSingleMessage(input);
|
|
255
|
+
const order = await toLedgerOrder(message);
|
|
256
|
+
const { deployed, seqno } = await getWalletState();
|
|
257
|
+
const timeout = Math.floor(Date.now() / 1000) + TRANSACTION_VALID_SECONDS;
|
|
258
|
+
|
|
259
|
+
const body = await runTonOperation({
|
|
260
|
+
operation: (app) =>
|
|
261
|
+
app.signTransaction(path, {
|
|
262
|
+
amount: order.amount,
|
|
263
|
+
bounce: order.bounce,
|
|
264
|
+
payload: order.payloadFormat,
|
|
265
|
+
sendMode,
|
|
266
|
+
seqno,
|
|
267
|
+
stateInit: order.stateInit,
|
|
268
|
+
timeout,
|
|
269
|
+
to: order.to,
|
|
270
|
+
}),
|
|
271
|
+
requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
|
|
272
|
+
});
|
|
273
|
+
await assertSignedTransfer(body, { ...order, sendMode, seqno, timeout, walletId: wallet.walletId });
|
|
274
|
+
|
|
275
|
+
const { beginCell, external, storeMessage } = await import("@ton/core");
|
|
276
|
+
return beginCell()
|
|
277
|
+
.store(storeMessage(external({ body, init: deployed ? undefined : wallet.init, to: wallet.address })))
|
|
278
|
+
.endCell();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { getAddress: () => address, publicKey, signTransaction };
|
|
282
|
+
}
|
|
@@ -22,7 +22,7 @@ import { executeLedgerDeviceAction, type LedgerDeviceActionStateHandler } from "
|
|
|
22
22
|
import { runLedgerJsOperation } from "../helpers/ledgerJsDmkBridge";
|
|
23
23
|
import { ZcashLedger as LegacyZcashLedger } from "./utxo";
|
|
24
24
|
|
|
25
|
-
// @swapkit/utxo-signer
|
|
25
|
+
// @swapkit/utxo-signer exports no NU6.2 activation height.
|
|
26
26
|
const NU6_2_ACTIVATION_HEIGHT = 3_364_600;
|
|
27
27
|
|
|
28
28
|
interface ZcashLedgerParams {
|
|
@@ -74,6 +74,11 @@ function parseZcashPath(path: DerivationPathArray | string): ParsedZcashPath {
|
|
|
74
74
|
return { account, accountPath, fullPath: `${accountPath}/${match[2] ?? 0}/${match[3] ?? 0}` };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
function deviceChangePath(path: DerivationPathArray | string) {
|
|
78
|
+
const { fullPath } = parseZcashPath(path);
|
|
79
|
+
return fullPath.split("/")[3] === "1" ? fullPath : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
function uint32LE(value: number) {
|
|
78
83
|
const bytes = new Uint8Array(4);
|
|
79
84
|
new DataView(bytes.buffer).setUint32(0, value >>> 0, true);
|
|
@@ -161,8 +166,7 @@ function parsePreviousTransaction({ bytes, version }: { bytes: Uint8Array; versi
|
|
|
161
166
|
return { ...transparentFields, extraData: reader.rest(), locktime, nExpiryHeight };
|
|
162
167
|
}
|
|
163
168
|
|
|
164
|
-
// Either byte order binds a txid equally:
|
|
165
|
-
// ZcashTransaction.fromBytes keeps them in wire order.
|
|
169
|
+
// Either byte order binds a txid equally: utxo-signer below 3.2 parsed raw transactions with wire-order txids.
|
|
166
170
|
function matchesTxid({ hash, txid }: { hash: string; txid: Uint8Array }) {
|
|
167
171
|
const normalized = hash.toLowerCase();
|
|
168
172
|
return normalized === hex.encode(txid) || normalized === hex.encode(txid.slice().reverse());
|
|
@@ -178,17 +182,21 @@ async function serializeOutputs(tx: ZcashTransaction) {
|
|
|
178
182
|
}
|
|
179
183
|
|
|
180
184
|
async function activationHeight(consensusBranchId: number) {
|
|
181
|
-
const { ZCASH_IRONWOOD_ACTIVATION_HEIGHT, ZcashConsensusBranchId } = await import(
|
|
185
|
+
const { ZCASH_IRONWOOD_ACTIVATION_HEIGHT, ZCASH_NU7_ACTIVATION_HEIGHT, ZcashConsensusBranchId } = await import(
|
|
186
|
+
"@swapkit/utxo-signer"
|
|
187
|
+
);
|
|
182
188
|
|
|
183
189
|
switch (consensusBranchId) {
|
|
184
190
|
case ZcashConsensusBranchId.NU6_2:
|
|
185
191
|
return NU6_2_ACTIVATION_HEIGHT;
|
|
186
192
|
case ZcashConsensusBranchId.IRONWOOD:
|
|
187
193
|
return ZCASH_IRONWOOD_ACTIVATION_HEIGHT;
|
|
194
|
+
case ZcashConsensusBranchId.NU7:
|
|
195
|
+
return ZCASH_NU7_ACTIVATION_HEIGHT;
|
|
188
196
|
default:
|
|
189
197
|
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
190
198
|
consensusBranchId,
|
|
191
|
-
reason: "Ledger Zcash DSK signing supports NU6.2 and
|
|
199
|
+
reason: "Ledger Zcash DSK signing supports NU6.2, Ironwood and NU7 target branches",
|
|
192
200
|
});
|
|
193
201
|
}
|
|
194
202
|
}
|
|
@@ -394,6 +402,8 @@ export function ZcashLedger({
|
|
|
394
402
|
utxo.index,
|
|
395
403
|
null,
|
|
396
404
|
tx.getInput(inputIndex).sequence ?? 0xffffffff,
|
|
405
|
+
// Frames a v4 previous transaction with this branch id; unset, the kit assumes the latest upgrade
|
|
406
|
+
blockHeight,
|
|
397
407
|
],
|
|
398
408
|
),
|
|
399
409
|
);
|
|
@@ -401,7 +411,7 @@ export function ZcashLedger({
|
|
|
401
411
|
additionals: ["zcash", "sapling"],
|
|
402
412
|
associatedKeysets: paths,
|
|
403
413
|
blockHeight,
|
|
404
|
-
changePath:
|
|
414
|
+
changePath: deviceChangePath(changePath ?? configuredPath.fullPath),
|
|
405
415
|
expiryHeight: uint32LE(tx.expiryHeight),
|
|
406
416
|
inputs,
|
|
407
417
|
lockTime: tx.lockTime,
|