diff --git a/libs/coin-modules/coin-ton/.eslintrc.js b/libs/coin-modules/coin-ton/.eslintrc.js new file mode 100644 index 000000000000..8f6848a860b9 --- /dev/null +++ b/libs/coin-modules/coin-ton/.eslintrc.js @@ -0,0 +1,20 @@ +module.exports = { + env: { + browser: true, + es6: true, + }, + overrides: [ + { + files: ["src/**/*.test.{ts,tsx}"], + env: { + "jest/globals": true, + }, + plugins: ["jest"], + }, + ], + rules: { + "no-console": ["error", { allow: ["warn", "error"] }], + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-explicit-any": "warn", + }, +}; diff --git a/libs/coin-modules/coin-ton/.unimportedrc.json b/libs/coin-modules/coin-ton/.unimportedrc.json new file mode 100644 index 000000000000..e7881c0c13fc --- /dev/null +++ b/libs/coin-modules/coin-ton/.unimportedrc.json @@ -0,0 +1,12 @@ +{ + "entry": [ + "src/bridge/js.ts", + "src/errors.ts", + "src/hw-getAddress.ts", + "src/types.ts", + "src/cli-transaction.ts" + ], + "ignoreUnimported": [ + "src/transaction.ts" + ] +} diff --git a/libs/coin-modules/coin-ton/jest.config.js b/libs/coin-modules/coin-ton/jest.config.js new file mode 100644 index 000000000000..f46ddfd2b0a4 --- /dev/null +++ b/libs/coin-modules/coin-ton/jest.config.js @@ -0,0 +1,6 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + testPathIgnorePatterns: ["lib/", "lib-es/"], +}; diff --git a/libs/coin-modules/coin-ton/package.json b/libs/coin-modules/coin-ton/package.json new file mode 100644 index 000000000000..c75c4710bb7a --- /dev/null +++ b/libs/coin-modules/coin-ton/package.json @@ -0,0 +1,84 @@ +{ + "name": "@ledgerhq/coin-ton", + "version": "0.3.11", + "description": "Ton Coin integration", + "keywords": [ + "Ledger", + "LedgerWallet", + "ton", + "Ton", + "Hardware Wallet" + ], + "repository": { + "type": "git", + "url": "https://github.com/LedgerHQ/ledger-live.git" + }, + "bugs": { + "url": "https://github.com/LedgerHQ/ledger-live/issues" + }, + "homepage": "https://github.com/LedgerHQ/ledger-live/tree/develop/libs/coin-modules/coin-ton", + "publishConfig": { + "access": "public" + }, + "typesVersions": { + "*": { + "lib/*": [ + "lib/*" + ], + "lib-es/*": [ + "lib-es/*" + ], + "*": [ + "lib/*" + ] + } + }, + "exports": { + "./lib/*": "./lib/*.js", + "./lib-es/*": "./lib-es/*.js", + "./*": { + "require": "./lib/*.js", + "default": "./lib-es/*.js" + }, + "./package.json": "./package.json" + }, + "license": "Apache-2.0", + "dependencies": { + "@ledgerhq/coin-framework": "workspace:^", + "@ledgerhq/devices": "workspace:^", + "@ledgerhq/errors": "workspace:^", + "@ledgerhq/live-env": "workspace:^", + "@ledgerhq/live-network": "workspace:^", + "@ledgerhq/logs": "workspace:^", + "@ledgerhq/types-cryptoassets": "workspace:^", + "@ledgerhq/types-live": "workspace:^", + "@ledgerhq/hw-transport": "workspace:^", + "@ton-community/ton-ledger": "^7.0.1", + "@ton/core": "^0.56.1", + "@ton/ton": "^13.11.1", + "@ton/crypto": "^3.2.0", + "bignumber.js": "^9.1.2", + "expect": "^27.4.6", + "invariant": "^2.2.2", + "lodash": "^4.17.21", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@types/invariant": "^2.2.2", + "@types/jest": "^29.5.10", + "@types/lodash": "^4.14.191", + "jest": "^29.7.0", + "ts-jest": "^29.1.1" + }, + "scripts": { + "clean": "rimraf lib lib-es", + "build": "tsc && tsc -m ES6 --outDir lib-es", + "prewatch": "pnpm build", + "watch": "tsc --watch", + "doc": "documentation readme src/** --section=API --pe ts --re ts --re d.ts", + "lint": "eslint ./src --no-error-on-unmatched-pattern --ext .ts,.tsx --cache", + "lint:fix": "pnpm lint --fix", + "test": "jest", + "unimported": "unimported" + } +} diff --git a/libs/coin-modules/coin-ton/src/bridge.integration.test.ts b/libs/coin-modules/coin-ton/src/bridge.integration.test.ts new file mode 100644 index 000000000000..003d919819a6 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/bridge.integration.test.ts @@ -0,0 +1,146 @@ +import { InvalidAddress, NotEnoughBalance } from "@ledgerhq/errors"; +import { CurrenciesData, DatasetTest } from "@ledgerhq/types-live"; +import BigNumber from "bignumber.js"; +import { TonCommentInvalid } from "./errors"; +import { fromTransactionRaw } from "./transaction"; +import { Transaction } from "./types"; + +const PUBKEY = "86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060"; +const ADDRESS = "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4"; +const ADDRESS_2 = "UQAui6M4jOYOezUGfmeONA22Ars9yjd34YIGdAR1Pcpp4sgR"; +const PATH = "44'/607'/0'/0'/0'/0'"; + +const ton: CurrenciesData = { + scanAccounts: [ + { + name: "ton seed 1", + apdus: ` + => e005000019068000002c8000025f80000000800000008000000080000000 + <= 86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a0609000 + => e005000019068000002c8000025f80000000800000008000000180000000 + <= b5177c2b32f9d72fa8c673cc3d61acec6a9f68eb5e4945445fdbb48a45eb48879000 + `, + test: (expect, accounts) => { + for (const account of accounts) { + expect(account.derivationMode).toEqual("ton"); + } + }, + }, + ], + accounts: [ + { + raw: { + id: `js:2:ton:${PUBKEY}:ton`, + currencyId: "ton", + seedIdentifier: PUBKEY, + name: "TON 1", + derivationMode: "ton", + index: 0, + freshAddress: ADDRESS, + freshAddressPath: PATH, + freshAddresses: [ + { + address: ADDRESS, + derivationPath: PATH, + }, + ], + xpub: PUBKEY, + blockHeight: 0, + operations: [], + pendingOperations: [], + unitMagnitude: 9, + lastSyncDate: "", + balance: "5000000000", + }, + transactions: [ + { + name: "Not a valid address", + transaction: fromTransactionRaw({ + family: "ton", + recipient: "novalidaddress", + fees: "10000000", + amount: "1000", + comment: { isEncrypted: false, text: "" }, + }), + expectedStatus: { + errors: { + recipient: new InvalidAddress(), + }, + warnings: {}, + }, + }, + { + name: "Not enough balance", + transaction: fromTransactionRaw({ + family: "ton", + recipient: ADDRESS_2, + fees: "10000000", + amount: (300 * 1e9).toString(), + comment: { isEncrypted: false, text: "" }, + }), + expectedStatus: { + errors: { + amount: new NotEnoughBalance(), + }, + warnings: {}, + }, + }, + { + name: "Invalid transferID/Memo", + transaction: fromTransactionRaw({ + family: "ton", + recipient: ADDRESS_2, + fees: "10000000", + amount: (1 * 1e9).toString(), + comment: { isEncrypted: false, text: "😀" }, + }), + expectedStatus: { + errors: { + comment: new TonCommentInvalid(), + }, + warnings: {}, + }, + }, + { + name: "New account and sufficient amount", + transaction: fromTransactionRaw({ + family: "ton", + recipient: ADDRESS_2, + fees: "10000000", + amount: "10000000", + comment: { isEncrypted: false, text: "Valid" }, + }), + expectedStatus: { + amount: new BigNumber("10000000"), + errors: {}, + warnings: {}, + }, + }, + ], + }, + ], +}; + +export const dataset: DatasetTest = { + implementations: ["js"], + currencies: { + ton, + }, +}; + +describe("Ton bridge", () => { + test.todo( + "This is an empty test to make jest command pass. Remove it once there is a real test.", + ); +}); + +/** + * NOTE: if tests are added to this file, + * like done in libs/coin-polkadot/src/bridge.integration.test.ts for example, + * this file fill need to be imported in ledger-live-common + * libs/ledger-live-common/src/families/ton/bridge.integration.test.ts + * like done for polkadot. + * cf. + * - libs/coin-polkadot/src/bridge.integration.test.ts + * - libs/ledger-live-common/src/families/polkadot/bridge.integration.test.ts + */ diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.ts similarity index 100% rename from libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts rename to libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.ts diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.types.ts similarity index 100% rename from libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts rename to libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.types.ts diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts similarity index 98% rename from libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts rename to libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts index 768f0918910d..2bdf1757c5d7 100644 --- a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts +++ b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts @@ -20,7 +20,7 @@ export async function getTransactions( // we found the last transaction if (tmpTxs.transactions.length === 1) break; // it should always match - if (hash !== tmpTxs[0].hash) throw Error("[ton] transaction hash does not match"); + if (hash !== tmpTxs.transactions[0].hash) throw Error("[ton] transaction hash does not match"); tmpTxs.transactions.shift(); // first element is repeated txs.transactions.push(...tmpTxs.transactions); txs.address_book = { ...txs.address_book, ...tmpTxs.address_book }; @@ -67,7 +67,6 @@ export function mapTxToOps( const date = new Date(tx.now * 1000); // now is defined in seconds const hash = tx.in_msg?.hash ?? tx.hash; // this is the hash we know in signature time - if (isReceiving) { if (tx.total_fees !== "0") { // these are small amount of fees payed when receiving diff --git a/libs/coin-modules/coin-ton/src/bridge/js.ts b/libs/coin-modules/coin-ton/src/bridge/js.ts new file mode 100644 index 000000000000..2e8fe188e6a2 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/bridge/js.ts @@ -0,0 +1,64 @@ +import getAddressWrapper from "@ledgerhq/coin-framework/bridge/getAddressWrapper"; +import { + defaultUpdateTransaction, + makeAccountBridgeReceive, + makeScanAccounts, +} from "@ledgerhq/coin-framework/bridge/jsHelpers"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; + +import type { AccountBridge, CurrencyBridge } from "@ledgerhq/types-live"; +import resolver from "../hw-getAddress"; +import broadcast from "../js-broadcast"; +import createTransaction from "../js-createTransaction"; +import estimateMaxSpendable from "../js-estimateMaxSpendable"; +import getTransactionStatus from "../js-getTransactionStatus"; +import prepareTransaction from "../js-prepareTransaction"; +import { buildSignOperation } from "../js-signOperation"; +import { getAccountShape, sync } from "../js-synchronisation"; +import { TonAddress, TonSignature, TonSigner } from "../signer"; +import type { Transaction } from "../types"; + +export function buildCurrencyBridge( + signerContext: SignerContext, +): CurrencyBridge { + const getAddress = resolver(signerContext); + + const scanAccounts = makeScanAccounts({ + getAccountShape, + getAddressFn: getAddress, + }); + + return { + preload: async () => Promise.resolve({}), + hydrate: () => {}, + scanAccounts, + }; +} + +export function buildAccountBridge( + signerContext: SignerContext, +): AccountBridge { + const getAddress = resolver(signerContext); + + const receive = makeAccountBridgeReceive(getAddressWrapper(getAddress)); + const signOperation = buildSignOperation(signerContext); + + return { + estimateMaxSpendable, + createTransaction, + updateTransaction: defaultUpdateTransaction, + getTransactionStatus, + prepareTransaction, + sync, + receive, + signOperation, + broadcast, + }; +} + +export function createBridges(signerContext: SignerContext) { + return { + currencyBridge: buildCurrencyBridge(signerContext), + accountBridge: buildAccountBridge(signerContext), + }; +} diff --git a/libs/coin-modules/coin-ton/src/cli-transaction.ts b/libs/coin-modules/coin-ton/src/cli-transaction.ts new file mode 100644 index 000000000000..7c3c47c7f072 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/cli-transaction.ts @@ -0,0 +1,32 @@ +import invariant from "invariant"; +import flatMap from "lodash/flatMap"; + +import type { AccountLike } from "@ledgerhq/types-live"; +import { Transaction } from "./types"; + +const options: any = []; + +function inferTransactions( + transactions: Array<{ + account: AccountLike; + transaction: Transaction; + }>, + opts: Record, +): Transaction[] { + return flatMap(transactions, ({ transaction }) => { + invariant(transaction.family === "ton", "ton family"); + + return { + ...transaction, + family: "ton", + mode: opts.mode || "send", + } as Transaction; + }); +} + +export default function makeCliTools() { + return { + options, + inferTransactions, + }; +} diff --git a/libs/coin-modules/coin-ton/src/errors.ts b/libs/coin-modules/coin-ton/src/errors.ts new file mode 100644 index 000000000000..0bb1ecce8bc1 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/errors.ts @@ -0,0 +1,6 @@ +import { createCustomErrorClass } from "@ledgerhq/errors"; + +/* + * When the recipient is a new named account, and needs to be created first. + */ +export const TonCommentInvalid: any = createCustomErrorClass("TonCommentInvalid"); diff --git a/libs/coin-modules/coin-ton/src/hw-getAddress.ts b/libs/coin-modules/coin-ton/src/hw-getAddress.ts new file mode 100644 index 000000000000..1509d4bfa0ed --- /dev/null +++ b/libs/coin-modules/coin-ton/src/hw-getAddress.ts @@ -0,0 +1,30 @@ +import { GetAddressFn } from "@ledgerhq/coin-framework/bridge/getAddressWrapper"; +import { GetAddressOptions } from "@ledgerhq/coin-framework/derivation"; +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import { TonAddress, TonSignature, TonSigner } from "./signer"; +import { getLedgerTonPath } from "./utils"; + +const resolver = ( + signerContext: SignerContext, +): GetAddressFn => { + return async (deviceId: string, { path, verify }: GetAddressOptions) => { + const ledgerPath = getLedgerTonPath(path); + + const sig = (await signerContext(deviceId, async signer => { + return verify + ? await signer.validateAddress(ledgerPath, { bounceable: false }) + : await signer.getAddress(ledgerPath, { bounceable: false }); + })) as TonAddress; + + if (!sig.address || !sig.publicKey.length) + throw Error(`[ton] Response is empty ${sig.address} ${sig.publicKey}`); + + return { + address: sig.address, + publicKey: sig.publicKey.toString("hex"), + path, + }; + }; +}; + +export default resolver; diff --git a/libs/coin-modules/coin-ton/src/hw-signMessage.ts b/libs/coin-modules/coin-ton/src/hw-signMessage.ts new file mode 100644 index 000000000000..fed12faf915a --- /dev/null +++ b/libs/coin-modules/coin-ton/src/hw-signMessage.ts @@ -0,0 +1,37 @@ +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import type { Account } from "@ledgerhq/types-live"; +import { AnyMessage } from "@ledgerhq/types-live"; +import { TonAddress, TonSignature, TonSigner } from "./signer"; +import { TonHwParams } from "./types"; +import { getLedgerTonPath } from "./utils"; + +export const signMessage = + (signerContext: SignerContext) => + async (deviceId: string, account: Account, { message }: AnyMessage) => { + if (typeof message !== "string") { + throw new Error("Invalid message type"); + } + + if (!message) throw new Error("Message cannot be empty"); + if (typeof message !== "string") throw new Error("Message must be a string"); + + const parsedMessage = JSON.parse(message); + const ledgerPath = getLedgerTonPath(account.freshAddressPath); + + const sig = (await signerContext(deviceId, signer => + signer.signTransaction(ledgerPath, parsedMessage as TonHwParams), + )) as TonSignature; + + if (!sig) { + throw new Error("No signature"); + } + + return { + rsv: { + r: "", + s: "", + v: 0, + }, + signature: sig.toString(), + }; + }; diff --git a/libs/coin-modules/coin-ton/src/js-broadcast.ts b/libs/coin-modules/coin-ton/src/js-broadcast.ts new file mode 100644 index 000000000000..bcd8ae5133e1 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-broadcast.ts @@ -0,0 +1,10 @@ +import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation"; +import type { BroadcastFnSignature } from "@ledgerhq/types-live"; +import { broadcastTx } from "./bridge/bridgeHelpers/api"; + +const broadcast: BroadcastFnSignature = async ({ signedOperation: { signature, operation } }) => { + const hash = await broadcastTx(signature); + return patchOperationWithHash(operation, hash); +}; + +export default broadcast; diff --git a/libs/coin-modules/coin-ton/src/js-createTransaction.ts b/libs/coin-modules/coin-ton/src/js-createTransaction.ts new file mode 100644 index 000000000000..0e2b743075e8 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-createTransaction.ts @@ -0,0 +1,16 @@ +import { BigNumber } from "bignumber.js"; +import type { Transaction } from "./types"; + +const createTransaction = (): Transaction => ({ + family: "ton", + amount: new BigNumber(0), + fees: new BigNumber(0), + recipient: "", + useAllAmount: false, + comment: { + isEncrypted: false, + text: "", + }, +}); + +export default createTransaction; diff --git a/libs/coin-modules/coin-ton/src/js-estimateMaxSpendable.ts b/libs/coin-modules/coin-ton/src/js-estimateMaxSpendable.ts new file mode 100644 index 000000000000..4178052ec295 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-estimateMaxSpendable.ts @@ -0,0 +1,39 @@ +import { getMainAccount } from "@ledgerhq/coin-framework/account/index"; +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import { BigNumber } from "bignumber.js"; +import { fetchAccountInfo } from "./bridge/bridgeHelpers/api"; +import type { Transaction } from "./types"; +import { getAddress, getTonEstimatedFees, transactionToHwParams } from "./utils"; + +const estimateMaxSpendable = async ({ + account, + parentAccount, + transaction, +}: { + account: AccountLike; + parentAccount?: Account | null | undefined; + transaction?: Transaction | null | undefined; +}): Promise => { + const a = getMainAccount(account, parentAccount); + let balance = a.spendableBalance; + + if (balance.eq(0)) return balance; + + const accountInfo = await fetchAccountInfo(getAddress(a).address); + const estimatedFees = transaction + ? transaction.fees ?? + (await getTonEstimatedFees( + a, + accountInfo.status === "uninit", + transactionToHwParams(transaction, accountInfo.seqno), + )) + : BigNumber(0); + + if (balance.lte(estimatedFees)) return new BigNumber(0); + + balance = balance.minus(estimatedFees); + + return balance; +}; + +export default estimateMaxSpendable; diff --git a/libs/coin-modules/coin-ton/src/js-getTransactionStatus.ts b/libs/coin-modules/coin-ton/src/js-getTransactionStatus.ts new file mode 100644 index 000000000000..109a3a0f6a6b --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-getTransactionStatus.ts @@ -0,0 +1,72 @@ +import { + AmountRequired, + InvalidAddress, + InvalidAddressBecauseDestinationIsAlsoSource, + NotEnoughBalance, + RecipientRequired, +} from "@ledgerhq/errors"; +import { Account } from "@ledgerhq/types-live"; +import { BigNumber } from "bignumber.js"; +import { TonCommentInvalid } from "./errors"; +import type { Transaction, TransactionStatus } from "./types"; +import { addressesAreEqual, commentIsValid, getAddress, isAddressValid } from "./utils"; + +const getTransactionStatus = async (a: Account, t: Transaction): Promise => { + const errors: TransactionStatus["errors"] = {}; + const warnings: TransactionStatus["warnings"] = {}; + + const { balance, spendableBalance } = a; + const { address } = getAddress(a); + const { recipient, useAllAmount } = t; + let { amount } = t; + + if (!recipient) { + errors.recipient = new RecipientRequired(); + } else if (!isAddressValid(recipient)) { + errors.recipient = new InvalidAddress("", { + currencyName: a.currency.name, + }); + } else if (addressesAreEqual(address, recipient)) { + errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource(); + } + + if (!isAddressValid(address)) { + errors.sender = new InvalidAddress("", { + currencyName: a.currency.name, + }); + } + + const estimatedFees = t.fees; + + let totalSpent = BigNumber(0); + + if (useAllAmount) { + totalSpent = spendableBalance; + amount = totalSpent.minus(estimatedFees); + if (amount.lte(0) || totalSpent.gt(balance)) { + errors.amount = new NotEnoughBalance(); + } + } else { + totalSpent = amount.plus(estimatedFees); + if (totalSpent.gt(spendableBalance)) { + errors.amount = new NotEnoughBalance(); + } + if (amount.eq(0)) { + errors.amount = new AmountRequired(); + } + } + + if (t.comment.isEncrypted || !commentIsValid(t.comment)) { + errors.comment = new TonCommentInvalid(); + } + + return { + errors, + warnings, + estimatedFees, + amount, + totalSpent, + }; +}; + +export default getTransactionStatus; diff --git a/libs/coin-modules/coin-ton/src/js-prepareTransaction.ts b/libs/coin-modules/coin-ton/src/js-prepareTransaction.ts new file mode 100644 index 000000000000..1b7785fe3c1e --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-prepareTransaction.ts @@ -0,0 +1,18 @@ +import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; +import { Account } from "@ledgerhq/types-live"; +import { fetchAccountInfo } from "./bridge/bridgeHelpers/api"; +import type { Transaction } from "./types"; +import { getAddress, getTonEstimatedFees, transactionToHwParams } from "./utils"; + +const prepareTransaction = async (a: Account, t: Transaction): Promise => { + const accountInfo = await fetchAccountInfo(getAddress(a).address); + const fees = await getTonEstimatedFees( + a, + accountInfo.status === "uninit", + transactionToHwParams(t, accountInfo.seqno), + ); + const amount = t.useAllAmount ? a.spendableBalance.minus(t.fees) : t.amount; + return defaultUpdateTransaction(t, { fees, amount }); +}; + +export default prepareTransaction; diff --git a/libs/coin-modules/coin-ton/src/js-signOperation.ts b/libs/coin-modules/coin-ton/src/js-signOperation.ts new file mode 100644 index 000000000000..f3f28115d835 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/js-signOperation.ts @@ -0,0 +1,97 @@ +import { SignerContext } from "@ledgerhq/coin-framework/signer"; +import type { + Account, + DeviceId, + SignOperationEvent, + SignOperationFnSignature, +} from "@ledgerhq/types-live"; +import { Observable } from "rxjs"; +import { fetchAccountInfo } from "./bridge/bridgeHelpers/api"; +import type { TonAddress, TonSignature, TonSigner } from "./signer"; +import type { TonOperation, Transaction } from "./types"; +import { getAddress, getLedgerTonPath, packTransaction, transactionToHwParams } from "./utils"; + +/** + * Sign Transaction with Ledger hardware + */ +export const buildSignOperation = + ( + signerContext: SignerContext, + ): SignOperationFnSignature => + ({ + account, + transaction, + deviceId, + }: { + account: Account; + transaction: Transaction; + deviceId: DeviceId; + }): Observable => + new Observable(o => { + let cancelled = false; + async function main() { + const { recipient, amount, fees, comment } = transaction; + const { id: accountId } = account; + + const { address, derivationPath } = getAddress(account); + const accountInfo = await fetchAccountInfo(address); + const tonTx = transactionToHwParams(transaction, accountInfo.seqno); + const ledgerPath = getLedgerTonPath(derivationPath); + + o.next({ type: "device-signature-requested" }); + + const sig = (await signerContext(deviceId, signer => + signer.signTransaction(ledgerPath, tonTx), + )) as TonSignature; + + if (cancelled) return; + + o.next({ type: "device-signature-granted" }); + + if (!sig) { + throw new Error("No signature"); + } + + const signature = packTransaction(account, accountInfo.status === "uninit", sig); + const hash = sig.hash().toString("hex"); + + const operation: TonOperation = { + id: hash, + hash, + type: "OUT", + senders: [address], + recipients: [recipient], + accountId: accountId, + value: amount.plus(fees), + fee: fees, + blockHash: null, + blockHeight: null, + date: new Date(), + extra: { + // we don't know yet, will be patched in final operation + lt: "", + explorerHash: "", + comment: comment, + }, + }; + + o.next({ + type: "signed", + signedOperation: { + operation, + signature, + }, + }); + } + + main().then( + () => o.complete(), + e => o.error(e), + ); + + return () => { + cancelled = true; + }; + }); + +export default buildSignOperation; diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts b/libs/coin-modules/coin-ton/src/js-synchronisation.ts similarity index 73% rename from libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts rename to libs/coin-modules/coin-ton/src/js-synchronisation.ts index ef790496effa..32785f64ff28 100644 --- a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts +++ b/libs/coin-modules/coin-ton/src/js-synchronisation.ts @@ -1,13 +1,13 @@ -import { GetAccountShape, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; +import { decodeAccountId, encodeAccountId } from "@ledgerhq/coin-framework/account/index"; +import { GetAccountShape, makeSync, mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { log } from "@ledgerhq/logs"; import { Account } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import flatMap from "lodash/flatMap"; -import { decodeAccountId, encodeAccountId } from "../../../../account"; -import { TonOperation } from "../../types"; -import { fetchAccountInfo, fetchLastBlockNumber } from "./api"; -import { TonTransactionsList } from "./api.types"; -import { getTransactions, mapTxToOps } from "./txn"; +import { fetchAccountInfo, fetchLastBlockNumber } from "./bridge/bridgeHelpers/api"; +import { TonTransactionsList } from "./bridge/bridgeHelpers/api.types"; +import { getTransactions, mapTxToOps } from "./bridge/bridgeHelpers/txn"; +import { TonOperation } from "./types"; export const getAccountShape: GetAccountShape = async info => { const { address, rest, currency, derivationMode, initialAccount } = info; @@ -60,6 +60,13 @@ export const getAccountShape: GetAccountShape = async info => { }; }; +const postSync = (_initial: Account, synced: Account): Account => { + const operations = synced.operations || []; + const initialPendingOps = synced.pendingOperations || []; + const pendingOperations = initialPendingOps.filter(pOp => !operations.some(o => o.id === pOp.id)); + return { ...synced, pendingOperations }; +}; + function reconciliatePubkey(publicKey?: string, initialAccount?: Account): string { if (publicKey?.length === 64) return publicKey; if (initialAccount) { @@ -69,3 +76,5 @@ function reconciliatePubkey(publicKey?: string, initialAccount?: Account): strin } throw Error("[ton] pubkey was not properly restored"); } + +export const sync = makeSync({ getAccountShape, postSync }); diff --git a/libs/coin-modules/coin-ton/src/signer.ts b/libs/coin-modules/coin-ton/src/signer.ts new file mode 100644 index 000000000000..7b99723db8f4 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/signer.ts @@ -0,0 +1,9 @@ +import { Cell } from "@ton/ton"; +import { TonTransport } from "@ton-community/ton-ledger"; + +export type TonAddress = { + publicKey: Buffer; + address: string; +}; +export type TonSignature = Cell | undefined; +export interface TonSigner extends TonTransport {}; diff --git a/libs/ledger-live-common/src/families/ton/transaction.ts b/libs/coin-modules/coin-ton/src/transaction.ts similarity index 90% rename from libs/ledger-live-common/src/families/ton/transaction.ts rename to libs/coin-modules/coin-ton/src/transaction.ts index 69a074b9b2f7..e2c138f547ed 100644 --- a/libs/ledger-live-common/src/families/ton/transaction.ts +++ b/libs/coin-modules/coin-ton/src/transaction.ts @@ -1,3 +1,5 @@ +import { getAccountUnit } from "@ledgerhq/coin-framework/account/index"; +import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index"; import { formatTransactionStatusCommon as formatTransactionStatus, fromTransactionCommonRaw, @@ -7,8 +9,6 @@ import { } from "@ledgerhq/coin-framework/transaction/common"; import type { Account } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; -import { getAccountUnit } from "../../account"; -import { formatCurrencyUnit } from "../../currencies"; import type { Transaction, TransactionRaw } from "./types"; export const formatTransaction = ( diff --git a/libs/coin-modules/coin-ton/src/types.ts b/libs/coin-modules/coin-ton/src/types.ts new file mode 100644 index 000000000000..ecff578bec7c --- /dev/null +++ b/libs/coin-modules/coin-ton/src/types.ts @@ -0,0 +1,46 @@ +import { + Operation, + TransactionCommon, + TransactionCommonRaw, + TransactionStatusCommon, + TransactionStatusCommonRaw, +} from "@ledgerhq/types-live"; +import { TonPayloadFormat } from "@ton-community/ton-ledger"; +import { Address, SendMode, StateInit } from "@ton/core"; +import BigNumber from "bignumber.js"; + +type FamilyType = "ton"; + +// ledger app does not support encrypted comments yet +// leaving the arch for the future +export interface TonComment { + isEncrypted: boolean; + text: string; +} + +export type Transaction = TransactionCommon & { + family: FamilyType; + fees: BigNumber; + comment: TonComment; +}; +export type TransactionRaw = TransactionCommonRaw & { + family: FamilyType; + fees: string; + comment: TonComment; +}; + +export type TransactionStatus = TransactionStatusCommon; +export type TransactionStatusRaw = TransactionStatusCommonRaw; + +export type TonOperation = Operation<{ comment: TonComment; lt: string; explorerHash: string }>; + +export interface TonHwParams { + to: Address; + sendMode: SendMode; + seqno: number; + timeout: number; + bounce: boolean; + amount: bigint; + stateInit?: StateInit; + payload?: TonPayloadFormat; +} diff --git a/libs/coin-modules/coin-ton/src/utils.ts b/libs/coin-modules/coin-ton/src/utils.ts new file mode 100644 index 000000000000..b5b861026984 --- /dev/null +++ b/libs/coin-modules/coin-ton/src/utils.ts @@ -0,0 +1,122 @@ +import { decodeAccountId } from "@ledgerhq/coin-framework/account/index"; +import { Account, Address } from "@ledgerhq/types-live"; +import { + Cell, + SendMode, + Address as TonAddress, + WalletContractV4, + beginCell, + comment, + external, + internal, + storeMessage, +} from "@ton/ton"; +import BigNumber from "bignumber.js"; +import { estimateFee } from "./bridge/bridgeHelpers/api"; +import { TonComment, TonHwParams, Transaction } from "./types"; + +export const getAddress = (a: Account): Address => + a.freshAddresses.length > 0 + ? a.freshAddresses[0] + : { address: a.freshAddress, derivationPath: a.freshAddressPath }; + +export const isAddressValid = (recipient: string) => + TonAddress.isRaw(recipient) || TonAddress.isFriendly(recipient); + +export const addressesAreEqual = (addr1: string, addr2: string) => + isAddressValid(addr1) && + isAddressValid(addr2) && + TonAddress.parse(addr1).equals(TonAddress.parse(addr2)); + +export const transactionToHwParams = (t: Transaction, seqno: number): TonHwParams => { + let recipient = t.recipient; + // if recipient is not valid calculate fees with empty address + // we handle invalid addresses in account bridge + try { + TonAddress.parse(recipient); + } catch { + recipient = new TonAddress(0, Buffer.alloc(32)).toRawString(); + } + return { + to: TonAddress.parse(recipient), + seqno, + amount: t.useAllAmount ? BigInt(0) : BigInt(t.amount.toFixed()), + bounce: TonAddress.isFriendly(recipient) + ? TonAddress.parseFriendly(recipient).isBounceable + : true, + timeout: getTransferExpirationTime(), + sendMode: t.useAllAmount + ? SendMode.CARRY_ALL_REMAINING_BALANCE + : SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, + payload: t.comment.text.length ? { type: "comment", text: t.comment.text } : undefined, + }; +}; + +export const packTransaction = (a: Account, needsInit: boolean, signature: Cell): string => { + const { address } = TonAddress.parseFriendly(getAddress(a).address); + let init: { code: Cell; data: Cell } | null = null; + if (needsInit) { + if (a.xpub?.length !== 64) throw Error("[ton] xpub can't be found"); + const wallet = WalletContractV4.create({ + workchain: 0, + publicKey: Buffer.from(a.xpub, "hex"), + }); + init = wallet.init; + } + const ext = external({ to: address, init, body: signature }); + return beginCell().store(storeMessage(ext)).endCell().toBoc().toString("base64"); +}; + +// max length is 120 and only ascii allowed +export const commentIsValid = (msg: TonComment) => + !msg.isEncrypted && msg.text.length <= 120 && /^[\x20-\x7F]*$/.test(msg.text); + +// 1 minute +export const getTransferExpirationTime = () => Math.floor(Date.now() / 1000 + 60); + +export const getTonEstimatedFees = async (a: Account, needsInit: boolean, tx: TonHwParams) => { + const { xpubOrAddress: pubKey } = decodeAccountId(a.id); + if (pubKey.length !== 64) throw Error("[ton] pubKey can't be found"); + if (tx.payload && tx.payload?.type !== "comment") { + throw Error("[ton] payload kind not expected"); + } + const contract = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from(pubKey, "hex") }); + const transfer = contract.createTransfer({ + seqno: tx.seqno, + secretKey: Buffer.alloc(64), // secretKey set to 0, signature is not verified + messages: [ + internal({ + bounce: tx.bounce, + to: tx.to, + value: tx.amount, + body: tx.payload?.text ? comment(tx.payload.text) : undefined, + }), + ], + sendMode: tx.sendMode, + }); + const initCode = needsInit ? contract.init.code.toBoc().toString("base64") : undefined; + const initData = needsInit ? contract.init.data.toBoc().toString("base64") : undefined; + const fee = await estimateFee( + getAddress(a).address, + transfer.toBoc().toString("base64"), + initCode, + initData, + ); + return BigNumber(fee.fwd_fee + fee.gas_fee + fee.in_fwd_fee + fee.storage_fee); +}; + +export const getLedgerTonPath = (path: string): number[] => { + const numPath: number[] = []; + if (!path) throw Error("[ton] Path is empty"); + if (path.startsWith("m/")) path = path.slice(2); + const pathEntries = path.split("/"); + if (pathEntries.length !== 6) throw Error(`[ton] Path length is not right ${path}`); + for (const entry of pathEntries) { + if (!entry.endsWith("'")) throw Error(`[ton] Path entry is not hardened ${path}`); + const num = parseInt(entry.slice(0, entry.length - 1)); + if (!Number.isInteger(num) || num < 0 || num >= 0x80000000) + throw Error(`[ton] Path entry is not right ${path}`); + numPath.push(num); + } + return numPath; +}; diff --git a/libs/coin-modules/coin-ton/tsconfig.json b/libs/coin-modules/coin-ton/tsconfig.json new file mode 100644 index 000000000000..cdb8be8ecb97 --- /dev/null +++ b/libs/coin-modules/coin-ton/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "module": "commonjs", + "downlevelIteration": true, + "lib": ["es2020", "dom"], + "outDir": "lib" + }, + "include": ["src/**/*"] +} diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index da89964714c9..0e6a5eefdda0 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -137,6 +137,7 @@ "@ledgerhq/coin-evm": "workspace:^", "@ledgerhq/coin-framework": "workspace:^", "@ledgerhq/coin-near": "workspace:^", + "@ledgerhq/coin-ton": "workspace:^", "@ledgerhq/coin-polkadot": "workspace:^", "@ledgerhq/compressjs": "github:LedgerHQ/compressjs#d9e8e4d994923e0ea76a32b97289bcccfe71b82e", "@ledgerhq/crypto-icons-ui": "workspace:^", @@ -188,8 +189,6 @@ "@taquito/utils": "^13.0.1", "@ton-community/ton-ledger": "^7.0.1", "@ton/core": "^0.56.1", - "@ton/crypto": "^3.2.0", - "@ton/ton": "^13.11.1", "@types/bchaddrjs": "^0.4.0", "@types/bs58check": "^2.1.0", "@types/pako": "^2.0.0", diff --git a/libs/ledger-live-common/scripts/sync-families-dispatch.mjs b/libs/ledger-live-common/scripts/sync-families-dispatch.mjs index d607674e9a60..439e1e9bc165 100644 --- a/libs/ledger-live-common/scripts/sync-families-dispatch.mjs +++ b/libs/ledger-live-common/scripts/sync-families-dispatch.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env zx -import "zx/globals"; import rimraf from "rimraf"; +import "zx/globals"; const targets = [ "hw-getAddress.ts", @@ -21,7 +21,7 @@ const targets = [ ]; // Coins using coin-framework -const familiesWPackage = ["algorand", "bitcoin", "evm", "near", "polkadot"]; +const familiesWPackage = ["algorand", "bitcoin", "evm", "near", "polkadot", "ton"]; cd(path.join(__dirname, "..", "src")); await rimraf("generated"); diff --git a/libs/ledger-live-common/src/families/ton/bridge.integration.test.ts b/libs/ledger-live-common/src/families/ton/bridge.integration.test.ts index 645f7a00b086..e67d43f44de4 100644 --- a/libs/ledger-live-common/src/families/ton/bridge.integration.test.ts +++ b/libs/ledger-live-common/src/families/ton/bridge.integration.test.ts @@ -1,134 +1,5 @@ -import "../../__tests__/test-helpers/setup"; - -import { InvalidAddress, NotEnoughBalance } from "@ledgerhq/errors"; -import { CurrenciesData, DatasetTest } from "@ledgerhq/types-live"; -import BigNumber from "bignumber.js"; +import { dataset } from "@ledgerhq/coin-ton/bridge.integration.test"; import { testBridge } from "../../__tests__/test-helpers/bridge"; -import { TonCommentInvalid } from "./errors"; -import { fromTransactionRaw } from "./transaction"; -import { Transaction } from "./types"; - -const PUBKEY = "86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060"; -const ADDRESS = "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4"; -const ADDRESS_2 = "UQAui6M4jOYOezUGfmeONA22Ars9yjd34YIGdAR1Pcpp4sgR"; -const PATH = "44'/607'/0'/0'/0'/0'"; - -const ton: CurrenciesData = { - scanAccounts: [ - { - name: "ton seed 1", - apdus: ` - => e005000019068000002c8000025f80000000800000008000000080000000 - <= 86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a0609000 - => e005000019068000002c8000025f80000000800000008000000180000000 - <= b5177c2b32f9d72fa8c673cc3d61acec6a9f68eb5e4945445fdbb48a45eb48879000 - `, - test: (expect, accounts) => { - for (const account of accounts) { - expect(account.derivationMode).toEqual("ton"); - } - }, - }, - ], - accounts: [ - { - raw: { - id: `js:2:ton:${PUBKEY}:ton`, - currencyId: "ton", - seedIdentifier: PUBKEY, - name: "TON 1", - derivationMode: "ton", - index: 0, - freshAddress: ADDRESS, - freshAddressPath: PATH, - freshAddresses: [ - { - address: ADDRESS, - derivationPath: PATH, - }, - ], - xpub: PUBKEY, - blockHeight: 0, - operations: [], - pendingOperations: [], - unitMagnitude: 9, - lastSyncDate: "", - balance: "5000000000", - }, - transactions: [ - { - name: "Not a valid address", - transaction: fromTransactionRaw({ - family: "ton", - recipient: "novalidaddress", - fees: "10000000", - amount: "1000", - comment: { isEncrypted: false, text: "" }, - }), - expectedStatus: { - errors: { - recipient: new InvalidAddress(), - }, - warnings: {}, - }, - }, - { - name: "Not enough balance", - transaction: fromTransactionRaw({ - family: "ton", - recipient: ADDRESS_2, - fees: "10000000", - amount: (300 * 1e9).toString(), - comment: { isEncrypted: false, text: "" }, - }), - expectedStatus: { - errors: { - amount: new NotEnoughBalance(), - }, - warnings: {}, - }, - }, - { - name: "Invalid transferID/Memo", - transaction: fromTransactionRaw({ - family: "ton", - recipient: ADDRESS_2, - fees: "10000000", - amount: (1 * 1e9).toString(), - comment: { isEncrypted: false, text: "😀" }, - }), - expectedStatus: { - errors: { - comment: new TonCommentInvalid(), - }, - warnings: {}, - }, - }, - { - name: "New account and sufficient amount", - transaction: fromTransactionRaw({ - family: "ton", - recipient: ADDRESS_2, - fees: "10000000", - amount: "10000000", - comment: { isEncrypted: false, text: "Valid" }, - }), - expectedStatus: { - amount: new BigNumber("10000000"), - errors: {}, - warnings: {}, - }, - }, - ], - }, - ], -}; - -const dataset: DatasetTest = { - implementations: ["js"], - currencies: { - ton, - }, -}; +import "../../__tests__/test-helpers/setup"; testBridge(dataset); diff --git a/libs/ledger-live-common/src/families/ton/bridge/account.ts b/libs/ledger-live-common/src/families/ton/bridge/account.ts deleted file mode 100644 index 70c6425e2230..000000000000 --- a/libs/ledger-live-common/src/families/ton/bridge/account.ts +++ /dev/null @@ -1,258 +0,0 @@ -import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers"; -import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation"; -import { - AmountRequired, - InvalidAddress, - InvalidAddressBecauseDestinationIsAlsoSource, - NotEnoughBalance, - RecipientRequired, -} from "@ledgerhq/errors"; -import { - Account, - AccountBridge, - AccountLike, - BroadcastFnSignature, - SignOperationEvent, - SignOperationFnSignature, -} from "@ledgerhq/types-live"; -import { TonTransport } from "@ton-community/ton-ledger"; -import BigNumber from "bignumber.js"; -import { Observable } from "rxjs"; -import { getMainAccount } from "../../../account/helpers"; -import { makeAccountBridgeReceive, makeSync } from "../../../bridge/jsHelpers"; -import { withDevice } from "../../../hw/deviceAccess"; -import { TonCommentInvalid } from "../errors"; -import { TonOperation, Transaction, TransactionStatus } from "../types"; -import { - addressesAreEqual, - commentIsValid, - getAddress, - getLedgerTonPath, - getTonEstimatedFees, - isAddressValid, - packTransaction, - transactionToHwParams, -} from "../utils"; -import { getAccountShape } from "./bridgeHelpers/accountShape"; -import { broadcastTx, fetchAccountInfo } from "./bridgeHelpers/api"; - -const estimateMaxSpendable = async ({ - account, - parentAccount, - transaction, -}: { - account: AccountLike; - parentAccount?: Account | null | undefined; - transaction?: Transaction | null | undefined; -}): Promise => { - const a = getMainAccount(account, parentAccount); - let balance = a.spendableBalance; - - if (balance.eq(0)) return balance; - - const accountInfo = await fetchAccountInfo(getAddress(a).address); - const estimatedFees = transaction - ? transaction.fees ?? - (await getTonEstimatedFees( - a, - accountInfo.status === "uninit", - transactionToHwParams(transaction, accountInfo.seqno), - )) - : BigNumber(0); - - if (balance.lte(estimatedFees)) return new BigNumber(0); - - balance = balance.minus(estimatedFees); - - return balance; -}; - -const createTransaction = (): Transaction => { - return { - family: "ton", - amount: new BigNumber(0), - fees: new BigNumber(0), - recipient: "", - useAllAmount: false, - comment: { - isEncrypted: false, - text: "", - }, - }; -}; - -const getTransactionStatus = async (a: Account, t: Transaction): Promise => { - const errors: TransactionStatus["errors"] = {}; - const warnings: TransactionStatus["warnings"] = {}; - - const { balance, spendableBalance } = a; - const { address } = getAddress(a); - const { recipient, useAllAmount } = t; - let { amount } = t; - - if (!recipient) { - errors.recipient = new RecipientRequired(); - } else if (!isAddressValid(recipient)) { - errors.recipient = new InvalidAddress("", { - currencyName: a.currency.name, - }); - } else if (addressesAreEqual(address, recipient)) { - errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource(); - } - - if (!isAddressValid(address)) { - errors.sender = new InvalidAddress("", { - currencyName: a.currency.name, - }); - } - - const estimatedFees = t.fees; - - let totalSpent = BigNumber(0); - - if (useAllAmount) { - totalSpent = spendableBalance; - amount = totalSpent.minus(estimatedFees); - if (amount.lte(0) || totalSpent.gt(balance)) { - errors.amount = new NotEnoughBalance(); - } - } else { - totalSpent = amount.plus(estimatedFees); - if (totalSpent.gt(spendableBalance)) { - errors.amount = new NotEnoughBalance(); - } - if (amount.eq(0)) { - errors.amount = new AmountRequired(); - } - } - - if (t.comment.isEncrypted || !commentIsValid(t.comment)) { - errors.comment = new TonCommentInvalid(); - } - - return { - errors, - warnings, - estimatedFees, - amount, - totalSpent, - }; -}; - -const prepareTransaction = async (a: Account, t: Transaction): Promise => { - const accountInfo = await fetchAccountInfo(getAddress(a).address); - const fees = await getTonEstimatedFees( - a, - accountInfo.status === "uninit", - transactionToHwParams(t, accountInfo.seqno), - ); - - const amount = t.useAllAmount ? a.spendableBalance.minus(t.fees) : t.amount; - - return defaultUpdateTransaction(t, { fees, amount }); -}; - -const sync = makeSync({ - getAccountShape, - postSync: (_, a) => { - const operations = a.operations || []; - const initialPendingOps = a.pendingOperations || []; - const pendingOperations = initialPendingOps.filter( - pOp => !operations.some(o => o.id === pOp.id), - ); - return { ...a, pendingOperations }; - }, -}); - -const receive = makeAccountBridgeReceive(); - -const signOperation: SignOperationFnSignature = ({ - account, - deviceId, - transaction, -}): Observable => - withDevice(deviceId)( - transport => - new Observable(o => { - async function main() { - // log("debug", "[signOperation] start fn"); - - const { recipient, amount, fees, comment } = transaction; - const { address, derivationPath } = getAddress(account); - const accountInfo = await fetchAccountInfo(address); - - const app = new TonTransport(transport); - - o.next({ - type: "device-signature-requested", - }); - - // Sign by device - // it already verifies the signature inside - const sig = await app.signTransaction( - getLedgerTonPath(derivationPath), - transactionToHwParams(transaction, accountInfo.seqno), - ); - - o.next({ - type: "device-signature-granted", - }); - - const signature = packTransaction(account, accountInfo.status === "uninit", sig); - const hash = sig.hash().toString("hex"); - - const operation: TonOperation = { - // we'll patch operation when broadcasting - id: hash, - hash, - type: "OUT", - senders: [address], - recipients: [recipient], - accountId: account.id, - value: amount.plus(fees), - fee: fees, - blockHash: null, - blockHeight: null, - date: new Date(), - extra: { - // we don't know yet, will be patched in final operation - lt: "", - explorerHash: "", - comment: comment, - }, - }; - - o.next({ - type: "signed", - signedOperation: { - operation, - signature, - }, - }); - } - - main().then( - () => o.complete(), - e => o.error(e), - ); - }), - ); - -const broadcast: BroadcastFnSignature = async ({ signedOperation: { signature, operation } }) => { - const hash = await broadcastTx(signature); - return patchOperationWithHash(operation, hash); -}; - -const accountBridge: AccountBridge = { - estimateMaxSpendable, - createTransaction, - updateTransaction: defaultUpdateTransaction, - getTransactionStatus, - prepareTransaction, - sync, - receive, - signOperation, - broadcast, -}; - -export { accountBridge }; diff --git a/libs/ledger-live-common/src/families/ton/bridge/currency.ts b/libs/ledger-live-common/src/families/ton/bridge/currency.ts deleted file mode 100644 index e6ed41cf0d52..000000000000 --- a/libs/ledger-live-common/src/families/ton/bridge/currency.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { CurrencyBridge } from "@ledgerhq/types-live"; -import { makeScanAccounts } from "../../../bridge/jsHelpers"; -import { getAccountShape } from "./bridgeHelpers/accountShape"; - -const scanAccounts = makeScanAccounts({ getAccountShape }); - -export const currencyBridge: CurrencyBridge = { - preload: () => Promise.resolve({}), - hydrate: () => {}, - scanAccounts, -}; diff --git a/libs/ledger-live-common/src/families/ton/bridge/js.ts b/libs/ledger-live-common/src/families/ton/bridge/js.ts deleted file mode 100644 index a49e6b6e35cb..000000000000 --- a/libs/ledger-live-common/src/families/ton/bridge/js.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { accountBridge } from "./account"; -import { currencyBridge } from "./currency"; - -export default { - currencyBridge, - accountBridge, -}; diff --git a/libs/ledger-live-common/src/families/ton/errors.ts b/libs/ledger-live-common/src/families/ton/errors.ts deleted file mode 100644 index 76ed2665a853..000000000000 --- a/libs/ledger-live-common/src/families/ton/errors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { createCustomErrorClass } from "@ledgerhq/errors"; - -export const TonCommentInvalid = createCustomErrorClass("TonCommentInvalid"); diff --git a/libs/ledger-live-common/src/families/ton/hw-getAddress.ts b/libs/ledger-live-common/src/families/ton/hw-getAddress.ts deleted file mode 100644 index 34fb0cde4609..000000000000 --- a/libs/ledger-live-common/src/families/ton/hw-getAddress.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { log } from "@ledgerhq/logs"; -import { TonTransport } from "@ton-community/ton-ledger"; - -import type { Resolver } from "../../hw/getAddress/types"; -import { getLedgerTonPath } from "./utils"; - -const resolver: Resolver = async (transport, { path, verify }) => { - log("debug", "[ton] start getAddress"); - - const app = new TonTransport(transport); - const ledgerPath = getLedgerTonPath(path); - - const { publicKey, address } = verify - ? await app.validateAddress(ledgerPath, { bounceable: false }) - : await app.getAddress(ledgerPath, { bounceable: false }); - - if (!address || !publicKey.length) throw Error(`[ton] Response is empty ${address} ${publicKey}`); - - return { path, publicKey: publicKey.toString("hex"), address }; -}; - -export default resolver; diff --git a/libs/ledger-live-common/src/families/ton/hw-signMessage.ts b/libs/ledger-live-common/src/families/ton/hw-signMessage.ts deleted file mode 100644 index 1636f19ae825..000000000000 --- a/libs/ledger-live-common/src/families/ton/hw-signMessage.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { log } from "@ledgerhq/logs"; -import { TonTransport } from "@ton-community/ton-ledger"; - -import type { Result, SignMessage } from "../../hw/signMessage/types"; -import { getLedgerTonPath } from "./utils"; - -const signMessage: SignMessage = async (transport, account, { message }): Promise => { - log("debug", "[ton] start signMessage process"); - - const app = new TonTransport(transport); - const ledgerPath = getLedgerTonPath(account.freshAddressPath); - - if (!message) throw new Error("Message cannot be empty"); - if (typeof message !== "string") throw new Error("Message must be a string"); - - const parsedMessage = JSON.parse(message); - - const r = await app.signTransaction(ledgerPath, parsedMessage); - - return { - rsv: { - r: "", - s: "", - v: 0, - }, - signature: r.toString(), - }; -}; - -export default { signMessage }; diff --git a/libs/ledger-live-common/src/families/ton/setup.ts b/libs/ledger-live-common/src/families/ton/setup.ts new file mode 100644 index 000000000000..add85df68f38 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/setup.ts @@ -0,0 +1,28 @@ +// Goal of this file is to inject all necessary device/signer dependency to coin-modules + +import { createBridges } from "@ledgerhq/coin-ton/bridge/js"; +import makeCliTools from "@ledgerhq/coin-ton/cli-transaction"; +import nearResolver from "@ledgerhq/coin-ton/hw-getAddress"; +import { signMessage } from "@ledgerhq/coin-ton/hw-signMessage"; +import { TonSigner } from "@ledgerhq/coin-ton/lib/signer"; +import { Transaction } from "@ledgerhq/coin-ton/types"; +import Transport from "@ledgerhq/hw-transport"; +import type { Bridge } from "@ledgerhq/types-live"; +import { TonTransport } from "@ton-community/ton-ledger"; +import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup"; +import type { Resolver } from "../../hw/getAddress/types"; + +const createSigner: CreateSigner = (transport: Transport) => + new TonTransport(transport) as TonSigner; + +const bridge: Bridge = createBridges(executeWithSigner(createSigner)); + +const messageSigner = { + signMessage, +}; + +const resolver: Resolver = createResolver(createSigner, nearResolver); + +const cliTools = makeCliTools(); + +export { bridge, cliTools, messageSigner, resolver }; diff --git a/libs/ledger-live-common/src/families/ton/types.ts b/libs/ledger-live-common/src/families/ton/types.ts index ecff578bec7c..ab1d666ee1d8 100644 --- a/libs/ledger-live-common/src/families/ton/types.ts +++ b/libs/ledger-live-common/src/families/ton/types.ts @@ -1,46 +1,2 @@ -import { - Operation, - TransactionCommon, - TransactionCommonRaw, - TransactionStatusCommon, - TransactionStatusCommonRaw, -} from "@ledgerhq/types-live"; -import { TonPayloadFormat } from "@ton-community/ton-ledger"; -import { Address, SendMode, StateInit } from "@ton/core"; -import BigNumber from "bignumber.js"; - -type FamilyType = "ton"; - -// ledger app does not support encrypted comments yet -// leaving the arch for the future -export interface TonComment { - isEncrypted: boolean; - text: string; -} - -export type Transaction = TransactionCommon & { - family: FamilyType; - fees: BigNumber; - comment: TonComment; -}; -export type TransactionRaw = TransactionCommonRaw & { - family: FamilyType; - fees: string; - comment: TonComment; -}; - -export type TransactionStatus = TransactionStatusCommon; -export type TransactionStatusRaw = TransactionStatusCommonRaw; - -export type TonOperation = Operation<{ comment: TonComment; lt: string; explorerHash: string }>; - -export interface TonHwParams { - to: Address; - sendMode: SendMode; - seqno: number; - timeout: number; - bounce: boolean; - amount: bigint; - stateInit?: StateInit; - payload?: TonPayloadFormat; -} +// Encapsulate for LLD et LLM +export * from "@ledgerhq/coin-ton/types"; diff --git a/libs/ledger-live-common/src/families/ton/utils.ts b/libs/ledger-live-common/src/families/ton/utils.ts index 2d561746479e..ec0622a69563 100644 --- a/libs/ledger-live-common/src/families/ton/utils.ts +++ b/libs/ledger-live-common/src/families/ton/utils.ts @@ -1,122 +1,2 @@ -import { Account, Address } from "@ledgerhq/types-live"; -import { - Cell, - SendMode, - Address as TonAddress, - WalletContractV4, - beginCell, - comment, - external, - internal, - storeMessage, -} from "@ton/ton"; -import BigNumber from "bignumber.js"; -import { decodeAccountId } from "../../account"; -import { estimateFee } from "./bridge/bridgeHelpers/api"; -import { TonComment, TonHwParams, Transaction } from "./types"; - -export const getAddress = (a: Account): Address => - a.freshAddresses.length > 0 - ? a.freshAddresses[0] - : { address: a.freshAddress, derivationPath: a.freshAddressPath }; - -export const isAddressValid = (recipient: string) => - TonAddress.isRaw(recipient) || TonAddress.isFriendly(recipient); - -export const addressesAreEqual = (addr1: string, addr2: string) => - isAddressValid(addr1) && - isAddressValid(addr2) && - TonAddress.parse(addr1).equals(TonAddress.parse(addr2)); - -export const transactionToHwParams = (t: Transaction, seqno: number): TonHwParams => { - let recipient = t.recipient; - // if recipient is not valid calculate fees with empty address - // we handle invalid addresses in account bridge - try { - TonAddress.parse(recipient); - } catch { - recipient = new TonAddress(0, Buffer.alloc(32)).toRawString(); - } - return { - to: TonAddress.parse(recipient), - seqno, - amount: t.useAllAmount ? BigInt(0) : BigInt(t.amount.toFixed()), - bounce: TonAddress.isFriendly(recipient) - ? TonAddress.parseFriendly(recipient).isBounceable - : true, - timeout: getTransferExpirationTime(), - sendMode: t.useAllAmount - ? SendMode.CARRY_ALL_REMAINING_BALANCE - : SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY, - payload: t.comment.text.length ? { type: "comment", text: t.comment.text } : undefined, - }; -}; - -export const packTransaction = (a: Account, needsInit: boolean, signature: Cell): string => { - const { address } = TonAddress.parseFriendly(getAddress(a).address); - let init: { code: Cell; data: Cell } | null = null; - if (needsInit) { - if (a.xpub?.length !== 64) throw Error("[ton] xpub can't be found"); - const wallet = WalletContractV4.create({ - workchain: 0, - publicKey: Buffer.from(a.xpub, "hex"), - }); - init = wallet.init; - } - const ext = external({ to: address, init, body: signature }); - return beginCell().store(storeMessage(ext)).endCell().toBoc().toString("base64"); -}; - -// max length is 120 and only ascii allowed -export const commentIsValid = (msg: TonComment) => - !msg.isEncrypted && msg.text.length <= 120 && /^[\x20-\x7F]*$/.test(msg.text); - -// 1 minute -export const getTransferExpirationTime = () => Math.floor(Date.now() / 1000 + 60); - -export const getTonEstimatedFees = async (a: Account, needsInit: boolean, tx: TonHwParams) => { - const { xpubOrAddress: pubKey } = decodeAccountId(a.id); - if (pubKey.length !== 64) throw Error("[ton] pubKey can't be found"); - if (tx.payload && tx.payload?.type !== "comment") { - throw Error("[ton] payload kind not expected"); - } - const contract = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from(pubKey, "hex") }); - const transfer = contract.createTransfer({ - seqno: tx.seqno, - secretKey: Buffer.alloc(64), // secretKey set to 0, signature is not verified - messages: [ - internal({ - bounce: tx.bounce, - to: tx.to, - value: tx.amount, - body: tx.payload && tx.payload.text ? comment(tx.payload.text) : undefined, - }), - ], - sendMode: tx.sendMode, - }); - const initCode = needsInit ? contract.init.code.toBoc().toString("base64") : undefined; - const initData = needsInit ? contract.init.data.toBoc().toString("base64") : undefined; - const fee = await estimateFee( - getAddress(a).address, - transfer.toBoc().toString("base64"), - initCode, - initData, - ); - return BigNumber(fee.fwd_fee + fee.gas_fee + fee.in_fwd_fee + fee.storage_fee); -}; - -export const getLedgerTonPath = (path: string): number[] => { - const numPath: number[] = []; - if (!path) throw Error("[ton] Path is empty"); - if (path.startsWith("m/")) path = path.slice(2); - const pathEntries = path.split("/"); - if (pathEntries.length !== 6) throw Error(`[ton] Path length is not right ${path}`); - for (const entry of pathEntries) { - if (!entry.endsWith("'")) throw Error(`[ton] Path entry is not hardened ${path}`); - const num = parseInt(entry.slice(0, entry.length - 1)); - if (!Number.isInteger(num) || num < 0 || num >= 0x80000000) - throw Error(`[ton] Path entry is not right ${path}`); - numPath.push(num); - } - return numPath; -}; +// Encapsulate for LLD et LLM +export * from "@ledgerhq/coin-ton/utils"; diff --git a/libs/ledger-live-common/src/generated/bridge/js.ts b/libs/ledger-live-common/src/generated/bridge/js.ts index c6723481d8a6..03f03e52c03a 100644 --- a/libs/ledger-live-common/src/generated/bridge/js.ts +++ b/libs/ledger-live-common/src/generated/bridge/js.ts @@ -12,7 +12,6 @@ import solana from "../../families/solana/bridge/js"; import stacks from "../../families/stacks/bridge/js"; import stellar from "../../families/stellar/bridge/js"; import tezos from "../../families/tezos/bridge/js"; -import ton from "../../families/ton/bridge/js"; import tron from "../../families/tron/bridge/js"; import vechain from "../../families/vechain/bridge/js"; import { bridge as algorand } from "../../families/algorand/setup"; @@ -20,6 +19,7 @@ import { bridge as bitcoin } from "../../families/bitcoin/setup"; import { bridge as evm } from "../../families/evm/setup"; import { bridge as near } from "../../families/near/setup"; import { bridge as polkadot } from "../../families/polkadot/setup"; +import { bridge as ton } from "../../families/ton/setup"; export default { cardano, @@ -36,7 +36,6 @@ export default { stacks, stellar, tezos, - ton, tron, vechain, algorand, @@ -44,4 +43,5 @@ export default { evm, near, polkadot, + ton, }; diff --git a/libs/ledger-live-common/src/generated/cli-transaction.ts b/libs/ledger-live-common/src/generated/cli-transaction.ts index 7f688cac39db..13b3718542dd 100644 --- a/libs/ledger-live-common/src/generated/cli-transaction.ts +++ b/libs/ledger-live-common/src/generated/cli-transaction.ts @@ -17,6 +17,7 @@ import { cliTools as bitcoin } from "../families/bitcoin/setup"; import { cliTools as evm } from "../families/evm/setup"; import { cliTools as near } from "../families/near/setup"; import { cliTools as polkadot } from "../families/polkadot/setup"; +import { cliTools as ton } from "../families/ton/setup"; export default { cardano, @@ -38,4 +39,5 @@ export default { evm, near, polkadot, + ton, }; diff --git a/libs/ledger-live-common/src/generated/hw-getAddress.ts b/libs/ledger-live-common/src/generated/hw-getAddress.ts index b7b97706f160..2eb5562fae96 100644 --- a/libs/ledger-live-common/src/generated/hw-getAddress.ts +++ b/libs/ledger-live-common/src/generated/hw-getAddress.ts @@ -12,7 +12,6 @@ import solana from "../families/solana/hw-getAddress"; import stacks from "../families/stacks/hw-getAddress"; import stellar from "../families/stellar/hw-getAddress"; import tezos from "../families/tezos/hw-getAddress"; -import ton from "../families/ton/hw-getAddress"; import tron from "../families/tron/hw-getAddress"; import vechain from "../families/vechain/hw-getAddress"; import { resolver as algorand } from "../families/algorand/setup"; @@ -20,6 +19,7 @@ import { resolver as bitcoin } from "../families/bitcoin/setup"; import { resolver as evm } from "../families/evm/setup"; import { resolver as near } from "../families/near/setup"; import { resolver as polkadot } from "../families/polkadot/setup"; +import { resolver as ton } from "../families/ton/setup"; export default { cardano, @@ -36,7 +36,6 @@ export default { stacks, stellar, tezos, - ton, tron, vechain, algorand, @@ -44,4 +43,5 @@ export default { evm, near, polkadot, + ton, }; diff --git a/libs/ledger-live-common/src/generated/hw-signMessage.ts b/libs/ledger-live-common/src/generated/hw-signMessage.ts index 0cf633d399e9..13b1774984a0 100644 --- a/libs/ledger-live-common/src/generated/hw-signMessage.ts +++ b/libs/ledger-live-common/src/generated/hw-signMessage.ts @@ -2,18 +2,18 @@ import casper from "../families/casper/hw-signMessage"; import filecoin from "../families/filecoin/hw-signMessage"; import internet_computer from "../families/internet_computer/hw-signMessage"; import stacks from "../families/stacks/hw-signMessage"; -import ton from "../families/ton/hw-signMessage"; import vechain from "../families/vechain/hw-signMessage"; import { messageSigner as bitcoin } from "../families/bitcoin/setup"; import { messageSigner as evm } from "../families/evm/setup"; +import { messageSigner as ton } from "../families/ton/setup"; export default { casper, filecoin, internet_computer, stacks, - ton, vechain, bitcoin, evm, + ton, }; diff --git a/libs/ledger-live-common/src/generated/transaction.ts b/libs/ledger-live-common/src/generated/transaction.ts index e468ef54b333..aaf47525eefb 100644 --- a/libs/ledger-live-common/src/generated/transaction.ts +++ b/libs/ledger-live-common/src/generated/transaction.ts @@ -12,7 +12,6 @@ import solana from "../families/solana/transaction"; import stacks from "../families/stacks/transaction"; import stellar from "../families/stellar/transaction"; import tezos from "../families/tezos/transaction"; -import ton from "../families/ton/transaction"; import tron from "../families/tron/transaction"; import vechain from "../families/vechain/transaction"; import algorand from "@ledgerhq/coin-algorand/transaction"; @@ -20,6 +19,7 @@ import bitcoin from "@ledgerhq/coin-bitcoin/transaction"; import evm from "@ledgerhq/coin-evm/transaction"; import near from "@ledgerhq/coin-near/transaction"; import polkadot from "@ledgerhq/coin-polkadot/transaction"; +import ton from "@ledgerhq/coin-ton/transaction"; export default { cardano, @@ -36,7 +36,6 @@ export default { stacks, stellar, tezos, - ton, tron, vechain, algorand, @@ -44,4 +43,5 @@ export default { evm, near, polkadot, + ton, }; diff --git a/libs/ledger-live-common/src/generated/types.ts b/libs/ledger-live-common/src/generated/types.ts index 89717ada84d1..d78ee0e8b014 100644 --- a/libs/ledger-live-common/src/generated/types.ts +++ b/libs/ledger-live-common/src/generated/types.ts @@ -74,10 +74,10 @@ import { Transaction as tezosTransaction } from "../families/tezos/types"; import { TransactionRaw as tezosTransactionRaw } from "../families/tezos/types"; import { TransactionStatus as tezosTransactionStatus } from "../families/tezos/types"; import { TransactionStatusRaw as tezosTransactionStatusRaw } from "../families/tezos/types"; -import { Transaction as tonTransaction } from "../families/ton/types"; -import { TransactionRaw as tonTransactionRaw } from "../families/ton/types"; -import { TransactionStatus as tonTransactionStatus } from "../families/ton/types"; -import { TransactionStatusRaw as tonTransactionStatusRaw } from "../families/ton/types"; +import { Transaction as tonTransaction } from "@ledgerhq/coin-ton/types"; +import { TransactionRaw as tonTransactionRaw } from "@ledgerhq/coin-ton/types"; +import { TransactionStatus as tonTransactionStatus } from "@ledgerhq/coin-ton/types"; +import { TransactionStatusRaw as tonTransactionStatusRaw } from "@ledgerhq/coin-ton/types"; import { Transaction as tronTransaction } from "../families/tron/types"; import { TransactionRaw as tronTransactionRaw } from "../families/tron/types"; import { TransactionStatus as tronTransactionStatus } from "../families/tron/types"; diff --git a/libs/ledgerjs/packages/types-live/src/derivation.ts b/libs/ledgerjs/packages/types-live/src/derivation.ts index bfdef23da0d0..98e3848a24e9 100644 --- a/libs/ledgerjs/packages/types-live/src/derivation.ts +++ b/libs/ledgerjs/packages/types-live/src/derivation.ts @@ -36,4 +36,5 @@ export type DerivationMode = | "nearbip44h" | "vechain" | "internet_computer" - | "stacks_wallet"; + | "stacks_wallet" + | "ton"; diff --git a/package.json b/package.json index 0a1a2931b1dc..a5f9c54d2037 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "coin:framework": "pnpm --filter coin-framework", "coin:near": "pnpm --filter coin-near", "coin:polkadot": "pnpm --filter coin-polkadot", + "coin:ton": "pnpm --filter coin-ton", "evm-tools": "pnpm --filter evm-tools", "domain": "pnpm --filter domain-service", "doc": "pnpm --filter docs", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97219ec9ae65..4158f7e178ba 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2085,6 +2085,79 @@ importers: specifier: ^29.1.1 version: 29.1.1(jest@29.7.0)(typescript@5.1.3) + libs/coin-modules/coin-ton: + dependencies: + '@ledgerhq/coin-framework': + specifier: workspace:^ + version: link:../../coin-framework + '@ledgerhq/devices': + specifier: workspace:* + version: link:../../ledgerjs/packages/devices + '@ledgerhq/errors': + specifier: workspace:^ + version: link:../../ledgerjs/packages/errors + '@ledgerhq/hw-transport': + specifier: workspace:^ + version: link:../../ledgerjs/packages/hw-transport + '@ledgerhq/live-env': + specifier: workspace:^ + version: link:../../env + '@ledgerhq/live-network': + specifier: workspace:^ + version: link:../../live-network + '@ledgerhq/logs': + specifier: workspace:^ + version: link:../../ledgerjs/packages/logs + '@ledgerhq/types-cryptoassets': + specifier: workspace:^ + version: link:../../ledgerjs/packages/types-cryptoassets + '@ledgerhq/types-live': + specifier: workspace:^ + version: link:../../ledgerjs/packages/types-live + '@ton-community/ton-ledger': + specifier: ^7.0.1 + version: 7.0.1(@ton/core@0.56.3) + '@ton/core': + specifier: ^0.56.1 + version: 0.56.3(@ton/crypto@3.2.0) + '@ton/crypto': + specifier: ^3.2.0 + version: 3.2.0 + '@ton/ton': + specifier: ^13.11.1 + version: 13.11.1(@ton/core@0.56.3)(@ton/crypto@3.2.0) + bignumber.js: + specifier: ^9.1.2 + version: 9.1.2 + expect: + specifier: ^27.4.6 + version: 27.5.1 + invariant: + specifier: ^2.2.2 + version: 2.2.4 + lodash: + specifier: ^4.17.21 + version: 4.17.21 + rxjs: + specifier: ^7.8.1 + version: 7.8.1 + devDependencies: + '@types/invariant': + specifier: ^2.2.2 + version: 2.2.36 + '@types/jest': + specifier: ^29.5.10 + version: 29.5.12 + '@types/lodash': + specifier: ^4.14.191 + version: 4.14.202 + jest: + specifier: ^29.7.0 + version: 29.7.0 + ts-jest: + specifier: ^29.1.1 + version: 29.1.2(jest@29.7.0)(typescript@5.1.3) + libs/device-core: dependencies: '@ledgerhq/devices': @@ -2403,6 +2476,9 @@ importers: '@ledgerhq/coin-polkadot': specifier: workspace:^ version: link:../coin-modules/coin-polkadot + '@ledgerhq/coin-ton': + specifier: workspace:^ + version: link:../coin-modules/coin-ton '@ledgerhq/compressjs': specifier: github:LedgerHQ/compressjs#d9e8e4d994923e0ea76a32b97289bcccfe71b82e version: github.com/LedgerHQ/compressjs/d9e8e4d994923e0ea76a32b97289bcccfe71b82e @@ -2552,13 +2628,7 @@ importers: version: 7.0.1(@ton/core@0.56.3) '@ton/core': specifier: ^0.56.1 - version: 0.56.3(@ton/crypto@3.2.0) - '@ton/crypto': - specifier: ^3.2.0 - version: 3.2.0 - '@ton/ton': - specifier: ^13.11.1 - version: 13.11.1(@ton/core@0.56.3)(@ton/crypto@3.2.0) + version: 0.56.3 '@types/bchaddrjs': specifier: ^0.4.0 version: 0.4.0 @@ -11685,7 +11755,7 @@ packages: outdent: 0.5.0 prettier: 2.8.8 resolve-from: 5.0.0 - semver: 5.7.1 + semver: 5.7.2 dev: true /@changesets/assemble-release-plan@5.2.3: @@ -11696,7 +11766,7 @@ packages: '@changesets/get-dependents-graph': 1.3.5 '@changesets/types': 5.2.1 '@manypkg/get-packages': 1.1.3 - semver: 5.7.1 + semver: 5.7.2 dev: true /@changesets/changelog-git@0.1.14: @@ -11779,7 +11849,7 @@ packages: '@manypkg/get-packages': 1.1.3 chalk: 2.4.2 fs-extra: 7.0.1 - semver: 5.7.1 + semver: 5.7.2 dev: true /@changesets/get-github-info@0.6.0: @@ -14890,7 +14960,7 @@ packages: getenv: 1.0.0 glob: 7.1.6 resolve-from: 5.0.0 - semver: 7.3.8 + semver: 7.6.0 slash: 3.0.0 xcode: 3.0.1 xml2js: 0.4.23 @@ -17844,7 +17914,7 @@ packages: resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} dependencies: '@gar/promisify': 1.1.3 - semver: 7.3.7 + semver: 7.6.0 /@npmcli/move-file@1.1.2: resolution: {integrity: sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==} @@ -24730,6 +24800,14 @@ packages: teslabot: 1.5.0 dev: false + /@ton/core@0.56.3: + resolution: {integrity: sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==} + peerDependencies: + '@ton/crypto': '>=3.2.0' + dependencies: + symbol.inspect: 1.0.1 + dev: false + /@ton/core@0.56.3(@ton/crypto@3.2.0): resolution: {integrity: sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==} peerDependencies: @@ -24979,7 +25057,7 @@ packages: /@types/connect@3.4.35: resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} dependencies: - '@types/node': 20.8.10 + '@types/node': 20.11.30 /@types/cookie@0.4.1: resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} @@ -25193,7 +25271,7 @@ packages: /@types/graceful-fs@4.1.8: resolution: {integrity: sha512-NhRH7YzWq8WiNKVavKPBmtLYZHxNY19Hh+az28O/phfp68CF45pMFud+ZzJ8ewnxnC5smIdF3dqFeiSUQ5I+pw==} dependencies: - '@types/node': 20.8.10 + '@types/node': 20.11.30 /@types/hammerjs@2.0.41: resolution: {integrity: sha512-ewXv/ceBaJprikMcxCmWU1FKyMAQ2X7a9Gtmzw8fcg2kIePI1crERDM818W+XYrxqdBBOdlf2rm137bU+BltCA==} @@ -26172,7 +26250,7 @@ packages: graphemer: 1.4.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 - semver: 7.5.4 + semver: 7.6.0 tsutils: 3.21.0(typescript@5.1.3) typescript: 5.1.3 transitivePeerDependencies: @@ -26475,7 +26553,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.7 + semver: 7.6.0 tsutils: 3.21.0(typescript@5.1.3) typescript: 5.1.3 transitivePeerDependencies: @@ -26496,7 +26574,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.3.7 + semver: 7.6.0 tsutils: 3.21.0(typescript@5.2.2) typescript: 5.2.2 transitivePeerDependencies: @@ -26699,7 +26777,7 @@ packages: '@typescript-eslint/types': 6.14.0 '@typescript-eslint/typescript-estree': 6.14.0(typescript@5.3.3) eslint: 8.55.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -26718,7 +26796,7 @@ packages: '@typescript-eslint/types': 6.2.0 '@typescript-eslint/typescript-estree': 6.2.0(typescript@5.1.3) eslint: 8.51.0 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - supports-color - typescript @@ -26925,8 +27003,8 @@ packages: '@vue/component-compiler-utils': 3.3.0(lodash@4.17.21)(react-dom@18.2.0)(react@18.2.0) '@vue/vue-loader-v15': /vue-loader@15.11.1(css-loader@6.7.1)(lodash@4.17.21)(prettier@3.0.3)(react-dom@18.2.0)(react@18.2.0)(vue-template-compiler@2.7.14)(webpack@5.88.2) '@vue/web-component-wrapper': 1.3.0 - acorn: 8.11.2 - acorn-walk: 8.2.0 + acorn: 8.11.3 + acorn-walk: 8.3.2 address: 1.2.0 autoprefixer: 10.4.8(postcss@8.4.32) browserslist: 4.22.1 @@ -27054,7 +27132,7 @@ packages: open: 8.4.2 ora: 5.4.1 read-pkg: 5.2.0 - semver: 7.3.7 + semver: 7.6.0 strip-ansi: 6.0.1 transitivePeerDependencies: - encoding @@ -27675,8 +27753,8 @@ packages: /acorn-globals@7.0.1: resolution: {integrity: sha512-umOSDSDrfHbTNPuNpC2NSnnA3LUrqpevPb4T9jRx4MagXNS0rs+gwiTcAvqCRmsD6utzsrzNt+ebm00SNWiC3Q==} dependencies: - acorn: 8.11.2 - acorn-walk: 8.2.0 + acorn: 8.11.3 + acorn-walk: 8.3.2 dev: true /acorn-import-assertions@1.8.0(acorn@8.7.1): @@ -27695,12 +27773,12 @@ packages: acorn: 8.8.2 dev: true - /acorn-import-assertions@1.9.0(acorn@8.11.2): + /acorn-import-assertions@1.9.0(acorn@8.11.3): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} peerDependencies: acorn: ^8 dependencies: - acorn: 8.11.2 + acorn: 8.11.3 /acorn-import-assertions@1.9.0(acorn@8.8.2): resolution: {integrity: sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==} @@ -27717,12 +27795,12 @@ packages: acorn: 7.4.1 dev: true - /acorn-jsx@5.3.2(acorn@8.11.2): + /acorn-jsx@5.3.2(acorn@8.11.3): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.11.2 + acorn: 8.11.3 /acorn-node@1.8.2: resolution: {integrity: sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==} @@ -27743,7 +27821,6 @@ packages: /acorn-walk@8.3.2: resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==} engines: {node: '>=0.4.0'} - dev: true /acorn@7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} @@ -27759,6 +27836,7 @@ packages: resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} hasBin: true + dev: true /acorn@8.11.3: resolution: {integrity: sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==} @@ -32455,7 +32533,7 @@ packages: postcss-modules-scope: 3.0.0(postcss@8.4.32) postcss-modules-values: 4.0.0(postcss@8.4.32) postcss-value-parser: 4.2.0 - semver: 7.3.8 + semver: 7.6.0 webpack: 5.88.2(metro@0.80.0) /css-loader@6.8.1(webpack@5.89.0): @@ -32471,7 +32549,7 @@ packages: postcss-modules-scope: 3.0.0(postcss@8.4.32) postcss-modules-values: 4.0.0(postcss@8.4.32) postcss-value-parser: 4.2.0 - semver: 7.5.4 + semver: 7.6.0 webpack: 5.89.0 /css-minimizer-webpack-plugin@3.4.1(metro@0.80.0)(webpack@5.88.2): @@ -35892,8 +35970,8 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) eslint-visitor-keys: 3.4.3 /esprima@1.2.2: @@ -37774,7 +37852,7 @@ packages: memfs: 3.5.3 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.3.7 + semver: 7.6.0 tapable: 1.1.3 typescript: 5.1.3 vue-template-compiler: 2.7.14 @@ -37807,7 +37885,7 @@ packages: memfs: 3.5.3 minimatch: 3.1.2 schema-utils: 2.7.0 - semver: 7.5.4 + semver: 7.6.0 tapable: 1.1.3 typescript: 5.1.3 webpack: 5.89.0 @@ -38445,7 +38523,7 @@ packages: fs.realpath: 1.0.0 minimatch: 8.0.4 minipass: 4.2.8 - path-scurry: 1.7.0 + path-scurry: 1.10.1 /global-agent@3.0.0: resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} @@ -38456,7 +38534,7 @@ packages: es6-error: 4.1.1 matcher: 3.0.0 roarr: 2.15.4 - semver: 7.3.7 + semver: 7.6.0 serialize-error: 7.0.1 dev: true optional: true @@ -41861,7 +41939,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.8 - '@types/node': 20.8.10 + '@types/node': 20.11.30 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -41882,7 +41960,7 @@ packages: dependencies: '@jest/types': 29.6.3 '@types/graceful-fs': 4.1.8 - '@types/node': 20.8.10 + '@types/node': 20.11.30 anymatch: 3.1.3 fb-watchman: 2.0.2 graceful-fs: 4.2.11 @@ -42249,7 +42327,7 @@ packages: '@jest/test-result': 29.7.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.8.10 + '@types/node': 20.11.30 chalk: 4.1.2 emittery: 0.13.1 graceful-fs: 4.2.11 @@ -42396,7 +42474,7 @@ packages: jest-util: 27.5.1 natural-compare: 1.4.0 pretty-format: 27.5.1 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - metro - supports-color @@ -42428,7 +42506,7 @@ packages: jest-util: 28.1.3 natural-compare: 1.4.0 pretty-format: 28.1.3 - semver: 7.5.4 + semver: 7.6.0 transitivePeerDependencies: - metro - supports-color @@ -42579,7 +42657,7 @@ packages: dependencies: '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 - '@types/node': 20.8.10 + '@types/node': 20.11.30 ansi-escapes: 4.3.2 chalk: 4.1.2 emittery: 0.13.1 @@ -43209,7 +43287,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.11.2 + acorn: 8.11.3 acorn-globals: 6.0.0 cssom: 0.5.0 cssstyle: 2.3.0 @@ -43251,7 +43329,7 @@ packages: optional: true dependencies: abab: 2.0.6 - acorn: 8.11.2 + acorn: 8.11.3 acorn-globals: 6.0.0 cssom: 0.5.0 cssstyle: 2.3.0 @@ -44608,10 +44686,6 @@ packages: engines: {node: '>=12'} dev: false - /lru-cache@9.1.2: - resolution: {integrity: sha512-ERJq3FOzJTxBbFjZ7iDs+NiK4VI9Wz+RdrrAB8dio1oV+YvdPzUEE4QNiT2VD51DkIbCYRUUzCRkssXCHqSnKQ==} - engines: {node: 14 || >=16.14} - /lru_map@0.3.3: resolution: {integrity: sha512-Pn9cox5CsMYngeDbmChANltQl+5pi6XmTrraMSzhPmMBbmgcxmqWry0U3PGapCU1yB4/LqCcom7qhHZiF/jGfQ==} dev: false @@ -46201,8 +46275,8 @@ packages: /micromark-extension-mdxjs@1.0.1: resolution: {integrity: sha512-7YA7hF6i5eKOfFUzZ+0z6avRG52GpWR8DL+kN47y3f2KhxbBZMhmxe7auOeaTBrW2DenbbZTf1ea9tA2hDpC2Q==} dependencies: - acorn: 8.11.2 - acorn-jsx: 5.3.2(acorn@8.11.2) + acorn: 8.11.3 + acorn-jsx: 5.3.2(acorn@8.11.3) micromark-extension-mdx-expression: 1.0.8 micromark-extension-mdx-jsx: 1.0.5 micromark-extension-mdx-md: 1.0.1 @@ -47520,7 +47594,7 @@ packages: dependencies: hosted-git-info: 2.8.9 resolve: 1.22.8 - semver: 5.7.1 + semver: 5.7.2 validate-npm-package-license: 3.0.4 /normalize-package-data@3.0.3: @@ -48369,13 +48443,6 @@ packages: lru-cache: 10.2.0 minipass: 7.0.4 - /path-scurry@1.7.0: - resolution: {integrity: sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==} - engines: {node: '>=16 || 14 >=14.17'} - dependencies: - lru-cache: 9.1.2 - minipass: 5.0.0 - /path-to-regexp@0.1.7: resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==} @@ -49255,7 +49322,7 @@ packages: postcss-flexbugs-fixes: 5.0.2(postcss@8.4.31) postcss-normalize: 10.0.1(browserslist@4.22.1)(postcss@8.4.31) postcss-preset-env: 7.8.3(postcss@8.4.31) - semver: 7.5.4 + semver: 7.6.0 webpack: 5.89.0 transitivePeerDependencies: - browserslist @@ -49274,7 +49341,7 @@ packages: postcss-flexbugs-fixes: 5.0.2(postcss@8.4.32) postcss-normalize: 10.0.1(browserslist@4.22.1)(postcss@8.4.32) postcss-preset-env: 7.8.3(postcss@8.4.32) - semver: 7.5.4 + semver: 7.6.0 webpack: 5.88.2 transitivePeerDependencies: - browserslist @@ -53619,6 +53686,7 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 + dev: true /semver@7.5.3: resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} @@ -55956,7 +56024,7 @@ packages: hasBin: true dependencies: '@jridgewell/source-map': 0.3.5 - acorn: 8.11.2 + acorn: 8.11.3 commander: 2.20.3 source-map-support: 0.5.21 @@ -56672,7 +56740,7 @@ packages: chalk: 4.1.2 enhanced-resolve: 5.15.0 micromatch: 4.0.5 - semver: 7.3.7 + semver: 7.6.0 typescript: 5.1.3 webpack: 5.88.2 dev: false @@ -57725,7 +57793,7 @@ packages: /unplugin@1.5.1: resolution: {integrity: sha512-0QkvG13z6RD+1L1FoibQqnvTwVBXvS4XSPwAyinVgoOCl2jAgwzdUKmEj05o4Lt8xwQI85Hb6mSyYkcAGwZPew==} dependencies: - acorn: 8.11.2 + acorn: 8.11.3 chokidar: 3.6.0 webpack-sources: 3.2.3 webpack-virtual-modules: 0.6.1 @@ -59592,8 +59660,8 @@ packages: hasBin: true dependencies: '@discoveryjs/json-ext': 0.5.7 - acorn: 8.11.2 - acorn-walk: 8.2.0 + acorn: 8.11.3 + acorn-walk: 8.3.2 commander: 7.2.0 escape-string-regexp: 4.0.0 gzip-size: 6.0.0 @@ -60198,8 +60266,8 @@ packages: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 @@ -60238,8 +60306,8 @@ packages: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 @@ -60279,8 +60347,8 @@ packages: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 @@ -60320,8 +60388,8 @@ packages: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 @@ -60361,8 +60429,8 @@ packages: '@webassemblyjs/ast': 1.11.6 '@webassemblyjs/wasm-edit': 1.11.6 '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.2 - acorn-import-assertions: 1.9.0(acorn@8.11.2) + acorn: 8.11.3 + acorn-import-assertions: 1.9.0(acorn@8.11.3) browserslist: 4.22.1 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0