@swapkit/wallet-hardware 5.0.0 → 6.0.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 +2344 -0
- package/dist/chunk-09e51wma.js +4 -0
- package/dist/chunk-09e51wma.js.map +167 -0
- package/dist/chunk-0arpsm5f.js +4 -0
- package/dist/chunk-0arpsm5f.js.map +10 -0
- package/dist/chunk-1amf58aq.js +4 -0
- package/dist/{chunk-vwv6z2mk.js.map → chunk-1amf58aq.js.map} +2 -2
- package/dist/chunk-3t17wpjg.js +17 -0
- package/dist/chunk-3t17wpjg.js.map +29 -0
- package/dist/chunk-5c7b7z2d.js +4 -0
- package/dist/{chunk-9gy0xve4.js.map → chunk-5c7b7z2d.js.map} +2 -2
- package/dist/chunk-5p3ttw6y.js +4 -0
- package/dist/{chunk-n05bv2n5.js.map → chunk-5p3ttw6y.js.map} +1 -1
- package/dist/chunk-dptbnkzm.js +64 -0
- package/dist/chunk-dptbnkzm.js.map +260 -0
- package/dist/chunk-ds2m83jp.js +5 -0
- package/dist/chunk-ds2m83jp.js.map +11 -0
- package/dist/chunk-eenx3cv6.js +4 -0
- package/dist/chunk-eenx3cv6.js.map +33 -0
- package/dist/chunk-gr3eswf1.js +4 -0
- package/dist/chunk-gr3eswf1.js.map +73 -0
- package/dist/{chunk-7fsaymh4.js → chunk-hg71hp4m.js} +3 -3
- package/dist/{chunk-7fsaymh4.js.map → chunk-hg71hp4m.js.map} +1 -1
- package/dist/chunk-jewm2ag3.js +5 -0
- package/dist/chunk-jewm2ag3.js.map +10 -0
- package/dist/chunk-jzrytcrf.js +4 -0
- package/dist/chunk-jzrytcrf.js.map +12 -0
- package/dist/chunk-k3thje6a.js +4 -0
- package/dist/chunk-k3thje6a.js.map +9 -0
- package/dist/chunk-nhva847d.js +5 -0
- package/dist/chunk-nhva847d.js.map +9 -0
- package/dist/chunk-tm47brht.js +4 -0
- package/dist/chunk-tm47brht.js.map +140 -0
- package/dist/chunk-xmvg4cds.js +4 -0
- package/dist/chunk-xmvg4cds.js.map +127 -0
- package/dist/chunk-xx7cwpzq.js +4 -0
- package/dist/chunk-xx7cwpzq.js.map +9 -0
- package/dist/chunk-yrxe491r.js +20 -0
- package/dist/chunk-yrxe491r.js.map +17 -0
- package/dist/index.cjs +2 -2
- package/dist/index.cjs.map +1 -1
- package/dist/keepkey/index.cjs +2 -2
- package/dist/keepkey/index.cjs.map +2 -2
- package/dist/keepkey/index.js +2 -2
- package/dist/keepkey/index.js.map +2 -2
- package/dist/ledger/index.cjs +90 -2
- package/dist/ledger/index.cjs.map +830 -23
- package/dist/ledger/index.js +2 -2
- package/dist/ledger/index.js.map +21 -19
- package/dist/trezor/index.cjs +2 -2
- package/dist/trezor/index.cjs.map +5 -4
- package/dist/trezor/index.js +2 -2
- package/dist/trezor/index.js.map +3 -3
- package/dist/types/helpers/psbt.d.ts +9 -0
- package/dist/types/helpers/psbt.d.ts.map +1 -0
- package/dist/types/ledger/clients/bitcoin.d.ts +36 -0
- package/dist/types/ledger/clients/bitcoin.d.ts.map +1 -0
- package/dist/types/ledger/clients/cosmos.d.ts +38 -10
- package/dist/types/ledger/clients/cosmos.d.ts.map +1 -1
- package/dist/types/ledger/clients/evm.d.ts +37 -46
- package/dist/types/ledger/clients/evm.d.ts.map +1 -1
- package/dist/types/ledger/clients/near.d.ts +5 -2
- package/dist/types/ledger/clients/near.d.ts.map +1 -1
- package/dist/types/ledger/clients/sui.d.ts +8 -12
- package/dist/types/ledger/clients/sui.d.ts.map +1 -1
- package/dist/types/ledger/clients/thorchain/index.d.ts +34 -8
- package/dist/types/ledger/clients/thorchain/index.d.ts.map +1 -1
- package/dist/types/ledger/clients/thorchain/protocol.d.ts +58 -0
- package/dist/types/ledger/clients/thorchain/protocol.d.ts.map +1 -0
- package/dist/types/ledger/clients/tron.d.ts +11 -8
- package/dist/types/ledger/clients/tron.d.ts.map +1 -1
- package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts +3 -2
- package/dist/types/ledger/clients/utxo-legacy-adapter.d.ts.map +1 -1
- package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts +18 -0
- package/dist/types/ledger/clients/utxo-legacy-outputs.d.ts.map +1 -0
- package/dist/types/ledger/clients/utxo.d.ts +10 -13
- package/dist/types/ledger/clients/utxo.d.ts.map +1 -1
- package/dist/types/ledger/clients/xrp.d.ts +4 -1
- package/dist/types/ledger/clients/xrp.d.ts.map +1 -1
- package/dist/types/ledger/clients/zcash.d.ts +53 -0
- package/dist/types/ledger/clients/zcash.d.ts.map +1 -0
- package/dist/types/ledger/helpers/dmk.d.ts +24 -0
- package/dist/types/ledger/helpers/dmk.d.ts.map +1 -0
- package/dist/types/ledger/helpers/executeDeviceAction.d.ts +34 -0
- package/dist/types/ledger/helpers/executeDeviceAction.d.ts.map +1 -0
- package/dist/types/ledger/helpers/getLedgerAddress.d.ts +1 -1
- package/dist/types/ledger/helpers/getLedgerAddress.d.ts.map +1 -1
- package/dist/types/ledger/helpers/getLedgerClient.d.ts +10 -3
- package/dist/types/ledger/helpers/getLedgerClient.d.ts.map +1 -1
- package/dist/types/ledger/helpers/index.d.ts +2 -1
- package/dist/types/ledger/helpers/index.d.ts.map +1 -1
- package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts +26 -0
- package/dist/types/ledger/helpers/ledgerJsDmkBridge.d.ts.map +1 -0
- package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts +13 -0
- package/dist/types/ledger/helpers/ledgerJsDmkTransport.d.ts.map +1 -0
- package/dist/types/ledger/helpers/lowS.d.ts +6 -0
- package/dist/types/ledger/helpers/lowS.d.ts.map +1 -0
- package/dist/types/ledger/helpers/rawTx.d.ts +2 -0
- package/dist/types/ledger/helpers/rawTx.d.ts.map +1 -0
- package/dist/types/ledger/index.d.ts +19 -7
- package/dist/types/ledger/index.d.ts.map +1 -1
- package/dist/types/ledger/types.d.ts +6 -10
- package/dist/types/ledger/types.d.ts.map +1 -1
- package/dist/types/trezor/index.d.ts +2 -2
- package/dist/types/trezor/index.d.ts.map +1 -1
- package/package.json +36 -36
- package/src/helpers/psbt.ts +26 -0
- package/src/ledger/clients/bitcoin.ts +575 -0
- package/src/ledger/clients/cosmos.ts +180 -51
- package/src/ledger/clients/evm.ts +238 -195
- package/src/ledger/clients/near.ts +43 -13
- package/src/ledger/clients/sui.ts +55 -68
- package/src/ledger/clients/thorchain/index.ts +210 -86
- package/src/ledger/clients/thorchain/protocol.ts +166 -0
- package/src/ledger/clients/tron.ts +49 -37
- package/src/ledger/clients/utxo-legacy-adapter.ts +35 -32
- package/src/ledger/clients/utxo-legacy-outputs.ts +122 -0
- package/src/ledger/clients/utxo.ts +108 -206
- package/src/ledger/clients/xrp.ts +39 -23
- package/src/ledger/clients/zcash.ts +440 -0
- package/src/ledger/helpers/dmk.ts +231 -0
- package/src/ledger/helpers/executeDeviceAction.ts +201 -0
- package/src/ledger/helpers/getLedgerClient.ts +101 -72
- package/src/ledger/helpers/index.ts +6 -1
- package/src/ledger/helpers/ledgerJsDmkBridge.ts +109 -0
- package/src/ledger/helpers/ledgerJsDmkTransport.ts +88 -0
- package/src/ledger/helpers/lowS.ts +30 -0
- package/src/ledger/helpers/rawTx.ts +12 -0
- package/src/ledger/index.ts +564 -369
- package/src/ledger/types.ts +15 -10
- package/src/trezor/index.ts +117 -25
- package/dist/chunk-3yr76n9s.js +0 -5
- package/dist/chunk-3yr76n9s.js.map +0 -10
- package/dist/chunk-9gy0xve4.js +0 -4
- package/dist/chunk-bwf31zxr.js +0 -4
- package/dist/chunk-bwf31zxr.js.map +0 -10
- package/dist/chunk-egtwbe6q.js +0 -4
- package/dist/chunk-egtwbe6q.js.map +0 -10
- package/dist/chunk-n05bv2n5.js +0 -5
- package/dist/chunk-vwv6z2mk.js +0 -4
- package/dist/types/ledger/clients/thorchain/common.d.ts +0 -28
- package/dist/types/ledger/clients/thorchain/common.d.ts.map +0 -1
- package/dist/types/ledger/clients/thorchain/helpers.d.ts +0 -7
- package/dist/types/ledger/clients/thorchain/helpers.d.ts.map +0 -1
- package/dist/types/ledger/clients/thorchain/lib.d.ts +0 -22
- package/dist/types/ledger/clients/thorchain/lib.d.ts.map +0 -1
- package/dist/types/ledger/clients/thorchain/utils.d.ts +0 -2
- package/dist/types/ledger/clients/thorchain/utils.d.ts.map +0 -1
- package/dist/types/ledger/clients/utxo-psbt.d.ts +0 -18
- package/dist/types/ledger/clients/utxo-psbt.d.ts.map +0 -1
- package/dist/types/ledger/cosmosTypes.d.ts +0 -40
- package/dist/types/ledger/cosmosTypes.d.ts.map +0 -1
- package/dist/types/ledger/helpers/getLedgerTransport.d.ts +0 -3
- package/dist/types/ledger/helpers/getLedgerTransport.d.ts.map +0 -1
- package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts +0 -14
- package/dist/types/ledger/helpers/ledgerNetworkInfo.d.ts.map +0 -1
- package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts +0 -14
- package/dist/types/ledger/interfaces/CosmosLedgerInterface.d.ts.map +0 -1
- package/src/ledger/clients/thorchain/common.ts +0 -93
- package/src/ledger/clients/thorchain/helpers.ts +0 -120
- package/src/ledger/clients/thorchain/lib.ts +0 -258
- package/src/ledger/clients/thorchain/utils.ts +0 -69
- package/src/ledger/clients/utxo-psbt.ts +0 -172
- package/src/ledger/cosmosTypes.ts +0 -98
- package/src/ledger/helpers/getLedgerTransport.ts +0 -102
- package/src/ledger/helpers/ledgerNetworkInfo.ts +0 -127
- package/src/ledger/interfaces/CosmosLedgerInterface.ts +0 -67
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
2
|
+
import { SwapKitError } from "@swapkit/helpers";
|
|
3
|
+
|
|
4
|
+
const CLA = 0x55;
|
|
5
|
+
const INS_GET_ADDRESS = 0x04;
|
|
6
|
+
const INS_GET_VERSION = 0x00;
|
|
7
|
+
const INS_SIGN = 0x02;
|
|
8
|
+
const P1_INIT = 0x00;
|
|
9
|
+
const P1_ADD = 0x01;
|
|
10
|
+
const P1_LAST = 0x02;
|
|
11
|
+
const STATUS_OK = 0x9000;
|
|
12
|
+
const SIGN_CHUNK_SIZE = 250;
|
|
13
|
+
|
|
14
|
+
interface ThorCommandParams {
|
|
15
|
+
data?: Uint8Array;
|
|
16
|
+
ins: number;
|
|
17
|
+
p1?: number;
|
|
18
|
+
p2?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ThorApduResponse {
|
|
22
|
+
data: Uint8Array;
|
|
23
|
+
statusCode: Uint8Array;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class ThorCommand {
|
|
27
|
+
readonly name = "THORChainCommand";
|
|
28
|
+
private readonly params: ThorCommandParams;
|
|
29
|
+
|
|
30
|
+
constructor(params: ThorCommandParams) {
|
|
31
|
+
this.params = params;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getApdu() {
|
|
35
|
+
const { data = new Uint8Array(), ins, p1 = 0, p2 = 0 } = this.params;
|
|
36
|
+
return { cla: CLA, data, ins, p1, p2 };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
parseResponse(response: ThorApduResponse) {
|
|
40
|
+
const status = (response.statusCode[0] ?? 0) * 0x100 + (response.statusCode[1] ?? 0);
|
|
41
|
+
if (status !== STATUS_OK) {
|
|
42
|
+
throw new Error(`THORChain app returned status 0x${status.toString(16).padStart(4, "0")}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return response.data;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function serializeThorPath({ path }: { path: readonly (number | undefined)[] }) {
|
|
50
|
+
if (path.length !== 5) {
|
|
51
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "THORChain path must contain five elements" });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const serialized = new Uint8Array(20);
|
|
55
|
+
const view = new DataView(serialized.buffer);
|
|
56
|
+
|
|
57
|
+
path.forEach((segment, index) => {
|
|
58
|
+
if (typeof segment !== "number" || !Number.isInteger(segment) || segment < 0 || segment > 0xffffffff) {
|
|
59
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: `Invalid THORChain path segment ${index}` });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The app hardens purpose, coin type and account; a hardened change or index would silently derive
|
|
63
|
+
// a different key if its bit were cleared, so reject it instead.
|
|
64
|
+
if (index >= 3 && segment >= 0x80000000) {
|
|
65
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
66
|
+
reason: `THORChain path segment ${index} must not be hardened`,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalized = segment & 0x7fffffff;
|
|
71
|
+
const value = index < 3 ? (normalized | 0x80000000) >>> 0 : normalized;
|
|
72
|
+
view.setUint32(index * 4, value, true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return serialized;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function serializeThorAddressPayload({ hrp, path }: { hrp: string; path: readonly (number | undefined)[] }) {
|
|
79
|
+
const encodedHrp = new TextEncoder().encode(hrp);
|
|
80
|
+
if (encodedHrp.length < 1 || encodedHrp.length > 83) {
|
|
81
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Invalid THORChain address prefix" });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const serializedPath = serializeThorPath({ path });
|
|
85
|
+
return Uint8Array.from([encodedHrp.length, ...encodedHrp, ...serializedPath]);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function getThorAddressCommand({
|
|
89
|
+
checkOnDevice,
|
|
90
|
+
hrp,
|
|
91
|
+
path,
|
|
92
|
+
}: {
|
|
93
|
+
checkOnDevice: boolean;
|
|
94
|
+
hrp: string;
|
|
95
|
+
path: readonly (number | undefined)[];
|
|
96
|
+
}) {
|
|
97
|
+
return new ThorCommand({
|
|
98
|
+
data: serializeThorAddressPayload({ hrp, path }),
|
|
99
|
+
ins: INS_GET_ADDRESS,
|
|
100
|
+
p1: checkOnDevice ? 1 : 0,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getThorSignCommands({ message, path }: { message: Uint8Array; path: readonly (number | undefined)[] }) {
|
|
105
|
+
const commands = [new ThorCommand({ data: serializeThorPath({ path }), ins: INS_SIGN, p1: P1_INIT })];
|
|
106
|
+
|
|
107
|
+
if (message.length === 0) {
|
|
108
|
+
commands.push(new ThorCommand({ data: new Uint8Array(), ins: INS_SIGN, p1: P1_LAST }));
|
|
109
|
+
return commands;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
for (let offset = 0; offset < message.length; offset += SIGN_CHUNK_SIZE) {
|
|
113
|
+
const end = Math.min(offset + SIGN_CHUNK_SIZE, message.length);
|
|
114
|
+
commands.push(
|
|
115
|
+
new ThorCommand({
|
|
116
|
+
data: message.slice(offset, end),
|
|
117
|
+
ins: INS_SIGN,
|
|
118
|
+
p1: end === message.length ? P1_LAST : P1_ADD,
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return commands;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseTransportResponse({ response }: { response: Uint8Array }) {
|
|
127
|
+
if (response.length < 2) {
|
|
128
|
+
throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain response is missing status bytes" });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return response.slice(0, -2);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getThorLegacyVersion({ transport }: { transport: Transport }) {
|
|
135
|
+
const response = await transport.send(CLA, INS_GET_VERSION, 0, 0, Buffer.alloc(0), [STATUS_OK]);
|
|
136
|
+
const data = parseTransportResponse({ response });
|
|
137
|
+
if (data.length < 4) {
|
|
138
|
+
throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain version response" });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return `${data[1]}.${data[2]}.${data[3]}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function sendThorLegacyCommand({ command, transport }: { command: ThorCommand; transport: Transport }) {
|
|
145
|
+
const apdu = command.getApdu();
|
|
146
|
+
const response = await transport.send(apdu.cla, apdu.ins, apdu.p1, apdu.p2, Buffer.from(apdu.data), [STATUS_OK]);
|
|
147
|
+
return parseTransportResponse({ response });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function parseThorAddressResponse({ response }: { response: Uint8Array }) {
|
|
151
|
+
if (response.length < 34) {
|
|
152
|
+
throw new SwapKitError("wallet_ledger_invalid_response", { reason: "Invalid THORChain address response" });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const publicKey = response.slice(0, 33);
|
|
156
|
+
const address = new TextDecoder().decode(response.slice(33));
|
|
157
|
+
if (!address) {
|
|
158
|
+
throw new SwapKitError("wallet_ledger_invalid_response", { reason: "THORChain address is empty" });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return { address, publicKey };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function invalidThorAppVersionMessage({ version }: { version?: string }) {
|
|
165
|
+
return `THORChain Ledger app major 2 is required, received ${version || "unknown"}`;
|
|
166
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UserInteractionRequired } from "@ledgerhq/device-management-kit";
|
|
1
2
|
import type TronApp from "@ledgerhq/hw-app-trx";
|
|
2
3
|
import type Transport from "@ledgerhq/hw-transport";
|
|
3
4
|
import {
|
|
@@ -8,79 +9,90 @@ import {
|
|
|
8
9
|
} from "@swapkit/helpers";
|
|
9
10
|
import type { TronSignedTransaction, TronSigner, TronTransaction } from "@swapkit/toolboxes/tron";
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
LEDGER_USER_INTERACTION_REQUIRED,
|
|
14
|
+
type LedgerJsClientParams,
|
|
15
|
+
normalizeLedgerJsClientParams,
|
|
16
|
+
runLedgerJsOperation,
|
|
17
|
+
} from "../helpers/ledgerJsDmkBridge";
|
|
18
|
+
|
|
19
|
+
type TronLedgerParams = LedgerJsClientParams<DerivationPathArray | string>;
|
|
12
20
|
|
|
13
21
|
export class TronLedgerInterface implements TronSigner {
|
|
14
|
-
derivationPath: string;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
private readonly injectedTransport?: Transport;
|
|
22
|
+
readonly derivationPath: string;
|
|
23
|
+
readonly ledgerTimeout = 50000;
|
|
24
|
+
private readonly connection: Omit<TronLedgerParams, "derivationPath">;
|
|
18
25
|
|
|
19
|
-
constructor(
|
|
26
|
+
constructor(paramsOrPath?: TronLedgerParams | DerivationPathArray | string, transport?: Transport) {
|
|
27
|
+
const { derivationPath, ...connection } = normalizeLedgerJsClientParams({ paramsOrPath, transport });
|
|
20
28
|
this.derivationPath =
|
|
21
29
|
typeof derivationPath === "string"
|
|
22
30
|
? derivationPath
|
|
23
31
|
: derivationPathToString(derivationPath || NetworkDerivationPath.TRON);
|
|
24
|
-
this.
|
|
32
|
+
this.connection = connection;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private async runTronOperation<Output>({
|
|
36
|
+
operation,
|
|
37
|
+
requiredUserInteraction,
|
|
38
|
+
}: {
|
|
39
|
+
operation: (app: InstanceType<typeof TronApp>) => Promise<Output>;
|
|
40
|
+
requiredUserInteraction?: UserInteractionRequired;
|
|
41
|
+
}) {
|
|
42
|
+
const TronApp = (await import("@ledgerhq/hw-app-trx")).default;
|
|
43
|
+
return runLedgerJsOperation({
|
|
44
|
+
appName: "Tron",
|
|
45
|
+
connection: this.connection,
|
|
46
|
+
createApp: (ledgerTransport) => new TronApp(ledgerTransport),
|
|
47
|
+
operation,
|
|
48
|
+
requiredUserInteraction,
|
|
49
|
+
});
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
checkOrCreateTransportAndLedger = async () => {
|
|
28
|
-
|
|
29
|
-
await this.createTransportAndLedger();
|
|
53
|
+
await this.runTronOperation({ operation: async () => true });
|
|
30
54
|
};
|
|
31
55
|
|
|
32
56
|
createTransportAndLedger = async () => {
|
|
33
|
-
|
|
34
|
-
const TronApp = (await import("@ledgerhq/hw-app-trx")).default;
|
|
35
|
-
|
|
36
|
-
this.ledgerApp = new TronApp(transport);
|
|
57
|
+
await this.runTronOperation({ operation: async () => true });
|
|
37
58
|
};
|
|
38
59
|
|
|
39
|
-
getAddress = async ()
|
|
60
|
+
getAddress = async () => {
|
|
40
61
|
const response = await this.getAddressAndPubKey();
|
|
41
62
|
if (!response) throw new SwapKitError("wallet_ledger_failed_to_get_address");
|
|
42
63
|
return response.address;
|
|
43
64
|
};
|
|
44
65
|
|
|
45
66
|
getAddressAndPubKey = async () => {
|
|
46
|
-
await this.
|
|
47
|
-
const result = await this.ledgerApp?.getAddress(this.derivationPath);
|
|
48
|
-
|
|
67
|
+
const result = await this.runTronOperation({ operation: (app) => app.getAddress(this.derivationPath) });
|
|
49
68
|
if (!result) throw new SwapKitError("wallet_ledger_failed_to_get_address");
|
|
50
69
|
|
|
51
70
|
return { address: result.address, publicKey: result.publicKey };
|
|
52
71
|
};
|
|
53
72
|
|
|
54
73
|
showAddressAndPubKey = async () => {
|
|
55
|
-
await this.
|
|
56
|
-
|
|
74
|
+
return await this.runTronOperation({
|
|
75
|
+
operation: (app) => app.getAddress(this.derivationPath, true),
|
|
76
|
+
requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.VerifyAddress,
|
|
77
|
+
});
|
|
57
78
|
};
|
|
58
79
|
|
|
59
80
|
signTransaction = async (transaction: TronTransaction): Promise<TronSignedTransaction> => {
|
|
60
|
-
await this.createTransportAndLedger();
|
|
61
|
-
|
|
62
|
-
if (!this.ledgerApp) {
|
|
63
|
-
throw new SwapKitError("wallet_ledger_transport_error");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
81
|
try {
|
|
67
|
-
const signature = await this.
|
|
68
|
-
this.derivationPath,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
);
|
|
72
|
-
|
|
73
|
-
if (!signature) {
|
|
74
|
-
throw new SwapKitError("wallet_ledger_signing_error");
|
|
75
|
-
}
|
|
82
|
+
const signature = await this.runTronOperation({
|
|
83
|
+
operation: (app) => app.signTransaction(this.derivationPath, transaction.raw_data_hex, []),
|
|
84
|
+
requiredUserInteraction: LEDGER_USER_INTERACTION_REQUIRED.SignTransaction,
|
|
85
|
+
});
|
|
76
86
|
|
|
77
|
-
|
|
87
|
+
if (!signature) throw new SwapKitError("wallet_ledger_signing_error");
|
|
78
88
|
return { ...transaction, signature: [signature] };
|
|
79
89
|
} catch (error) {
|
|
90
|
+
if (error instanceof SwapKitError) throw error;
|
|
80
91
|
throw new SwapKitError("wallet_ledger_signing_error", { error });
|
|
81
92
|
}
|
|
82
93
|
};
|
|
83
94
|
}
|
|
84
95
|
|
|
85
|
-
export
|
|
86
|
-
new TronLedgerInterface(
|
|
96
|
+
export function TronLedger(paramsOrPath?: TronLedgerParams | DerivationPathArray, transport?: Transport) {
|
|
97
|
+
return new TronLedgerInterface(paramsOrPath, transport);
|
|
98
|
+
}
|
|
@@ -2,6 +2,7 @@ import { hex } from "@scure/base";
|
|
|
2
2
|
import { SwapKitError, type UTXOChain } from "@swapkit/helpers";
|
|
3
3
|
import type { UTXOType } from "@swapkit/toolboxes/utxo";
|
|
4
4
|
import type { Transaction } from "@swapkit/utxo-signer";
|
|
5
|
+
import { createCachedRawTxResolver } from "../helpers/rawTx";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Extract per-input metadata from a V3 PSBT in the shape the legacy
|
|
@@ -16,50 +17,51 @@ import type { Transaction } from "@swapkit/utxo-signer";
|
|
|
16
17
|
export async function extractInputsFromPsbt(tx: Transaction, chain: UTXOChain): Promise<UTXOType[]> {
|
|
17
18
|
const { RawTx } = await import("@swapkit/utxo-signer");
|
|
18
19
|
const { getUtxoApi } = await import("@swapkit/toolboxes/utxo");
|
|
19
|
-
const
|
|
20
|
+
const utxoApi = getUtxoApi(chain);
|
|
21
|
+
const getRawTx = createCachedRawTxResolver((txid) => utxoApi.getRawTx(txid));
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
return Promise.all(
|
|
24
|
+
Array.from({ length: tx.inputsLength }, async (_, inputIndex) => {
|
|
25
|
+
const input = tx.getInput(inputIndex);
|
|
23
26
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (!input.txid || input.index === undefined) {
|
|
28
|
+
throw new Error(`PSBT input ${inputIndex} is missing txid/index`);
|
|
29
|
+
}
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
:
|
|
43
|
-
const nonWitnessPrevout = input.index !== undefined ? input.nonWitnessUtxo?.outputs?.[input.index] : undefined;
|
|
44
|
-
const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0);
|
|
31
|
+
const txid = hex.encode(input.txid);
|
|
32
|
+
const txHex = input.nonWitnessUtxo ? hex.encode(RawTx.encode(input.nonWitnessUtxo)) : await getRawTx(txid);
|
|
33
|
+
if (!txHex) {
|
|
34
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
35
|
+
chain,
|
|
36
|
+
inputIndex,
|
|
37
|
+
reason: "Unable to resolve previous transaction hex for Ledger signing",
|
|
38
|
+
txid,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
const witnessUtxo = input.witnessUtxo
|
|
42
|
+
? { script: input.witnessUtxo.script, value: Number(input.witnessUtxo.amount) }
|
|
43
|
+
: undefined;
|
|
44
|
+
const nonWitnessPrevout = input.nonWitnessUtxo?.outputs?.[input.index];
|
|
45
|
+
const value = witnessUtxo?.value ?? (nonWitnessPrevout ? Number(nonWitnessPrevout.amount) : 0);
|
|
45
46
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return inputs;
|
|
47
|
+
return { hash: txid, index: input.index, txHex, value, witnessUtxo } as UTXOType;
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
export async function signLegacyPsbtTransaction({
|
|
53
|
-
legacyClient,
|
|
54
53
|
chain,
|
|
54
|
+
inputUtxos,
|
|
55
|
+
legacyClient,
|
|
55
56
|
tx,
|
|
56
57
|
}: {
|
|
57
|
-
legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
|
|
58
58
|
chain: UTXOChain;
|
|
59
|
+
inputUtxos?: UTXOType[];
|
|
60
|
+
legacyClient: { signTransaction: (tx: Transaction, inputUtxos: UTXOType[]) => Promise<string> };
|
|
59
61
|
tx: Transaction;
|
|
60
62
|
}): Promise<string> {
|
|
61
|
-
const
|
|
62
|
-
return legacyClient.signTransaction(tx,
|
|
63
|
+
const resolvedInputUtxos = inputUtxos ?? (await extractInputsFromPsbt(tx, chain));
|
|
64
|
+
return legacyClient.signTransaction(tx, resolvedInputUtxos);
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -86,7 +88,8 @@ export function createLegacyPsbtSigner({
|
|
|
86
88
|
const { Transaction: TxClass } = await import("@swapkit/utxo-signer");
|
|
87
89
|
// `Transaction.fromRaw` parses a serialised tx (no PSBT envelope) — exactly
|
|
88
90
|
// what `createPaymentTransaction` returns.
|
|
89
|
-
|
|
91
|
+
// Memo transactions carry an OP_RETURN output, which the parser treats as unknown.
|
|
92
|
+
return TxClass.fromRaw(hex.decode(signedTxHex), { allowUnknownOutputs: true });
|
|
90
93
|
},
|
|
91
94
|
};
|
|
92
95
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
2
|
+
import { SwapKitError } from "@swapkit/helpers";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* lib-app-bitcoin, behind the Bitcoin Cash, Dash, Dogecoin and Litecoin apps, collects the output it is checking in a
|
|
6
|
+
* 100-byte buffer (MAX_OUTPUT_TO_CHECK) while signing the first input. It tests every HASH_INPUT_FINALIZE_FULL payload
|
|
7
|
+
* against the room left in that buffer before appending it, and rejects one that does not fit with 0x6a80 ("Output is
|
|
8
|
+
* too long to be checked"); a complete output leaves the buffer once the user has approved it.
|
|
9
|
+
*/
|
|
10
|
+
const LEGACY_APP_MAX_OUTPUT_LENGTH = 100;
|
|
11
|
+
|
|
12
|
+
const HASH_INPUT_FINALIZE_FULL = { cla: 0xe0, ins: 0x4a } as const;
|
|
13
|
+
const FINALIZE_P1_MORE = 0x00;
|
|
14
|
+
const FINALIZE_P1_LAST = 0x80;
|
|
15
|
+
// The app's own reply to a FINALIZE_P1_MORE APDU: a zero byte and 0x9000.
|
|
16
|
+
const FINALIZE_MORE_RESPONSE = [0x00, 0x90, 0x00];
|
|
17
|
+
|
|
18
|
+
// Bitcoin CompactSize. The 8-byte form describes nothing the app accepts, so it reads as malformed.
|
|
19
|
+
function readCompactSize(bytes: Uint8Array, offset: number): { size: number; value: number } | undefined {
|
|
20
|
+
const marker = bytes[offset];
|
|
21
|
+
if (marker === undefined || marker === 0xff) return undefined;
|
|
22
|
+
if (marker < 0xfd) return { size: 1, value: marker };
|
|
23
|
+
|
|
24
|
+
const width = marker === 0xfd ? 2 : 4;
|
|
25
|
+
if (offset + 1 + width > bytes.length) return undefined;
|
|
26
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset + offset + 1, width);
|
|
27
|
+
return { size: 1 + width, value: width === 2 ? view.getUint16(0, true) : view.getUint32(0, true) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Splits serialised transaction outputs (the output count, then each output) into one piece per output, the first
|
|
32
|
+
* carrying the count, so that every piece the legacy app receives ends on an output boundary. Rejects a malformed
|
|
33
|
+
* stream and any piece the app cannot buffer.
|
|
34
|
+
*/
|
|
35
|
+
export function splitOutputsForLegacyApp(outputs: Uint8Array, chain: string): Uint8Array[] {
|
|
36
|
+
const invalidOutputs = (reason: string, info: Record<string, number> = {}) =>
|
|
37
|
+
new SwapKitError({
|
|
38
|
+
errorKey: "wallet_ledger_invalid_params",
|
|
39
|
+
info: {
|
|
40
|
+
chain,
|
|
41
|
+
maxLength: LEGACY_APP_MAX_OUTPUT_LENGTH,
|
|
42
|
+
operation: "splitOutputsForLegacyApp",
|
|
43
|
+
outputsLength: outputs.length,
|
|
44
|
+
reason,
|
|
45
|
+
...info,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const count = readCompactSize(outputs, 0);
|
|
50
|
+
if (!count?.value) throw invalidOutputs("Transaction outputs do not start with a readable non-zero output count");
|
|
51
|
+
|
|
52
|
+
const pieces: Uint8Array[] = [];
|
|
53
|
+
let offset = count.size;
|
|
54
|
+
for (let outputIndex = 0; outputIndex < count.value; outputIndex += 1) {
|
|
55
|
+
// An output is an 8-byte amount, the script length and the script; the first piece also carries the count.
|
|
56
|
+
const pieceStart = outputIndex === 0 ? 0 : offset;
|
|
57
|
+
const scriptLength = readCompactSize(outputs, offset + 8);
|
|
58
|
+
const outputEnd = scriptLength ? offset + 8 + scriptLength.size + scriptLength.value : undefined;
|
|
59
|
+
if (!outputEnd || outputEnd > outputs.length) {
|
|
60
|
+
throw invalidOutputs("Transaction output is truncated", { outputIndex });
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const pieceLength = outputEnd - pieceStart;
|
|
64
|
+
if (pieceLength > LEGACY_APP_MAX_OUTPUT_LENGTH) {
|
|
65
|
+
throw invalidOutputs("Output is longer than the Ledger app can check; shorten the memo", {
|
|
66
|
+
outputIndex,
|
|
67
|
+
pieceLength,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
pieces.push(outputs.subarray(pieceStart, outputEnd));
|
|
72
|
+
offset = outputEnd;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (offset !== outputs.length) {
|
|
76
|
+
throw invalidOutputs("Transaction outputs are followed by trailing bytes", {
|
|
77
|
+
trailingBytes: outputs.length - offset,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return pieces;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* hw-app-btc streams the outputs in fixed 50-byte HASH_INPUT_FINALIZE_FULL chunks. Each chunk has to fit the app's
|
|
86
|
+
* buffer beside whatever unfinished output earlier chunks left there, which fails for longer memo outputs (in the
|
|
87
|
+
* SDK's [vault, memo, change] order, from a 56-byte memo with P2PKH outputs and a 59-byte memo on Litecoin). Collect
|
|
88
|
+
* the chunks of each stream and resend it one output per APDU: each APDU then meets an empty buffer, so any output of
|
|
89
|
+
* up to 100 bytes (the first together with the output count) fits, and the bytes, and so the hashes, stay the same.
|
|
90
|
+
* hw-app-btc ignores every response to these APDUs, so a collected chunk gets the app's own reply and the last one
|
|
91
|
+
* returns the device's. The change-path APDU (P1 0xff) and every other APDU pass straight through.
|
|
92
|
+
*/
|
|
93
|
+
export function withOutputAlignedFinalize(transport: Transport, chain: string): Transport {
|
|
94
|
+
let chunks: Buffer[] = [];
|
|
95
|
+
|
|
96
|
+
const send: Transport["send"] = async (cla, ins, p1, p2, data = Buffer.alloc(0), ...rest) => {
|
|
97
|
+
const isOutputChunk =
|
|
98
|
+
cla === HASH_INPUT_FINALIZE_FULL.cla &&
|
|
99
|
+
ins === HASH_INPUT_FINALIZE_FULL.ins &&
|
|
100
|
+
(p1 === FINALIZE_P1_MORE || p1 === FINALIZE_P1_LAST);
|
|
101
|
+
if (!isOutputChunk) return transport.send(cla, ins, p1, p2, data, ...rest);
|
|
102
|
+
|
|
103
|
+
chunks.push(data);
|
|
104
|
+
if (p1 === FINALIZE_P1_MORE) return Buffer.from(FINALIZE_MORE_RESPONSE);
|
|
105
|
+
|
|
106
|
+
const outputs = Buffer.concat(chunks);
|
|
107
|
+
chunks = [];
|
|
108
|
+
const pieces = splitOutputsForLegacyApp(outputs, chain);
|
|
109
|
+
|
|
110
|
+
let response: Buffer = Buffer.alloc(0);
|
|
111
|
+
for (const [index, piece] of pieces.entries()) {
|
|
112
|
+
const pieceP1 = index === pieces.length - 1 ? FINALIZE_P1_LAST : FINALIZE_P1_MORE;
|
|
113
|
+
response = await transport.send(cla, ins, pieceP1, p2, Buffer.from(piece), ...rest);
|
|
114
|
+
}
|
|
115
|
+
return response;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// A Proxy rather than a copy, so the app-API lock and every other field stay on the caller's transport.
|
|
119
|
+
return new Proxy(transport, {
|
|
120
|
+
get: (target, property, receiver) => (property === "send" ? send : Reflect.get(target, property, receiver)),
|
|
121
|
+
});
|
|
122
|
+
}
|