@swapkit/wallet-hardware 5.0.1 → 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-xkh15edk.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-20wx60bn.js.map → chunk-5c7b7z2d.js.map} +2 -2
- package/dist/chunk-5p3ttw6y.js +4 -0
- package/dist/{chunk-1jpcb30j.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-4tbtxdxa.js.map → chunk-jewm2ag3.js.map} +2 -2
- 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 -24
- 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 -5
- package/dist/trezor/index.js +2 -2
- package/dist/trezor/index.js.map +3 -3
- 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/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 +115 -27
- package/dist/chunk-1jpcb30j.js +0 -5
- package/dist/chunk-20wx60bn.js +0 -4
- package/dist/chunk-4tbtxdxa.js +0 -5
- package/dist/chunk-7v7cgm69.js +0 -4
- package/dist/chunk-7v7cgm69.js.map +0 -10
- package/dist/chunk-83g13ck6.js +0 -5
- package/dist/chunk-83g13ck6.js.map +0 -10
- package/dist/chunk-h2r568rz.js +0 -4
- package/dist/chunk-h2r568rz.js.map +0 -10
- package/dist/chunk-xkh15edk.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 -204
- 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,201 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DeviceActionIntermediateValue,
|
|
3
|
+
DeviceActionState,
|
|
4
|
+
DeviceActionStatus,
|
|
5
|
+
ExecuteDeviceActionReturnType,
|
|
6
|
+
UserInteractionRequired,
|
|
7
|
+
} from "@ledgerhq/device-management-kit";
|
|
8
|
+
import { SwapKitError } from "@swapkit/helpers";
|
|
9
|
+
import { match } from "ts-pattern";
|
|
10
|
+
|
|
11
|
+
const DEVICE_ACTION_STATUS = {
|
|
12
|
+
Completed: "completed" as DeviceActionStatus.Completed,
|
|
13
|
+
Error: "error" as DeviceActionStatus.Error,
|
|
14
|
+
Stopped: "stopped" as DeviceActionStatus.Stopped,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export const LEDGER_USER_INTERACTION_REQUIRED = {
|
|
18
|
+
None: "none" as UserInteractionRequired.None,
|
|
19
|
+
SignTransaction: "sign-transaction" as UserInteractionRequired.SignTransaction,
|
|
20
|
+
VerifyAddress: "verify-address" as UserInteractionRequired.VerifyAddress,
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type LedgerDeviceActionState = DeviceActionState<unknown, unknown, DeviceActionIntermediateValue>;
|
|
24
|
+
export type LedgerDeviceActionStateHandler = (state: LedgerDeviceActionState) => void;
|
|
25
|
+
|
|
26
|
+
const USER_REFUSED_STATUS_WORDS = new Set(["6985", "5501"]);
|
|
27
|
+
/**
|
|
28
|
+
* Zondax-built apps (Cosmos, THORChain) reject a request on the device with COMMAND_NOT_ALLOWED. The Zcash app uses
|
|
29
|
+
* the same word to refuse a request, so only the clients for the Zondax apps treat it as a user rejection.
|
|
30
|
+
*/
|
|
31
|
+
export const ZONDAX_USER_REFUSED_STATUS_WORDS: readonly string[] = ["6986"];
|
|
32
|
+
const LOCKED_STATUS_WORDS = new Set(["5515"]);
|
|
33
|
+
const WRONG_APP_STATUS_WORDS = new Set(["6511", "6807", "6d00", "6e00"]);
|
|
34
|
+
const DISCONNECTED_ERROR_TAGS = new Set([
|
|
35
|
+
"DeviceDisconnectedBeforeSendingApdu",
|
|
36
|
+
"DeviceDisconnectedWhileSendingError",
|
|
37
|
+
"DeviceNotRecognizedError",
|
|
38
|
+
"DeviceSessionNotFound",
|
|
39
|
+
"NoAccessibleDeviceError",
|
|
40
|
+
"ReconnectionFailedError",
|
|
41
|
+
]);
|
|
42
|
+
// LedgerJS transports (hw-transport-webhid, -webusb) reject an exchange cut off by an unplug with these errors. The
|
|
43
|
+
// @ledgerhq packages advise matching on `name`, which also holds across duplicate copies of @ledgerhq/errors.
|
|
44
|
+
const DISCONNECTED_ERROR_NAMES = new Set(["DisconnectedDevice", "DisconnectedDeviceDuringOperation"]);
|
|
45
|
+
// hw-app-cosmos turns the app's final 0x6986 on a signature into this @ledgerhq/errors class, which carries no word.
|
|
46
|
+
const USER_REFUSED_ERROR_NAMES = new Set(["UserRefusedOnDevice"]);
|
|
47
|
+
// DMK nests causes in `originalError` and the LedgerJS bridge wraps once more, so real chains are two or three
|
|
48
|
+
// levels deep; the bound stops a malformed or cyclic chain.
|
|
49
|
+
const MAX_NESTED_ERROR_DEPTH = 4;
|
|
50
|
+
const STATUS_WORD_PATTERN = /^(?:0x)?([0-9a-f]{4})$/i;
|
|
51
|
+
|
|
52
|
+
type DeviceErrorLike = {
|
|
53
|
+
_tag?: unknown;
|
|
54
|
+
errorCode?: unknown;
|
|
55
|
+
message?: unknown;
|
|
56
|
+
name?: unknown;
|
|
57
|
+
originalError?: unknown;
|
|
58
|
+
statusCode?: unknown;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export interface LedgerDeviceErrorOptions {
|
|
62
|
+
/** App-specific status words that also mean the user rejected the request on the device. */
|
|
63
|
+
userRefusedStatusWords?: readonly string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function errorChainOf(error: unknown) {
|
|
67
|
+
const errors: DeviceErrorLike[] = [];
|
|
68
|
+
let current = error;
|
|
69
|
+
|
|
70
|
+
while (errors.length < MAX_NESTED_ERROR_DEPTH && typeof current === "object" && current !== null) {
|
|
71
|
+
const deviceError: DeviceErrorLike = current;
|
|
72
|
+
errors.push(deviceError);
|
|
73
|
+
current = deviceError.originalError;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return errors;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// DMK command errors carry the word as an `errorCode` string (the Cosmos signer kit keys its own table "0x6986");
|
|
80
|
+
// hw-transport status errors carry it as a numeric `statusCode`.
|
|
81
|
+
function statusWordOf({ errorCode, statusCode }: DeviceErrorLike) {
|
|
82
|
+
const fromErrorCode = typeof errorCode === "string" ? STATUS_WORD_PATTERN.exec(errorCode)?.[1] : undefined;
|
|
83
|
+
if (fromErrorCode) return fromErrorCode.toLowerCase();
|
|
84
|
+
return typeof statusCode === "number" ? statusCode.toString(16).padStart(4, "0") : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* DMK rejects with plain tagged objects (and the LedgerJS bridge nests hw-app status errors inside
|
|
89
|
+
* them), so callers could not tell a user rejection from a wrong app or a disconnected device.
|
|
90
|
+
*/
|
|
91
|
+
export function toLedgerDeviceError(error: unknown, { userRefusedStatusWords = [] }: LedgerDeviceErrorOptions = {}) {
|
|
92
|
+
const errorChain = errorChainOf(error);
|
|
93
|
+
// A SwapKitError thrown inside a bridged LedgerJS operation (input validation) already says what failed.
|
|
94
|
+
const swapKitError = errorChain.find((nested): nested is SwapKitError => nested instanceof SwapKitError);
|
|
95
|
+
if (swapKitError) return swapKitError;
|
|
96
|
+
|
|
97
|
+
const errorTags = errorChain.flatMap(({ _tag }) => (typeof _tag === "string" ? [_tag] : []));
|
|
98
|
+
const errorNames = errorChain.flatMap(({ name }) => (typeof name === "string" ? [name] : []));
|
|
99
|
+
const statusWord = errorChain.map(statusWordOf).find((word) => word !== undefined);
|
|
100
|
+
// The innermost cause says what failed; DMK and the bridge wrap it in generic "device exchange" messages.
|
|
101
|
+
const message = errorChain
|
|
102
|
+
.map((nested) => nested.message)
|
|
103
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0)
|
|
104
|
+
.at(-1);
|
|
105
|
+
const refusedStatusWords = new Set([...USER_REFUSED_STATUS_WORDS, ...userRefusedStatusWords]);
|
|
106
|
+
|
|
107
|
+
const errorKey = match({ errorNames, errorTags, statusWord })
|
|
108
|
+
.when(
|
|
109
|
+
({ errorNames, errorTags, statusWord }) =>
|
|
110
|
+
errorTags.includes("RefusedByUserDAError") ||
|
|
111
|
+
errorNames.some((name) => USER_REFUSED_ERROR_NAMES.has(name)) ||
|
|
112
|
+
(!!statusWord && refusedStatusWords.has(statusWord)),
|
|
113
|
+
() => "wallet_connection_rejected_by_user" as const,
|
|
114
|
+
)
|
|
115
|
+
.when(
|
|
116
|
+
({ errorTags, statusWord }) =>
|
|
117
|
+
errorTags.includes("DeviceLockedError") || (!!statusWord && LOCKED_STATUS_WORDS.has(statusWord)),
|
|
118
|
+
() => "wallet_ledger_device_locked" as const,
|
|
119
|
+
)
|
|
120
|
+
.when(
|
|
121
|
+
({ errorTags, statusWord }) =>
|
|
122
|
+
errorTags.includes("UnsupportedApplicationDAError") || (!!statusWord && WRONG_APP_STATUS_WORDS.has(statusWord)),
|
|
123
|
+
() => "wallet_ledger_app_not_open" as const,
|
|
124
|
+
)
|
|
125
|
+
.when(
|
|
126
|
+
({ errorNames, errorTags }) =>
|
|
127
|
+
errorTags.some((errorTag) => DISCONNECTED_ERROR_TAGS.has(errorTag)) ||
|
|
128
|
+
errorNames.some((name) => DISCONNECTED_ERROR_NAMES.has(name)),
|
|
129
|
+
() => "wallet_ledger_connection_error" as const,
|
|
130
|
+
)
|
|
131
|
+
.otherwise(() => "wallet_ledger_transport_error" as const);
|
|
132
|
+
|
|
133
|
+
return new SwapKitError({ errorKey, info: { errorTag: errorTags[0], message, statusWord } }, error);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* `action` is a thunk because DMK throws, instead of returning an action, when it cannot start one (e.g.
|
|
138
|
+
* DeviceSessionNotFound for a session it has dropped), and the signer kits call DMK synchronously. Calling it here
|
|
139
|
+
* maps that throw like any other device error. Put only the device call inside it, or a plain error from building
|
|
140
|
+
* the request is reported as a transport error.
|
|
141
|
+
*/
|
|
142
|
+
export function executeLedgerDeviceAction<
|
|
143
|
+
Output,
|
|
144
|
+
ActionError,
|
|
145
|
+
IntermediateValue extends DeviceActionIntermediateValue,
|
|
146
|
+
>({
|
|
147
|
+
action,
|
|
148
|
+
onDeviceActionState,
|
|
149
|
+
userRefusedStatusWords,
|
|
150
|
+
}: {
|
|
151
|
+
action: () => ExecuteDeviceActionReturnType<Output, ActionError, IntermediateValue>;
|
|
152
|
+
onDeviceActionState?: LedgerDeviceActionStateHandler;
|
|
153
|
+
} & LedgerDeviceErrorOptions) {
|
|
154
|
+
return new Promise<Output>((resolve, reject) => {
|
|
155
|
+
let observable: ExecuteDeviceActionReturnType<Output, ActionError, IntermediateValue>["observable"];
|
|
156
|
+
try {
|
|
157
|
+
observable = action().observable;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
reject(toLedgerDeviceError(error, { userRefusedStatusWords }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let settled = false;
|
|
164
|
+
let subscription: { unsubscribe: () => void } | undefined;
|
|
165
|
+
|
|
166
|
+
function settle(callback: () => void) {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
subscription?.unsubscribe();
|
|
170
|
+
callback();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
subscription = observable.subscribe({
|
|
174
|
+
complete: () => {
|
|
175
|
+
settle(() => reject(new SwapKitError("wallet_ledger_invalid_response")));
|
|
176
|
+
},
|
|
177
|
+
error: (error) => {
|
|
178
|
+
settle(() => reject(toLedgerDeviceError(error, { userRefusedStatusWords })));
|
|
179
|
+
},
|
|
180
|
+
next: (state) => {
|
|
181
|
+
try {
|
|
182
|
+
onDeviceActionState?.(state);
|
|
183
|
+
} catch (error) {
|
|
184
|
+
void error;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
match(state)
|
|
188
|
+
.with({ status: DEVICE_ACTION_STATUS.Completed }, ({ output }) => settle(() => resolve(output)))
|
|
189
|
+
.with({ status: DEVICE_ACTION_STATUS.Error }, ({ error }) =>
|
|
190
|
+
settle(() => reject(toLedgerDeviceError(error, { userRefusedStatusWords }))),
|
|
191
|
+
)
|
|
192
|
+
.with({ status: DEVICE_ACTION_STATUS.Stopped }, () =>
|
|
193
|
+
settle(() => reject(new SwapKitError("wallet_ledger_connection_error"))),
|
|
194
|
+
)
|
|
195
|
+
.otherwise(() => undefined);
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
if (settled) subscription.unsubscribe();
|
|
200
|
+
});
|
|
201
|
+
}
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type Transport from "@ledgerhq/hw-transport";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
Chain,
|
|
4
|
+
type DerivationPathArray,
|
|
5
|
+
type EVMChain,
|
|
6
|
+
EVMChains,
|
|
7
|
+
NetworkDerivationPath,
|
|
8
|
+
SwapKitError,
|
|
9
|
+
WalletOption,
|
|
10
|
+
} from "@swapkit/helpers";
|
|
11
|
+
import { BitcoinLedger } from "../clients/bitcoin";
|
|
4
12
|
import { CosmosLedger } from "../clients/cosmos";
|
|
5
13
|
import {
|
|
6
14
|
ArbitrumLedger,
|
|
@@ -21,15 +29,11 @@ import { getNearLedgerClient } from "../clients/near";
|
|
|
21
29
|
import { SuiLedger } from "../clients/sui";
|
|
22
30
|
import { THORChainLedger } from "../clients/thorchain";
|
|
23
31
|
import { TronLedger } from "../clients/tron";
|
|
24
|
-
import {
|
|
25
|
-
BitcoinCashLedger,
|
|
26
|
-
BitcoinLedger,
|
|
27
|
-
DashLedger,
|
|
28
|
-
DogecoinLedger,
|
|
29
|
-
LitecoinLedger,
|
|
30
|
-
ZcashLedger,
|
|
31
|
-
} from "../clients/utxo";
|
|
32
|
+
import { BitcoinCashLedger, DashLedger, DogecoinLedger, LitecoinLedger } from "../clients/utxo";
|
|
32
33
|
import { XRPLedger } from "../clients/xrp";
|
|
34
|
+
import { ZcashLedger } from "../clients/zcash";
|
|
35
|
+
import type { LedgerDMKSession } from "./dmk";
|
|
36
|
+
import type { LedgerDeviceActionStateHandler } from "./executeDeviceAction";
|
|
33
37
|
|
|
34
38
|
type LedgerSignerMap = {
|
|
35
39
|
[Chain.Arbitrum]: ReturnType<typeof ArbitrumLedger>;
|
|
@@ -61,75 +65,100 @@ type LedgerSignerMap = {
|
|
|
61
65
|
|
|
62
66
|
type LedgerSupportedChain = keyof LedgerSignerMap;
|
|
63
67
|
|
|
64
|
-
export
|
|
68
|
+
export async function getLedgerClient<T extends LedgerSupportedChain>({
|
|
65
69
|
chain,
|
|
70
|
+
dmkSession,
|
|
66
71
|
derivationPath,
|
|
72
|
+
onDeviceActionState,
|
|
73
|
+
originToken,
|
|
67
74
|
transport,
|
|
68
75
|
}: {
|
|
69
76
|
chain: T;
|
|
77
|
+
dmkSession?: LedgerDMKSession;
|
|
70
78
|
derivationPath?: DerivationPathArray;
|
|
79
|
+
onDeviceActionState?: LedgerDeviceActionStateHandler;
|
|
80
|
+
originToken?: string;
|
|
71
81
|
transport?: Transport;
|
|
72
|
-
}): Promise<LedgerSignerMap[T]>
|
|
82
|
+
}): Promise<LedgerSignerMap[T]> {
|
|
83
|
+
const isEVMChain = EVMChains.includes(chain as EVMChain);
|
|
84
|
+
|
|
85
|
+
if (dmkSession && transport) {
|
|
86
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
87
|
+
message: "Provide either a Ledger DMK session or a LedgerJS transport, not both",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (isEVMChain && transport) {
|
|
92
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
93
|
+
message: "Ledger EVM clients require a DMK session instead of a LedgerJS transport",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The default session is resolved per operation by each client, so a reconnected device is picked up.
|
|
98
|
+
const clientParams = {
|
|
99
|
+
derivationPath: derivationPath ?? NetworkDerivationPath[chain],
|
|
100
|
+
dmkSession,
|
|
101
|
+
onDeviceActionState,
|
|
102
|
+
transport,
|
|
103
|
+
};
|
|
73
104
|
const { match } = await import("ts-pattern");
|
|
74
105
|
|
|
75
|
-
return (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
Chain.Gnosis,
|
|
106
|
-
Chain.Monad,
|
|
107
|
-
Chain.Optimism,
|
|
108
|
-
Chain.Polygon,
|
|
109
|
-
Chain.Base,
|
|
110
|
-
Chain.XLayer,
|
|
111
|
-
async () => {
|
|
112
|
-
const { getProvider } = await import("@swapkit/toolboxes/evm");
|
|
113
|
-
const params = { derivationPath, provider: await getProvider(chain as EVMChain), transport };
|
|
106
|
+
return match(chain as LedgerSupportedChain)
|
|
107
|
+
.returnType<Promise<LedgerSignerMap[T]>>()
|
|
108
|
+
.with(Chain.THORChain, () => Promise.resolve(new THORChainLedger(clientParams) as LedgerSignerMap[T]))
|
|
109
|
+
.with(Chain.Cosmos, () => Promise.resolve(new CosmosLedger(clientParams) as LedgerSignerMap[T]))
|
|
110
|
+
.with(Chain.Bitcoin, () => Promise.resolve(BitcoinLedger(clientParams) as LedgerSignerMap[T]))
|
|
111
|
+
.with(Chain.BitcoinCash, () => Promise.resolve(BitcoinCashLedger(clientParams) as LedgerSignerMap[T]))
|
|
112
|
+
.with(Chain.Dash, () => Promise.resolve(DashLedger(clientParams) as LedgerSignerMap[T]))
|
|
113
|
+
.with(Chain.Dogecoin, () => Promise.resolve(DogecoinLedger(clientParams) as LedgerSignerMap[T]))
|
|
114
|
+
.with(Chain.Litecoin, () => Promise.resolve(LitecoinLedger(clientParams) as LedgerSignerMap[T]))
|
|
115
|
+
.with(Chain.Zcash, () => Promise.resolve(ZcashLedger(clientParams) as LedgerSignerMap[T]))
|
|
116
|
+
.with(Chain.Ripple, () => Promise.resolve(XRPLedger(clientParams) as LedgerSignerMap[T]))
|
|
117
|
+
.with(Chain.Tron, () => Promise.resolve(TronLedger(clientParams) as LedgerSignerMap[T]))
|
|
118
|
+
.with(Chain.Sui, () => Promise.resolve(SuiLedger(clientParams) as LedgerSignerMap[T]))
|
|
119
|
+
.with(Chain.Near, () => Promise.resolve(getNearLedgerClient(clientParams) as LedgerSignerMap[T]))
|
|
120
|
+
.with(
|
|
121
|
+
Chain.Arbitrum,
|
|
122
|
+
Chain.Arc,
|
|
123
|
+
Chain.Aurora,
|
|
124
|
+
Chain.Avalanche,
|
|
125
|
+
Chain.Berachain,
|
|
126
|
+
Chain.BinanceSmartChain,
|
|
127
|
+
Chain.Ethereum,
|
|
128
|
+
Chain.Gnosis,
|
|
129
|
+
Chain.Monad,
|
|
130
|
+
Chain.Optimism,
|
|
131
|
+
Chain.Polygon,
|
|
132
|
+
Chain.Base,
|
|
133
|
+
Chain.XLayer,
|
|
134
|
+
async () => {
|
|
135
|
+
const { getProvider } = await import("@swapkit/toolboxes/evm");
|
|
114
136
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
137
|
+
const params = {
|
|
138
|
+
derivationPath,
|
|
139
|
+
dmkSession,
|
|
140
|
+
onDeviceActionState,
|
|
141
|
+
originToken,
|
|
142
|
+
provider: await getProvider(chain as EVMChain),
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
return match(chain as Chain)
|
|
146
|
+
.with(Chain.BinanceSmartChain, () => BinanceSmartChainLedger(params) as LedgerSignerMap[T])
|
|
147
|
+
.with(Chain.Avalanche, () => AvalancheLedger(params) as LedgerSignerMap[T])
|
|
148
|
+
.with(Chain.Arbitrum, () => ArbitrumLedger(params) as LedgerSignerMap[T])
|
|
149
|
+
.with(Chain.Arc, () => ArcLedger(params) as LedgerSignerMap[T])
|
|
150
|
+
.with(Chain.Berachain, () => BerachainLedger(params) as LedgerSignerMap[T])
|
|
151
|
+
.with(Chain.Optimism, () => OptimismLedger(params) as LedgerSignerMap[T])
|
|
152
|
+
.with(Chain.Polygon, () => PolygonLedger(params) as LedgerSignerMap[T])
|
|
153
|
+
.with(Chain.Base, () => BaseLedger(params) as LedgerSignerMap[T])
|
|
154
|
+
.with(Chain.Aurora, () => AuroraLedger(params) as LedgerSignerMap[T])
|
|
155
|
+
.with(Chain.Gnosis, () => GnosisLedger(params) as LedgerSignerMap[T])
|
|
156
|
+
.with(Chain.Monad, () => MonadLedger(params) as LedgerSignerMap[T])
|
|
157
|
+
.with(Chain.XLayer, () => XLayerLedger(params) as LedgerSignerMap[T])
|
|
158
|
+
.otherwise(() => EthereumLedger(params) as LedgerSignerMap[T]);
|
|
159
|
+
},
|
|
160
|
+
)
|
|
161
|
+
.otherwise(() => {
|
|
162
|
+
throw new SwapKitError("wallet_chain_not_supported", { chain, wallet: WalletOption.LEDGER });
|
|
163
|
+
});
|
|
164
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
export { disconnectLedgerDMKSession, getLedgerDMKSession, type LedgerDMKSession, preloadLedgerDMK } from "./dmk";
|
|
2
|
+
export {
|
|
3
|
+
executeLedgerDeviceAction,
|
|
4
|
+
type LedgerDeviceActionState,
|
|
5
|
+
type LedgerDeviceActionStateHandler,
|
|
6
|
+
} from "./executeDeviceAction";
|
|
1
7
|
export { getLedgerAddress } from "./getLedgerAddress";
|
|
2
8
|
export { getLedgerClient } from "./getLedgerClient";
|
|
3
|
-
export { getLedgerTransport } from "./getLedgerTransport";
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import type { UnknownDeviceExchangeError, UserInteractionRequired } from "@ledgerhq/device-management-kit";
|
|
2
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
3
|
+
import { type DerivationPathArray, SwapKitError } from "@swapkit/helpers";
|
|
4
|
+
|
|
5
|
+
import { getLedgerDMKSession, type LedgerDMKSession } from "./dmk";
|
|
6
|
+
import {
|
|
7
|
+
executeLedgerDeviceAction,
|
|
8
|
+
LEDGER_USER_INTERACTION_REQUIRED,
|
|
9
|
+
type LedgerDeviceActionStateHandler,
|
|
10
|
+
toLedgerDeviceError,
|
|
11
|
+
} from "./executeDeviceAction";
|
|
12
|
+
|
|
13
|
+
export { LEDGER_USER_INTERACTION_REQUIRED };
|
|
14
|
+
|
|
15
|
+
export interface LedgerJsClientConnection {
|
|
16
|
+
dmkSession?: LedgerDMKSession;
|
|
17
|
+
onDeviceActionState?: LedgerDeviceActionStateHandler;
|
|
18
|
+
transport?: Transport;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface LedgerJsClientParams<Path extends DerivationPathArray | string = DerivationPathArray | string>
|
|
22
|
+
extends LedgerJsClientConnection {
|
|
23
|
+
derivationPath?: Path;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// `Array.isArray` does not narrow the readonly `DerivationPathArray` tuples out of the union.
|
|
27
|
+
function isLedgerJsClientParams<Path extends DerivationPathArray | string>(
|
|
28
|
+
value: LedgerJsClientParams<Path> | Path | undefined,
|
|
29
|
+
): value is LedgerJsClientParams<Path> {
|
|
30
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeLedgerJsClientParams<Path extends DerivationPathArray | string>({
|
|
34
|
+
paramsOrPath,
|
|
35
|
+
transport,
|
|
36
|
+
}: {
|
|
37
|
+
paramsOrPath?: LedgerJsClientParams<Path> | Path;
|
|
38
|
+
transport?: Transport;
|
|
39
|
+
}): LedgerJsClientParams<Path> {
|
|
40
|
+
if (isLedgerJsClientParams(paramsOrPath)) return paramsOrPath;
|
|
41
|
+
return { derivationPath: paramsOrPath as Path | undefined, transport };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function runLedgerJsOperation<App, Output>({
|
|
45
|
+
appName,
|
|
46
|
+
connection,
|
|
47
|
+
createApp,
|
|
48
|
+
operation,
|
|
49
|
+
requiredUserInteraction = LEDGER_USER_INTERACTION_REQUIRED.None,
|
|
50
|
+
}: {
|
|
51
|
+
appName: string;
|
|
52
|
+
connection: LedgerJsClientConnection;
|
|
53
|
+
createApp: (transport: Transport) => App;
|
|
54
|
+
operation: (app: App) => Promise<Output> | Output;
|
|
55
|
+
requiredUserInteraction?: UserInteractionRequired;
|
|
56
|
+
}) {
|
|
57
|
+
const { dmkSession, onDeviceActionState, transport } = connection;
|
|
58
|
+
|
|
59
|
+
if (dmkSession && transport) {
|
|
60
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
61
|
+
message: "Provide either a Ledger DMK session or a LedgerJS transport, not both",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (transport) {
|
|
66
|
+
try {
|
|
67
|
+
return await operation(createApp(transport));
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// hw-transport rejects with status errors; map them as the DMK path below does.
|
|
70
|
+
throw toLedgerDeviceError(error);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const session = dmkSession ?? (await getLedgerDMKSession());
|
|
75
|
+
|
|
76
|
+
// Loaded before the action is built, so a chunk that fails to load rejects before the app opens. The bridge
|
|
77
|
+
// transport stays lazy because its hw-transport base class does not load under Node ESM (see its module).
|
|
78
|
+
const [{ CallTaskInAppDeviceAction, DmkResultFactory, UnknownDeviceExchangeError }, { LedgerJsDmkTransport }] =
|
|
79
|
+
await Promise.all([import("@ledgerhq/device-management-kit"), import("./ledgerJsDmkTransport")]);
|
|
80
|
+
|
|
81
|
+
const deviceAction = new CallTaskInAppDeviceAction<
|
|
82
|
+
{ output: Output },
|
|
83
|
+
UnknownDeviceExchangeError,
|
|
84
|
+
UserInteractionRequired
|
|
85
|
+
>({
|
|
86
|
+
input: {
|
|
87
|
+
appName,
|
|
88
|
+
requiredUserInteraction,
|
|
89
|
+
skipOpenApp: false,
|
|
90
|
+
task: async (internalApi) => {
|
|
91
|
+
try {
|
|
92
|
+
const output = await operation(createApp(new LedgerJsDmkTransport(internalApi)));
|
|
93
|
+
return DmkResultFactory<{ output: Output }, UnknownDeviceExchangeError>({ data: { output } });
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return DmkResultFactory<{ output: Output }, UnknownDeviceExchangeError>({
|
|
96
|
+
error: new UnknownDeviceExchangeError(error),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result = await executeLedgerDeviceAction({
|
|
104
|
+
action: () => session.dmk.executeDeviceAction({ deviceAction, sessionId: session.sessionId }),
|
|
105
|
+
onDeviceActionState,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return result.output;
|
|
109
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Apdu,
|
|
3
|
+
type ApduResponse,
|
|
4
|
+
type Command,
|
|
5
|
+
CommandResultFactory,
|
|
6
|
+
type InternalApi,
|
|
7
|
+
isSuccessCommandResult,
|
|
8
|
+
UnknownDeviceExchangeError,
|
|
9
|
+
} from "@ledgerhq/device-management-kit";
|
|
10
|
+
import Transport, { TransportError } from "@ledgerhq/hw-transport";
|
|
11
|
+
|
|
12
|
+
// Only ever loaded through `import()` from ledgerJsDmkBridge: subclassing needs hw-transport at runtime, and its
|
|
13
|
+
// lib-es build pulls in @ledgerhq/errors with extensionless relative imports that Node ESM cannot resolve, so a
|
|
14
|
+
// static import from a package entry would break `import "@swapkit/wallet-hardware/ledger"` outside a bundler.
|
|
15
|
+
|
|
16
|
+
const STATUS_CODE_LENGTH = 2;
|
|
17
|
+
const TransportBase =
|
|
18
|
+
typeof Transport === "function" ? Transport : (Transport as unknown as { default: typeof Transport }).default;
|
|
19
|
+
|
|
20
|
+
interface RawApduResponse {
|
|
21
|
+
response: Uint8Array;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
class RawApduCommand implements Command<RawApduResponse> {
|
|
25
|
+
readonly name = "ledgerJsRawApdu";
|
|
26
|
+
|
|
27
|
+
constructor(private readonly apdu: Apdu) {}
|
|
28
|
+
|
|
29
|
+
getApdu() {
|
|
30
|
+
return this.apdu;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
parseResponse({ data, statusCode }: ApduResponse) {
|
|
34
|
+
if (statusCode.length !== STATUS_CODE_LENGTH) {
|
|
35
|
+
return CommandResultFactory<RawApduResponse>({
|
|
36
|
+
error: new UnknownDeviceExchangeError(new Error("Ledger returned an invalid APDU status code")),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const response = new Uint8Array(data.length + statusCode.length);
|
|
41
|
+
response.set(data);
|
|
42
|
+
response.set(statusCode, data.length);
|
|
43
|
+
|
|
44
|
+
return CommandResultFactory<RawApduResponse>({ data: { response } });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function parseShortApdu(apdu: Buffer) {
|
|
49
|
+
if (apdu.length < 5) {
|
|
50
|
+
throw new TransportError("Ledger APDU must contain a five-byte short-APDU header", "InvalidAPDU");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dataLength = apdu[4];
|
|
54
|
+
if (dataLength === undefined || apdu.length !== dataLength + 5) {
|
|
55
|
+
throw new TransportError("Ledger APDU payload length does not match its short-APDU header", "InvalidAPDU");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const cla = apdu[0];
|
|
59
|
+
const ins = apdu[1];
|
|
60
|
+
const p1 = apdu[2];
|
|
61
|
+
const p2 = apdu[3];
|
|
62
|
+
if (cla === undefined || ins === undefined || p1 === undefined || p2 === undefined) {
|
|
63
|
+
throw new TransportError("Ledger APDU header is incomplete", "InvalidAPDU");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { cla, data: apdu.subarray(5), ins, p1, p2 };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export class LedgerJsDmkTransport extends TransportBase {
|
|
70
|
+
constructor(private readonly internalApi: InternalApi) {
|
|
71
|
+
super();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async exchange(apdu: Buffer, { abortTimeoutMs }: { abortTimeoutMs?: number } = {}) {
|
|
75
|
+
const { cla, data, ins, p1, p2 } = parseShortApdu(apdu);
|
|
76
|
+
const command = new RawApduCommand(new Apdu(cla, ins, p1, p2, data));
|
|
77
|
+
// Forward only an explicit timeout: hw-transport's 30 s default would cut off a user still confirming
|
|
78
|
+
// on the device once DMK honours the argument.
|
|
79
|
+
const result = await this.internalApi.sendCommand(command, abortTimeoutMs);
|
|
80
|
+
|
|
81
|
+
if (!isSuccessCommandResult(result)) throw result.error;
|
|
82
|
+
return Buffer.from(result.data.response);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
close() {
|
|
86
|
+
return Promise.resolve();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const SECP256K1_ORDER = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
|
2
|
+
const HALF_ORDER = SECP256K1_ORDER >> 1n;
|
|
3
|
+
|
|
4
|
+
function bytesToBigInt(bytes: Uint8Array) {
|
|
5
|
+
return bytes.reduce((value, byte) => (value << 8n) | BigInt(byte), 0n);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function bigIntTo32Bytes(value: bigint) {
|
|
9
|
+
const bytes = new Uint8Array(32);
|
|
10
|
+
let remaining = value;
|
|
11
|
+
for (let index = 31; index >= 0; index -= 1) {
|
|
12
|
+
bytes[index] = Number(remaining & 0xffn);
|
|
13
|
+
remaining >>= 8n;
|
|
14
|
+
}
|
|
15
|
+
return bytes;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Cosmos SDK rejects high-S secp256k1 signatures. Ledger firmware should already return low-S, but
|
|
20
|
+
* normalising a fixed-length `r || s` signature here removes the dependency on it.
|
|
21
|
+
*/
|
|
22
|
+
export function toLowSSignature(signature: Uint8Array) {
|
|
23
|
+
const s = bytesToBigInt(signature.subarray(32, 64));
|
|
24
|
+
if (s <= HALF_ORDER) return signature;
|
|
25
|
+
|
|
26
|
+
const normalized = new Uint8Array(64);
|
|
27
|
+
normalized.set(signature.subarray(0, 32), 0);
|
|
28
|
+
normalized.set(bigIntTo32Bytes(SECP256K1_ORDER - s), 32);
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createCachedRawTxResolver(getRawTx: (txid: string) => Promise<string>) {
|
|
2
|
+
const requests = new Map<string, Promise<string>>();
|
|
3
|
+
|
|
4
|
+
return function resolveRawTx(txid: string) {
|
|
5
|
+
const cachedRequest = requests.get(txid);
|
|
6
|
+
if (cachedRequest) return cachedRequest;
|
|
7
|
+
|
|
8
|
+
const request = getRawTx(txid);
|
|
9
|
+
requests.set(txid, request);
|
|
10
|
+
return request;
|
|
11
|
+
};
|
|
12
|
+
}
|