diff --git a/integration/src/wallets/ledger.ts b/integration/src/wallets/ledger.ts index f3fc78c78..3459d58eb 100644 --- a/integration/src/wallets/ledger.ts +++ b/integration/src/wallets/ledger.ts @@ -222,21 +222,20 @@ export class MockTransport extends ledger.LedgerTransport { ); // Thorchain - const compress_pk = toByteArray("AxUZcTuLQr3DZxEtMxMs8Uzt+SisV3HURLpFm5SXEXuj"); this.memoize( - "Rune", - "getAddressAndPubKey", + "Thorchain", + "getAddress", JSON.parse(`[[${core.bip32ToAddressNList("m/44'/931'/0'/0/0")}], "thor"]`), JSON.parse( - `{"success":true,"coin":"Rune","method":"getAddressAndPubkey","payload":{"bech32_address":"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az","compressed_pk":[${compress_pk}]}}` + `{"success":true,"coin":"Rune","method":"getAddressAndPubkey","payload":{"address":"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az","publicKey":"031519713b8b42bdc367112d33132cf14cedf928ac5771d444ba459b9497117ba3"}}` ) ); this.memoize( - "Rune", + "Thorchain", "sign", JSON.parse( - '[{"tx":{"account_number":"17","chain_id":"thorchain-mainnet-v1","sequence":"2","fee":{"amount":[{"amount":"3000","denom":"rune"}],"gas":"200000"},"memo":"","msg":[{"type":"thorchain/MsgSend","value":{"amount":[{"amount":"100","denom":"rune"}],"from_address":"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az","to_address":"thor1wy58774wagy4hkljz9mchhqtgk949zdwwe80d5"}}],"signatures":[]},"addressNList":[2147483692,2147484579,2147483648,0,0],"chain_id":"thorchain-mainnet-v1","account_number":"17","sequence":"2"}]' + '[[2147483692,2147484579,2147483648,0,0],"{\\"account_number\\":\\"17\\",\\"chain_id\\":\\"thorchain-mainnet-v1\\",\\"fee\\":{\\"amount\\":[{\\"amount\\":\\"3000\\",\\"denom\\":\\"rune\\"}],\\"gas\\":\\"200000\\"},\\"memo\\":\\"\\",\\"msgs\\":[{\\"type\\":\\"thorchain/MsgSend\\",\\"value\\":{\\"amount\\":[{\\"amount\\":\\"100\\",\\"denom\\":\\"rune\\"}],\\"from_address\\":\\"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az\\",\\"to_address\\":\\"thor1wy58774wagy4hkljz9mchhqtgk949zdwwe80d5\\"}}],\\"sequence\\":\\"2\\"}"]' ), { success: true, @@ -251,10 +250,10 @@ export class MockTransport extends ledger.LedgerTransport { ); this.memoize( - "Rune", + "Thorchain", "sign", JSON.parse( - '[{"tx":{"account_number":"2722","chain_id":"thorchain-mainnet-v1","sequence":"4","fee":{"amount":[{"amount":"0","denom":"rune"}],"gas":"350000"},"memo":"","msg":[{"type":"thorchain/MsgDeposit","value":{"coins":[{"asset":"THOR.RUNE","amount":"50994000"}],"memo":"SWAP:BNB.BNB:bnb12splwpg8jenr9pjw3dwc5rr35t8792y8pc4mtf:348953501","signer":"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az"}}],"signatures":[]},"addressNList":[2147483692,2147484579,2147483648,0,0],"chain_id":"thorchain-mainnet-v1","account_number":"2722","sequence":"4"}]' + '[[2147483692,2147484579,2147483648,0,0],"{\\"account_number\\":\\"2722\\",\\"chain_id\\":\\"thorchain-mainnet-v1\\",\\"fee\\":{\\"amount\\":[{\\"amount\\":\\"0\\",\\"denom\\":\\"rune\\"}],\\"gas\\":\\"350000\\"},\\"memo\\":\\"\\",\\"msgs\\":[{\\"type\\":\\"thorchain/MsgDeposit\\",\\"value\\":{\\"coins\\":[{\\"amount\\":\\"50994000\\",\\"asset\\":\\"THOR.RUNE\\"}],\\"memo\\":\\"SWAP:BNB.BNB:bnb12splwpg8jenr9pjw3dwc5rr35t8792y8pc4mtf:348953501\\",\\"signer\\":\\"thor1ls33ayg26kmltw7jjy55p32ghjna09zp74t4az\\"}}],\\"sequence\\":\\"4\\"}"]' ), { success: true, diff --git a/packages/hdwallet-ledger-webusb/src/transport.ts b/packages/hdwallet-ledger-webusb/src/transport.ts index 6ce6e7271..96028d97b 100644 --- a/packages/hdwallet-ledger-webusb/src/transport.ts +++ b/packages/hdwallet-ledger-webusb/src/transport.ts @@ -6,7 +6,7 @@ import getAppAndVersion from "@ledgerhq/live-common/lib/hw/getAppAndVersion"; import getDeviceInfo from "@ledgerhq/live-common/lib/hw/getDeviceInfo"; import openApp from "@ledgerhq/live-common/lib/hw/openApp"; import * as core from "@shapeshiftoss/hdwallet-core"; -import * as ledger from "@shapeshiftoss/hdwallet-ledger"; +import { LedgerTransport, Thorchain } from "@shapeshiftoss/hdwallet-ledger"; import { LedgerResponse, LedgerTransportCoinType, @@ -80,11 +80,6 @@ export async function translateCoinAndMethod> { switch (coin) { - case "Rune": { - const thor = new ledger.THORChainApp(transport); - const methodInstance = thor[method as LedgerTransportMethodName<"Rune">].bind(thor); - return methodInstance as LedgerTransportMethod; - } case "Btc": { const btc = new Btc({ transport }); const methodInstance = btc[method as LedgerTransportMethodName<"Btc">].bind(btc); @@ -95,6 +90,11 @@ export async function translateCoinAndMethod].bind(eth); return methodInstance as LedgerTransportMethod; } + case "Thorchain": { + const thorchain = new Thorchain(transport); + const methodInstance = thorchain[method as LedgerTransportMethodName<"Thorchain">].bind(thorchain); + return methodInstance as LedgerTransportMethod; + } case "Cosmos": { const cosmos = new Cosmos(transport); const methodInstance = cosmos[method as LedgerTransportMethodName<"Cosmos">].bind(cosmos); @@ -130,7 +130,7 @@ export async function translateCoinAndMethod { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - - let targetId = 0; - if (response.length >= 9) { - targetId = (response[5] << 24) + (response[6] << 16) + (response[7] << 8) + (response[8] << 0); - } - - return { - return_code: returnCode, - error_message: errorCodeToString(returnCode), - // /// - test_mode: response[0] !== 0, - major: response[1], - minor: response[2], - patch: response[3], - device_locked: response[4] === 1, - target_id: targetId.toString(16), - }; - }, processErrorResponse); -} diff --git a/packages/hdwallet-ledger/src/thorchain/helpers.ts b/packages/hdwallet-ledger/src/thorchain/helpers.ts deleted file mode 100644 index 14a8893df..000000000 --- a/packages/hdwallet-ledger/src/thorchain/helpers.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { CLA, ErrorCode, errorCodeToString, INS, PAYLOAD_TYPE, processErrorResponse } from "./common"; - -const signSendChunkv1 = async (app: any, chunkIdx: any, chunkNum: any, chunk: any) => { - return app.transport - .send(CLA, INS.SIGN_SECP256K1, chunkIdx, chunkNum, chunk, [ErrorCode.NoError, 0x6984, 0x6a80]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - let errorMessage = errorCodeToString(returnCode); - - if (returnCode === 0x6a80 || returnCode === 0x6984) { - errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`; - } - - let signature = null; - if (response.length > 2) { - signature = response.slice(0, response.length - 2); - } - - return { - signature, - return_code: returnCode, - error_message: errorMessage, - }; - }, processErrorResponse); -}; - -export const serializePathv2 = (path: Array) => { - if (!path || path.length !== 5) { - throw new Error("Invalid path."); - } - - const buf = Buffer.alloc(20); - buf.writeUInt32LE(path[0], 0); - buf.writeUInt32LE(path[1], 4); - buf.writeUInt32LE(path[2], 8); - buf.writeUInt32LE(path[3], 12); - buf.writeUInt32LE(path[4], 16); - - return buf; -}; - -export const signSendChunkv2 = async (app: any, chunkIdx: any, chunkNum: any, chunk: any) => { - let payloadType = PAYLOAD_TYPE.ADD; - if (chunkIdx === 1) { - payloadType = PAYLOAD_TYPE.INIT; - } - if (chunkIdx === chunkNum) { - payloadType = PAYLOAD_TYPE.LAST; - } - - return signSendChunkv1(app, payloadType, 0, chunk); -}; - -export const publicKeyv2 = async (app: any, data: any) => { - return app.transport.send(CLA, INS.GET_ADDR_SECP256K1, 0, 0, data, [ErrorCode.NoError]).then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - const compressedPk = Buffer.from(response.slice(0, 33)); - - return { - pk: "OBSOLETE PROPERTY", - compressed_pk: compressedPk, - return_code: returnCode, - error_message: errorCodeToString(returnCode), - }; - }, processErrorResponse); -}; diff --git a/packages/hdwallet-ledger/src/thorchain/hw-app-thor.ts b/packages/hdwallet-ledger/src/thorchain/hw-app-thor.ts index 1ff37717b..13a1874d4 100644 --- a/packages/hdwallet-ledger/src/thorchain/hw-app-thor.ts +++ b/packages/hdwallet-ledger/src/thorchain/hw-app-thor.ts @@ -14,10 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. ******************************************************************************* */ -import { bech32 } from "@scure/base"; -import * as core from "@shapeshiftoss/hdwallet-core"; -import crypto from "crypto"; -import Ripemd160 from "ripemd160"; +import type Transport from "@ledgerhq/hw-transport"; import { APP_KEY, @@ -25,270 +22,127 @@ import { CLA, ErrorCode, errorCodeToString, - getVersion, INS, P1_VALUES, + PAYLOAD_TYPE, processErrorResponse, } from "./common"; -import { publicKeyv2, serializePathv2, signSendChunkv2 } from "./helpers"; - -const THOR_CHAIN = "thorchain-mainnet-v1"; export type GetAddressAndPubKeyResponse = { - bech32_address: string; - compressed_pk: Uint8Array; + address: string; + publicKey: string; error_message: string; return_code: number; }; export type SignResponse = { - signature: Uint8Array; + signature: null | Buffer; error_message: string; return_code: number; }; -const recursivelyOrderKeys = (unordered: any) => { - // If it's an array - recursively order any - // dictionary items within the array - if (Array.isArray(unordered)) { - unordered.forEach((item, index) => { - unordered[index] = recursivelyOrderKeys(item); - }); - return unordered; - } - - // If it's an object - let's order the keys - if (typeof unordered !== "object") return unordered; - const ordered: any = {}; - Object.keys(unordered) - .sort() - .forEach((key) => (ordered[key] = recursivelyOrderKeys(unordered[key]))); - return ordered; -}; - -const stringifyKeysInOrder = (data: any) => JSON.stringify(recursivelyOrderKeys(data)); - -class THORChainApp { - transport: any; +export class Thorchain { + transport: Transport; versionResponse: any; - constructor(transport: any, scrambleKey = APP_KEY) { + constructor(transport: Transport, scrambleKey = APP_KEY) { if (!transport) { throw new Error("Transport has not been defined"); } - this.transport = transport as any; - transport.decorateAppAPIMethods.bind(transport)( - this, - ["getVersion", "sign", "getAddressAndPubKey", "appInfo", "deviceInfo", "getBech32FromPK"], - scrambleKey - ); + this.transport = transport; + transport.decorateAppAPIMethods.bind(transport)(this, ["getAddress", "sign"], scrambleKey); } - static serializeHRP(hrp: any) { - if (hrp == null || hrp.length < 3 || hrp.length > 83) { + getVersion() { + return this.transport.send(CLA, INS.GET_VERSION, 0, 0).then((response) => { + const errorCodeData = response.slice(-2); + const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + + let targetId = 0; + if (response.length >= 9) { + targetId = (response[5] << 24) + (response[6] << 16) + (response[7] << 8) + (response[8] << 0); + } + + return { + return_code: returnCode, + error_message: errorCodeToString(returnCode), + test_mode: response[0] !== 0, + major: response[1], + minor: response[2], + patch: response[3], + device_locked: response[4] === 1, + target_id: targetId.toString(16), + }; + }, processErrorResponse); + } + + serializeHRP(hrp: string) { + if (hrp == null || hrp.length === 0 || hrp.length > 83) { throw new Error("Invalid HRP"); } + const buf = Buffer.alloc(1 + hrp.length); buf.writeUInt8(hrp.length, 0); buf.write(hrp, 1); return buf; } - static getBech32FromPK(hrp: any, pk: any) { - if (pk.length !== 33) { - throw new Error("expected compressed public key [31 bytes]"); - } - const hashSha256 = crypto.createHash("sha256").update(pk).digest(); - const hashRip = new Ripemd160().update(hashSha256).digest(); - // ts is drunk and doesn't like bech32.bech32 here - const encode = bech32.encode || (bech32 as any).bech32?.encode; - // ts is drunk and doesn't like bech32.bech32 here - const toWords = bech32.toWords || (bech32 as any).bech32?.toWords; - - return encode(hrp, toWords(hashRip)); - } - async serializePath(path: Array) { - this.versionResponse = await getVersion(this.transport); + this.versionResponse = await this.getVersion(); if (this.versionResponse.return_code !== ErrorCode.NoError) { throw this.versionResponse; } switch (this.versionResponse.major) { - case 2: - return serializePathv2(path); - default: - return { - return_code: 0x6400, - error_message: "App Version is not supported", - }; - } - } - - async signGetChunks(path: Array, message: any) { - const serializedPath = await this.serializePath(path); - - const chunks = []; - chunks.push(serializedPath); - const buffer = Buffer.from(message); - - for (let i = 0; i < buffer.length; i += CHUNK_SIZE) { - let end = i + CHUNK_SIZE; - if (i > buffer.length) { - end = buffer.length; - } - chunks.push(buffer.slice(i, end)); - } - - return chunks; - } - - async getVersion() { - try { - this.versionResponse = await getVersion(this.transport); - return this.versionResponse; - } catch (e) { - return processErrorResponse(e); - } - } + case 2: { + if (!path || path.length !== 5) { + throw new Error("Invalid path."); + } - async appInfo() { - return this.transport.send(0xb0, 0x01, 0, 0).then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + const buf = Buffer.alloc(20); + buf.writeUInt32LE(path[0], 0); + buf.writeUInt32LE(path[1], 4); + buf.writeUInt32LE(path[2], 8); + buf.writeUInt32LE(path[3], 12); + buf.writeUInt32LE(path[4], 16); - const result = {} as any; - - let appName = "err"; - let appVersion = "err"; - let flagLen = 0; - let flagsValue = 0; - - if (response[0] !== 1) { - // Ledger responds with format ID 1. There is no spec for any format != 1 - result.error_message = "response format ID not recognized"; - result.return_code = 0x9001; - } else { - const appNameLen = response[1]; - appName = response.slice(2, 2 + appNameLen).toString("ascii"); - let idx = 2 + appNameLen; - const appVersionLen = response[idx]; - idx += 1; - appVersion = response.slice(idx, idx + appVersionLen).toString("ascii"); - idx += appVersionLen; - const appFlagsLen = response[idx]; - idx += 1; - flagLen = appFlagsLen; - flagsValue = response[idx]; + return buf; } - - return { - return_code: returnCode, - error_message: errorCodeToString(returnCode), - // // - appName, - appVersion, - flagLen, - flagsValue, - - flag_recovery: (flagsValue & 1) !== 0, - - flag_signed_mcu_code: (flagsValue & 2) !== 0, - - flag_onboarded: (flagsValue & 4) !== 0, - - flag_pin_validated: (flagsValue & 128) !== 0, - }; - }, processErrorResponse); - } - - async deviceInfo() { - return this.transport.send(0xe0, 0x01, 0, 0, Buffer.from([]), [ErrorCode.NoError, 0x6e00]).then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; - - if (returnCode === 0x6e00) { + default: return { - return_code: returnCode, - error_message: "This command is only available in the Dashboard", + return_code: 0x6400, + error_message: "App Version is not supported", }; - } - - const targetId = response.slice(0, 4).toString("hex"); - - let pos = 4; - const secureElementVersionLen = response[pos]; - pos += 1; - const seVersion = response.slice(pos, pos + secureElementVersionLen).toString(); - pos += secureElementVersionLen; - - const flagsLen = response[pos]; - pos += 1; - const flag = response.slice(pos, pos + flagsLen).toString("hex"); - pos += flagsLen; - - const mcuVersionLen = response[pos]; - pos += 1; - // Patch issue in mcu version - let tmp = response.slice(pos, pos + mcuVersionLen); - if (tmp[mcuVersionLen - 1] === 0) { - tmp = response.slice(pos, pos + mcuVersionLen - 1); - } - const mcuVersion = tmp.toString(); - - return { - return_code: returnCode, - error_message: errorCodeToString(returnCode), - // // - targetId, - seVersion, - flag, - mcuVersion, - }; - }, processErrorResponse); - } - - async publicKey(path: Array) { - try { - const serializedPath = await this.serializePath(path); - - switch (this.versionResponse.major) { - case 2: { - const data = Buffer.concat([THORChainApp.serializeHRP("thor"), serializedPath as any]); - return await publicKeyv2(this, data); - } - default: - return { - return_code: 0x6400, - error_message: "App Version is not supported", - }; - } - } catch (e) { - return processErrorResponse(e); } } - async getAddressAndPubKey(path: Array, hrp: any): Promise { + async getAddress(path: Array, hrp: string, boolDisplay?: boolean): Promise { try { return await this.serializePath(path) .then((serializedPath) => { - const data = Buffer.concat([THORChainApp.serializeHRP(hrp), serializedPath as any]); - return this.transport - .send(CLA, INS.GET_ADDR_SECP256K1, P1_VALUES.ONLY_RETRIEVE, 0, data, [ErrorCode.NoError]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + if (!Buffer.isBuffer(serializedPath)) return serializedPath; - const compressedPk = Buffer.from(response.slice(0, 33)); - const bech32Address = Buffer.from(response.slice(33, -2)).toString(); + const data = Buffer.concat([this.serializeHRP(hrp), serializedPath]); + return this.transport + .send( + CLA, + INS.GET_ADDR_SECP256K1, + boolDisplay ? P1_VALUES.SHOW_ADDRESS_IN_DEVICE : P1_VALUES.ONLY_RETRIEVE, + 0, + data, + [ErrorCode.NoError] + ) + .then((response) => { + const errorCodeData = response.slice(-2); + const return_code = errorCodeData[0] * 256 + errorCodeData[1]; return { - bech32_address: bech32Address, - compressed_pk: compressedPk, - return_code: returnCode, - error_message: errorCodeToString(returnCode), + address: Buffer.from(response.slice(33, -2)).toString(), + publicKey: Buffer.from(response.slice(0, 33)).toString("hex"), + return_code, + error_message: errorCodeToString(return_code), }; }, processErrorResponse); }) @@ -298,59 +152,73 @@ class THORChainApp { } } - async showAddressAndPubKey(path: Array, hrp: any) { - try { - return await this.serializePath(path) - .then((serializedPath) => { - const data = Buffer.concat([THORChainApp.serializeHRP(hrp), serializedPath as any]); - return this.transport - .send(CLA, INS.GET_ADDR_SECP256K1, P1_VALUES.SHOW_ADDRESS_IN_DEVICE, 0, data, [ErrorCode.NoError]) - .then((response: any) => { - const errorCodeData = response.slice(-2); - const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + async signGetChunks(path: Array, message: string) { + const serializedPath = await this.serializePath(path); - const compressedPk = Buffer.from(response.slice(0, 33)); - const bech32Address = Buffer.from(response.slice(33, -2)).toString(); + if (!Buffer.isBuffer(serializedPath)) return serializedPath; - return { - bech32_address: bech32Address, - compressed_pk: compressedPk, - return_code: returnCode, - error_message: errorCodeToString(returnCode), - }; - }, processErrorResponse); - }) - .catch((err) => processErrorResponse(err)); - } catch (e) { - return processErrorResponse(e); + const chunks = []; + chunks.push(serializedPath); + const buffer = Buffer.from(message); + + for (let i = 0; i < buffer.length; i += CHUNK_SIZE) { + let end = i + CHUNK_SIZE; + if (i > buffer.length) { + end = buffer.length; + } + chunks.push(buffer.slice(i, end)); } + + return chunks; } - async signSendChunk(chunkIdx: number, chunkNum: number, chunk: any) { + async signSendChunk(chunkIdx: number, chunkNum: number, chunk: Buffer): Promise { switch (this.versionResponse.major) { - case 2: - return signSendChunkv2(this, chunkIdx, chunkNum, chunk); + case 2: { + chunkIdx = (() => { + if (chunkIdx === 1) return PAYLOAD_TYPE.INIT; + if (chunkIdx === chunkNum) return PAYLOAD_TYPE.LAST; + return PAYLOAD_TYPE.ADD; + })(); + + return this.transport + .send(CLA, INS.SIGN_SECP256K1, chunkIdx, 0, chunk, [ErrorCode.NoError, 0x6984, 0x6a80]) + .then((response) => { + const errorCodeData = response.slice(-2); + const returnCode = errorCodeData[0] * 256 + errorCodeData[1]; + let errorMessage = errorCodeToString(returnCode); + + if (returnCode === 0x6a80 || returnCode === 0x6984) { + errorMessage = `${errorMessage} : ${response.slice(0, response.length - 2).toString("ascii")}`; + } + + let signature: Buffer | null = null; + if (response.length > 2) { + signature = response.slice(0, response.length - 2); + } + + return { + signature, + return_code: returnCode, + error_message: errorMessage, + }; + }, processErrorResponse); + } default: return { + signature: null, return_code: 0x6400, error_message: "App Version is not supported", }; } } - async sign(msg: core.ThorchainSignTx): Promise { - const rawTx = stringifyKeysInOrder({ - account_number: msg.account_number, - chain_id: THOR_CHAIN, - fee: { amount: msg.tx.fee.amount, gas: msg.tx.fee.gas }, - memo: msg.tx.memo, - msgs: msg.tx.msg, - sequence: msg.sequence, - }); - - return this.signGetChunks(msg.addressNList, rawTx).then((chunks) => { + async sign(path: Array, message: string): Promise { + return this.signGetChunks(path, message).then((chunks) => { + if (!Array.isArray(chunks)) return chunks; + return this.signSendChunk(1, chunks.length, chunks[0]).then(async (response) => { - let result = { + let result: SignResponse = { return_code: response.return_code, error_message: response.error_message, signature: null, @@ -372,5 +240,3 @@ class THORChainApp { }, processErrorResponse); } } - -export { THORChainApp }; diff --git a/packages/hdwallet-ledger/src/thorchain/index.ts b/packages/hdwallet-ledger/src/thorchain/index.ts index e9e595cde..17ce7b2ec 100644 --- a/packages/hdwallet-ledger/src/thorchain/index.ts +++ b/packages/hdwallet-ledger/src/thorchain/index.ts @@ -1,52 +1,63 @@ import type { AccountData, AminoSignResponse, OfflineAminoSigner, StdSignDoc, StdTx } from "@cosmjs/amino"; +import { Secp256k1Signature } from "@cosmjs/crypto"; import type { SignerData } from "@cosmjs/stargate"; import * as core from "@shapeshiftoss/hdwallet-core"; import { fromByteArray } from "base64-js"; import PLazy from "p-lazy"; -import { handleError, LedgerTransport } from ".."; -import { getSignature } from "./utils"; +import { handleError, LedgerTransport, stringifyKeysInOrder } from ".."; export * from "./common"; -export * from "./helpers"; export * from "./hw-app-thor"; const protoTxBuilder = PLazy.from(() => import("@shapeshiftoss/proto-tx-builder")); +const THOR_CHAIN = "thorchain-mainnet-v1"; + export const thorchainGetAddress = async ( transport: LedgerTransport, msg: core.ThorchainGetAddress ): Promise => { - const addressAndPubkey = await transport.call("Rune", "getAddressAndPubKey", msg.addressNList, "thor"); + const res = await transport.call("Thorchain", "getAddress", msg.addressNList, "thor"); - handleError(addressAndPubkey, transport, "Unable to obtain address and public key from device."); + handleError(res, transport, "Unable to obtain address and public key from device."); - return addressAndPubkey.payload.bech32_address; + return res.payload.address; }; export const thorchainSignTx = async ( transport: LedgerTransport, msg: core.ThorchainSignTx ): Promise => { - const addressAndPubkey = await transport.call("Rune", "getAddressAndPubKey", msg.addressNList, "thor"); + const getAddressResponse = await transport.call("Thorchain", "getAddress", msg.addressNList, "thor"); + + handleError(getAddressResponse, transport, "Unable to obtain address and public key from device."); - handleError(addressAndPubkey, transport, "Unable to obtain address and public key from device."); + const { address, publicKey } = getAddressResponse.payload; - const { bech32_address: address, compressed_pk } = addressAndPubkey.payload; - const pubkey = fromByteArray(compressed_pk); + const unsignedTx = stringifyKeysInOrder({ + account_number: msg.account_number, + chain_id: THOR_CHAIN, + fee: { amount: msg.tx.fee.amount, gas: msg.tx.fee.gas }, + memo: msg.tx.memo, + msgs: msg.tx.msg, + sequence: msg.sequence, + }); - const signResponse = await transport.call("Rune", "sign", msg); + const signResponse = await transport.call("Thorchain", "sign", msg.addressNList, unsignedTx); handleError(signResponse, transport, "Unable to obtain signature from device."); const signature = signResponse.payload.signature; + if (!signature) throw new Error("No signature returned from device"); + const offlineSigner: OfflineAminoSigner = { async getAccounts(): Promise { return [ { address, algo: "secp256k1", - pubkey: compressed_pk, + pubkey: Buffer.from(publicKey, "hex"), }, ]; }, @@ -59,9 +70,9 @@ export const thorchainSignTx = async ( signature: { pub_key: { type: "tendermint/PubKeySecp256k1", - value: pubkey, + value: publicKey, }, - signature: getSignature(signature), + signature: fromByteArray(Secp256k1Signature.fromDer(signature).toFixedLength()), }, }; }, diff --git a/packages/hdwallet-ledger/src/thorchain/utils.ts b/packages/hdwallet-ledger/src/thorchain/utils.ts deleted file mode 100644 index 1c4b14b7c..000000000 --- a/packages/hdwallet-ledger/src/thorchain/utils.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { fromByteArray } from "base64-js"; - -export const getSignature = (signatureArray: Uint8Array) => { - // Check Type Length Value encoding - if (signatureArray.length < 64) { - throw new Error("Invalid Signature: Too short"); - } - if (signatureArray[0] !== 0x30) { - throw new Error("Invalid Ledger Signature TLV encoding: expected first byte 0x30"); - } - if (signatureArray[1] + 2 !== signatureArray.length) { - throw new Error("Invalid Signature: signature length does not match TLV"); - } - if (signatureArray[2] !== 0x02) { - throw new Error("Invalid Ledger Signature TLV encoding: expected length type 0x02"); - } - - // r signature - const rLength = signatureArray[3]; - let rSignature = signatureArray.slice(4, rLength + 4); - - // Drop leading zero on some 'r' signatures that are 33 bytes. - if (rSignature.length === 33 && rSignature[0] === 0) { - rSignature = rSignature.slice(1, 33); - } else if (rSignature.length === 33) { - throw new Error('Invalid signature: "r" too long'); - } - - // add leading zero's to pad to 32 bytes - while (rSignature.length < 32) { - const rSignaturePadded = new Uint8Array(32); - rSignaturePadded.set(rSignature, 32 - rSignature.length); - rSignature = rSignaturePadded; - } - - // s signature - if (signatureArray[rLength + 4] !== 0x02) { - throw new Error("Invalid Ledger Signature TLV encoding: expected length type 0x02"); - } - - const sLength = signatureArray[rLength + 5]; - - if (4 + rLength + 2 + sLength !== signatureArray.length) { - throw new Error("Invalid Ledger Signature: TLV byte lengths do not match message length"); - } - - let sSignature = signatureArray.slice(rLength + 6, signatureArray.length); - - // Drop leading zero on 's' signatures that are 33 bytes. This shouldn't occur since ledger signs using "Small s" math. But just to be sure... - if (sSignature.length === 33 && sSignature[0] === 0) { - sSignature = sSignature.slice(1, 33); - } else if (sSignature.length === 33) { - throw new Error('Invalid signature: "s" too long'); - } - - // add leading zero's to pad to 32 bytes - while (sSignature.length < 32) { - const sSignaturePadded = new Uint8Array(32); - sSignaturePadded.set(sSignature, 32 - sSignature.length); - sSignature = sSignaturePadded; - } - - if (rSignature.length !== 32 || sSignature.length !== 32) { - throw new Error("Invalid signatures: must be 32 bytes each"); - } - - return fromByteArray(Buffer.concat([rSignature, sSignature])); -}; diff --git a/packages/hdwallet-ledger/src/transport.ts b/packages/hdwallet-ledger/src/transport.ts index 1bbb13d04..8a1f4610f 100644 --- a/packages/hdwallet-ledger/src/transport.ts +++ b/packages/hdwallet-ledger/src/transport.ts @@ -7,7 +7,7 @@ import type getDeviceInfo from "@ledgerhq/live-common/lib/hw/getDeviceInfo"; import type openApp from "@ledgerhq/live-common/lib/hw/openApp"; import * as core from "@shapeshiftoss/hdwallet-core"; -import { THORChainApp } from "./thorchain"; +import { Thorchain } from "./thorchain"; type MethodsOnly = { [k in keyof T as T[k] extends (...args: any) => any ? k : never]: T[k]; @@ -15,7 +15,7 @@ type MethodsOnly = { type UnwrapPromise = T extends Promise ? R : T; type DefinitelyCallable = T extends (...args: any) => any ? T : never; -export type LedgerTransportCoinType = null | "Btc" | "Eth" | "Rune" | "Cosmos"; +export type LedgerTransportCoinType = null | "Btc" | "Eth" | "Thorchain" | "Cosmos"; type CurriedWithTransport any> = T extends ( transport: Transport, ...args: infer R @@ -36,8 +36,8 @@ type LedgerTransportMethodMap = T extends nul ? MethodsOnly : T extends "Eth" ? MethodsOnly - : T extends "Rune" - ? MethodsOnly + : T extends "Thorchain" + ? MethodsOnly : T extends "Cosmos" ? MethodsOnly : never;