@swapkit/wallet-hardware 4.8.0 → 4.8.2
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/keepkey/index.cjs.map +2 -2
- package/dist/keepkey/index.js.map +2 -2
- package/dist/types/keepkey/chains/utxo.d.ts +5 -379
- package/dist/types/keepkey/chains/utxo.d.ts.map +1 -1
- package/package.json +5 -7
- package/src/index.ts +1 -0
- package/src/keepkey/chains/cosmos.ts +69 -0
- package/src/keepkey/chains/evm.ts +141 -0
- package/src/keepkey/chains/mayachain.ts +98 -0
- package/src/keepkey/chains/ripple.ts +88 -0
- package/src/keepkey/chains/thorchain.ts +93 -0
- package/src/keepkey/chains/utxo.ts +364 -0
- package/src/keepkey/coins.ts +67 -0
- package/src/keepkey/index.ts +174 -0
- package/src/ledger/clients/cosmos.ts +84 -0
- package/src/ledger/clients/evm.ts +186 -0
- package/src/ledger/clients/near.ts +63 -0
- package/src/ledger/clients/sui.ts +130 -0
- package/src/ledger/clients/thorchain/common.ts +93 -0
- package/src/ledger/clients/thorchain/helpers.ts +120 -0
- package/src/ledger/clients/thorchain/index.ts +87 -0
- package/src/ledger/clients/thorchain/lib.ts +258 -0
- package/src/ledger/clients/thorchain/utils.ts +69 -0
- package/src/ledger/clients/tron.ts +85 -0
- package/src/ledger/clients/utxo-legacy-adapter.ts +71 -0
- package/src/ledger/clients/utxo-psbt.ts +145 -0
- package/src/ledger/clients/utxo.ts +359 -0
- package/src/ledger/clients/xrp.ts +50 -0
- package/src/ledger/cosmosTypes.ts +98 -0
- package/src/ledger/helpers/getLedgerAddress.ts +76 -0
- package/src/ledger/helpers/getLedgerClient.ts +124 -0
- package/src/ledger/helpers/getLedgerTransport.ts +102 -0
- package/src/ledger/helpers/index.ts +3 -0
- package/src/ledger/index.ts +546 -0
- package/src/ledger/interfaces/CosmosLedgerInterface.ts +54 -0
- package/src/ledger/types.ts +42 -0
- package/src/trezor/evmSigner.ts +210 -0
- package/src/trezor/index.ts +847 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type Sui from "@ledgerhq/hw-app-sui";
|
|
2
|
+
import {
|
|
3
|
+
Chain,
|
|
4
|
+
type DerivationPathArray,
|
|
5
|
+
derivationPathToString,
|
|
6
|
+
NetworkDerivationPath,
|
|
7
|
+
SwapKitError,
|
|
8
|
+
} from "@swapkit/helpers";
|
|
9
|
+
|
|
10
|
+
import { getLedgerTransport } from "../helpers/getLedgerTransport";
|
|
11
|
+
|
|
12
|
+
export class SuiLedgerInterface {
|
|
13
|
+
derivationPath: string;
|
|
14
|
+
ledgerApp: InstanceType<typeof Sui> | null = null;
|
|
15
|
+
address: string | null = null;
|
|
16
|
+
publicKey: Uint8Array | null = null;
|
|
17
|
+
|
|
18
|
+
constructor(derivationPath?: DerivationPathArray | string) {
|
|
19
|
+
this.derivationPath =
|
|
20
|
+
typeof derivationPath === "string"
|
|
21
|
+
? derivationPath
|
|
22
|
+
: derivationPathToString(derivationPath || NetworkDerivationPath[Chain.Sui]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Transform derivation path for SUI Ledger format.
|
|
27
|
+
* SUI Ledger expects: 44'/784'/0'/0'/0' (all hardened, no 'm/' prefix)
|
|
28
|
+
* Standard format is: m/44'/784'/0'/0/0 (only first 3 hardened, has 'm/' prefix)
|
|
29
|
+
*/
|
|
30
|
+
private getLedgerPath(): string {
|
|
31
|
+
return this.derivationPath
|
|
32
|
+
.replace(/^m\//, "") // Remove 'm/' prefix
|
|
33
|
+
.replace(/\/(\d+)\/(\d+)$/, "/$1'/$2'"); // Make last two components hardened
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private async createTransportAndLedger() {
|
|
37
|
+
if (this.ledgerApp) return;
|
|
38
|
+
|
|
39
|
+
const transport = await getLedgerTransport();
|
|
40
|
+
const SuiApp = (await import("@ledgerhq/hw-app-sui")).default;
|
|
41
|
+
this.ledgerApp = new SuiApp(transport);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async connect(): Promise<string> {
|
|
45
|
+
await this.createTransportAndLedger();
|
|
46
|
+
|
|
47
|
+
if (!this.ledgerApp) {
|
|
48
|
+
throw new SwapKitError("wallet_ledger_transport_error");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ledgerPath = this.getLedgerPath();
|
|
52
|
+
const result = await this.ledgerApp.getPublicKey(ledgerPath);
|
|
53
|
+
|
|
54
|
+
if (!result?.publicKey) {
|
|
55
|
+
throw new SwapKitError("wallet_ledger_failed_to_get_address");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this.publicKey = result.publicKey;
|
|
59
|
+
this.address = `0x${Buffer.from(result.address).toString("hex")}`;
|
|
60
|
+
|
|
61
|
+
return this.address;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
toSuiAddress(): string {
|
|
65
|
+
if (!this.address) {
|
|
66
|
+
throw new SwapKitError("wallet_ledger_failed_to_get_address");
|
|
67
|
+
}
|
|
68
|
+
return this.address;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async getAddress(): Promise<string> {
|
|
72
|
+
if (this.address) return this.address;
|
|
73
|
+
return await this.connect();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async signTransaction(
|
|
77
|
+
input: Uint8Array | { transaction: Uint8Array },
|
|
78
|
+
): Promise<{ bytes: string; signature: string }> {
|
|
79
|
+
const txBytes = input instanceof Uint8Array ? input : input.transaction;
|
|
80
|
+
await this.createTransportAndLedger();
|
|
81
|
+
|
|
82
|
+
if (!this.ledgerApp) {
|
|
83
|
+
throw new SwapKitError("wallet_ledger_transport_error");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!this.publicKey) {
|
|
87
|
+
throw new SwapKitError("wallet_ledger_failed_to_get_address");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const ledgerPath = this.getLedgerPath();
|
|
92
|
+
|
|
93
|
+
// SUI intent message format for TransactionData:
|
|
94
|
+
// [intent_scope=0 (TransactionData), intent_version=0, app_id=0] + transaction_bytes
|
|
95
|
+
// The Ledger SUI app expects the INTENT MESSAGE, not raw transaction bytes
|
|
96
|
+
const intentMessage = new Uint8Array(3 + txBytes.length);
|
|
97
|
+
intentMessage[0] = 0; // IntentScope: TransactionData
|
|
98
|
+
intentMessage[1] = 0; // IntentVersion: V0
|
|
99
|
+
intentMessage[2] = 0; // AppId: Sui
|
|
100
|
+
intentMessage.set(txBytes, 3);
|
|
101
|
+
|
|
102
|
+
const result = await this.ledgerApp.signTransaction(ledgerPath, intentMessage);
|
|
103
|
+
|
|
104
|
+
if (!result?.signature) {
|
|
105
|
+
throw new SwapKitError("wallet_ledger_signing_error");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// SUI signature format: [scheme_flag (1 byte)] + [signature (64 bytes)] + [public_key (32 bytes)]
|
|
109
|
+
// Scheme flag 0x00 = Ed25519
|
|
110
|
+
const pubKey = this.publicKey.length === 33 ? this.publicKey.slice(1) : this.publicKey;
|
|
111
|
+
if (pubKey.length !== 32) {
|
|
112
|
+
throw new SwapKitError("wallet_ledger_signing_error", { error: "Invalid public key length" });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const serializedSignature = new Uint8Array(1 + 64 + 32);
|
|
116
|
+
serializedSignature[0] = 0x00; // Ed25519 scheme flag
|
|
117
|
+
serializedSignature.set(result.signature, 1);
|
|
118
|
+
serializedSignature.set(pubKey, 65);
|
|
119
|
+
|
|
120
|
+
const signatureBase64 = Buffer.from(serializedSignature).toString("base64");
|
|
121
|
+
const bytesBase64 = Buffer.from(txBytes).toString("base64");
|
|
122
|
+
|
|
123
|
+
return { bytes: bytesBase64, signature: signatureBase64 };
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new SwapKitError("wallet_ledger_signing_error", { error });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const SuiLedger = (derivationPath?: DerivationPathArray) => new SuiLedgerInterface(derivationPath);
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export const CLA = 0x55;
|
|
2
|
+
export const CHUNK_SIZE = 250;
|
|
3
|
+
export const APP_KEY = "CSM";
|
|
4
|
+
|
|
5
|
+
export const INS = {
|
|
6
|
+
GET_ADDR_SECP256K1: 0x04,
|
|
7
|
+
GET_VERSION: 0x00,
|
|
8
|
+
INS_PUBLIC_KEY_SECP256K1: 0x01, // Obsolete
|
|
9
|
+
SIGN_SECP256K1: 0x02,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const PAYLOAD_TYPE = { ADD: 0x01, INIT: 0x00, LAST: 0x02 };
|
|
13
|
+
|
|
14
|
+
export const P1_VALUES = { ONLY_RETRIEVE: 0x00, SHOW_ADDRESS_IN_DEVICE: 0x01 };
|
|
15
|
+
|
|
16
|
+
export const P2_VALUES = { JSON: 0x0 };
|
|
17
|
+
|
|
18
|
+
export const ERROR_CODE = { NoError: 0x9000 };
|
|
19
|
+
|
|
20
|
+
const ERROR_DESCRIPTION: any = {
|
|
21
|
+
1: "U2F: Unknown",
|
|
22
|
+
2: "U2F: Bad request",
|
|
23
|
+
3: "U2F: Configuration unsupported",
|
|
24
|
+
4: "U2F: Device Ineligible",
|
|
25
|
+
5: "U2F: Timeout",
|
|
26
|
+
14: "Timeout",
|
|
27
|
+
25600: "Execution Error",
|
|
28
|
+
26368: "Wrong Length",
|
|
29
|
+
26626: "Error deriving keys",
|
|
30
|
+
27010: "Empty Buffer",
|
|
31
|
+
27011: "Output buffer too small",
|
|
32
|
+
27012: "Data is invalid",
|
|
33
|
+
27013: "Conditions not satisfied",
|
|
34
|
+
27014: "Transaction rejected",
|
|
35
|
+
27264: "Bad key handle",
|
|
36
|
+
27392: "Invalid P1/P2",
|
|
37
|
+
27904: "Instruction not supported",
|
|
38
|
+
28160: "App does not seem to be open",
|
|
39
|
+
28416: "Unknown error",
|
|
40
|
+
28417: "Sign/verify error",
|
|
41
|
+
36864: "No errors",
|
|
42
|
+
36865: "Device is busy",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export function errorCodeToString(statusCode: number) {
|
|
46
|
+
if (statusCode in ERROR_DESCRIPTION) return ERROR_DESCRIPTION[statusCode];
|
|
47
|
+
return `Unknown Status Code: ${statusCode}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isDict(v: any) {
|
|
51
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function processErrorResponse(response: any) {
|
|
55
|
+
if (response) {
|
|
56
|
+
if (isDict(response)) {
|
|
57
|
+
if (Object.hasOwn(response, "statusCode")) {
|
|
58
|
+
return { error_message: errorCodeToString(response.statusCode), return_code: response.statusCode };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (Object.hasOwn(response, "return_code") && Object.hasOwn(response, "error_message")) {
|
|
62
|
+
return response;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { error_message: response.toString(), return_code: 0xffff };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { error_message: response.toString(), return_code: 0xffff };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function getVersion(transport: any) {
|
|
72
|
+
return transport.send(CLA, INS.GET_VERSION, 0, 0).then((response: any) => {
|
|
73
|
+
const errorCodeData = response.slice(-2);
|
|
74
|
+
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
|
|
75
|
+
|
|
76
|
+
let targetId = 0;
|
|
77
|
+
if (response.length >= 9) {
|
|
78
|
+
targetId = (response[5] << 24) + (response[6] << 16) + (response[7] << 8) + (response[8] << 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
device_locked: response[4] === 1,
|
|
83
|
+
error_message: errorCodeToString(returnCode),
|
|
84
|
+
major: response[1],
|
|
85
|
+
minor: response[2],
|
|
86
|
+
patch: response[3],
|
|
87
|
+
return_code: returnCode,
|
|
88
|
+
target_id: targetId.toString(16),
|
|
89
|
+
// ///
|
|
90
|
+
test_mode: response[0] !== 0,
|
|
91
|
+
};
|
|
92
|
+
}, processErrorResponse);
|
|
93
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { SwapKitError } from "@swapkit/helpers";
|
|
2
|
+
import { CLA, ERROR_CODE, errorCodeToString, INS, P2_VALUES, PAYLOAD_TYPE, processErrorResponse } from "./common";
|
|
3
|
+
|
|
4
|
+
export function serializePathv1(path: number[]) {
|
|
5
|
+
if (path == null || path.length < 3) {
|
|
6
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path too short" });
|
|
7
|
+
}
|
|
8
|
+
if (path.length > 10) {
|
|
9
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path too long" });
|
|
10
|
+
}
|
|
11
|
+
const buf = Buffer.alloc(1 + 4 * path.length);
|
|
12
|
+
buf.writeUInt8(path.length, 0);
|
|
13
|
+
for (let i = 0; i < path.length; i += 1) {
|
|
14
|
+
let v = path[i] || 0;
|
|
15
|
+
if (i < 3) {
|
|
16
|
+
// eslint-disable-next-line no-bitwise
|
|
17
|
+
v |= 0x80000000; // Harden
|
|
18
|
+
}
|
|
19
|
+
buf.writeInt32LE(v, 1 + i * 4);
|
|
20
|
+
}
|
|
21
|
+
return buf;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function signSendChunkv1(app: any, chunkIdx: number, _chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) {
|
|
25
|
+
return app.transport
|
|
26
|
+
.send(CLA, INS.SIGN_SECP256K1, chunkIdx, txType, chunk, [ERROR_CODE.NoError, 0x6984, 0x6a80])
|
|
27
|
+
.then((response: any) => {
|
|
28
|
+
const errorCodeData = response.slice(-2);
|
|
29
|
+
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
|
|
30
|
+
let errorMessage = errorCodeToString(returnCode);
|
|
31
|
+
|
|
32
|
+
if (returnCode === 0x6a80 || returnCode === 0x6984) {
|
|
33
|
+
errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let signature: any = null;
|
|
37
|
+
if (response.length > 2) {
|
|
38
|
+
signature = response.slice(0, response.length - 2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { error_message: errorMessage, return_code: returnCode, signature };
|
|
42
|
+
}, processErrorResponse);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compressPublicKey(publicKey: Buffer) {
|
|
46
|
+
if (publicKey.length !== 65) {
|
|
47
|
+
throw new SwapKitError("wallet_ledger_invalid_params", {
|
|
48
|
+
reason: "decompressed public key length should be 65 bytes",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
const y = publicKey.slice(33, 65);
|
|
52
|
+
|
|
53
|
+
// @ts-expect-error
|
|
54
|
+
const z = Buffer.from([2 + (y[y.length - 1] & 1)]);
|
|
55
|
+
return Buffer.concat([z, publicKey.slice(1, 33)]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function publicKeyv1(app: any, data: Buffer) {
|
|
59
|
+
return app.transport
|
|
60
|
+
.send(CLA, INS.INS_PUBLIC_KEY_SECP256K1, 0, 0, data, [ERROR_CODE.NoError])
|
|
61
|
+
.then((response: any) => {
|
|
62
|
+
const errorCodeData = response.slice(-2);
|
|
63
|
+
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
|
|
64
|
+
const pk = Buffer.from(response.slice(0, 65));
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
compressed_pk: compressPublicKey(pk),
|
|
68
|
+
error_message: errorCodeToString(returnCode),
|
|
69
|
+
pk,
|
|
70
|
+
return_code: returnCode,
|
|
71
|
+
};
|
|
72
|
+
}, processErrorResponse);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function serializePathv2(path: number[]) {
|
|
76
|
+
if (!path || path.length !== 5) {
|
|
77
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Path must be exactly 5 elements" });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const buf = Buffer.alloc(20);
|
|
81
|
+
// @ts-expect-error
|
|
82
|
+
buf.writeUInt32LE(0x80000000 + path[0], 0);
|
|
83
|
+
// @ts-expect-error
|
|
84
|
+
buf.writeUInt32LE(0x80000000 + path[1], 4);
|
|
85
|
+
// @ts-expect-error
|
|
86
|
+
buf.writeUInt32LE(0x80000000 + path[2], 8);
|
|
87
|
+
// @ts-expect-error
|
|
88
|
+
buf.writeUInt32LE(path[3], 12);
|
|
89
|
+
// @ts-expect-error
|
|
90
|
+
buf.writeUInt32LE(path[4], 16);
|
|
91
|
+
|
|
92
|
+
return buf;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function signSendChunkv2(app: any, chunkIdx: number, chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) {
|
|
96
|
+
let payloadType = PAYLOAD_TYPE.ADD;
|
|
97
|
+
if (chunkIdx === 1) {
|
|
98
|
+
payloadType = PAYLOAD_TYPE.INIT;
|
|
99
|
+
}
|
|
100
|
+
if (chunkIdx === chunkNum) {
|
|
101
|
+
payloadType = PAYLOAD_TYPE.LAST;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return signSendChunkv1(app, payloadType, 0, chunk, txType);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function publicKeyv2(app: any, data: Buffer) {
|
|
108
|
+
return app.transport.send(CLA, INS.GET_ADDR_SECP256K1, 0, 0, data, [ERROR_CODE.NoError]).then((response: any) => {
|
|
109
|
+
const errorCodeData = response.slice(-2);
|
|
110
|
+
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
|
|
111
|
+
const compressedPk = Buffer.from(response.slice(0, 33));
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
compressed_pk: compressedPk,
|
|
115
|
+
error_message: errorCodeToString(returnCode),
|
|
116
|
+
pk: "OBSOLETE PROPERTY",
|
|
117
|
+
return_code: returnCode,
|
|
118
|
+
};
|
|
119
|
+
}, processErrorResponse);
|
|
120
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { base64 } from "@scure/base";
|
|
2
|
+
import { type DerivationPathArray, NetworkDerivationPath, SwapKitError } from "@swapkit/helpers";
|
|
3
|
+
|
|
4
|
+
import { CosmosLedgerInterface } from "../../interfaces/CosmosLedgerInterface";
|
|
5
|
+
import type { GetAddressAndPubKeyResponse } from "../../types";
|
|
6
|
+
import { getSignature } from "./utils";
|
|
7
|
+
|
|
8
|
+
export class THORChainLedger extends CosmosLedgerInterface {
|
|
9
|
+
private pubKey: string | null = null;
|
|
10
|
+
|
|
11
|
+
derivationPath: DerivationPathArray;
|
|
12
|
+
|
|
13
|
+
constructor(derivationPath: DerivationPathArray = NetworkDerivationPath.THOR) {
|
|
14
|
+
super();
|
|
15
|
+
this.chain = "thor";
|
|
16
|
+
this.derivationPath = derivationPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get pubkey() {
|
|
20
|
+
return this.pubKey;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
connect = async () => {
|
|
24
|
+
await this.checkOrCreateTransportAndLedger();
|
|
25
|
+
const { compressed_pk, bech32_address }: GetAddressAndPubKeyResponse = await this.getAddressAndPubKey();
|
|
26
|
+
|
|
27
|
+
this.pubKey = base64.encode(compressed_pk);
|
|
28
|
+
|
|
29
|
+
return bech32_address;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
getAddressAndPubKey = async () => {
|
|
33
|
+
await this.checkOrCreateTransportAndLedger(true);
|
|
34
|
+
|
|
35
|
+
const response: GetAddressAndPubKeyResponse = await this.ledgerApp.getAddressAndPubKey(
|
|
36
|
+
this.derivationPath,
|
|
37
|
+
this.chain,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
this.validateResponse(response.return_code, response.error_message);
|
|
41
|
+
|
|
42
|
+
return response;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
showAddressAndPubKey = async () => {
|
|
46
|
+
await this.checkOrCreateTransportAndLedger(true);
|
|
47
|
+
|
|
48
|
+
const response: GetAddressAndPubKeyResponse = await this.ledgerApp.showAddressAndPubKey(
|
|
49
|
+
this.derivationPath,
|
|
50
|
+
this.chain,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
this.validateResponse(response.return_code, response.error_message);
|
|
54
|
+
|
|
55
|
+
return response;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
signTransaction = async (rawTx: string, sequence = "0") => {
|
|
59
|
+
await this.checkOrCreateTransportAndLedger(true);
|
|
60
|
+
|
|
61
|
+
const { return_code, error_message, signature } = await this.ledgerApp.sign(this.derivationPath, rawTx);
|
|
62
|
+
|
|
63
|
+
if (!this.pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found");
|
|
64
|
+
|
|
65
|
+
this.validateResponse(return_code, error_message);
|
|
66
|
+
|
|
67
|
+
return [
|
|
68
|
+
{
|
|
69
|
+
pub_key: { type: "tendermint/PubKeySecp256k1", value: this.pubKey },
|
|
70
|
+
sequence,
|
|
71
|
+
signature: getSignature(signature),
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
sign = async (message: string) => {
|
|
77
|
+
await this.checkOrCreateTransportAndLedger(true);
|
|
78
|
+
|
|
79
|
+
const { return_code, error_message, signature } = await this.ledgerApp.sign(this.derivationPath, message);
|
|
80
|
+
|
|
81
|
+
if (!this.pubKey) throw new SwapKitError("wallet_ledger_pubkey_not_found");
|
|
82
|
+
|
|
83
|
+
this.validateResponse(return_code, error_message);
|
|
84
|
+
|
|
85
|
+
return getSignature(signature);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type Transport from "@ledgerhq/hw-transport";
|
|
2
|
+
import { SwapKitError } from "@swapkit/helpers";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
CHUNK_SIZE,
|
|
6
|
+
CLA,
|
|
7
|
+
ERROR_CODE,
|
|
8
|
+
errorCodeToString,
|
|
9
|
+
getVersion,
|
|
10
|
+
INS,
|
|
11
|
+
P1_VALUES,
|
|
12
|
+
P2_VALUES,
|
|
13
|
+
processErrorResponse,
|
|
14
|
+
} from "./common";
|
|
15
|
+
import {
|
|
16
|
+
publicKeyv1,
|
|
17
|
+
publicKeyv2,
|
|
18
|
+
serializePathv1,
|
|
19
|
+
serializePathv2,
|
|
20
|
+
signSendChunkv1,
|
|
21
|
+
signSendChunkv2,
|
|
22
|
+
} from "./helpers";
|
|
23
|
+
|
|
24
|
+
export class THORChainApp {
|
|
25
|
+
transport: Transport;
|
|
26
|
+
versionResponse: any;
|
|
27
|
+
|
|
28
|
+
constructor(transport: any) {
|
|
29
|
+
if (!transport) {
|
|
30
|
+
throw new SwapKitError("wallet_ledger_transport_not_defined");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.transport = transport;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static serializeHRP(hrp: string) {
|
|
37
|
+
if (hrp == null || hrp.length < 3 || hrp.length > 83) {
|
|
38
|
+
throw new SwapKitError("wallet_ledger_invalid_params", { reason: "Invalid HRP" });
|
|
39
|
+
}
|
|
40
|
+
const buf = Buffer.alloc(1 + hrp.length);
|
|
41
|
+
buf.writeUInt8(hrp.length, 0);
|
|
42
|
+
buf.write(hrp, 1);
|
|
43
|
+
return buf;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async serializePath(path: number[]) {
|
|
47
|
+
this.versionResponse = await getVersion(this.transport);
|
|
48
|
+
|
|
49
|
+
if (this.versionResponse.return_code !== ERROR_CODE.NoError) {
|
|
50
|
+
throw this.versionResponse;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (this.versionResponse.major) {
|
|
54
|
+
case 1:
|
|
55
|
+
return serializePathv1(path);
|
|
56
|
+
case 2:
|
|
57
|
+
return serializePathv2(path);
|
|
58
|
+
default:
|
|
59
|
+
return Buffer.alloc(0);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async signGetChunks(path: number[], buffer: Buffer) {
|
|
64
|
+
const serializedPath = await this.serializePath(path);
|
|
65
|
+
|
|
66
|
+
const chunks = [];
|
|
67
|
+
chunks.push(serializedPath);
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < buffer.length; i += CHUNK_SIZE) {
|
|
70
|
+
let end = i + CHUNK_SIZE;
|
|
71
|
+
if (i > buffer.length) {
|
|
72
|
+
end = buffer.length;
|
|
73
|
+
}
|
|
74
|
+
chunks.push(buffer.slice(i, end));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return chunks;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getVersion() {
|
|
81
|
+
try {
|
|
82
|
+
this.versionResponse = await getVersion(this.transport);
|
|
83
|
+
return this.versionResponse;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
return processErrorResponse(e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
appInfo() {
|
|
90
|
+
return this.transport.send(0xb0, 0x01, 0, 0).then((response: any) => {
|
|
91
|
+
const returnCode = response.readUInt16BE(response.length - 2);
|
|
92
|
+
|
|
93
|
+
let appName = "";
|
|
94
|
+
let appVersion = "";
|
|
95
|
+
let flagLen = 0;
|
|
96
|
+
let flagsValue = 0;
|
|
97
|
+
|
|
98
|
+
if (response[0] !== 1) {
|
|
99
|
+
// Ledger responds with format ID 1. There is no spec for any format != 1
|
|
100
|
+
return { error_message: "response format ID not recognized", return_code: 0x9001 };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const appNameLen = response[1];
|
|
104
|
+
appName = response.slice(2, 2 + appNameLen).toString("ascii");
|
|
105
|
+
let idx = 2 + appNameLen;
|
|
106
|
+
const appVersionLen = response[idx];
|
|
107
|
+
idx += 1;
|
|
108
|
+
appVersion = response.slice(idx, idx + appVersionLen).toString("ascii");
|
|
109
|
+
idx += appVersionLen;
|
|
110
|
+
const appFlagsLen = response[idx];
|
|
111
|
+
idx += 1;
|
|
112
|
+
flagLen = appFlagsLen;
|
|
113
|
+
flagsValue = response[idx];
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
appName,
|
|
117
|
+
appVersion,
|
|
118
|
+
error_message: errorCodeToString(returnCode),
|
|
119
|
+
flag_onboarded: (flagsValue & 4) !== 0,
|
|
120
|
+
flag_pin_validated: (flagsValue & 128) !== 0,
|
|
121
|
+
flag_recovery: (flagsValue & 1) !== 0,
|
|
122
|
+
flag_signed_mcu_code: (flagsValue & 2) !== 0,
|
|
123
|
+
flagLen,
|
|
124
|
+
flagsValue,
|
|
125
|
+
return_code: returnCode,
|
|
126
|
+
};
|
|
127
|
+
}, processErrorResponse);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
deviceInfo() {
|
|
131
|
+
return this.transport
|
|
132
|
+
.send(0xe0, 0x01, 0, 0, Buffer.from([]), [ERROR_CODE.NoError, 0x6e00])
|
|
133
|
+
.then((response: any) => {
|
|
134
|
+
const errorCodeData = response.slice(-2);
|
|
135
|
+
const returnCode = errorCodeData[0] * 256 + errorCodeData[1];
|
|
136
|
+
|
|
137
|
+
if (returnCode === 0x6e00) {
|
|
138
|
+
return { error_message: "This command is only available in the Dashboard", return_code: returnCode };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const targetId = response.slice(0, 4).toString("hex");
|
|
142
|
+
|
|
143
|
+
let pos = 4;
|
|
144
|
+
const secureElementVersionLen = response[pos];
|
|
145
|
+
pos += 1;
|
|
146
|
+
const seVersion = response.slice(pos, pos + secureElementVersionLen).toString();
|
|
147
|
+
pos += secureElementVersionLen;
|
|
148
|
+
|
|
149
|
+
const flagsLen = response[pos];
|
|
150
|
+
pos += 1;
|
|
151
|
+
const flag = response.slice(pos, pos + flagsLen).toString("hex");
|
|
152
|
+
pos += flagsLen;
|
|
153
|
+
|
|
154
|
+
const mcuVersionLen = response[pos];
|
|
155
|
+
pos += 1;
|
|
156
|
+
// Patch issue in mcu version
|
|
157
|
+
let tmp = response.slice(pos, pos + mcuVersionLen);
|
|
158
|
+
if (tmp[mcuVersionLen - 1] === 0) {
|
|
159
|
+
tmp = response.slice(pos, pos + mcuVersionLen - 1);
|
|
160
|
+
}
|
|
161
|
+
const mcuVersion = tmp.toString();
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
error_message: errorCodeToString(returnCode),
|
|
165
|
+
flag,
|
|
166
|
+
mcuVersion,
|
|
167
|
+
return_code: returnCode,
|
|
168
|
+
seVersion,
|
|
169
|
+
// //
|
|
170
|
+
targetId,
|
|
171
|
+
};
|
|
172
|
+
}, processErrorResponse);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async publicKey(path: number[]) {
|
|
176
|
+
try {
|
|
177
|
+
const serializedPath = await this.serializePath(path);
|
|
178
|
+
|
|
179
|
+
switch (this.versionResponse.major) {
|
|
180
|
+
case 1:
|
|
181
|
+
return publicKeyv1(this, serializedPath);
|
|
182
|
+
case 2: {
|
|
183
|
+
const data = Buffer.concat([THORChainApp.serializeHRP("thor"), serializedPath]);
|
|
184
|
+
return publicKeyv2(this, data);
|
|
185
|
+
}
|
|
186
|
+
default:
|
|
187
|
+
return { error_message: "App Version is not supported", return_code: 0x6400 };
|
|
188
|
+
}
|
|
189
|
+
} catch (e) {
|
|
190
|
+
return processErrorResponse(e);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getAddressAndPubKey(path: number[], hrp: string, showInDevice = false) {
|
|
195
|
+
try {
|
|
196
|
+
const serializedPath = await this.serializePath(path);
|
|
197
|
+
const data = Buffer.concat([THORChainApp.serializeHRP(hrp), serializedPath]);
|
|
198
|
+
const response = await this.transport.send(
|
|
199
|
+
CLA,
|
|
200
|
+
INS.GET_ADDR_SECP256K1,
|
|
201
|
+
showInDevice ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE,
|
|
202
|
+
0,
|
|
203
|
+
data,
|
|
204
|
+
[ERROR_CODE.NoError],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const compressedPk = Buffer.from(response.slice(0, 33));
|
|
208
|
+
const bech32Address = Buffer.from(response.slice(33, -2)).toString();
|
|
209
|
+
const returnCode = response.readUInt16BE(response.length - 2);
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
bech32_address: bech32Address,
|
|
213
|
+
compressed_pk: compressedPk,
|
|
214
|
+
error_message: errorCodeToString(returnCode),
|
|
215
|
+
return_code: returnCode,
|
|
216
|
+
};
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return processErrorResponse(err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
showAddressAndPubKey(path: number[], hrp: string) {
|
|
223
|
+
return this.getAddressAndPubKey(path, hrp, true);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
signSendChunk(chunkIdx: number, chunkNum: number, chunk: Buffer, txType = P2_VALUES.JSON) {
|
|
227
|
+
switch (this.versionResponse.major) {
|
|
228
|
+
case 1:
|
|
229
|
+
return signSendChunkv1(this, chunkIdx, chunkNum, chunk, txType);
|
|
230
|
+
case 2:
|
|
231
|
+
return signSendChunkv2(this, chunkIdx, chunkNum, chunk, txType);
|
|
232
|
+
default:
|
|
233
|
+
return { error_message: "App Version is not supported", return_code: 0x6400 };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async sign(path: number[], message: string, txType = P2_VALUES.JSON) {
|
|
238
|
+
const buffer = Buffer.from(message);
|
|
239
|
+
let chunks: Buffer[] = [];
|
|
240
|
+
let response: any;
|
|
241
|
+
try {
|
|
242
|
+
chunks = await this.signGetChunks(path, buffer);
|
|
243
|
+
response = await this.signSendChunk(1, chunks.length, chunks[0] as Buffer, txType);
|
|
244
|
+
} catch (error) {
|
|
245
|
+
processErrorResponse(error);
|
|
246
|
+
}
|
|
247
|
+
let result = { error_message: response.error_message, return_code: response.return_code, signature: null };
|
|
248
|
+
|
|
249
|
+
for (let i = 1; i < chunks.length; i += 1) {
|
|
250
|
+
result = await this.signSendChunk(1 + i, chunks.length, chunks[i] as Buffer, txType);
|
|
251
|
+
if (result.return_code !== ERROR_CODE.NoError) {
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { error_message: result.error_message, return_code: result.return_code, signature: result.signature };
|
|
257
|
+
}
|
|
258
|
+
}
|