From 268c422ea564444c5dc8165136818105ce2d7ca7 Mon Sep 17 00:00:00 2001 From: Alexandrine Boissiere <108733454+aboissiere-ledger@users.noreply.github.com> Date: Thu, 4 Jul 2024 17:45:03 +0200 Subject: [PATCH 01/60] chore: update codeowners (#7261) --- CODEOWNERS | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 5d8dd0a75e5a..888e565119b7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -96,29 +96,28 @@ libs/ledger-live-common/src/wallet-api/ @ledgerhq/wallet-api libs/test-utils/ @ledgerhq/wallet-api # Devices team -apps/cli/src/commands/devices @ledgerhq/live-devices -**/src/renderer/screens/manager/ @ledgerhq/live-devices -**/screens/CustomImage @ledgerhq/live-devices -**/components/CustomImage @ledgerhq/live-devices -**/SyncOnboarding/** @ledgerhq/live-devices -apps/**/components/DeviceAction/ @ledgerhq/live-devices -apps/ledger-live-mobile/src/screens/MyLedger*/ @ledgerhq/live-devices -apps/ledger-live-mobile/src/newArch/features/FirmwareUpdate/ @ledgerhq/live-devices -apps/web-tools/repl/ @ledgerhq/live-devices -libs/**/devices/ @ledgerhq/live-devices -libs/**/hw-transport-*/ @ledgerhq/live-devices -libs/**/react-native-*/ @ledgerhq/live-devices -libs/**/swift-*/ @ledgerhq/live-devices -libs/**/types-devices/ @ledgerhq/live-devices -libs/ledger-live-common/src/apps/ @ledgerhq/live-devices -libs/ledger-live-common/src/hw/ @ledgerhq/live-devices -libs/ledger-live-common/src/manager/ @ledgerhq/live-devices -libs/ledger-live-common/src/device/ @ledgerhq/live-devices -libs/ledger-live-common/src/device-react/ @ledgerhq/live-devices -libs/ledger-live-common/src/device-core/ @ledgerhq/live-devices -libs/device-core/ @ledgerhq/live-devices -libs/device-react/ @ledgerhq/live-devices -libs/speculos-transport/ @ledgerhq/live-devices +apps/cli/src/commands/devices @ledgerhq/live-devices +**/src/renderer/screens/manager/ @ledgerhq/live-devices +**/screens/CustomImage @ledgerhq/live-devices +**/components/CustomImage @ledgerhq/live-devices +**/SyncOnboarding/** @ledgerhq/live-devices +apps/**/components/DeviceAction/ @ledgerhq/live-devices +apps/ledger-live-mobile/src/screens/MyLedger*/ @ledgerhq/live-devices +apps/ledger-live-mobile/src/newArch/features/FirmwareUpdate/ @ledgerhq/live-devices +apps/web-tools/repl/ @ledgerhq/live-devices +libs/**/devices/ @ledgerhq/live-devices +libs/**/hw-transport-*/ @ledgerhq/live-devices +libs/**/react-native-*/ @ledgerhq/live-devices +libs/**/swift-*/ @ledgerhq/live-devices +libs/**/types-devices/ @ledgerhq/live-devices +libs/ledger-live-common/src/apps/ @ledgerhq/live-devices +libs/ledger-live-common/src/hw/ @ledgerhq/live-devices +libs/ledger-live-common/src/manager/ @ledgerhq/live-devices +libs/ledger-live-common/src/device/ @ledgerhq/live-devices +libs/ledger-live-common/src/deviceSDK/ @ledgerhq/live-devices +libs/device-core/ @ledgerhq/live-devices +libs/device-react/ @ledgerhq/live-devices +libs/speculos-transport/ @ledgerhq/live-devices # Recover team apps/ledger-live-desktop/src/renderer/components/RecoverBanner/ @ledgerhq/recover-software From d2fa8bee224a6626a19d217fc91dd11d19e15ee8 Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Tue, 28 Nov 2023 11:36:59 +0100 Subject: [PATCH 02/60] feat(ton): add initial definitions --- apps/cli/src/live-common-setup-base.ts | 1 + libs/coin-framework/src/derivation.ts | 6 +++++ .../src/families/ton/types.ts | 14 +++++++++++ .../src/featureFlags/defaultFeatures.ts | 1 + .../ledger-live-common/src/generated/types.ts | 4 ++++ .../packages/cryptoassets/src/currencies.ts | 24 +++++++++++++++++++ .../packages/types-cryptoassets/src/index.ts | 1 + .../packages/types-cryptoassets/src/slip44.ts | 1 + .../packages/types-live/src/feature.ts | 1 + libs/ui/packages/crypto-icons/src/svg/TON.svg | 4 ++++ 10 files changed, 57 insertions(+) create mode 100644 libs/ledger-live-common/src/families/ton/types.ts create mode 100644 libs/ui/packages/crypto-icons/src/svg/TON.svg diff --git a/apps/cli/src/live-common-setup-base.ts b/apps/cli/src/live-common-setup-base.ts index 1f19f46ac74a..367b81754d9b 100644 --- a/apps/cli/src/live-common-setup-base.ts +++ b/apps/cli/src/live-common-setup-base.ts @@ -91,6 +91,7 @@ setSupportedCurrencies([ "lukso", "filecoin", "linea", + "ton", "linea_sepolia", "blast", "blast_sepolia", diff --git a/libs/coin-framework/src/derivation.ts b/libs/coin-framework/src/derivation.ts index c7a360ba0354..a0a10df26cde 100644 --- a/libs/coin-framework/src/derivation.ts +++ b/libs/coin-framework/src/derivation.ts @@ -173,6 +173,9 @@ const modes: Readonly>> = Object.freeze( startsAt: 1, tag: "third-party", }, + ton: { + overridesDerivation: "44'/607'/0'/0'/'/0'", + }, }); modes as Record; // eslint-disable-line @@ -191,6 +194,7 @@ const legacyDerivations: Partial> near: ["nearbip44h"], vechain: ["vechain"], stacks: ["stacks_wallet"], + ton: ["ton"], ethereum: ["ethM", "ethMM"], ethereum_classic: ["ethM", "ethMM", "etcM"], solana: ["solanaMain", "solanaSub"], @@ -322,6 +326,7 @@ const disableBIP44: Record = { internet_computer: true, casper: true, filecoin: true, + ton: true, }; type SeedInfo = { purpose: number; @@ -340,6 +345,7 @@ const seedIdentifierPath: Record = { internet_computer: ({ purpose, coinType }) => `${purpose}'/${coinType}'/0'/0/0`, near: ({ purpose, coinType }) => `${purpose}'/${coinType}'/0'/0'/0'`, vechain: ({ purpose, coinType }) => `${purpose}'/${coinType}'/0'/0/0`, + ton: ({ purpose, coinType }) => `${purpose}'/${coinType}'/0'/0'/0'/0'`, _: ({ purpose, coinType }) => `${purpose}'/${coinType}'/0'`, }; export const getSeedIdentifierDerivation = ( diff --git a/libs/ledger-live-common/src/families/ton/types.ts b/libs/ledger-live-common/src/families/ton/types.ts new file mode 100644 index 000000000000..cf8f98be0b40 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/types.ts @@ -0,0 +1,14 @@ +import { + TransactionCommon, + TransactionCommonRaw, + TransactionStatusCommon, + TransactionStatusCommonRaw, +} from "@ledgerhq/types-live"; + +type FamilyType = "ton"; + +export type Transaction = TransactionCommon & { family: FamilyType }; +export type TransactionRaw = TransactionCommonRaw & { family: FamilyType }; +export type TransactionStatus = TransactionStatusCommon; + +export type TransactionStatusRaw = TransactionStatusCommonRaw; diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts index 0de760de3634..4172bdcb500c 100644 --- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts +++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts @@ -76,6 +76,7 @@ export const CURRENCY_DEFAULT_FEATURES = { currencyBlastSepolia: DEFAULT_FEATURE, currencyScroll: DEFAULT_FEATURE, currencyScrollSepolia: DEFAULT_FEATURE, + currencyTon: DEFAULT_FEATURE, }; /** diff --git a/libs/ledger-live-common/src/generated/types.ts b/libs/ledger-live-common/src/generated/types.ts index 3128310c07fd..f1e01e78fdf0 100644 --- a/libs/ledger-live-common/src/generated/types.ts +++ b/libs/ledger-live-common/src/generated/types.ts @@ -144,6 +144,7 @@ export type Transaction = | stacksTransaction | stellarTransaction | tezosTransaction + | tonTransaction | tronTransaction | vechainTransaction | xrpTransaction; @@ -167,6 +168,7 @@ export type TransactionRaw = | stacksTransactionRaw | stellarTransactionRaw | tezosTransactionRaw + | tonTransactionRaw | tronTransactionRaw | vechainTransactionRaw | xrpTransactionRaw; @@ -190,6 +192,7 @@ export type TransactionStatus = | stacksTransactionStatus | stellarTransactionStatus | tezosTransactionStatus + | tonTransactionStatus | tronTransactionStatus | vechainTransactionStatus | xrpTransactionStatus; @@ -213,6 +216,7 @@ export type TransactionStatusRaw = | stacksTransactionStatusRaw | stellarTransactionStatusRaw | tezosTransactionStatusRaw + | tonTransactionStatusRaw | tronTransactionStatusRaw | vechainTransactionStatusRaw | xrpTransactionStatusRaw; diff --git a/libs/ledgerjs/packages/cryptoassets/src/currencies.ts b/libs/ledgerjs/packages/cryptoassets/src/currencies.ts index a0701399efb8..772660918f0c 100644 --- a/libs/ledgerjs/packages/cryptoassets/src/currencies.ts +++ b/libs/ledgerjs/packages/cryptoassets/src/currencies.ts @@ -2802,6 +2802,30 @@ export const cryptocurrenciesById: Record = { }, ], }, + ton: { + type: "CryptoCurrency", + id: "ton", + coinType: CoinType.TON, + name: "TON", + managerAppName: "TON", + ticker: "TON", + scheme: "ton", + color: "#0098ea", + family: "ton", + units: [ + { + name: "TON", + code: "TON", + magnitude: 9, + }, + ], + explorerViews: [ + { + tx: "https://testnet.tonscan.org/tx/$hash", // TODO: TON switch to mainnet + address: "https://testnet.tonscan.org/address/$address", + }, + ], + }, tron: { type: "CryptoCurrency", id: "tron", diff --git a/libs/ledgerjs/packages/types-cryptoassets/src/index.ts b/libs/ledgerjs/packages/types-cryptoassets/src/index.ts index ddaa06ccc324..98bd2ead5f45 100644 --- a/libs/ledgerjs/packages/types-cryptoassets/src/index.ts +++ b/libs/ledgerjs/packages/types-cryptoassets/src/index.ts @@ -106,6 +106,7 @@ export type CryptoCurrencyId = | "tezos" | "thundercore" | "tomo" + | "ton" | "tron" | "ubiq" | "umee" diff --git a/libs/ledgerjs/packages/types-cryptoassets/src/slip44.ts b/libs/ledgerjs/packages/types-cryptoassets/src/slip44.ts index d7c55718c6f8..ef4fd5f8f3aa 100644 --- a/libs/ledgerjs/packages/types-cryptoassets/src/slip44.ts +++ b/libs/ledgerjs/packages/types-cryptoassets/src/slip44.ts @@ -88,6 +88,7 @@ export enum CoinType { TEZOS = 1729, THUNDERCORE = 1001, TOMO = 889, + TON = 607, TRON = 195, UBIQ = 108, VECHAIN = 818, diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts index d04c4669f1f6..46298a284cb6 100644 --- a/libs/ledgerjs/packages/types-live/src/feature.ts +++ b/libs/ledgerjs/packages/types-live/src/feature.ts @@ -117,6 +117,7 @@ export type CurrencyFeatures = { currencyBlastSepolia: DefaultFeature; currencyScroll: DefaultFeature; currencyScrollSepolia: DefaultFeature; + currencyTon: DefaultFeature; }; /** diff --git a/libs/ui/packages/crypto-icons/src/svg/TON.svg b/libs/ui/packages/crypto-icons/src/svg/TON.svg new file mode 100644 index 000000000000..9ba795799e62 --- /dev/null +++ b/libs/ui/packages/crypto-icons/src/svg/TON.svg @@ -0,0 +1,4 @@ + + + + From fa2a91e6130b9138b7f2195e237ee1192fc30886 Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Fri, 1 Dec 2023 12:04:48 +0100 Subject: [PATCH 03/60] feat(ton): add hw-getAddress and hw-signMessage --- libs/ledger-live-common/package.json | 3 ++ .../src/families/ton/hw-getAddress.ts | 22 ++++++++++++++ .../src/families/ton/hw-signMessage.ts | 30 +++++++++++++++++++ .../src/families/ton/utils.ts | 15 ++++++++++ .../src/generated/hw-getAddress.ts | 2 ++ .../src/generated/hw-signMessage.ts | 2 ++ 6 files changed, 74 insertions(+) create mode 100644 libs/ledger-live-common/src/families/ton/hw-getAddress.ts create mode 100644 libs/ledger-live-common/src/families/ton/hw-signMessage.ts create mode 100644 libs/ledger-live-common/src/families/ton/utils.ts diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index a49ba3b1a7d3..9e8f8bc5c259 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -189,6 +189,9 @@ "@stacks/transactions": "6.11.0", "@stricahq/typhonjs": "^1.2.6", "@taquito/ledger-signer": "^20.0.0", + "@ton/crypto": "^3.2.0", + "@ton/core": "^0.56.1", + "@ton-community/ton-ledger": "^7.0.1", "@types/bchaddrjs": "^0.4.0", "@types/pako": "^2.0.0", "@types/qs": "^6.9.7", diff --git a/libs/ledger-live-common/src/families/ton/hw-getAddress.ts b/libs/ledger-live-common/src/families/ton/hw-getAddress.ts new file mode 100644 index 000000000000..34fb0cde4609 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/hw-getAddress.ts @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000000..1636f19ae825 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/hw-signMessage.ts @@ -0,0 +1,30 @@ +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/utils.ts b/libs/ledger-live-common/src/families/ton/utils.ts new file mode 100644 index 000000000000..44ce7eba440d --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/utils.ts @@ -0,0 +1,15 @@ +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/ledger-live-common/src/generated/hw-getAddress.ts b/libs/ledger-live-common/src/generated/hw-getAddress.ts index 8aef79ae7d4d..adfe9caa4183 100644 --- a/libs/ledger-live-common/src/generated/hw-getAddress.ts +++ b/libs/ledger-live-common/src/generated/hw-getAddress.ts @@ -7,6 +7,7 @@ import filecoin from "../families/filecoin/hw-getAddress"; import hedera from "../families/hedera/hw-getAddress"; import internet_computer from "../families/internet_computer/hw-getAddress"; import stacks from "../families/stacks/hw-getAddress"; +import ton from "../families/ton/hw-getAddress"; import vechain from "../families/vechain/hw-getAddress"; import { resolver as algorand } from "../families/algorand/setup"; import { resolver as bitcoin } from "../families/bitcoin/setup"; @@ -30,6 +31,7 @@ export default { hedera, internet_computer, stacks, + ton, vechain, algorand, bitcoin, diff --git a/libs/ledger-live-common/src/generated/hw-signMessage.ts b/libs/ledger-live-common/src/generated/hw-signMessage.ts index 34c5e93b10be..0cf633d399e9 100644 --- a/libs/ledger-live-common/src/generated/hw-signMessage.ts +++ b/libs/ledger-live-common/src/generated/hw-signMessage.ts @@ -2,6 +2,7 @@ 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"; @@ -11,6 +12,7 @@ export default { filecoin, internet_computer, stacks, + ton, vechain, bitcoin, evm, From c843d35af5f7adf4ee2bfefcac19c61f562c356f Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Mon, 15 Jan 2024 11:54:54 +0100 Subject: [PATCH 04/60] feat(ton): add currencyBridge --- libs/env/src/env.ts | 12 ++ libs/ledger-live-common/package.json | 3 +- .../ton/bridge/bridgeHelpers/accountShape.ts | 71 ++++++++ .../families/ton/bridge/bridgeHelpers/api.ts | 83 ++++++++++ .../ton/bridge/bridgeHelpers/api.types.ts | 146 +++++++++++++++++ .../families/ton/bridge/bridgeHelpers/txn.ts | 155 ++++++++++++++++++ .../src/families/ton/bridge/currency.ts | 11 ++ .../src/families/ton/bridge/js.ts | 7 + .../src/generated/bridge/js.ts | 2 + 9 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts create mode 100644 libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts create mode 100644 libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts create mode 100644 libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts create mode 100644 libs/ledger-live-common/src/families/ton/bridge/currency.ts create mode 100644 libs/ledger-live-common/src/families/ton/bridge/js.ts diff --git a/libs/env/src/env.ts b/libs/env/src/env.ts index 403c561e35c8..9e27dfc4113f 100644 --- a/libs/env/src/env.ts +++ b/libs/env/src/env.ts @@ -803,6 +803,18 @@ const envDefinitions = { parser: boolParser, desc: "Enable logs for drawers", }, + // TODO: TON switch to mainnet + API_TON_ENDPOINT: { + def: "https://testnet.toncenter.com/api/v3", + parser: stringParser, + desc: "Toncenter API for TON", + }, + // TODO: TON remove apikey + API_TON_KEY: { + def: "1fe6f81ec629684a4242a578b179991990830616ccdd854393ca6379d5d3199a", + parser: stringParser, + desc: "Toncenter APIKEY for TON", + }, }; export const getDefinition = (name: string): EnvDef => { diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index 9e8f8bc5c259..fa1f6c47c588 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -190,8 +190,7 @@ "@stricahq/typhonjs": "^1.2.6", "@taquito/ledger-signer": "^20.0.0", "@ton/crypto": "^3.2.0", - "@ton/core": "^0.56.1", - "@ton-community/ton-ledger": "^7.0.1", + "@ton/ton": "^13.11.1", "@types/bchaddrjs": "^0.4.0", "@types/pako": "^2.0.0", "@types/qs": "^6.9.7", diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts new file mode 100644 index 000000000000..ef790496effa --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/accountShape.ts @@ -0,0 +1,71 @@ +import { GetAccountShape, 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"; + +export const getAccountShape: GetAccountShape = async info => { + const { address, rest, currency, derivationMode, initialAccount } = info; + + const publicKey = reconciliatePubkey(rest?.publicKey, initialAccount); + + const accountId = encodeAccountId({ + type: "js", + version: "2", + currencyId: currency.id, + xpubOrAddress: publicKey, + derivationMode, + }); + + log("debug", `Generation account shape for ${address}`); + + const newTxs: TonTransactionsList = { transactions: [], address_book: {} }; + const oldOps = (initialAccount?.operations ?? []) as TonOperation[]; + const { last_transaction_lt, balance } = await fetchAccountInfo(address); + // if last_transaction_lt is empty, then there are no transactions in account + if (last_transaction_lt != null) { + if (oldOps.length === 0) { + const tmpTxs = await getTransactions(address); + newTxs.transactions.push(...tmpTxs.transactions); + newTxs.address_book = { ...newTxs.address_book, ...tmpTxs.address_book }; + } else { + // if they are the same, we have no new ops + if (oldOps[0].extra.lt !== last_transaction_lt) { + const tmpTxs = await getTransactions(address, oldOps[0].extra.lt); + newTxs.transactions.push(...tmpTxs.transactions); + newTxs.address_book = { ...newTxs.address_book, ...tmpTxs.address_book }; + } + } + } + + const operations = mergeOps( + oldOps, + flatMap(newTxs.transactions, mapTxToOps(accountId, address, newTxs.address_book)), + ); + + const blockHeight = await fetchLastBlockNumber(); + + return { + id: accountId, + balance: new BigNumber(balance), + spendableBalance: new BigNumber(balance), + operations, + blockHeight, + xpub: publicKey, + }; +}; + +function reconciliatePubkey(publicKey?: string, initialAccount?: Account): string { + if (publicKey?.length === 64) return publicKey; + if (initialAccount) { + if (initialAccount.xpub?.length === 64) return initialAccount.xpub; + const { xpubOrAddress } = decodeAccountId(initialAccount.id); + if (xpubOrAddress.length === 64) return xpubOrAddress; + } + throw Error("[ton] pubkey was not properly restored"); +} diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts new file mode 100644 index 000000000000..98e4a0c607a7 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts @@ -0,0 +1,83 @@ +import { getEnv } from "@ledgerhq/live-env"; +import network from "@ledgerhq/live-network/network"; +import { Address } from "@ton/ton"; +import { + TonAccountInfo, + TonResponseAccountInfo, + TonResponseMasterchainInfo, + TonResponseMessage, + TonResponseWalletInfo, + TonTransactionsList, +} from "./api.types"; + +const getTonUrl = (path?: string): string => { + const baseUrl = getEnv("API_TON_ENDPOINT"); + if (!baseUrl) throw new Error("API base URL not available"); + + return `${baseUrl}${path ?? ""}`; +}; + +const fetch = async (path: string): Promise => { + const url = getTonUrl(path); + + const { data } = await network({ + method: "GET", + url, + headers: { "X-API-Key": getEnv("API_TON_KEY") }, + }); + + return data; +}; + +const send = async (path: string, data: Record) => { + const url = getTonUrl(path); + + const { data: dataResponse } = await network({ + method: "POST", + url, + data: JSON.stringify(data), + headers: { "X-API-Key": getEnv("API_TON_KEY"), "Content-Type": "application/json" }, + }); + + return dataResponse; +}; + +export async function fetchLastBlockNumber(): Promise { + const data = await fetch("/masterchainInfo"); + return data.last.seqno; +} + +export async function fetchTransactions( + addr: string, + opts?: { startLt?: string; endLt?: string }, +): Promise { + const address = Address.parse(addr); + const urlAddr = address.toString({ bounceable: false, urlSafe: true }); + let url = `/transactions?account=${urlAddr}&limit=256`; + if (opts?.startLt != null) url += `&start_lt=${opts.startLt}`; + if (opts?.endLt != null) url += `&end_lt=${opts.endLt}`; + return await fetch(url); +} + +export async function fetchAccountInfo(addr: string): Promise { + const address = Address.parse(addr); + const urlAddr = address.toString({ bounceable: false, urlSafe: true }); + const data = await fetch(`/account?address=${urlAddr}`); + if (data.status === "uninit" || data.status === "nonexist") { + return { + balance: data.balance, + last_transaction_lt: data.last_transaction_lt, + last_transaction_hash: data.last_transaction_hash, + status: data.status, + seqno: 0, + }; + } + const { seqno } = await fetch(`/wallet?address=${urlAddr}`); + return { + balance: data.balance, + last_transaction_lt: data.last_transaction_lt, + last_transaction_hash: data.last_transaction_hash, + status: data.status, + seqno: seqno || 0, + }; +} diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts new file mode 100644 index 000000000000..d999d573eb08 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts @@ -0,0 +1,146 @@ +type TonAccountStatus = "uninit" | "frozen" | "active" | "nonexist"; + +interface TonAccountState { + hash: string; + balance: string | null; + account_status: TonAccountStatus | null; + frozen_hash: string | null; + code_hash: string | null; + data_hash: string | null; +} + +interface TonMessage { + hash: string; + source: string | null; + destination: string | null; + value: string | null; + fwd_fee: string | null; + ihr_fee: string | null; + created_lt: string | null; + created_at: string | null; + opcode: string | null; + ihr_disabled: boolean | null; + bounce: boolean | null; + bounced: boolean | null; + import_fee: string | null; + message_content: { + hash: string; + body: string; + decoded: + | { + type: "text_comment"; + comment: string; + } + | { + type: "binary_comment"; + hex_comment: string; + } + | null; + } | null; + init_state: { hash: string; body: string } | null; +} + +interface BlockReference { + workchain: number; + shard: string; + seqno: number; +} + +interface TonBlock { + workchain: number; + shard: string; + seqno: number; + root_hash: string; + file_hash: string; + global_id: number; + version: number; + after_merge: boolean; + before_split: boolean; + after_split: boolean; + want_merge: boolean; + want_split: boolean; + key_block: boolean; + vert_seqno_incr: boolean; + flags: number; + gen_utime: string; + start_lt: string; + end_lt: string; + validator_list_hash_short: number; + gen_catchain_seqno: number; + min_ref_mc_seqno: number; + prev_key_block_seqno: number; + vert_seqno: number; + master_ref_seqno: number | null; + rand_seed: string; + created_by: string; + tx_count: number | null; + masterchain_block_ref: BlockReference | null; + prev_blocks: BlockReference[]; +} + +export interface TonTransaction { + account: string; + hash: string; + lt: string; + now: number; + orig_status: TonAccountStatus; + end_status: TonAccountStatus; + total_fees: string; + prev_trans_hash: string; + prev_trans_lt: string; + description: unknown; + block_ref: { + workchain: number; + shard: string; + seqno: number; + } | null; + in_msg: TonMessage | null; + out_msgs: TonMessage[]; + account_state_before: TonAccountState | null; + account_state_after: TonAccountState | null; + mc_block_seqno: number | null; +} + +export interface TonAddressBook { + [key: string]: { + user_friendly: string; + }; +} + +export interface TonAccountInfo { + balance: string; + last_transaction_lt: string | null; + last_transaction_hash: string | null; + status: TonAccountStatus; + seqno: number; +} + +export interface TonResponseMasterchainInfo { + first: TonBlock; + last: TonBlock; +} + +export interface TonTransactionsList { + transactions: TonTransaction[]; + address_book: TonAddressBook; +} + +export interface TonResponseAccountInfo { + balance: string; + code: string | null; + data: string | null; + last_transaction_lt: string | null; + last_transaction_hash: string | null; + frozen_hash: string | null; + status: TonAccountStatus; +} + +export interface TonResponseWalletInfo { + balance: string; + wallet_type: string | null; + seqno: number | null; + wallet_id: number | null; + last_transaction_lt: string | null; + last_transaction_hash: string | null; + status: TonAccountState; +} diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts new file mode 100644 index 000000000000..768f0918910d --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/txn.ts @@ -0,0 +1,155 @@ +import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; +import { Address } from "@ton/ton"; +import BigNumber from "bignumber.js"; +import { TonOperation } from "../../types"; +import { isAddressValid } from "../../utils"; +import { fetchTransactions } from "./api"; +import { TonAddressBook, TonTransaction, TonTransactionsList } from "./api.types"; + +export async function getTransactions( + addr: string, + startLt?: string, +): Promise { + const txs = await fetchTransactions(addr, { startLt }); + if (txs.transactions.length === 0) return txs; + let tmpTxs: TonTransactionsList; + // eslint-disable-next-line no-constant-condition + while (true) { + const { lt, hash } = txs.transactions[txs.transactions.length - 1]; + tmpTxs = await fetchTransactions(addr, { startLt, endLt: lt }); + // 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"); + tmpTxs.transactions.shift(); // first element is repeated + txs.transactions.push(...tmpTxs.transactions); + txs.address_book = { ...txs.address_book, ...tmpTxs.address_book }; + } + return txs; +} + +function getFriendlyAddress(addressBook: TonAddressBook, rawAddr?: string | null): string[] { + if (!rawAddr) return []; + if (addressBook[rawAddr]) return [addressBook[rawAddr].user_friendly]; + if (!isAddressValid(rawAddr)) throw new Error("[ton] address is not valid"); + return [Address.parse(rawAddr).toString({ urlSafe: true, bounceable: false })]; +} + +export function mapTxToOps( + accountId: string, + addr: string, + addressBook: TonAddressBook, +): (tx: TonTransaction) => TonOperation[] { + return (tx: TonTransaction): TonOperation[] => { + const ops: TonOperation[] = []; + + if (tx.out_msgs.length > 1) throw Error(`[ton] txn with > 1 output not expected ${tx}`); + + const accountAddr = Address.parse(tx.account).toString({ urlSafe: true, bounceable: false }); + + if (accountAddr !== addr) throw Error(`[ton] unexpected address ${accountAddr} ${addr}`); + + const isReceiving = + tx.in_msg && + tx.in_msg.source && + tx.in_msg.source !== "" && + tx.in_msg.value && + tx.in_msg.value !== "0" && + tx.account === tx.in_msg.destination; + + const isSending = + tx.out_msgs.length !== 0 && + tx.out_msgs[0].source && + tx.out_msgs[0].source !== "" && + tx.out_msgs[0].value && + tx.out_msgs[0].value !== "0" && + tx.account === tx.out_msgs[0].source; + + 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 + // we don't want to show them in the charts + ops.push({ + id: encodeOperationId(accountId, hash, "NONE"), + hash, + type: "NONE", + value: BigNumber(tx.total_fees), + fee: BigNumber(0), + blockHeight: tx.mc_block_seqno ?? 1, + blockHash: null, + hasFailed: false, + accountId, + senders: [accountAddr], + recipients: [], + date, + extra: { + lt: tx.lt, + explorerHash: tx.hash, + comment: { + isEncrypted: false, + text: "", + }, + }, + }); + } + ops.push({ + id: encodeOperationId(accountId, hash, "IN"), + hash, + type: "IN", + value: BigNumber(tx.in_msg?.value ?? 0), + fee: BigNumber(tx.total_fees), + blockHeight: tx.mc_block_seqno ?? 1, + blockHash: null, + hasFailed: false, + accountId, + senders: getFriendlyAddress(addressBook, tx.in_msg?.source), + recipients: [accountAddr], + date, + extra: { + lt: tx.lt, + explorerHash: tx.hash, + comment: { + isEncrypted: tx.in_msg?.message_content?.decoded?.type === "binary_comment", + text: + tx.in_msg?.message_content?.decoded?.type === "text_comment" + ? tx.in_msg.message_content.decoded.comment + : "", + }, + }, + }); + } + + if (isSending) { + ops.push({ + id: encodeOperationId(accountId, hash, "OUT"), + hash: tx.out_msgs[0].hash, // this hash matches with in_msg.hash of IN transaction + type: "OUT", + value: BigNumber(tx.out_msgs[0].value ?? 0).plus(BigNumber(tx.total_fees)), + fee: BigNumber(tx.total_fees), + blockHeight: tx.mc_block_seqno ?? 1, + blockHash: null, + hasFailed: false, + accountId, + senders: [accountAddr], + recipients: getFriendlyAddress(addressBook, tx.out_msgs[0].destination), + date, + extra: { + lt: tx.lt, + explorerHash: tx.hash, + comment: { + isEncrypted: tx.out_msgs[0].message_content?.decoded?.type === "binary_comment", + text: + tx.out_msgs[0].message_content?.decoded?.type === "text_comment" + ? tx.out_msgs[0].message_content.decoded.comment + : "", + }, + }, + }); + } + + return ops; + }; +} diff --git a/libs/ledger-live-common/src/families/ton/bridge/currency.ts b/libs/ledger-live-common/src/families/ton/bridge/currency.ts new file mode 100644 index 000000000000..e6ed41cf0d52 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/currency.ts @@ -0,0 +1,11 @@ +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 new file mode 100644 index 000000000000..a49e6b6e35cb --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/js.ts @@ -0,0 +1,7 @@ +import { accountBridge } from "./account"; +import { currencyBridge } from "./currency"; + +export default { + currencyBridge, + accountBridge, +}; diff --git a/libs/ledger-live-common/src/generated/bridge/js.ts b/libs/ledger-live-common/src/generated/bridge/js.ts index 7d203dd3f578..3a6447c9ab4f 100644 --- a/libs/ledger-live-common/src/generated/bridge/js.ts +++ b/libs/ledger-live-common/src/generated/bridge/js.ts @@ -7,6 +7,7 @@ import filecoin from "../../families/filecoin/bridge/js"; import hedera from "../../families/hedera/bridge/js"; import internet_computer from "../../families/internet_computer/bridge/js"; import stacks from "../../families/stacks/bridge/js"; +import ton from "../../families/ton/bridge/js"; import vechain from "../../families/vechain/bridge/js"; import { bridge as algorand } from "../../families/algorand/setup"; import { bridge as bitcoin } from "../../families/bitcoin/setup"; @@ -30,6 +31,7 @@ export default { hedera, internet_computer, stacks, + ton, vechain, algorand, bitcoin, From 6e3fc94033cc08fc7164284a288e8c448e445268 Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Mon, 15 Jan 2024 11:58:59 +0100 Subject: [PATCH 05/60] feat(ton): add accountBridge --- .../src/families/ton/bridge/account.ts | 258 ++++++++++++++++++ .../families/ton/bridge/bridgeHelpers/api.ts | 23 ++ .../ton/bridge/bridgeHelpers/api.types.ts | 16 ++ .../families/ton/deviceTransactionConfig.ts | 41 +++ .../src/families/ton/errors.ts | 3 + .../src/families/ton/transaction.ts | 60 ++++ .../src/families/ton/types.ts | 38 ++- .../src/families/ton/utils.test.ts | 58 ++++ .../src/families/ton/utils.ts | 107 ++++++++ .../src/generated/deviceTransactionConfig.ts | 2 + .../src/generated/transaction.ts | 2 + 11 files changed, 605 insertions(+), 3 deletions(-) create mode 100644 libs/ledger-live-common/src/families/ton/bridge/account.ts create mode 100644 libs/ledger-live-common/src/families/ton/deviceTransactionConfig.ts create mode 100644 libs/ledger-live-common/src/families/ton/errors.ts create mode 100644 libs/ledger-live-common/src/families/ton/transaction.ts create mode 100644 libs/ledger-live-common/src/families/ton/utils.test.ts diff --git a/libs/ledger-live-common/src/families/ton/bridge/account.ts b/libs/ledger-live-common/src/families/ton/bridge/account.ts new file mode 100644 index 000000000000..70c6425e2230 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/bridge/account.ts @@ -0,0 +1,258 @@ +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/bridgeHelpers/api.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts index 98e4a0c607a7..9e43c11f6f36 100644 --- a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.ts @@ -3,7 +3,9 @@ import network from "@ledgerhq/live-network/network"; import { Address } from "@ton/ton"; import { TonAccountInfo, + TonFee, TonResponseAccountInfo, + TonResponseEstimateFee, TonResponseMasterchainInfo, TonResponseMessage, TonResponseWalletInfo, @@ -81,3 +83,24 @@ export async function fetchAccountInfo(addr: string): Promise { seqno: seqno || 0, }; } + +export async function estimateFee( + address: string, + body: string, + initCode?: string, + initData?: string, +): Promise { + return ( + await send("/estimateFee", { + address, + body, + init_code: initCode, + init_data: initData, + ignore_chksig: true, + }) + ).source_fees; +} + +export async function broadcastTx(bocBase64: string): Promise { + return (await send("/message", { boc: bocBase64 })).message_hash; +} diff --git a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts index d999d573eb08..97280677068a 100644 --- a/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts +++ b/libs/ledger-live-common/src/families/ton/bridge/bridgeHelpers/api.types.ts @@ -115,6 +115,13 @@ export interface TonAccountInfo { seqno: number; } +export interface TonFee { + in_fwd_fee: number; + storage_fee: number; + gas_fee: number; + fwd_fee: number; +} + export interface TonResponseMasterchainInfo { first: TonBlock; last: TonBlock; @@ -144,3 +151,12 @@ export interface TonResponseWalletInfo { last_transaction_hash: string | null; status: TonAccountState; } + +export interface TonResponseEstimateFee { + source_fees: TonFee; + destination_fees: TonFee[]; +} + +export interface TonResponseMessage { + message_hash: string; +} diff --git a/libs/ledger-live-common/src/families/ton/deviceTransactionConfig.ts b/libs/ledger-live-common/src/families/ton/deviceTransactionConfig.ts new file mode 100644 index 000000000000..890c49a01f86 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/deviceTransactionConfig.ts @@ -0,0 +1,41 @@ +import type { Account, AccountLike } from "@ledgerhq/types-live"; +import { DeviceTransactionField } from "../../transaction"; +import type { Transaction, TransactionStatus } from "./types"; + +function getDeviceTransactionConfig(input: { + account: AccountLike; + parentAccount: Account | null | undefined; + transaction: Transaction; + status: TransactionStatus; +}): Array { + const fields: Array = []; + + fields.push({ + type: "address", + label: "To", + address: input.transaction.recipient, + }); + if (input.transaction.useAllAmount) { + fields.push({ + type: "text", + label: "Amount", + value: "ALL YOUR TONs", + }); + } else { + fields.push({ + type: "amount", + label: "Amount", + }); + } + if (!input.transaction.comment.isEncrypted && input.transaction.comment.text) { + fields.push({ + type: "text", + label: "Comment", + value: input.transaction.comment.text, + }); + } + + return fields; +} + +export default getDeviceTransactionConfig; diff --git a/libs/ledger-live-common/src/families/ton/errors.ts b/libs/ledger-live-common/src/families/ton/errors.ts new file mode 100644 index 000000000000..76ed2665a853 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/errors.ts @@ -0,0 +1,3 @@ +import { createCustomErrorClass } from "@ledgerhq/errors"; + +export const TonCommentInvalid = createCustomErrorClass("TonCommentInvalid"); diff --git a/libs/ledger-live-common/src/families/ton/transaction.ts b/libs/ledger-live-common/src/families/ton/transaction.ts new file mode 100644 index 000000000000..69a074b9b2f7 --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/transaction.ts @@ -0,0 +1,60 @@ +import { + formatTransactionStatusCommon as formatTransactionStatus, + fromTransactionCommonRaw, + fromTransactionStatusRawCommon as fromTransactionStatusRaw, + toTransactionCommonRaw, + toTransactionStatusRawCommon as toTransactionStatusRaw, +} 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 = ( + { recipient, useAllAmount, amount }: Transaction, + account: Account, +): string => ` +SEND ${ + useAllAmount + ? "MAX" + : amount.isZero() + ? "" + : " " + + formatCurrencyUnit(getAccountUnit(account), amount, { + showCode: true, + disableRounding: true, + }) +} +TO ${recipient}`; + +export const fromTransactionRaw = (tr: TransactionRaw): Transaction => { + const common = fromTransactionCommonRaw(tr); + return { + ...common, + family: tr.family, + fees: new BigNumber(tr.fees), + comment: tr.comment, + }; +}; + +const toTransactionRaw = (t: Transaction): TransactionRaw => { + const common = toTransactionCommonRaw(t); + + return { + ...common, + family: t.family, + amount: t.amount.toFixed(), + fees: t.fees.toFixed(), + comment: t.comment, + }; +}; + +export default { + formatTransaction, + fromTransactionRaw, + toTransactionRaw, + fromTransactionStatusRaw, + toTransactionStatusRaw, + formatTransactionStatus, +}; diff --git a/libs/ledger-live-common/src/families/ton/types.ts b/libs/ledger-live-common/src/families/ton/types.ts index cf8f98be0b40..ecff578bec7c 100644 --- a/libs/ledger-live-common/src/families/ton/types.ts +++ b/libs/ledger-live-common/src/families/ton/types.ts @@ -1,14 +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"; -export type Transaction = TransactionCommon & { family: FamilyType }; -export type TransactionRaw = TransactionCommonRaw & { family: FamilyType }; -export type TransactionStatus = TransactionStatusCommon; +// 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/ledger-live-common/src/families/ton/utils.test.ts b/libs/ledger-live-common/src/families/ton/utils.test.ts new file mode 100644 index 000000000000..f5cf6d7d196a --- /dev/null +++ b/libs/ledger-live-common/src/families/ton/utils.test.ts @@ -0,0 +1,58 @@ +import { TonComment } from "./types"; +import { addressesAreEqual, commentIsValid, isAddressValid } from "./utils"; + +describe("TON addresses", () => { + const addr = { + raw: "0:074c7194d64e8218f2cfaab8e79b34201adbed0f8fa7f2773e604dd39969b5ff", + rawWrong: "0:074c7194d64e8218f2cfaab8e79b34201adbed0f8fa7f2773e604dd39969b5f", + bounceUrl: "EQAHTHGU1k6CGPLPqrjnmzQgGtvtD4-n8nc-YE3TmWm1_1JZ", + bounceNoUrl: "EQAHTHGU1k6CGPLPqrjnmzQgGtvtD4+n8nc+YE3TmWm1/1JZ", + bounceWrong: "EQAHTHGU1k6CGPLPqrjnmzQgGtvtD4+n8nc+YE3TmWm1/1J", + noBounceUrl: "UQAHTHGU1k6CGPLPqrjnmzQgGtvtD4-n8nc-YE3TmWm1_w-c", + noBounceNoUrl: "UQAHTHGU1k6CGPLPqrjnmzQgGtvtD4+n8nc+YE3TmWm1/w+c", + noBounceWrong: "UQAHTHGU1k6CGPLPqrjnmzQgGtvtD4+n8nc+YE3TmWm1/w+", + diff: "UQBjrXgZbYDCpxLKpgMnBe985kYDfUeriuYUafbuKgdBpWuJ", + }; + test("Check if addresses are valid", () => { + expect(isAddressValid(addr.raw)).toBe(true); + expect(isAddressValid(addr.bounceUrl)).toBe(true); + expect(isAddressValid(addr.bounceNoUrl)).toBe(true); + expect(isAddressValid(addr.noBounceUrl)).toBe(true); + expect(isAddressValid(addr.noBounceNoUrl)).toBe(true); + expect(isAddressValid(addr.rawWrong)).toBe(false); + expect(isAddressValid(addr.bounceWrong)).toBe(false); + expect(isAddressValid(addr.noBounceWrong)).toBe(false); + expect(isAddressValid(addr.diff)).toBe(true); + }); + test("Compare addresses", () => { + expect(addressesAreEqual(addr.raw, addr.bounceUrl)).toBe(true); + expect(addressesAreEqual(addr.raw, addr.noBounceUrl)).toBe(true); + expect(addressesAreEqual(addr.bounceUrl, addr.noBounceUrl)).toBe(true); + expect(addressesAreEqual(addr.rawWrong, addr.noBounceUrl)).toBe(false); + expect(addressesAreEqual(addr.noBounceNoUrl, addr.diff)).toBe(false); + }); +}); + +test("TON Comments are valid", () => { + const msg = (e: boolean, m: string): TonComment => ({ isEncrypted: e, text: m }); + expect(commentIsValid(msg(false, ""))).toBe(true); + expect(commentIsValid(msg(false, "Hello world!"))).toBe(true); + expect( + commentIsValid( + msg( + false, + " 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789", // 120 chars + ), + ), + ).toBe(true); + expect( + commentIsValid( + msg( + false, + " 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 123456789 ", // 121 chars + ), + ), + ).toBe(false); + expect(commentIsValid(msg(false, "😀"))).toBe(false); + expect(commentIsValid(msg(true, ""))).toBe(false); +}); diff --git a/libs/ledger-live-common/src/families/ton/utils.ts b/libs/ledger-live-common/src/families/ton/utils.ts index 44ce7eba440d..2d561746479e 100644 --- a/libs/ledger-live-common/src/families/ton/utils.ts +++ b/libs/ledger-live-common/src/families/ton/utils.ts @@ -1,3 +1,110 @@ +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"); diff --git a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts index d5c760f6f832..8741d2b4beba 100644 --- a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts +++ b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts @@ -7,6 +7,7 @@ import filecoin from "../families/filecoin/deviceTransactionConfig"; import hedera from "../families/hedera/deviceTransactionConfig"; import internet_computer from "../families/internet_computer/deviceTransactionConfig"; import stacks from "../families/stacks/deviceTransactionConfig"; +import ton from "../families/ton/deviceTransactionConfig"; import algorand from "@ledgerhq/coin-algorand/deviceTransactionConfig"; import bitcoin from "@ledgerhq/coin-bitcoin/deviceTransactionConfig"; import cardano from "@ledgerhq/coin-cardano/deviceTransactionConfig"; @@ -29,6 +30,7 @@ export default { hedera, internet_computer, stacks, + ton, algorand, bitcoin, cardano, diff --git a/libs/ledger-live-common/src/generated/transaction.ts b/libs/ledger-live-common/src/generated/transaction.ts index 7433bfca4dfe..b3e390e01683 100644 --- a/libs/ledger-live-common/src/generated/transaction.ts +++ b/libs/ledger-live-common/src/generated/transaction.ts @@ -7,6 +7,7 @@ import filecoin from "../families/filecoin/transaction"; import hedera from "../families/hedera/transaction"; import internet_computer from "../families/internet_computer/transaction"; import stacks from "../families/stacks/transaction"; +import ton from "../families/ton/transaction"; import vechain from "../families/vechain/transaction"; import algorand from "@ledgerhq/coin-algorand/transaction"; import bitcoin from "@ledgerhq/coin-bitcoin/transaction"; @@ -30,6 +31,7 @@ export default { hedera, internet_computer, stacks, + ton, vechain, algorand, bitcoin, From 76acffd70de682d10d73fc02df050438d7439d6a Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Mon, 15 Jan 2024 11:59:24 +0100 Subject: [PATCH 06/60] feat(ton): add LLD integration --- .../live-common-set-supported-currencies.ts | 1 + .../families/ton/AccountSubHeader.tsx | 6 +++ .../renderer/families/ton/CommentField.tsx | 51 +++++++++++++++++++ .../families/ton/SendAmountFields.tsx | 46 +++++++++++++++++ .../src/renderer/families/ton/index.ts | 22 ++++++++ .../families/ton/operationDetails.tsx | 31 +++++++++++ .../src/renderer/families/types.ts | 2 +- .../AddAccounts/steps/StepChooseCurrency.tsx | 3 ++ .../static/i18n/en/app.json | 8 ++- 9 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 apps/ledger-live-desktop/src/renderer/families/ton/AccountSubHeader.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/families/ton/CommentField.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/families/ton/SendAmountFields.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/families/ton/index.ts create mode 100644 apps/ledger-live-desktop/src/renderer/families/ton/operationDetails.tsx diff --git a/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts b/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts index 185d97523226..c933482bbe15 100644 --- a/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts +++ b/apps/ledger-live-desktop/src/live-common-set-supported-currencies.ts @@ -90,4 +90,5 @@ setSupportedCurrencies([ "blast_sepolia", "scroll", "scroll_sepolia", + "ton", ]); diff --git a/apps/ledger-live-desktop/src/renderer/families/ton/AccountSubHeader.tsx b/apps/ledger-live-desktop/src/renderer/families/ton/AccountSubHeader.tsx new file mode 100644 index 000000000000..023ecd14bb59 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/ton/AccountSubHeader.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import AccountSubHeader from "../../components/AccountSubHeader/index"; + +export default function TonAccountSubHeader() { + return ; +} diff --git a/apps/ledger-live-desktop/src/renderer/families/ton/CommentField.tsx b/apps/ledger-live-desktop/src/renderer/families/ton/CommentField.tsx new file mode 100644 index 000000000000..13284ace82c7 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/ton/CommentField.tsx @@ -0,0 +1,51 @@ +import React, { useCallback } from "react"; +import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; +import Input from "~/renderer/components/Input"; +import invariant from "invariant"; +import { Account } from "@ledgerhq/types-live"; +import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/ton/types"; +import { useTranslation } from "react-i18next"; + +const CommentField = ({ + onChange, + account, + transaction, + status, +}: { + onChange: (a: Transaction) => void; + account: Account; + transaction: Transaction; + status: TransactionStatus; +}) => { + invariant(transaction.family === "ton", "Comment: TON family expected"); + + const { t } = useTranslation(); + + const bridge = getAccountBridge(account); + + const onCommentFieldChange = useCallback( + (value: string) => { + onChange( + bridge.updateTransaction(transaction, { + comment: { isEncrypted: false, text: value ?? "" }, + }), + ); + }, + [onChange, transaction, bridge], + ); + + // We use transaction as an error here. + // on the ledger-live mobile + return ( + + ); +}; + +export default CommentField; diff --git a/apps/ledger-live-desktop/src/renderer/families/ton/SendAmountFields.tsx b/apps/ledger-live-desktop/src/renderer/families/ton/SendAmountFields.tsx new file mode 100644 index 000000000000..ab5e6a854a92 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/ton/SendAmountFields.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Trans } from "react-i18next"; +import CommentField from "./CommentField"; +import Box from "~/renderer/components/Box"; +import Label from "~/renderer/components/Label"; +import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip"; +import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/ton/types"; +import { Account } from "@ledgerhq/types-live"; + +const Root = (props: { + account: Account; + transaction: Transaction; + status: TransactionStatus; + onChange: (a: Transaction) => void; + trackProperties?: object; +}) => { + return ( + + + + + + + + + + + ); +}; + +export default { + component: Root, + // Transaction is used here to prevent user to forward + fields: ["comment", "transaction"], +}; diff --git a/apps/ledger-live-desktop/src/renderer/families/ton/index.ts b/apps/ledger-live-desktop/src/renderer/families/ton/index.ts new file mode 100644 index 000000000000..35b184613ea3 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/ton/index.ts @@ -0,0 +1,22 @@ +import { + TonOperation, + Transaction, + TransactionStatus, +} from "@ledgerhq/live-common/families/ton/types"; +import { LLDCoinFamily } from "../types"; +import operationDetails from "./operationDetails"; +import AccountSubHeader from "./AccountSubHeader"; +import sendAmountFields from "./SendAmountFields"; +import { Account } from "@ledgerhq/types-live"; + +const family: LLDCoinFamily = { + operationDetails, + AccountSubHeader, + sendAmountFields, + getTransactionExplorer: (explorerView, operation) => + explorerView && + explorerView.tx && + explorerView.tx.replace("$hash", operation.extra.explorerHash), +}; + +export default family; diff --git a/apps/ledger-live-desktop/src/renderer/families/ton/operationDetails.tsx b/apps/ledger-live-desktop/src/renderer/families/ton/operationDetails.tsx new file mode 100644 index 000000000000..a8fe5f70be26 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/families/ton/operationDetails.tsx @@ -0,0 +1,31 @@ +import React from "react"; +import { Trans } from "react-i18next"; +import { + OpDetailsTitle, + OpDetailsData, + OpDetailsSection, +} from "~/renderer/drawers/OperationDetails/styledComponents"; +import Ellipsis from "~/renderer/components/Ellipsis"; +import { TonOperation } from "@ledgerhq/live-common/families/ton/types"; + +type OperationDetailsExtraProps = { + operation: TonOperation; +}; + +const OperationDetailsExtra = ({ operation }: OperationDetailsExtraProps) => { + const { extra } = operation; + return !extra.comment.text ? null : ( + + + + + + {extra.comment.text} + + + ); +}; + +export default { + OperationDetailsExtra, +}; diff --git a/apps/ledger-live-desktop/src/renderer/families/types.ts b/apps/ledger-live-desktop/src/renderer/families/types.ts index 68cf611e4126..191df3d03239 100644 --- a/apps/ledger-live-desktop/src/renderer/families/types.ts +++ b/apps/ledger-live-desktop/src/renderer/families/types.ts @@ -309,7 +309,7 @@ export type LLDCoinFamily< */ getTransactionExplorer?: ( explorerView: ExplorerView | null | undefined, - operation: Operation, + operation: O, ) => string | null | undefined; nft?: { diff --git a/apps/ledger-live-desktop/src/renderer/modals/AddAccounts/steps/StepChooseCurrency.tsx b/apps/ledger-live-desktop/src/renderer/modals/AddAccounts/steps/StepChooseCurrency.tsx index 4c60ddcfc40c..4a22a1094d3e 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/AddAccounts/steps/StepChooseCurrency.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/AddAccounts/steps/StepChooseCurrency.tsx @@ -79,6 +79,7 @@ const StepChooseCurrency = ({ currency, setCurrency }: StepProps) => { const blastSepolia = useFeature("currencyBlastSepolia"); const scroll = useFeature("currencyScroll"); const scrollSepolia = useFeature("currencyScrollSepolia"); + const ton = useFeature("currencyTon"); const featureFlaggedCurrencies = useMemo( (): Partial | null>> => ({ @@ -121,6 +122,7 @@ const StepChooseCurrency = ({ currency, setCurrency }: StepProps) => { neon_evm: neonEvm, lukso, linea, + ton, linea_sepolia: lineaSepolia, blast, blast_sepolia: blastSepolia, @@ -167,6 +169,7 @@ const StepChooseCurrency = ({ currency, setCurrency }: StepProps) => { neonEvm, lukso, linea, + ton, lineaSepolia, blast, blastSepolia, diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index b887b95e5007..806ea427758a 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -1813,7 +1813,8 @@ "withdrawUnbondedAmount": "Withdrawn Amount", "palletMethod": "Method", "transferAmount": "Transfer Amount", - "validatorsCount": "Validators ({{number}})" + "validatorsCount": "Validators ({{number}})", + "comment": "Comment" } }, "operationList": { @@ -5174,6 +5175,11 @@ "memo": "Memo", "memoWarningText": "When using a Memo, carefully verify the type used with the recipient" }, + "ton": { + "commentPlaceholder": "Optional", + "comment": "Comment", + "commentWarningText": "Comment must be up to 120 ascii chars" + }, "stellar": { "memo": "Memo", "memoType": { From 6c8dabccf2a5652517d920515f55ab7412b69223 Mon Sep 17 00:00:00 2001 From: Carlo Sala Date: Mon, 15 Jan 2024 11:59:33 +0100 Subject: [PATCH 07/60] feat(ton): add LLM integration --- apps/ledger-live-mobile/babel.config.js | 2 + apps/ledger-live-mobile/package.json | 1 + .../RootNavigator/types/SendFundsNavigator.ts | 15 +++ .../types/SignTransactionNavigator.ts | 15 +++ .../RootNavigator/types/SwapNavigator.ts | 15 +++ .../src/const/navigation.ts | 3 + apps/ledger-live-mobile/src/families/index.ts | 1 + .../src/families/ton/AccountSubHeader.tsx | 6 + .../src/families/ton/ScreenEditComment.tsx | 120 ++++++++++++++++++ .../src/families/ton/SendRowComment.tsx | 81 ++++++++++++ .../src/families/ton/SendRowsCustom.tsx | 29 +++++ .../src/families/ton/index.ts | 3 + .../src/families/ton/operationDetails.tsx | 28 ++++ .../src/live-common-setup.ts | 1 + .../src/locales/en/common.json | 8 +- .../screens/AddAccounts/01-SelectCrypto.tsx | 3 + 16 files changed, 330 insertions(+), 1 deletion(-) create mode 100644 apps/ledger-live-mobile/src/families/ton/AccountSubHeader.tsx create mode 100644 apps/ledger-live-mobile/src/families/ton/ScreenEditComment.tsx create mode 100644 apps/ledger-live-mobile/src/families/ton/SendRowComment.tsx create mode 100644 apps/ledger-live-mobile/src/families/ton/SendRowsCustom.tsx create mode 100644 apps/ledger-live-mobile/src/families/ton/index.ts create mode 100644 apps/ledger-live-mobile/src/families/ton/operationDetails.tsx diff --git a/apps/ledger-live-mobile/babel.config.js b/apps/ledger-live-mobile/babel.config.js index d9162952346b..6c35be935593 100644 --- a/apps/ledger-live-mobile/babel.config.js +++ b/apps/ledger-live-mobile/babel.config.js @@ -12,6 +12,8 @@ module.exports = { "@babel/plugin-transform-named-capturing-groups-regex", "@babel/plugin-proposal-export-namespace-from", "@babel/plugin-transform-class-static-block", + "@babel/plugin-transform-flow-strip-types", + ["@babel/plugin-transform-private-methods", { loose: true }], "react-native-reanimated/plugin", // react-native-reanimated/plugin has to be listed last. ], }; diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json index 259464ff52cb..62cc25ecafd9 100644 --- a/apps/ledger-live-mobile/package.json +++ b/apps/ledger-live-mobile/package.json @@ -168,6 +168,7 @@ "react-native-extra-dimensions-android": "^1.2.5", "react-native-fast-crypto": "^2.2.0", "react-native-fast-image": "^8.5.11", + "react-native-fast-pbkdf2": "^0.3.1", "react-native-gesture-handler": "^2.9.0", "react-native-get-random-values": "^1.11.0", "react-native-haptic-feedback": "^2.0.3", diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/SendFundsNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/SendFundsNavigator.ts index ed3e52360107..579d8c69d1ce 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/SendFundsNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/SendFundsNavigator.ts @@ -35,6 +35,7 @@ import type { Transaction as ICPTransaction } from "@ledgerhq/live-common/famili import type { Transaction as StellarTransaction } from "@ledgerhq/live-common/families/stellar/types"; import type { Transaction as StacksTransaction } from "@ledgerhq/live-common/families/stacks/types"; import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/families/casper/types"; +import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types"; import BigNumber from "bignumber.js"; import { Result } from "@ledgerhq/live-common/bridge/useBridgeTransaction"; import { ScreenName } from "~/const"; @@ -344,4 +345,18 @@ export type SendFundsNavigatorStackParamList = { | ScreenName.SendSelectDevice | ScreenName.SwapForm; }; + [ScreenName.TonEditComment]: { + accountId: string; + account: Account; + parentId?: string; + transaction: TonTransaction; + currentNavigation: + | ScreenName.SignTransactionSummary + | ScreenName.SendSummary + | ScreenName.SwapForm; + nextNavigation: + | ScreenName.SignTransactionSelectDevice + | ScreenName.SendSelectDevice + | ScreenName.SwapForm; + }; }; diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/SignTransactionNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/SignTransactionNavigator.ts index 98f6f6940c5e..83371f35a83c 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/SignTransactionNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/SignTransactionNavigator.ts @@ -31,6 +31,7 @@ import type { Transaction as RippleTransaction } from "@ledgerhq/live-common/fam import type { Transaction as StellarTransaction } from "@ledgerhq/live-common/families/stellar/types"; import type { Transaction as StacksTransaction } from "@ledgerhq/live-common/families/stacks/types"; import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/families/casper/types"; +import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types"; import { Device } from "@ledgerhq/live-common/hw/actions/types"; import { Account, Operation, SignedOperation } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; @@ -310,4 +311,18 @@ export type SignTransactionNavigatorParamList = { | ScreenName.SendSelectDevice | ScreenName.SwapForm; }; + [ScreenName.TonEditComment]: { + accountId: string; + account: Account; + parentId?: string; + transaction: TonTransaction; + currentNavigation: + | ScreenName.SignTransactionSummary + | ScreenName.SendSummary + | ScreenName.SwapForm; + nextNavigation: + | ScreenName.SignTransactionSelectDevice + | ScreenName.SendSelectDevice + | ScreenName.SwapForm; + }; }; diff --git a/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts b/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts index cfdf9a08788a..fabdb2249ade 100644 --- a/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts +++ b/apps/ledger-live-mobile/src/components/RootNavigator/types/SwapNavigator.ts @@ -41,6 +41,7 @@ import type { Transaction as ICPTransaction } from "@ledgerhq/live-common/famili import type { Transaction as StellarTransaction } from "@ledgerhq/live-common/families/stellar/types"; import type { Transaction as StacksTransaction } from "@ledgerhq/live-common/families/stacks/types"; import type { Transaction as CasperTransaction } from "@ledgerhq/live-common/families/casper/types"; +import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types"; import BigNumber from "bignumber.js"; import { Account, Operation } from "@ledgerhq/types-live"; import { ScreenName } from "~/const"; @@ -314,4 +315,18 @@ export type SwapNavigatorParamList = { | ScreenName.SendSelectDevice | ScreenName.SwapForm; }; + [ScreenName.TonEditComment]: { + accountId: string; + account: Account; + parentId?: string; + transaction: TonTransaction; + currentNavigation: + | ScreenName.SignTransactionSummary + | ScreenName.SendSummary + | ScreenName.SwapForm; + nextNavigation: + | ScreenName.SignTransactionSelectDevice + | ScreenName.SendSelectDevice + | ScreenName.SwapForm; + }; }; diff --git a/apps/ledger-live-mobile/src/const/navigation.ts b/apps/ledger-live-mobile/src/const/navigation.ts index 5c532970c129..525eb42b42d4 100644 --- a/apps/ledger-live-mobile/src/const/navigation.ts +++ b/apps/ledger-live-mobile/src/const/navigation.ts @@ -311,6 +311,9 @@ export enum ScreenName { // internet_computer InternetComputerEditMemo = "InternetComputerEditMemo", + // ton + TonEditComment = "TonEditComment", + // crypto_org CryptoOrgEditMemo = "CryptoOrgEditMemo", diff --git a/apps/ledger-live-mobile/src/families/index.ts b/apps/ledger-live-mobile/src/families/index.ts index 546e1c6eb966..c272b073973f 100644 --- a/apps/ledger-live-mobile/src/families/index.ts +++ b/apps/ledger-live-mobile/src/families/index.ts @@ -17,3 +17,4 @@ export * from "./casper"; export * from "./stellar"; export * from "./tezos"; export * from "./tron"; +export * from "./ton"; diff --git a/apps/ledger-live-mobile/src/families/ton/AccountSubHeader.tsx b/apps/ledger-live-mobile/src/families/ton/AccountSubHeader.tsx new file mode 100644 index 000000000000..c4dc2c5f22c0 --- /dev/null +++ b/apps/ledger-live-mobile/src/families/ton/AccountSubHeader.tsx @@ -0,0 +1,6 @@ +import React from "react"; +import AccountSubHeader from "~/components/AccountSubHeader"; + +export default function TonAccountSubHeader() { + return ; +} diff --git a/apps/ledger-live-mobile/src/families/ton/ScreenEditComment.tsx b/apps/ledger-live-mobile/src/families/ton/ScreenEditComment.tsx new file mode 100644 index 000000000000..99990cf4aa3b --- /dev/null +++ b/apps/ledger-live-mobile/src/families/ton/ScreenEditComment.tsx @@ -0,0 +1,120 @@ +import invariant from "invariant"; +import React, { useCallback, useState } from "react"; +import { View, StyleSheet, ScrollView } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import i18next from "i18next"; +import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; +import { useIsFocused, useTheme } from "@react-navigation/native"; +import KeyboardView from "~/components/KeyboardView"; +import Button from "~/components/Button"; +import { ScreenName } from "~/const"; +import { accountScreenSelector } from "~/reducers/accounts"; +import TextInput from "~/components/FocusedTextInput"; +import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; +import { SendFundsNavigatorStackParamList } from "~/components/RootNavigator/types/SendFundsNavigator"; +import { SignTransactionNavigatorParamList } from "~/components/RootNavigator/types/SignTransactionNavigator"; +import { SwapNavigatorParamList } from "~/components/RootNavigator/types/SwapNavigator"; + +type NavigationProps = BaseComposite< + StackNavigatorProps< + SendFundsNavigatorStackParamList | SignTransactionNavigatorParamList | SwapNavigatorParamList, + ScreenName.TonEditComment + > +>; + +function TonEditComment({ navigation, route }: NavigationProps) { + const isFocused = useIsFocused(); + const { colors } = useTheme(); + const { t } = useTranslation(); + const { account } = useSelector(accountScreenSelector(route)); + invariant(account, "account is required"); + const [comment, setComment] = useState( + !route.params.transaction.comment.isEncrypted ? route.params.transaction.comment.text : "", + ); + const onChangeCommentValue = useCallback((str: string) => { + setComment(str); + }, []); + const onValidateText = useCallback(() => { + const bridge = getAccountBridge(account); + const { transaction } = route.params; + // @ts-expect-error FIXME: No current / next navigation params? + navigation.navigate(ScreenName.SendSummary, { + accountId: account.id, + transaction: bridge.updateTransaction(transaction, { + comment: { isEncrypted: false, text: comment }, + }), + }); + }, [navigation, route.params, account, comment]); + return ( + + + + {isFocused && ( + + )} + + +