diff --git a/.changeset/eleven-hotels-share.md b/.changeset/eleven-hotels-share.md
new file mode 100644
index 000000000000..ae8faa7e0449
--- /dev/null
+++ b/.changeset/eleven-hotels-share.md
@@ -0,0 +1,15 @@
+---
+"@ledgerhq/types-cryptoassets": patch
+"@ledgerhq/cryptoassets": patch
+"@ledgerhq/types-live": patch
+"@ledgerhq/crypto-icons-ui": patch
+"@actions/turbo-affected": patch
+"@ledgerhq/coin-ton": patch
+"ledger-live-desktop": patch
+"live-mobile": patch
+"@ledgerhq/live-common": patch
+"@ledgerhq/coin-framework": patch
+"@ledgerhq/live-cli": patch
+---
+
+add support for ton
\ No newline at end of file
diff --git a/.changeset/mean-trees-invite.md b/.changeset/mean-trees-invite.md
new file mode 100644
index 000000000000..9b900af14266
--- /dev/null
+++ b/.changeset/mean-trees-invite.md
@@ -0,0 +1,15 @@
+---
+"@ledgerhq/types-cryptoassets": minor
+"@ledgerhq/cryptoassets": minor
+"@ledgerhq/types-live": minor
+"@ledgerhq/crypto-icons-ui": minor
+"ledger-live-desktop": minor
+"live-mobile": minor
+"@ledgerhq/live-common": minor
+"@ledgerhq/coin-framework": minor
+"@ledgerhq/live-config": minor
+"@ledgerhq/live-cli": minor
+"@ledgerhq/live-env": minor
+---
+
+Support for TON blockchain
diff --git a/apps/cli/src/live-common-setup-base.ts b/apps/cli/src/live-common-setup-base.ts
index 61ab6b5d54c4..ff2370140a86 100644
--- a/apps/cli/src/live-common-setup-base.ts
+++ b/apps/cli/src/live-common-setup-base.ts
@@ -93,6 +93,7 @@ setSupportedCurrencies([
"lukso",
"filecoin",
"linea",
+ "ton",
"linea_sepolia",
"blast",
"blast_sepolia",
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 8777771a8878..418f4e80f3d1 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
@@ -92,4 +92,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..cfe658d4d54b
--- /dev/null
+++ b/apps/ledger-live-desktop/src/renderer/families/ton/CommentField.tsx
@@ -0,0 +1,51 @@
+import { getAccountBridge } from "@ledgerhq/live-common/bridge/index";
+import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/ton/types";
+import { Account } from "@ledgerhq/types-live";
+import invariant from "invariant";
+import React, { useCallback } from "react";
+import { useTranslation } from "react-i18next";
+import Input from "~/renderer/components/Input";
+
+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/SendRecipientFields.tsx b/apps/ledger-live-desktop/src/renderer/families/ton/SendRecipientFields.tsx
new file mode 100644
index 000000000000..9872ce458d1b
--- /dev/null
+++ b/apps/ledger-live-desktop/src/renderer/families/ton/SendRecipientFields.tsx
@@ -0,0 +1,41 @@
+import { Transaction, TransactionStatus } from "@ledgerhq/live-common/families/ton/types";
+import { Account } from "@ledgerhq/types-live";
+import React from "react";
+import { Trans } from "react-i18next";
+import Box from "~/renderer/components/Box";
+import Label from "~/renderer/components/Label";
+import LabelInfoTooltip from "~/renderer/components/LabelInfoTooltip";
+import CommentField from "./CommentField";
+
+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
+ // If he format a comment incorrectly
+ 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..b008483ca834
--- /dev/null
+++ b/apps/ledger-live-desktop/src/renderer/families/ton/index.ts
@@ -0,0 +1,27 @@
+import {
+ TonOperation,
+ Transaction,
+ TransactionStatus,
+} from "@ledgerhq/live-common/families/ton/types";
+import { Account } from "@ledgerhq/types-live";
+import { LLDCoinFamily } from "../types";
+import AccountSubHeader from "./AccountSubHeader";
+import sendRecipientFields from "./SendRecipientFields";
+import operationDetails from "./operationDetails";
+
+const family: LLDCoinFamily = {
+ operationDetails,
+ AccountSubHeader,
+ sendRecipientFields,
+ getTransactionExplorer: (explorerView, operation) =>
+ explorerView &&
+ explorerView.tx &&
+ explorerView.tx.replace(
+ "$hash",
+ operation.extra.explorerHash && operation.extra.explorerHash !== ""
+ ? operation.extra.explorerHash
+ : `by-msg-hash/${operation.hash}`,
+ ),
+};
+
+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..4d83de4576ac
--- /dev/null
+++ b/apps/ledger-live-desktop/src/renderer/families/ton/operationDetails.tsx
@@ -0,0 +1,31 @@
+import { TonOperation } from "@ledgerhq/live-common/families/ton/types";
+import React from "react";
+import { Trans } from "react-i18next";
+import Ellipsis from "~/renderer/components/Ellipsis";
+import {
+ OpDetailsData,
+ OpDetailsSection,
+ OpDetailsTitle,
+} from "~/renderer/drawers/OperationDetails/styledComponents";
+
+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/modals/AddAccounts/steps/StepChooseCurrency.tsx b/apps/ledger-live-desktop/src/renderer/modals/AddAccounts/steps/StepChooseCurrency.tsx
index 535310839afb..639ce7ccf863 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
@@ -80,6 +80,7 @@ const StepChooseCurrency = ({ currency, setCurrency }: StepProps) => {
const scroll = useFeature("currencyScroll");
const scrollSepolia = useFeature("currencyScrollSepolia");
const icon = useFeature("currencyIcon");
+ const ton = useFeature("currencyTon");
const featureFlaggedCurrencies = useMemo(
(): Partial | null>> => ({
@@ -122,6 +123,7 @@ const StepChooseCurrency = ({ currency, setCurrency }: StepProps) => {
neon_evm: neonEvm,
lukso,
linea,
+ ton,
linea_sepolia: lineaSepolia,
blast,
blast_sepolia: blastSepolia,
@@ -169,6 +171,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 0cef4f748299..35047370b80e 100644
--- a/apps/ledger-live-desktop/static/i18n/en/app.json
+++ b/apps/ledger-live-desktop/static/i18n/en/app.json
@@ -5174,6 +5174,10 @@
"memo": "Memo",
"memoWarningText": "When using a Memo, carefully verify the type used with the recipient"
},
+ "ton": {
+ "commentPlaceholder": "Optional",
+ "comment": "Comment"
+ },
"stellar": {
"memo": "Memo",
"memoType": {
@@ -5720,6 +5724,9 @@
"title": "Sequence number error",
"description": "Please close the window and try again later"
},
+ "TonCommentInvalid": {
+ "title": "Comment must not exceed 120 characters without special characters"
+ },
"CantScanQRCode": {
"title": "Couldn't scan this QR code: auto-verification not supported by this address"
},
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/ios/Podfile.lock b/apps/ledger-live-mobile/ios/Podfile.lock
index b074a321eefd..20e63575bfe3 100644
--- a/apps/ledger-live-mobile/ios/Podfile.lock
+++ b/apps/ledger-live-mobile/ios/Podfile.lock
@@ -47,6 +47,8 @@ PODS:
- React-NativeModulesApple
- React-RCTAppDelegate
- ReactCommon/turbomodule/core
+ - ExpoRandom (13.6.0):
+ - ExpoModulesCore
- FBLazyVector (0.73.6)
- FBReactNativeSpec (0.73.6):
- RCT-Folly (= 2022.05.16.00)
@@ -1098,6 +1100,8 @@ PODS:
- React-Core
- react-native-fast-crypto (2.2.0):
- React
+ - react-native-fast-pbkdf2 (0.3.1):
+ - React-Core
- react-native-flipper (0.163.0):
- React-Core
- react-native-flipper-performance-plugin (0.4.0):
@@ -1418,6 +1422,7 @@ DEPENDENCIES:
- "ExpoImageManipulator (from `../../../node_modules/.pnpm/expo-image-manipulator@11.8.0_expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24._sgbzbk7nfhtlihtns5cja4huei/node_modules/expo-image-manipulator/ios`)"
- "ExpoKeepAwake (from `../../../node_modules/.pnpm/expo-keep-awake@12.8.2_expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react_gqhplzyroote7enqgwhytprwhq/node_modules/expo-keep-awake/ios`)"
- "ExpoModulesCore (from `../../../node_modules/.pnpm/expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react@18.2.0__react@18.2.0/node_modules/expo-modules-core`)"
+ - "ExpoRandom (from `../../../node_modules/.pnpm/expo-random@13.6.0_expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react@18._74hzmsxcfy4hq2gp3ztpqsecuu/node_modules/expo-random/ios`)"
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
- FBReactNativeSpec (from `../node_modules/react-native/React/FBReactNativeSpec`)
- Flipper (= 0.201.0)
@@ -1474,6 +1479,7 @@ DEPENDENCIES:
- react-native-ble-plx (from `../node_modules/react-native-ble-plx`)
- react-native-config (from `../node_modules/react-native-config`)
- react-native-fast-crypto (from `../node_modules/react-native-fast-crypto`)
+ - react-native-fast-pbkdf2 (from `../node_modules/react-native-fast-pbkdf2`)
- react-native-flipper (from `../node_modules/react-native-flipper`)
- react-native-flipper-performance-plugin (from `../node_modules/react-native-flipper-performance-plugin`)
- react-native-get-random-values (from `../node_modules/react-native-get-random-values`)
@@ -1607,6 +1613,8 @@ EXTERNAL SOURCES:
:path: "../../../node_modules/.pnpm/expo-keep-awake@12.8.2_expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react_gqhplzyroote7enqgwhytprwhq/node_modules/expo-keep-awake/ios"
ExpoModulesCore:
:path: "../../../node_modules/.pnpm/expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react@18.2.0__react@18.2.0/node_modules/expo-modules-core"
+ ExpoRandom:
+ :path: "../../../node_modules/.pnpm/expo-random@13.6.0_expo-modules-core@1.11.12_react-native@0.73.6_@babel+core@7.24.3_react@18._74hzmsxcfy4hq2gp3ztpqsecuu/node_modules/expo-random/ios"
FBLazyVector:
:path: "../node_modules/react-native/Libraries/FBLazyVector"
FBReactNativeSpec:
@@ -1670,6 +1678,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/react-native-config"
react-native-fast-crypto:
:path: "../node_modules/react-native-fast-crypto"
+ react-native-fast-pbkdf2:
+ :path: "../node_modules/react-native-fast-pbkdf2"
react-native-flipper:
:path: "../node_modules/react-native-flipper"
react-native-flipper-performance-plugin:
@@ -1818,6 +1828,7 @@ SPEC CHECKSUMS:
ExpoImageManipulator: c1d7cb865eacd620a35659f3da34c70531f10b59
ExpoKeepAwake: 0f5cad99603a3268e50af9a6eb8b76d0d9ac956c
ExpoModulesCore: 61dc57c6e2a35f2f84baf488146db624e03af4cd
+ ExpoRandom: f0cd58e154e463d913462f3b445870b12d1c2f12
FBLazyVector: f64d1e2ea739b4d8f7e4740cde18089cd97fe864
FBReactNativeSpec: 7351d9daa8a692bc3af6eb00a56e4cdb07403431
Firebase: 5f8193dff4b5b7c5d5ef72ae54bb76c08e2b841d
@@ -1844,7 +1855,7 @@ SPEC CHECKSUMS:
libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009
lottie-ios: fcb5e73e17ba4c983140b7d21095c834b3087418
lottie-react-native: 13cd0c4782c3e6bb26bfa4cc2d08bfb84f6d1ab6
- MultiplatformBleAdapter: b1fddd0d499b96b607e00f0faa8e60648343dc1d
+ MultiplatformBleAdapter: ea8bac405ec200d0ca9de0f89afef6f06fb2abbc
nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
OpenSSL-Universal: ebc357f1e6bc71fa463ccb2fe676756aff50e88c
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
@@ -1874,6 +1885,7 @@ SPEC CHECKSUMS:
react-native-ble-plx: c040d0123518e121bf4cda02061bf72644f68d15
react-native-config: 86038147314e2e6d10ea9972022aa171e6b1d4d8
react-native-fast-crypto: 5943c42466b86ad70be60d3a5f64bd22251e5d9e
+ react-native-fast-pbkdf2: 44d6ffa0346863e14100294004a1595ec76b2e9f
react-native-flipper: 2d552a8178d839ef378220101fb7f0cd5b2a8003
react-native-flipper-performance-plugin: 42ec5017abd26e7c5a1f527f2db92c14a90cabdb
react-native-get-random-values: 21325b2244dfa6b58878f51f9aa42821e7ba3d06
diff --git a/apps/ledger-live-mobile/package.json b/apps/ledger-live-mobile/package.json
index a12b392987d5..5cbe844af72d 100644
--- a/apps/ledger-live-mobile/package.json
+++ b/apps/ledger-live-mobile/package.json
@@ -145,6 +145,7 @@
"expo-keep-awake": "~12.8.2",
"expo-modules-autolinking": "^1.10.2",
"expo-modules-core": "^1.11.8",
+ "expo-random": "^13.6.0",
"fuse.js": "^6.4.6",
"hoist-non-react-statics": "3.3.2",
"i18next": "20.6.1",
@@ -171,6 +172,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 2b81da03310a..8b183cfc3c12 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 && (
+
+ )}
+
+
+
+
+
+
+
+ );
+}
+
+const options = {
+ title: i18next.t("send.summary.comment"),
+ headerLeft: undefined,
+};
+export { TonEditComment as component, options };
+const styles = StyleSheet.create({
+ root: {
+ flex: 1,
+ },
+ body: {
+ flexDirection: "column",
+ flex: 1,
+ },
+ textInputAS: {
+ padding: 16,
+ fontSize: 24,
+ },
+ buttonContainer: {
+ marginHorizontal: 16,
+ },
+ flex: {
+ flex: 1,
+ flexDirection: "column",
+ justifyContent: "flex-end",
+ paddingBottom: 16,
+ },
+});
diff --git a/apps/ledger-live-mobile/src/families/ton/SendRowComment.tsx b/apps/ledger-live-mobile/src/families/ton/SendRowComment.tsx
new file mode 100644
index 000000000000..e9fd4f6b546c
--- /dev/null
+++ b/apps/ledger-live-mobile/src/families/ton/SendRowComment.tsx
@@ -0,0 +1,80 @@
+import type { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types";
+import type { Account } from "@ledgerhq/types-live";
+import { useNavigation, useRoute, useTheme } from "@react-navigation/native";
+import React, { useCallback } from "react";
+import { Trans } from "react-i18next";
+import { StyleSheet, View } from "react-native";
+import LText from "~/components/LText";
+import { SendFundsNavigatorStackParamList } from "~/components/RootNavigator/types/SendFundsNavigator";
+import { SignTransactionNavigatorParamList } from "~/components/RootNavigator/types/SignTransactionNavigator";
+import { SwapNavigatorParamList } from "~/components/RootNavigator/types/SwapNavigator";
+import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
+import { ScreenName } from "~/const";
+import SummaryRow from "~/screens/SendFunds/SummaryRow";
+
+type Navigation = BaseComposite<
+ | StackNavigatorProps
+ | StackNavigatorProps
+ | StackNavigatorProps
+>;
+
+type Props = {
+ account: Account;
+ transaction: TonTransaction;
+} & Navigation;
+export default function TonCommentRow({ account, transaction }: Props) {
+ const { colors } = useTheme();
+ const navigation = useNavigation();
+ const route = useRoute();
+ const editComment = useCallback(() => {
+ navigation.navigate(ScreenName.TonEditComment, {
+ ...route.params,
+ accountId: account.id,
+ parentId: undefined,
+ account,
+ transaction,
+ });
+ }, [navigation, route.params, account, transaction]);
+ const comment = transaction.comment;
+
+ return (
+
+ } onPress={editComment}>
+ {!comment || comment.isEncrypted || !transaction.comment.text ? (
+
+
+
+ ) : (
+
+ {String(comment.text)}
+
+ )}
+
+
+ );
+}
+const styles = StyleSheet.create({
+ commentContainer: {
+ flexDirection: "row",
+ },
+ tagText: {
+ fontSize: 14,
+ },
+ link: {
+ textDecorationStyle: "solid",
+ textDecorationLine: "underline",
+ marginLeft: 8,
+ },
+ comment: {
+ marginBottom: 10,
+ },
+});
diff --git a/apps/ledger-live-mobile/src/families/ton/SendRowsCustom.tsx b/apps/ledger-live-mobile/src/families/ton/SendRowsCustom.tsx
new file mode 100644
index 000000000000..d2d61f03cc6e
--- /dev/null
+++ b/apps/ledger-live-mobile/src/families/ton/SendRowsCustom.tsx
@@ -0,0 +1,25 @@
+import { Transaction as TonTransaction } from "@ledgerhq/live-common/families/ton/types";
+import type { Transaction } from "@ledgerhq/live-common/generated/types";
+import type { Account } from "@ledgerhq/types-live";
+import React from "react";
+import { SendFundsNavigatorStackParamList } from "~/components/RootNavigator/types/SendFundsNavigator";
+import { SignTransactionNavigatorParamList } from "~/components/RootNavigator/types/SignTransactionNavigator";
+import { SwapNavigatorParamList } from "~/components/RootNavigator/types/SwapNavigator";
+import { BaseComposite, StackNavigatorProps } from "~/components/RootNavigator/types/helpers";
+import { ScreenName } from "~/const";
+import SendRowComment from "./SendRowComment";
+
+type Navigation = BaseComposite<
+ | StackNavigatorProps
+ | StackNavigatorProps
+ | StackNavigatorProps
+>;
+
+type Props = {
+ transaction: Transaction;
+ account: Account;
+} & Navigation;
+export default function TonSendRowsCustom(props: Props) {
+ const { transaction, ...rest } = props;
+ return ;
+}
diff --git a/apps/ledger-live-mobile/src/families/ton/index.ts b/apps/ledger-live-mobile/src/families/ton/index.ts
new file mode 100644
index 000000000000..f24002520a0e
--- /dev/null
+++ b/apps/ledger-live-mobile/src/families/ton/index.ts
@@ -0,0 +1,3 @@
+import * as TonEditComment from "./ScreenEditComment";
+
+export { TonEditComment };
diff --git a/apps/ledger-live-mobile/src/families/ton/operationDetails.tsx b/apps/ledger-live-mobile/src/families/ton/operationDetails.tsx
new file mode 100644
index 000000000000..e68260a32b97
--- /dev/null
+++ b/apps/ledger-live-mobile/src/families/ton/operationDetails.tsx
@@ -0,0 +1,25 @@
+import { TonOperation } from "@ledgerhq/live-common/families/ton/types";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import Section from "~/screens/OperationDetails/Section";
+
+type Props = {
+ operation: TonOperation;
+};
+
+function OperationDetailsExtra({ operation }: Props) {
+ const { t } = useTranslation();
+ return (
+ <>
+ {operation.extra.comment &&
+ !operation.extra.comment.isEncrypted &&
+ operation.extra.comment.text && (
+
+ )}
+ >
+ );
+}
+
+export default {
+ OperationDetailsExtra,
+};
diff --git a/apps/ledger-live-mobile/src/live-common-setup.ts b/apps/ledger-live-mobile/src/live-common-setup.ts
index d13e99f3db49..e6e4e721f9ad 100644
--- a/apps/ledger-live-mobile/src/live-common-setup.ts
+++ b/apps/ledger-live-mobile/src/live-common-setup.ts
@@ -125,6 +125,7 @@ setSupportedCurrencies([
"blast_sepolia",
"scroll",
"scroll_sepolia",
+ "ton",
]);
if (Config.BLE_LOG_LEVEL) BluetoothTransport.setLogLevel(Config.BLE_LOG_LEVEL);
diff --git a/apps/ledger-live-mobile/src/locales/en/common.json b/apps/ledger-live-mobile/src/locales/en/common.json
index ab424f7ce706..c98603deb871 100644
--- a/apps/ledger-live-mobile/src/locales/en/common.json
+++ b/apps/ledger-live-mobile/src/locales/en/common.json
@@ -927,6 +927,9 @@
"title": "Sequence number error",
"description": "Please close the window and try again later"
},
+ "TonCommentInvalid": {
+ "title": "Comment must not exceed 120 characters without special characters"
+ },
"TezosUnrevealedAccount": {
"title": "Swap unavailable: Initial send/delegate needed with Tezos account or change pair."
}
@@ -3647,6 +3650,8 @@
"value": "Memo value"
},
"validateMemo": "Validate memo",
+ "comment": "Comment",
+ "validateComment": "Validate comment",
"quantity": "Quantity"
},
"validation": {
diff --git a/apps/ledger-live-mobile/src/screens/AddAccounts/01-SelectCrypto.tsx b/apps/ledger-live-mobile/src/screens/AddAccounts/01-SelectCrypto.tsx
index 7dea88b9be71..a3806464fd17 100644
--- a/apps/ledger-live-mobile/src/screens/AddAccounts/01-SelectCrypto.tsx
+++ b/apps/ledger-live-mobile/src/screens/AddAccounts/01-SelectCrypto.tsx
@@ -102,6 +102,7 @@ export default function AddAccountsSelectCrypto({ navigation, route }: Props) {
const scroll = useFeature("currencyScroll");
const scrollSepolia = useFeature("currencyScrollSepolia");
const icon = useFeature("currencyIcon");
+ const ton = useFeature("currencyTon");
const featureFlaggedCurrencies = useMemo(
(): Partial | null>> => ({
@@ -144,6 +145,7 @@ export default function AddAccountsSelectCrypto({ navigation, route }: Props) {
neon_evm: neonEvm,
lukso,
linea,
+ ton,
linea_sepolia: lineaSepolia,
blast,
blast_sepolia: blastSepolia,
@@ -191,6 +193,7 @@ export default function AddAccountsSelectCrypto({ navigation, route }: Props) {
neonEvm,
lukso,
linea,
+ ton,
lineaSepolia,
blast,
blastSepolia,
diff --git a/libs/coin-framework/src/currencies/__snapshots__/formatCurrencyUnit.test.ts.snap b/libs/coin-framework/src/currencies/__snapshots__/formatCurrencyUnit.test.ts.snap
index 5fa831beb050..f0e049bd042b 100644
--- a/libs/coin-framework/src/currencies/__snapshots__/formatCurrencyUnit.test.ts.snap
+++ b/libs/coin-framework/src/currencies/__snapshots__/formatCurrencyUnit.test.ts.snap
@@ -286,6 +286,8 @@ exports[`formatCurrencyUnit with custom options with locale de-DE should correct
exports[`formatCurrencyUnit with custom options with locale de-DE should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale de-DE should correctly format TON unit (TON) 1`] = `"12.345.678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale de-DE should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale de-DE should correctly format Tezos unit (XTZ) 1`] = `"12.345.678.900,000000- -XTZ"`;
@@ -610,6 +612,8 @@ exports[`formatCurrencyUnit with custom options with locale en-US should correct
exports[`formatCurrencyUnit with custom options with locale en-US should correctly format Syscoin unit (SYS) 1`] = `"0.012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale en-US should correctly format TON unit (TON) 1`] = `"12,345,678.900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale en-US should correctly format Telos unit (TLOS) 1`] = `"0.012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale en-US should correctly format Tezos unit (XTZ) 1`] = `"12,345,678,900.000000- -XTZ"`;
@@ -934,6 +938,8 @@ exports[`formatCurrencyUnit with custom options with locale es-ES should correct
exports[`formatCurrencyUnit with custom options with locale es-ES should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale es-ES should correctly format TON unit (TON) 1`] = `"12.345.678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale es-ES should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale es-ES should correctly format Tezos unit (XTZ) 1`] = `"12.345.678.900,000000- -XTZ"`;
@@ -1258,6 +1264,8 @@ exports[`formatCurrencyUnit with custom options with locale fr-FR should correct
exports[`formatCurrencyUnit with custom options with locale fr-FR should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale fr-FR should correctly format TON unit (TON) 1`] = `"12 345 678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale fr-FR should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale fr-FR should correctly format Tezos unit (XTZ) 1`] = `"12 345 678 900,000000- -XTZ"`;
@@ -1582,6 +1590,8 @@ exports[`formatCurrencyUnit with custom options with locale ja-JP should correct
exports[`formatCurrencyUnit with custom options with locale ja-JP should correctly format Syscoin unit (SYS) 1`] = `"0.012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale ja-JP should correctly format TON unit (TON) 1`] = `"12,345,678.900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale ja-JP should correctly format Telos unit (TLOS) 1`] = `"0.012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale ja-JP should correctly format Tezos unit (XTZ) 1`] = `"12,345,678,900.000000- -XTZ"`;
@@ -1906,6 +1916,8 @@ exports[`formatCurrencyUnit with custom options with locale ko-KR should correct
exports[`formatCurrencyUnit with custom options with locale ko-KR should correctly format Syscoin unit (SYS) 1`] = `"0.012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale ko-KR should correctly format TON unit (TON) 1`] = `"12,345,678.900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale ko-KR should correctly format Telos unit (TLOS) 1`] = `"0.012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale ko-KR should correctly format Tezos unit (XTZ) 1`] = `"12,345,678,900.000000- -XTZ"`;
@@ -2230,6 +2242,8 @@ exports[`formatCurrencyUnit with custom options with locale pt-BR should correct
exports[`formatCurrencyUnit with custom options with locale pt-BR should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale pt-BR should correctly format TON unit (TON) 1`] = `"12.345.678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale pt-BR should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale pt-BR should correctly format Tezos unit (XTZ) 1`] = `"12.345.678.900,000000- -XTZ"`;
@@ -2554,6 +2568,8 @@ exports[`formatCurrencyUnit with custom options with locale ru-RU should correct
exports[`formatCurrencyUnit with custom options with locale ru-RU should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale ru-RU should correctly format TON unit (TON) 1`] = `"12 345 678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale ru-RU should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale ru-RU should correctly format Tezos unit (XTZ) 1`] = `"12 345 678 900,000000- -XTZ"`;
@@ -2878,6 +2894,8 @@ exports[`formatCurrencyUnit with custom options with locale tr-TR should correct
exports[`formatCurrencyUnit with custom options with locale tr-TR should correctly format Syscoin unit (SYS) 1`] = `"0,012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale tr-TR should correctly format TON unit (TON) 1`] = `"12.345.678,900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale tr-TR should correctly format Telos unit (TLOS) 1`] = `"0,012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale tr-TR should correctly format Tezos unit (XTZ) 1`] = `"12.345.678.900,000000- -XTZ"`;
@@ -3202,6 +3220,8 @@ exports[`formatCurrencyUnit with custom options with locale zh-CN should correct
exports[`formatCurrencyUnit with custom options with locale zh-CN should correctly format Syscoin unit (SYS) 1`] = `"0.012345678900000000- -SYS"`;
+exports[`formatCurrencyUnit with custom options with locale zh-CN should correctly format TON unit (TON) 1`] = `"12,345,678.900000000- -TON"`;
+
exports[`formatCurrencyUnit with custom options with locale zh-CN should correctly format Telos unit (TLOS) 1`] = `"0.012345678900000000- -TLOS"`;
exports[`formatCurrencyUnit with custom options with locale zh-CN should correctly format Tezos unit (XTZ) 1`] = `"12,345,678,900.000000- -XTZ"`;
@@ -3526,6 +3546,8 @@ exports[`formatCurrencyUnit with default options should correctly format Stride
exports[`formatCurrencyUnit with default options should correctly format Syscoin unit (SYS) 1`] = `"0.0123456"`;
+exports[`formatCurrencyUnit with default options should correctly format TON unit (TON) 1`] = `"12,345,678"`;
+
exports[`formatCurrencyUnit with default options should correctly format Telos unit (TLOS) 1`] = `"0.0123456"`;
exports[`formatCurrencyUnit with default options should correctly format Tezos unit (XTZ) 1`] = `"12,345,678,900"`;
diff --git a/libs/coin-framework/src/derivation.ts b/libs/coin-framework/src/derivation.ts
index f24e6dcd3b91..2dedd78dc160 100644
--- a/libs/coin-framework/src/derivation.ts
+++ b/libs/coin-framework/src/derivation.ts
@@ -1,12 +1,12 @@
-import invariant from "invariant";
-import { Observable, defer, of, range, empty } from "rxjs";
-import { catchError, switchMap, concatMap, takeWhile, map } from "rxjs/operators";
-import { log } from "@ledgerhq/logs";
+import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/index";
import { TransportStatusError, UserRefusedAddress } from "@ledgerhq/errors";
-import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { getEnv } from "@ledgerhq/live-env";
+import { log } from "@ledgerhq/logs";
+import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets";
import { DerivationMode } from "@ledgerhq/types-live";
-import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/index";
+import invariant from "invariant";
+import { Observable, defer, empty, of, range } from "rxjs";
+import { catchError, concatMap, map, switchMap, takeWhile } from "rxjs/operators";
export type ModeSpec = {
mandatoryEmptyAccountSkip?: number;
@@ -176,6 +176,9 @@ const modes: Readonly>> = Object.freeze(
startsAt: 1,
tag: "third-party",
},
+ ton: {
+ overridesDerivation: "44'/607'/0'/0'/'/0'",
+ },
});
modes as Record; // eslint-disable-line
@@ -196,6 +199,7 @@ const legacyDerivations: Partial>
icon_berlin_testnet: ["icon"],
vechain: ["vechain"],
stacks: ["stacks_wallet"],
+ ton: ["ton"],
ethereum: ["ethM", "ethMM"],
ethereum_classic: ["ethM", "ethMM", "etcM"],
solana: ["solanaMain", "solanaSub"],
@@ -329,6 +333,7 @@ const disableBIP44: Record = {
internet_computer: true,
casper: true,
filecoin: true,
+ ton: true,
};
type SeedInfo = {
purpose: number;
@@ -347,6 +352,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/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..806ec6bdbc9d
--- /dev/null
+++ b/libs/coin-modules/coin-ton/jest.config.js
@@ -0,0 +1,12 @@
+/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
+module.exports = {
+ collectCoverageFrom: ["src/**/*.ts"],
+ coverageDirectory: "coverage",
+ preset: "ts-jest",
+ testEnvironment: "node",
+ testPathIgnorePatterns: ["lib/", "lib-es/"],
+ modulePathIgnorePatterns: [
+ "__tests__/fixtures",
+ "__tests__/integration/bridge.integration.test.ts", // this file is tested at the live-common level
+ ],
+};
diff --git a/libs/coin-modules/coin-ton/package.json b/libs/coin-modules/coin-ton/package.json
new file mode 100644
index 000000000000..d3727cb10fa0
--- /dev/null
+++ b/libs/coin-modules/coin-ton/package.json
@@ -0,0 +1,88 @@
+{
+ "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/cryptoassets": "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:^",
+ "@ton/core": "^0.56.1",
+ "@ton/ton": "^13.11.1",
+ "@ton/crypto": "^3.2.0",
+ "bignumber.js": "^9.1.2",
+ "expect": "^27.4.6",
+ "imurmurhash": "^0.1.4",
+ "invariant": "^2.2.2",
+ "lodash": "^4.17.21",
+ "msw": "^2.0.11",
+ "rxjs": "^7.8.1"
+ },
+ "devDependencies": {
+ "@types/imurmurhash": "^0.1.4",
+ "@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",
+ "coverage": "jest --coverage --testPathIgnorePatterns='/bridge.integration.test.ts|node_modules|lib-es|lib/' --passWithNoTests && mv coverage/coverage-final.json coverage/coverage-ton.json",
+ "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",
+ "prettier": "prettier --write 'src/**/*.?s'",
+ "test": "jest",
+ "unimported": "unimported"
+ }
+}
\ No newline at end of file
diff --git a/libs/coin-modules/coin-ton/src/__tests__/fixtures/api.fixtures.ts b/libs/coin-modules/coin-ton/src/__tests__/fixtures/api.fixtures.ts
new file mode 100644
index 000000000000..296b0a347be8
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/fixtures/api.fixtures.ts
@@ -0,0 +1,39 @@
+import { HttpResponse, http } from "msw";
+import { setupServer } from "msw/node";
+import {
+ lastBlockNumber,
+ tonAccount,
+ tonEstimateFee,
+ tonTransactionResponse,
+ tonWallet,
+} from "./common.fixtures";
+
+// Define the mock base URL for the TON API
+export const API_TON_ENDPOINT = "https://ton.coin.ledger.com/api/v3";
+
+// Create request handlers for the mock server
+const handlers = [
+ // Handle GET request for masterchainInfo endpoint
+ http.get(`${API_TON_ENDPOINT}/masterchainInfo`, () => {
+ return HttpResponse.json(lastBlockNumber);
+ }),
+ // Handle GET request for transactions endpoint
+ http.get(`${API_TON_ENDPOINT}/transactions`, () => {
+ return HttpResponse.json(tonTransactionResponse);
+ }),
+ // Handle GET request for account endpoint
+ http.get(`${API_TON_ENDPOINT}/account`, () => {
+ return HttpResponse.json(tonAccount);
+ }),
+ // Handle GET request for wallet endpoint
+ http.get(`${API_TON_ENDPOINT}/wallet`, () => {
+ return HttpResponse.json(tonWallet);
+ }),
+ // Handle POST request for estimate fee endpoint
+ http.post(`${API_TON_ENDPOINT}/estimateFee`, () => HttpResponse.json(tonEstimateFee)),
+];
+
+// Set up the mock server with the defined handlers
+const mockServer = setupServer(...handlers);
+
+export default mockServer;
diff --git a/libs/coin-modules/coin-ton/src/__tests__/fixtures/common.fixtures.ts b/libs/coin-modules/coin-ton/src/__tests__/fixtures/common.fixtures.ts
new file mode 100644
index 000000000000..4a1a08715e5b
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/fixtures/common.fixtures.ts
@@ -0,0 +1,139 @@
+import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
+import { Account } from "@ledgerhq/types-live";
+import BigNumber from "bignumber.js";
+import {
+ TonAccountInfo,
+ TonResponseEstimateFee,
+ TonResponseWalletInfo,
+ TonTransactionsList,
+} from "../../bridge/bridgeHelpers/api.types";
+import type { Transaction } from "../../types";
+
+export const mockAddress = "UQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbMol";
+export const mockAccountId =
+ "js:2:ton:b19891a06654f21c64147550b3321bef63acd25b5dd61b688b022c42fac4831d:ton";
+
+export const account = {
+ id: mockAccountId,
+ freshAddress: mockAddress,
+ freshAddressPath: "44'/607'/0'/0'/0'/0'",
+ xpub: "",
+ type: "Account",
+ currency: getCryptoCurrencyById("ton"),
+ spendableBalance: new BigNumber("1000000000"),
+ balance: new BigNumber("1000000000"),
+ seedIdentifier: "seedIdentifier",
+} as Account;
+
+export const transaction = {
+ mode: "send",
+ recipient: "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ amount: new BigNumber("1000000"),
+ useAllAmount: false,
+ comment: { isEncrypted: false, text: "" },
+ payload: "",
+ family: "ton",
+} as unknown as Transaction;
+
+export const fees = {
+ in_fwd_fee: 10000,
+ storage_fee: 10000,
+ gas_fee: 10000,
+ fwd_fee: 10000,
+};
+
+export const totalFees = BigNumber(
+ fees.fwd_fee + fees.gas_fee + fees.in_fwd_fee + fees.storage_fee,
+);
+
+export const lastBlockNumber = {
+ last: {
+ seqno: 38574413,
+ },
+ first: {
+ seqno: 3,
+ },
+};
+
+export const tonAccount: TonAccountInfo = {
+ balance: "1000000000",
+ last_transaction_lt: "47055058000008",
+ last_transaction_hash: "psVQqt6rf/Lo6xyLzxx0to0jUIx8I2/4znOVf2KhAI0=",
+ status: "active",
+ seqno: 3,
+};
+
+export const tonWallet: TonResponseWalletInfo = {
+ balance: "7726736262",
+ wallet_type: "wallet v4 r2",
+ seqno: 22,
+ wallet_id: 698983191,
+ last_transaction_lt: "47055058000008",
+ last_transaction_hash: "psVQqt6rf/Lo6xyLzxx0to0jUIx8I2/4znOVf2KhAI0=",
+ status: "active",
+};
+
+export const tonEstimateFee: TonResponseEstimateFee = {
+ source_fees: fees,
+ destination_fees: [],
+};
+
+export const tonTransactionResponse: TonTransactionsList = {
+ transactions: [
+ {
+ account: mockAddress,
+ hash: "hash",
+ lt: "lt",
+ now: 1718241443,
+ orig_status: "active",
+ end_status: "active",
+ total_fees: "0",
+ prev_trans_hash: "",
+ prev_trans_lt: "",
+ description: {
+ aborted: true,
+ destroyed: false,
+ compute_ph: {
+ success: false,
+ exit_code: -14,
+ },
+ credit_first: true,
+ },
+ block_ref: null,
+ in_msg: {
+ source: "0:959EAA8BD0E3A2662D814278D51A6F997946207D48478008BEBE7F45F3EF781F",
+ destination: mockAddress,
+ value: "13509565",
+ hash: "inMsgHash",
+ fwd_fee: "266669",
+ ihr_fee: "0",
+ created_lt: "47055058000007",
+ created_at: "1718241443",
+ opcode: "0xd53276db",
+ ihr_disabled: true,
+ bounce: false,
+ bounced: false,
+ import_fee: null,
+ message_content: {
+ hash: "Qa0w2xg42wA9taurO/aCVOqGTzjOeP3EpzD2Sl7tTss=",
+ body: "te6cckEBAQEADgAAGNUydtsAAAAAAAAAAfRC8y4=",
+ decoded: null,
+ },
+ init_state: null,
+ },
+ out_msgs: [],
+ account_state_before: null,
+ account_state_after: null,
+ mc_block_seqno: 3,
+ },
+ ],
+ address_book: {},
+};
+
+export const accountInfo: TonAccountInfo = {
+ balance: "7726736262",
+ last_transaction_lt: "47055058000008",
+ last_transaction_hash: "lastTransactionHash",
+ status: "active",
+ seqno: 22,
+};
diff --git a/libs/coin-modules/coin-ton/src/__tests__/integration/bridge.integration.test.ts b/libs/coin-modules/coin-ton/src/__tests__/integration/bridge.integration.test.ts
new file mode 100644
index 000000000000..f258b6399605
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/integration/bridge.integration.test.ts
@@ -0,0 +1,135 @@
+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: [
+ {
+ FIXME_tests: ["balance is sum of ops"],
+ raw: {
+ id: `js:2:ton:${PUBKEY}:ton`,
+ currencyId: "ton",
+ seedIdentifier: PUBKEY,
+ name: "TON 1",
+ derivationMode: "ton",
+ index: 0,
+ freshAddress: ADDRESS,
+ freshAddressPath: PATH,
+ xpub: PUBKEY,
+ blockHeight: 0,
+ operations: [],
+ pendingOperations: [],
+ lastSyncDate: "",
+ balance: "5000000000",
+ subAccounts: [],
+ },
+ 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,
+ },
+};
+
+/**
+ * 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/coin-modules/coin-ton/src/__tests__/unit/api.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/api.unit.test.ts
new file mode 100644
index 000000000000..ef29c1f3877c
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/api.unit.test.ts
@@ -0,0 +1,60 @@
+import {
+ estimateFee,
+ fetchAccountInfo,
+ fetchLastBlockNumber,
+ fetchTransactions,
+} from "../../bridge/bridgeHelpers/api";
+import { setCoinConfig } from "../../config";
+import mockServer, { API_TON_ENDPOINT } from "../fixtures/api.fixtures";
+import {
+ lastBlockNumber,
+ mockAddress,
+ tonAccount,
+ tonEstimateFee,
+ tonTransactionResponse,
+ tonWallet,
+} from "../fixtures/common.fixtures";
+
+describe("getAccount", () => {
+ beforeAll(() => {
+ setCoinConfig(() => ({
+ status: {
+ type: "active",
+ },
+ infra: {
+ API_TON_ENDPOINT: API_TON_ENDPOINT,
+ },
+ }));
+ mockServer.listen();
+ });
+
+ afterAll(() => {
+ mockServer.close();
+ });
+
+ it("should return last block number", async () => {
+ const result = await fetchLastBlockNumber();
+ expect(result).toEqual(lastBlockNumber.last.seqno);
+ });
+
+ it("should return the transactions of an address", async () => {
+ const result = await fetchTransactions(mockAddress);
+ expect(result).toEqual(tonTransactionResponse);
+ });
+
+ it("should return the ton account info of an address", async () => {
+ const result = await fetchAccountInfo(mockAddress);
+ expect(result).toEqual({
+ balance: tonAccount.balance,
+ last_transaction_lt: tonAccount.last_transaction_lt,
+ last_transaction_hash: tonAccount.last_transaction_hash,
+ status: tonAccount.status,
+ seqno: tonWallet.seqno,
+ });
+ });
+
+ it("should return the estimated fees", async () => {
+ const result = await estimateFee(mockAddress, "");
+ expect(result).toEqual(tonEstimateFee.source_fees);
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/broadcast.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/broadcast.unit.test.ts
new file mode 100644
index 000000000000..c35da8df8df4
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/broadcast.unit.test.ts
@@ -0,0 +1,36 @@
+import { encodeOperationId } from "@ledgerhq/coin-framework/lib/operation";
+import { broadcastTx } from "../../bridge/bridgeHelpers/api";
+import broadcast from "../../broadcast";
+import { buildOptimisticOperation } from "../../signOperation";
+import { account, transaction } from "../fixtures/common.fixtures";
+
+jest.mock("../../bridge/bridgeHelpers/api");
+const mockedHash = "validHash";
+
+describe("broadcast", () => {
+ beforeAll(() => {
+ const broadcastTxMock = jest.mocked(broadcastTx);
+ broadcastTxMock.mockReturnValue(Promise.resolve(mockedHash));
+ });
+
+ it("should broadcast the coin transaction and add the hash in the optimistic transaction", async () => {
+ const optimisticCoinOperation = buildOptimisticOperation(account, transaction);
+
+ const finalOperation = await broadcast({
+ account,
+ signedOperation: {
+ operation: optimisticCoinOperation,
+ signature: "0xS1gn4tUR3",
+ },
+ });
+
+ expect(broadcastTx).toHaveBeenCalled();
+ expect(finalOperation).toEqual({
+ ...optimisticCoinOperation,
+ id: encodeOperationId(account.id, mockedHash, "OUT"),
+ hash: mockedHash,
+ subOperations: [],
+ nftOperations: [],
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/createTransaction.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/createTransaction.unit.test.ts
new file mode 100644
index 000000000000..09cde74f8c66
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/createTransaction.unit.test.ts
@@ -0,0 +1,20 @@
+import BigNumber from "bignumber.js";
+import createTransaction from "../../createTransaction";
+import { account } from "../fixtures/common.fixtures";
+
+describe("createTransaction", () => {
+ it("should create a valid transaction", async () => {
+ const res = createTransaction(account);
+ expect(res).toEqual({
+ family: "ton",
+ amount: new BigNumber(0),
+ fees: new BigNumber(0),
+ recipient: "",
+ useAllAmount: false,
+ comment: {
+ isEncrypted: false,
+ text: "",
+ },
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/deviceTransactionConfig.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/deviceTransactionConfig.unit.test.ts
new file mode 100644
index 000000000000..bc74b8857a57
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/deviceTransactionConfig.unit.test.ts
@@ -0,0 +1,69 @@
+import BigNumber from "bignumber.js";
+import getDeviceTransactionConfig from "../../deviceTransactionConfig";
+import { account, transaction as baseTransaction } from "../fixtures/common.fixtures";
+
+const status = {
+ errors: {},
+ warnings: {},
+ estimatedFees: new BigNumber(0),
+ amount: new BigNumber(0),
+ totalSpent: new BigNumber(0),
+};
+
+describe("deviceTransactionConfig", () => {
+ describe("TON transaction", () => {
+ it("should return the fields for a transaction when there is a valid comment", async () => {
+ const transaction = {
+ ...baseTransaction,
+ comment: { isEncrypted: false, text: "validComment" },
+ };
+ const res = await getDeviceTransactionConfig({
+ account: account,
+ parentAccount: undefined,
+ transaction,
+ status,
+ });
+ expect(res).toEqual([
+ {
+ type: "address",
+ label: "To",
+ address: transaction.recipient,
+ },
+ {
+ type: "amount",
+ label: "Amount",
+ },
+ { type: "fees", label: "Fee" },
+ { type: "text", label: "Comment", value: "validComment" },
+ ]);
+ });
+
+ it("should return the fields for a transaction when useAllAmount is true and there is a valid comment", async () => {
+ const transaction = {
+ ...baseTransaction,
+ useAllAmount: true,
+ comment: { isEncrypted: false, text: "validComment" },
+ };
+ const res = await getDeviceTransactionConfig({
+ account: account,
+ parentAccount: undefined,
+ transaction,
+ status,
+ });
+ expect(res).toEqual([
+ {
+ type: "address",
+ label: "To",
+ address: transaction.recipient,
+ },
+ {
+ type: "text",
+ label: "Amount",
+ value: "ALL YOUR TONs",
+ },
+ { type: "fees", label: "Fee" },
+ { type: "text", label: "Comment", value: "validComment" },
+ ]);
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/estimateMaxSpendable.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/estimateMaxSpendable.unit.test.ts
new file mode 100644
index 000000000000..fcd3639961fa
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/estimateMaxSpendable.unit.test.ts
@@ -0,0 +1,19 @@
+import { estimateFee, fetchAccountInfo } from "../../bridge/bridgeHelpers/api";
+import estimateMaxSpendable from "../../estimateMaxSpendable";
+import { account, accountInfo, fees, totalFees, transaction } from "../fixtures/common.fixtures";
+
+jest.mock("../../bridge/bridgeHelpers/api");
+
+describe("estimateMaxSpendable", () => {
+ beforeAll(() => {
+ const fetchAccountInfoMock = jest.mocked(fetchAccountInfo);
+ fetchAccountInfoMock.mockReturnValue(Promise.resolve(accountInfo));
+ const fetchEstimateFeeMock = jest.mocked(estimateFee);
+ fetchEstimateFeeMock.mockReturnValue(Promise.resolve(fees));
+ });
+
+ it("should return the max spendable for a TON transaction", async () => {
+ const res = await estimateMaxSpendable({ account, transaction });
+ expect(res).toEqual(account.balance.minus(totalFees));
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/getTransactionStatus.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/getTransactionStatus.unit.test.ts
new file mode 100644
index 000000000000..dc869ff09988
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/getTransactionStatus.unit.test.ts
@@ -0,0 +1,119 @@
+import {
+ AmountRequired,
+ InvalidAddress,
+ InvalidAddressBecauseDestinationIsAlsoSource,
+ NotEnoughBalance,
+ RecipientRequired,
+} from "@ledgerhq/errors";
+import BigNumber from "bignumber.js";
+import { TonCommentInvalid } from "../../errors";
+import getTransactionStatus from "../../getTransactionStatus";
+import { account, transaction as baseTransaction } from "../fixtures/common.fixtures";
+
+describe("getTransactionStatus", () => {
+ describe("Recipient", () => {
+ it("should detect the missing recipient and have an error", async () => {
+ const transaction = { ...baseTransaction, recipient: "" };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ recipient: new RecipientRequired(),
+ }),
+ );
+ });
+
+ it("should detect the incorrect recipient and have an error", async () => {
+ const transaction = { ...baseTransaction, recipient: "isInvalid" };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ recipient: new InvalidAddress("", {
+ currencyName: account.currency.name,
+ }),
+ }),
+ );
+ });
+
+ it("should detect the recipient and the sender are the same and have an error", async () => {
+ const transaction = {
+ ...baseTransaction,
+ recipient: "UQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbMol",
+ };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ recipient: new InvalidAddressBecauseDestinationIsAlsoSource("", {
+ currencyName: account.currency.name,
+ }),
+ }),
+ );
+ });
+ });
+
+ describe("Sender", () => {
+ it("should detect the sender is not correct and have an error", async () => {
+ const tempAccount = { ...account, freshAddress: "isInvalid" };
+ const res = await getTransactionStatus(tempAccount, baseTransaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ sender: new InvalidAddress(),
+ }),
+ );
+ });
+ });
+
+ describe("Amount", () => {
+ it("should detect the amount is missing and have an error", async () => {
+ const transaction = { ...baseTransaction, amount: new BigNumber(0) };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ amount: new AmountRequired(),
+ }),
+ );
+ });
+
+ it("should detect the amount is greater than the spendable amount and have an error", async () => {
+ const transaction = {
+ ...baseTransaction,
+ amount: BigNumber(1000000002),
+ fees: new BigNumber("20"),
+ };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ amount: new NotEnoughBalance(),
+ }),
+ );
+ });
+
+ describe("Comment", () => {
+ it("should detect the comment is not valid and have an error", async () => {
+ const transaction = {
+ ...baseTransaction,
+ comment: { isEncrypted: false, text: "comment\nInvalid" },
+ };
+ const res = await getTransactionStatus(account, transaction);
+ expect(res.errors).toEqual(
+ expect.objectContaining({
+ comment: new TonCommentInvalid(),
+ }),
+ );
+ });
+ });
+
+ describe("Successful transaction", () => {
+ it("should not have errors", async () => {
+ const successfulResult = {
+ amount: baseTransaction.amount,
+ errors: {},
+ warnings: {},
+ estimatedFees: baseTransaction.fees,
+ totalSpent: baseTransaction.amount.plus(baseTransaction.fees),
+ };
+ const res = await getTransactionStatus(account, baseTransaction);
+ expect(res).toEqual(successfulResult);
+ });
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/hw-getAddress.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/hw-getAddress.unit.test.ts
new file mode 100644
index 000000000000..81552ba1e131
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/hw-getAddress.unit.test.ts
@@ -0,0 +1,58 @@
+import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
+import resolver from "../../hw-getAddress";
+import { TonSigner } from "../../signer";
+import { getLedgerTonPath } from "../../utils";
+
+const address = "0xc3f95102d5c8f2c83e49ce3acfb905edfb7f37de";
+const publicKey = "mockedPublicKey";
+const spyGetAddress = jest.fn().mockImplementation(async () =>
+ Promise.resolve({
+ publicKey,
+ address: address.toLowerCase(),
+ }),
+);
+const spyValidateAddress = jest.fn().mockImplementation(async () =>
+ Promise.resolve({
+ publicKey,
+ address: address.toLowerCase(),
+ }),
+);
+
+const mockSignerFactory = (_: string, fn: (signer: TonSigner) => Promise): Promise =>
+ fn({
+ getAddress: spyGetAddress,
+ validateAddress: spyValidateAddress,
+ signTransaction: jest.fn(),
+ });
+
+describe("hw-getAddress", () => {
+ it("should return an encoded address and a public key when verifiy is false", async () => {
+ const getAddress = resolver(mockSignerFactory);
+ const response = await getAddress("deviceId", {
+ path: "44'/607'/0'/0'/0'/0'",
+ verify: false,
+ currency: getCryptoCurrencyById("ton"),
+ derivationMode: "ton",
+ });
+ expect(response.address).toBe(address);
+ expect(response.publicKey).toBe(publicKey);
+ expect(spyGetAddress).toHaveBeenCalledWith(getLedgerTonPath("44'/607'/0'/0'/0'/0'"), {
+ bounceable: false,
+ });
+ });
+
+ it("should return an encoded address and a public key when verifiy is true", async () => {
+ const getAddress = resolver(mockSignerFactory);
+ const response = await getAddress("deviceId", {
+ path: "44'/607'/0'/0'/0'/0'",
+ verify: true,
+ currency: getCryptoCurrencyById("ton"),
+ derivationMode: "ton",
+ });
+ expect(response.address).toBe(address);
+ expect(response.publicKey).toBe(publicKey);
+ expect(spyValidateAddress).toHaveBeenCalledWith(getLedgerTonPath("44'/607'/0'/0'/0'/0'"), {
+ bounceable: false,
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/prepareTransaction.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/prepareTransaction.unit.test.ts
new file mode 100644
index 000000000000..6da681b83059
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/prepareTransaction.unit.test.ts
@@ -0,0 +1,52 @@
+import { estimateFee, fetchAccountInfo } from "../../bridge/bridgeHelpers/api";
+import prepareTransaction from "../../prepareTransaction";
+import {
+ account,
+ accountInfo,
+ transaction as baseTransaction,
+ fees,
+ totalFees,
+} from "../fixtures/common.fixtures";
+
+jest.mock("../../bridge/bridgeHelpers/api");
+
+describe("prepareTransaction", () => {
+ beforeAll(() => {
+ const fetchAccountInfoMock = jest.mocked(fetchAccountInfo);
+ fetchAccountInfoMock.mockReturnValue(Promise.resolve(accountInfo));
+ const fetchEstimateFeeMock = jest.mocked(estimateFee);
+ fetchEstimateFeeMock.mockReturnValue(Promise.resolve(fees));
+ });
+
+ describe("Ton Transaction", () => {
+ it("should return the transaction with the updated amount and fees", async () => {
+ const transaction = await prepareTransaction(account, baseTransaction);
+
+ expect(transaction).toEqual({
+ ...baseTransaction,
+ fees: totalFees,
+ });
+ });
+
+ it("should preserve the reference when no change is detected on the transaction", async () => {
+ const transaction = await prepareTransaction(account, { ...baseTransaction });
+ const transaction2 = await prepareTransaction(account, transaction);
+
+ expect(transaction).toBe(transaction2);
+ });
+
+ it("should create a coin transaction using the spendableBalance in the account", async () => {
+ const transaction = await prepareTransaction(account, {
+ ...baseTransaction,
+ useAllAmount: true,
+ });
+
+ expect(transaction).toEqual({
+ ...baseTransaction,
+ useAllAmount: true,
+ fees: totalFees,
+ amount: account.spendableBalance.minus(totalFees),
+ });
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/signOperation.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/signOperation.unit.test.ts
new file mode 100644
index 000000000000..9b3be5180ced
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/signOperation.unit.test.ts
@@ -0,0 +1,90 @@
+import { SignerContext } from "@ledgerhq/coin-framework/signer";
+import { Address, Cell, beginCell, storeMessage } from "@ton/core";
+import BigNumber from "bignumber.js";
+import { fetchAccountInfo } from "../../bridge/bridgeHelpers/api";
+import { buildSignOperation } from "../../signOperation";
+import { TonSigner } from "../../signer";
+import { account, accountInfo, totalFees, transaction } from "../fixtures/common.fixtures";
+
+jest.mock("../../bridge/bridgeHelpers/api");
+
+const spySignTransaction = jest.fn().mockImplementation(async () =>
+ Promise.resolve(
+ beginCell()
+ .store(
+ storeMessage({
+ info: {
+ type: "external-in",
+ dest: Address.parse("EQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbJfg"),
+ importFee: BigInt(0),
+ },
+ body: new Cell(),
+ }),
+ )
+ .endCell(),
+ ),
+);
+
+const mockSignerContext: SignerContext = (
+ _: string,
+ fn: (signer: TonSigner) => Promise,
+) => {
+ return fn({
+ signTransaction: spySignTransaction,
+ getAddress: jest.fn(),
+ validateAddress: jest.fn(),
+ });
+};
+
+describe("signOperation", () => {
+ beforeAll(() => {
+ const fetchAccountInfoMock = jest.mocked(fetchAccountInfo);
+ fetchAccountInfoMock.mockReturnValue(Promise.resolve(accountInfo));
+ });
+
+ it("should return an optimistic operation and a signed hash returned by the app bindings", done => {
+ const signOperation = buildSignOperation(mockSignerContext);
+
+ const signOpObservable = signOperation({
+ account,
+ transaction: { ...transaction, fees: totalFees },
+ deviceId: "",
+ });
+
+ signOpObservable.subscribe(obs => {
+ if (obs.type === "signed") {
+ const {
+ signedOperation: { signature, operation },
+ } = obs;
+
+ const { amount } = transaction;
+
+ expect(operation).toEqual({
+ id: "",
+ hash: "",
+ type: "OUT",
+ value: new BigNumber(amount).plus(totalFees),
+ fee: totalFees,
+ blockHash: null,
+ blockHeight: null,
+ senders: [account.freshAddress],
+ recipients: [transaction.recipient],
+ accountId: account.id,
+ date: expect.any(Date),
+ extra: {
+ comment: {
+ isEncrypted: false,
+ text: "",
+ },
+ explorerHash: "",
+ lt: "",
+ },
+ });
+ expect(signature).toBe(
+ "te6cckEBAQEASAAAi4gB5u+NPAnKfR1MYfspXMxuPEc0/C8N9jmU1gWKEgWontgEQA83fGngTlPo6mMP2UrmY3HiOafheG+xzKawLFCQLUT2wCDSMh+F",
+ );
+ done();
+ }
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/txn.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/txn.unit.test.ts
new file mode 100644
index 000000000000..1cb062a186f9
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/txn.unit.test.ts
@@ -0,0 +1,121 @@
+import { encodeOperationId } from "@ledgerhq/coin-framework/lib/operation";
+import BigNumber from "bignumber.js";
+// eslint-disable-next-line no-restricted-imports
+import { flatMap } from "lodash";
+import { TonTransaction } from "../../bridge/bridgeHelpers/api.types";
+import { mapTxToOps } from "../../bridge/bridgeHelpers/txn";
+import { mockAccountId, mockAddress, tonTransactionResponse } from "../fixtures/common.fixtures";
+
+describe("Transaction functions", () => {
+ describe("mapTxToOps", () => {
+ it("should map an IN ton transaction without total_fees to a ledger operation", async () => {
+ const { now, lt, hash, in_msg, total_fees, mc_block_seqno } =
+ tonTransactionResponse.transactions[0];
+
+ const finalOperation = flatMap(
+ tonTransactionResponse.transactions,
+ mapTxToOps(mockAccountId, mockAddress, tonTransactionResponse.address_book),
+ );
+
+ expect(finalOperation).toEqual([
+ {
+ accountId: mockAccountId,
+ blockHash: null,
+ blockHeight: mc_block_seqno,
+ date: new Date(now * 1000), // now is defined in seconds
+ extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
+ fee: BigNumber(total_fees),
+ hasFailed: false,
+ hash: in_msg?.hash,
+ id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "IN"),
+ recipients: [in_msg?.destination],
+ senders: ["EQCVnqqL0OOiZi2BQnjVGm-ZeUYgfUhHgAi-vn9F8-94HwrH"],
+ type: "IN",
+ value: BigNumber(in_msg?.value ?? 0),
+ },
+ ]);
+ });
+
+ it("should map an IN ton transaction with total_fees to a ledger operation", async () => {
+ const transactions = [{ ...tonTransactionResponse.transactions[0], total_fees: "15" }];
+ const { now, lt, hash, in_msg, total_fees, mc_block_seqno, account } = transactions[0];
+
+ const finalOperation = flatMap(
+ transactions,
+ mapTxToOps(mockAccountId, mockAddress, tonTransactionResponse.address_book),
+ );
+
+ expect(finalOperation).toEqual([
+ {
+ id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "NONE"),
+ hash: in_msg?.hash,
+ type: "NONE",
+ value: BigNumber(total_fees),
+ fee: BigNumber(0),
+ blockHash: null,
+ blockHeight: mc_block_seqno,
+ hasFailed: false,
+ accountId: mockAccountId,
+ senders: [account],
+ recipients: [],
+ date: new Date(now * 1000), // now is defined in seconds
+ extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
+ },
+ {
+ accountId: mockAccountId,
+ blockHash: null,
+ blockHeight: mc_block_seqno,
+ date: new Date(now * 1000), // now is defined in seconds
+ extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
+ fee: BigNumber(total_fees),
+ hasFailed: false,
+ hash: in_msg?.hash,
+ id: encodeOperationId(mockAccountId, in_msg?.hash ?? "", "IN"),
+ recipients: [in_msg?.destination],
+ senders: ["EQCVnqqL0OOiZi2BQnjVGm-ZeUYgfUhHgAi-vn9F8-94HwrH"],
+ type: "IN",
+ value: BigNumber(in_msg?.value ?? 0),
+ },
+ ]);
+ });
+
+ it("should map an OUT ton transaction to a ledger operation", async () => {
+ // The IN transaction will be used as OUT transaction and it will be adjusted
+ const transactions: TonTransaction[] = [
+ {
+ ...tonTransactionResponse.transactions[0],
+ in_msg: null,
+ },
+ ];
+ if (tonTransactionResponse.transactions[0].in_msg) {
+ transactions[0].out_msgs = [
+ { ...tonTransactionResponse.transactions[0].in_msg, source: transactions[0].account },
+ ];
+ }
+ const { now, lt, hash, out_msgs, total_fees, mc_block_seqno, account } = transactions[0];
+
+ const finalOperation = flatMap(
+ transactions,
+ mapTxToOps(mockAccountId, mockAddress, tonTransactionResponse.address_book),
+ );
+
+ expect(finalOperation).toEqual([
+ {
+ id: encodeOperationId(mockAccountId, hash ?? "", "OUT"),
+ hash: out_msgs?.[0].hash,
+ type: "OUT",
+ value: BigNumber(out_msgs[0].value ?? 0).plus(BigNumber(total_fees)),
+ fee: BigNumber(total_fees),
+ blockHeight: mc_block_seqno,
+ blockHash: null,
+ hasFailed: false,
+ accountId: mockAccountId,
+ senders: [account],
+ recipients: ["EQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbJfg"],
+ date: new Date(now * 1000), // now is defined in seconds
+ extra: { comment: { isEncrypted: false, text: "" }, explorerHash: hash, lt },
+ },
+ ]);
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/__tests__/unit/utils.unit.test.ts b/libs/coin-modules/coin-ton/src/__tests__/unit/utils.unit.test.ts
new file mode 100644
index 000000000000..064e8b6fa46c
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/__tests__/unit/utils.unit.test.ts
@@ -0,0 +1,126 @@
+import { Address } from "@ton/core";
+import { TonComment } from "../../types";
+import {
+ addressesAreEqual,
+ buildTonTransaction,
+ commentIsValid,
+ getLedgerTonPath,
+ getTransferExpirationTime,
+ isAddressValid,
+} from "../../utils";
+import { transaction as baseTransaction } from "../fixtures/common.fixtures";
+
+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);
+});
+
+describe("Get TON paths", () => {
+ const correctPath = ["44'/607'/0'/0'/0'/0'", "m/44'/607'/0'/0'/0'/0'"];
+ const wrongPaths = [
+ "44'/607'/0'/0'/0'",
+ "44'/607'/0'/0/'/0'",
+ "44'/607'/0'/0'/x'/0'",
+ "44'/607'/0'/-3'/0'/0'",
+ "44'/607'/0'/2147483650'/0'/0'",
+ ];
+
+ test("Correct paths return a correct response", () => {
+ correctPath.map((path: string) => {
+ expect(getLedgerTonPath(path)).toStrictEqual([44, 607, 0, 0, 0, 0]);
+ });
+ });
+
+ test("Wrong paths fail", () => {
+ wrongPaths.map((path: string) => {
+ expect(() => getLedgerTonPath(path)).toThrow(/^(\[ton\] Path)/);
+ });
+ });
+});
+
+describe("Build TON transaction", () => {
+ const seqno = 22;
+
+ test("Build TON transaction with an specific amount", () => {
+ const tonTransaction = buildTonTransaction(baseTransaction, seqno);
+
+ // Convert the Address to string to compare
+ expect({ ...tonTransaction, to: tonTransaction.to.toString() }).toEqual({
+ to: Address.parse(baseTransaction.recipient).toString(),
+ seqno,
+ amount: BigInt(baseTransaction.amount.toString()),
+ bounce: false,
+ timeout: getTransferExpirationTime(),
+ sendMode: 3,
+ });
+ });
+
+ test("Build TON transaction when useAllAmount is true and there is a comment", () => {
+ const transaction = { ...baseTransaction };
+ transaction.useAllAmount = true;
+ transaction.comment.text = "valid coment";
+ const tonTransaction = buildTonTransaction(transaction, seqno);
+
+ // Convert the Address to string to compare
+ expect({ ...tonTransaction, to: tonTransaction.to.toString() }).toEqual({
+ to: Address.parse(transaction.recipient).toString(),
+ seqno,
+ amount: BigInt(0),
+ bounce: false,
+ timeout: getTransferExpirationTime(),
+ sendMode: 128,
+ payload: { type: "comment", text: transaction.comment.text },
+ });
+ });
+});
diff --git a/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.ts
new file mode 100644
index 000000000000..008ec4435760
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.ts
@@ -0,0 +1,104 @@
+import network from "@ledgerhq/live-network";
+import { Address } from "@ton/ton";
+import { getCoinConfig } from "../../config";
+import {
+ TonAccountInfo,
+ TonFee,
+ TonResponseAccountInfo,
+ TonResponseEstimateFee,
+ TonResponseMasterchainInfo,
+ TonResponseMessage,
+ TonResponseWalletInfo,
+ TonTransactionsList,
+} from "./api.types";
+
+const getTonUrl = (path?: string): string => {
+ const currencyConfig = getCoinConfig();
+
+ return `${currencyConfig.infra.API_TON_ENDPOINT}${path ?? ""}`;
+};
+
+const fetch = async (path: string): Promise => {
+ const url = getTonUrl(path);
+
+ const { data } = await network({
+ method: "GET",
+ url,
+ });
+
+ 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: { "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,
+ };
+}
+
+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/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.types.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.types.ts
new file mode 100644
index 000000000000..adab71fdbc62
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/api.types.ts
@@ -0,0 +1,178 @@
+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[];
+}
+
+interface TrComputePhase {
+ exit_code: number;
+ success: boolean;
+}
+
+interface TonDescription {
+ aborted: boolean;
+ compute_ph: TrComputePhase;
+ credit_first: boolean;
+ storage_ph?: unknown;
+ credit_ph?: unknown;
+ action?: unknown;
+ bounce?: unknown;
+ destroyed: boolean;
+}
+
+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: TonDescription;
+ 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 TonFee {
+ in_fwd_fee: number;
+ storage_fee: number;
+ gas_fee: number;
+ fwd_fee: 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: TonAccountStatus;
+}
+
+export interface TonResponseEstimateFee {
+ source_fees: TonFee;
+ destination_fees: TonFee[];
+}
+
+export interface TonResponseMessage {
+ message_hash: string;
+}
diff --git a/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts
new file mode 100644
index 000000000000..e8b19084275c
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/bridge/bridgeHelpers/txn.ts
@@ -0,0 +1,166 @@
+import { encodeOperationId } from "@ledgerhq/coin-framework/operation";
+import { Operation } from "@ledgerhq/types-live";
+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;
+ let isUncompletedResult = true;
+
+ while (isUncompletedResult) {
+ 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) {
+ isUncompletedResult = false;
+ break;
+ }
+ // it should always 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 };
+ }
+ 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: true })];
+}
+
+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
+ const hasFailed =
+ tx.description.compute_ph.success === false && tx.description.compute_ph.exit_code !== 0;
+
+ if (isReceiving) {
+ let subOperations: Operation[] | undefined;
+ if (tx.total_fees !== "0") {
+ // these are small amount of fees payed when receiving
+ // we don't want to show them in the charts
+ subOperations = [
+ {
+ 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,
+ 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,
+ 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
+ : "",
+ },
+ },
+ subOperations,
+ });
+ }
+
+ 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),
+ fee: BigNumber(tx.total_fees),
+ blockHeight: tx.mc_block_seqno ?? 1,
+ blockHash: null,
+ hasFailed,
+ 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/coin-modules/coin-ton/src/bridge/js.ts b/libs/coin-modules/coin-ton/src/bridge/js.ts
new file mode 100644
index 000000000000..b1171ff7a344
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/bridge/js.ts
@@ -0,0 +1,65 @@
+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 broadcast from "../broadcast";
+import { TonCoinConfig, setCoinConfig } from "../config";
+import createTransaction from "../createTransaction";
+import estimateMaxSpendable from "../estimateMaxSpendable";
+import getTransactionStatus from "../getTransactionStatus";
+import resolver from "../hw-getAddress";
+import prepareTransaction from "../prepareTransaction";
+import { buildSignOperation } from "../signOperation";
+import { TonSigner } from "../signer";
+import { getAccountShape, sync } from "../synchronisation";
+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, coinConfig: TonCoinConfig) {
+ setCoinConfig(coinConfig);
+
+ return {
+ currencyBridge: buildCurrencyBridge(signerContext),
+ accountBridge: buildAccountBridge(signerContext),
+ };
+}
diff --git a/libs/coin-modules/coin-ton/src/broadcast.ts b/libs/coin-modules/coin-ton/src/broadcast.ts
new file mode 100644
index 000000000000..d8887d310cdf
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/broadcast.ts
@@ -0,0 +1,13 @@
+import { patchOperationWithHash } from "@ledgerhq/coin-framework/operation";
+import type { AccountBridge } from "@ledgerhq/types-live";
+import { broadcastTx } from "./bridge/bridgeHelpers/api";
+import { Transaction } from "./types";
+
+const broadcast: AccountBridge["broadcast"] = 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/cli-transaction.ts b/libs/coin-modules/coin-ton/src/cli-transaction.ts
new file mode 100644
index 000000000000..c0dc11e6fe11
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/cli-transaction.ts
@@ -0,0 +1,30 @@
+import invariant from "invariant";
+import flatMap from "lodash/flatMap";
+
+import type { AccountLike } from "@ledgerhq/types-live";
+import { Transaction } from "./types";
+
+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/config.ts b/libs/coin-modules/coin-ton/src/config.ts
new file mode 100644
index 000000000000..976edd2c3373
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/config.ts
@@ -0,0 +1,21 @@
+import { CurrencyConfig } from "@ledgerhq/coin-framework/config";
+
+export type TonCoinConfig = () => CurrencyConfig & {
+ infra: {
+ API_TON_ENDPOINT: string;
+ };
+};
+
+let coinConfig: TonCoinConfig | undefined;
+
+export const setCoinConfig = (config: TonCoinConfig): void => {
+ coinConfig = config;
+};
+
+export const getCoinConfig = (): ReturnType => {
+ if (!coinConfig?.()) {
+ throw new Error("Ton module config not set");
+ }
+
+ return coinConfig();
+};
diff --git a/libs/coin-modules/coin-ton/src/createTransaction.ts b/libs/coin-modules/coin-ton/src/createTransaction.ts
new file mode 100644
index 000000000000..648ee65fe7bc
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/createTransaction.ts
@@ -0,0 +1,17 @@
+import { AccountBridge } from "@ledgerhq/types-live";
+import { BigNumber } from "bignumber.js";
+import type { Transaction } from "./types";
+
+const createTransaction: AccountBridge["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/deviceTransactionConfig.ts b/libs/coin-modules/coin-ton/src/deviceTransactionConfig.ts
new file mode 100644
index 000000000000..1eba181d0967
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/deviceTransactionConfig.ts
@@ -0,0 +1,48 @@
+import { CommonDeviceTransactionField as DeviceTransactionField } from "@ledgerhq/coin-framework/transaction/common";
+import type { Account, AccountLike } from "@ledgerhq/types-live";
+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",
+ });
+ }
+
+ fields.push({
+ type: "fees",
+ label: "Fee",
+ });
+
+ 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/coin-modules/coin-ton/src/errors.ts b/libs/coin-modules/coin-ton/src/errors.ts
new file mode 100644
index 000000000000..5ae1da5285db
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/errors.ts
@@ -0,0 +1,6 @@
+import { createCustomErrorClass } from "@ledgerhq/errors";
+
+/*
+ * When the comment is invalid.
+ */
+export const TonCommentInvalid = createCustomErrorClass("TonCommentInvalid");
diff --git a/libs/coin-modules/coin-ton/src/estimateMaxSpendable.ts b/libs/coin-modules/coin-ton/src/estimateMaxSpendable.ts
new file mode 100644
index 000000000000..666773f68f0a
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/estimateMaxSpendable.ts
@@ -0,0 +1,40 @@
+import { getMainAccount } from "@ledgerhq/coin-framework/account/index";
+import type { Account, AccountBridge, AccountLike } from "@ledgerhq/types-live";
+import { BigNumber } from "bignumber.js";
+import { fetchAccountInfo } from "./bridge/bridgeHelpers/api";
+import type { Transaction } from "./types";
+import { buildTonTransaction, getTonEstimatedFees } from "./utils";
+
+const estimateMaxSpendable: AccountBridge["estimateMaxSpendable"] = async ({
+ account,
+ parentAccount,
+ transaction,
+}: {
+ account: AccountLike;
+ parentAccount?: Account | null | undefined;
+ transaction?: Transaction | null | undefined;
+}): Promise => {
+ const mainAccount = getMainAccount(account, parentAccount);
+ let balance = mainAccount.spendableBalance;
+
+ if (balance.eq(0)) return balance;
+
+ const accountInfo = await fetchAccountInfo(mainAccount.freshAddress);
+
+ const estimatedFees = transaction
+ ? transaction.fees ??
+ (await getTonEstimatedFees(
+ mainAccount,
+ accountInfo.status === "uninit",
+ buildTonTransaction(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/getTransactionStatus.ts b/libs/coin-modules/coin-ton/src/getTransactionStatus.ts
new file mode 100644
index 000000000000..33c84531359d
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/getTransactionStatus.ts
@@ -0,0 +1,130 @@
+import {
+ AmountRequired,
+ InvalidAddress,
+ InvalidAddressBecauseDestinationIsAlsoSource,
+ NotEnoughBalance,
+ RecipientRequired,
+} from "@ledgerhq/errors";
+import { Account, AccountBridge, SubAccount } from "@ledgerhq/types-live";
+import BigNumber from "bignumber.js";
+import { TonCommentInvalid } from "./errors";
+import { Transaction, TransactionStatus } from "./types";
+import { addressesAreEqual, commentIsValid, isAddressValid } from "./utils";
+
+type ValidatedTransactionFields = "recipient" | "sender" | "amount" | "comment";
+type ValidationIssues = Partial>;
+
+/**
+ * Validate an address for account transaction
+ */
+const validateRecipient = (account: Account, tx: Transaction): Array => {
+ const errors: ValidationIssues = {};
+
+ if (tx.recipient) {
+ // Check if recipient is matching the format of account valid eth address or not
+ const isRecipientValidate = isAddressValid(tx.recipient);
+
+ if (!isRecipientValidate) {
+ errors.recipient = new InvalidAddress("", {
+ currencyName: account.currency.name,
+ });
+ }
+ if (addressesAreEqual(account.freshAddress, tx.recipient)) {
+ errors.recipient = new InvalidAddressBecauseDestinationIsAlsoSource("", {
+ currencyName: account.currency.name,
+ });
+ }
+ } else {
+ errors.recipient = new RecipientRequired(); // ""
+ }
+
+ return [errors];
+};
+
+/**
+ * Validate the sender address for account transaction
+ */
+const validateSender = (account: Account): Array => {
+ const errors: ValidationIssues = {};
+
+ // Check if sender is matching the format of account valid ton address or not
+ const isSenderValidate = isAddressValid(account.freshAddress);
+
+ if (!isSenderValidate) {
+ errors.sender = new InvalidAddress("", {
+ currencyName: account.currency.name,
+ });
+ }
+
+ return [errors];
+};
+
+const validateAmount = (
+ account: Account | SubAccount,
+ transaction: Transaction,
+ totalSpent: BigNumber,
+): Array => {
+ const errors: ValidationIssues = {};
+ const warnings: ValidationIssues = {};
+
+ // if no amount or 0
+ if (!transaction.amount || transaction.amount.isZero()) {
+ errors.amount = new AmountRequired(); // "Amount required"
+ } else if (totalSpent.isGreaterThan(account.balance)) {
+ // if not enough to make the transaction
+ errors.amount = new NotEnoughBalance(); // "Sorry, insufficient funds"
+ }
+
+ return [errors, warnings];
+};
+
+const validateComment = (transaction: Transaction): Array => {
+ const errors: ValidationIssues = {};
+
+ // if the comment isn'transaction encrypted, it should be valid
+ if (transaction.comment.isEncrypted || !commentIsValid(transaction.comment)) {
+ errors.comment = new TonCommentInvalid();
+ }
+ return [errors];
+};
+
+export const getTransactionStatus: AccountBridge<
+ Transaction,
+ Account,
+ TransactionStatus
+>["getTransactionStatus"] = async (
+ account: Account,
+ transaction: Transaction,
+): Promise => {
+ const totalSpent = transaction.amount.plus(transaction.fees);
+
+ // Recipient related errors and warnings
+ const [recipientErr] = validateRecipient(account, transaction);
+ // Sender related errors and warnings
+ const [senderErr] = validateSender(account);
+ // Amount related errors and warnings
+ const [amountErr, amountWarn] = validateAmount(account, transaction, totalSpent);
+ // Comment related errors and warnings
+ const [commentErr] = validateComment(transaction);
+
+ const errors: ValidationIssues = {
+ ...recipientErr,
+ ...senderErr,
+ ...amountErr,
+ ...commentErr,
+ };
+
+ const warnings: ValidationIssues = {
+ ...amountWarn,
+ };
+
+ return {
+ amount: transaction.amount,
+ errors,
+ warnings,
+ estimatedFees: transaction.fees,
+ totalSpent,
+ };
+};
+
+export default getTransactionStatus;
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..7d1e4bce78b8
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/hw-getAddress.ts
@@ -0,0 +1,28 @@
+import { GetAddressFn } from "@ledgerhq/coin-framework/bridge/getAddressWrapper";
+import { GetAddressOptions } from "@ledgerhq/coin-framework/derivation";
+import { SignerContext } from "@ledgerhq/coin-framework/signer";
+import { 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 });
+ });
+
+ 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..c2e69898df81
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/hw-signMessage.ts
@@ -0,0 +1,31 @@
+import { SignerContext } from "@ledgerhq/coin-framework/signer";
+import { Account, AnyMessage } from "@ledgerhq/types-live";
+import { TonSigner } from "./signer";
+import { TonTransaction } 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 value");
+
+ const parsedMessage = JSON.parse(message);
+ const ledgerPath = getLedgerTonPath(account.freshAddressPath);
+
+ const sig = await signerContext(deviceId, signer =>
+ signer.signTransaction(ledgerPath, parsedMessage as TonTransaction),
+ );
+
+ 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/prepareTransaction.ts b/libs/coin-modules/coin-ton/src/prepareTransaction.ts
new file mode 100644
index 000000000000..86855f39c2b9
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/prepareTransaction.ts
@@ -0,0 +1,27 @@
+import { defaultUpdateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers";
+import { Account, AccountBridge } from "@ledgerhq/types-live";
+import { fetchAccountInfo } from "./bridge/bridgeHelpers/api";
+import type { Transaction } from "./types";
+import { buildTonTransaction, getTonEstimatedFees } from "./utils";
+
+const prepareTransaction: AccountBridge["prepareTransaction"] = async (
+ account: Account,
+ transaction: Transaction,
+): Promise => {
+ const accountInfo = await fetchAccountInfo(account.freshAddress);
+
+ const simpleTx = buildTonTransaction(transaction, accountInfo.seqno);
+
+ const fees = await getTonEstimatedFees(account, accountInfo.status === "uninit", simpleTx);
+
+ let amount;
+ if (transaction.useAllAmount) {
+ amount = account.spendableBalance.minus(fees);
+ } else {
+ amount = transaction.amount;
+ }
+
+ return defaultUpdateTransaction(transaction, { fees, amount });
+};
+
+export default prepareTransaction;
diff --git a/libs/coin-modules/coin-ton/src/signOperation.ts b/libs/coin-modules/coin-ton/src/signOperation.ts
new file mode 100644
index 000000000000..b25868f4310d
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/signOperation.ts
@@ -0,0 +1,115 @@
+import { SignerContext } from "@ledgerhq/coin-framework/signer";
+import type { Account, AccountBridge, DeviceId, SignOperationEvent } from "@ledgerhq/types-live";
+import { Address, beginCell, external, storeMessage } from "@ton/core";
+import { WalletContractV4 } from "@ton/ton";
+import { Observable } from "rxjs";
+import { fetchAccountInfo } from "./bridge/bridgeHelpers/api";
+import type { TonSigner } from "./signer";
+import type { TonCell, TonOperation, Transaction } from "./types";
+import { buildTonTransaction, getLedgerTonPath } from "./utils";
+
+const packTransaction = (account: Account, needsInit: boolean, signature: TonCell): string => {
+ const { address } = Address.parseFriendly(account.freshAddress);
+ let init: { code: TonCell; data: TonCell } | null = null;
+ if (needsInit) {
+ if (account.xpub?.length !== 64) throw Error("[ton] xpub can't be found");
+ const wallet = WalletContractV4.create({
+ workchain: 0,
+ publicKey: Buffer.from(account.xpub, "hex"),
+ });
+ init = wallet.init;
+ }
+ const ext = external({ to: address, init, body: signature });
+ return beginCell().store(storeMessage(ext)).endCell().toBoc().toString("base64");
+};
+
+/**
+ * Sign Transaction with Ledger hardware
+ */
+export const buildSignOperation =
+ (signerContext: SignerContext): AccountBridge["signOperation"] =>
+ ({
+ account,
+ transaction,
+ deviceId,
+ }: {
+ account: Account;
+ transaction: Transaction;
+ deviceId: DeviceId;
+ }): Observable =>
+ new Observable(o => {
+ let cancelled = false;
+ async function main() {
+ const address = account.freshAddress;
+ const accountInfo = await fetchAccountInfo(address);
+
+ const tonTx = buildTonTransaction(transaction, accountInfo.seqno);
+
+ const ledgerPath = getLedgerTonPath(account.freshAddressPath);
+
+ o.next({ type: "device-signature-requested" });
+ const sig = await signerContext(deviceId, signer =>
+ signer.signTransaction(ledgerPath, tonTx),
+ );
+
+ 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 operation = buildOptimisticOperation(account, transaction);
+
+ o.next({
+ type: "signed",
+ signedOperation: {
+ operation,
+ signature,
+ },
+ });
+ }
+
+ main().then(
+ () => o.complete(),
+ e => o.error(e),
+ );
+
+ return () => {
+ cancelled = true;
+ };
+ });
+
+export const buildOptimisticOperation = (
+ account: Account,
+ transaction: Transaction,
+): TonOperation => {
+ const { recipient, amount, fees, comment } = transaction;
+ const { id: accountId } = account;
+
+ const op: TonOperation = {
+ id: "",
+ hash: "",
+ type: "OUT",
+ senders: [account.freshAddress],
+ recipients: [recipient],
+ 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,
+ },
+ };
+ return op;
+};
+
+export default buildSignOperation;
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..15e1b3acc8bf
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/signer.ts
@@ -0,0 +1,32 @@
+import { TonCell, TonTransaction } from "./types";
+
+export type TonAddress = {
+ publicKey: Buffer;
+ address: string;
+};
+export type TonSignature = TonCell | undefined;
+export interface TonSigner {
+ getAddress(
+ path: number[],
+ opts?: {
+ testOnly?: boolean;
+ bounceable?: boolean;
+ chain?: number;
+ },
+ ): Promise<{
+ address: string;
+ publicKey: Buffer;
+ }>;
+ validateAddress(
+ path: number[],
+ opts?: {
+ testOnly?: boolean;
+ bounceable?: boolean;
+ chain?: number;
+ },
+ ): Promise<{
+ address: string;
+ publicKey: Buffer;
+ }>;
+ signTransaction: (path: number[], transaction: TonTransaction) => Promise;
+}
diff --git a/libs/coin-modules/coin-ton/src/specs.ts b/libs/coin-modules/coin-ton/src/specs.ts
new file mode 100644
index 000000000000..aba0ea27bb5b
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/specs.ts
@@ -0,0 +1,122 @@
+import { botTest, pickSiblings } from "@ledgerhq/coin-framework/bot/specs";
+import type { AppSpec, TransactionDestinationTestInput } from "@ledgerhq/coin-framework/bot/types";
+import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets";
+import { DeviceModelId } from "@ledgerhq/devices";
+import BigNumber from "bignumber.js";
+import expect from "expect";
+import invariant from "invariant";
+import { acceptTransaction } from "./speculos-deviceActions";
+import { Transaction } from "./types";
+
+const MIN_SAFE = new BigNumber(1.5e7); // approx two txs' fees (0.015 TON)
+
+export const testDestination = ({
+ destination,
+ destinationBeforeTransaction,
+ sendingOperation,
+}: TransactionDestinationTestInput): void => {
+ const amount = sendingOperation.value.minus(sendingOperation.fee);
+ const inOp = destination.operations.find(
+ op => op.hash === sendingOperation.hash && op.type === "IN",
+ );
+ const inFees = inOp?.fee ?? BigNumber(0);
+ botTest("account balance increased with transaction amount", () =>
+ expect(destination.balance.toString()).toBe(
+ destinationBeforeTransaction.balance.plus(amount).minus(inFees).toString(),
+ ),
+ );
+ botTest("operation amount is consistent with sendingOperation", () =>
+ expect({
+ type: inOp?.type,
+ amount: inOp?.value?.toString(),
+ }).toMatchObject({
+ type: "IN",
+ amount: amount.toString(),
+ }),
+ );
+};
+
+const tonSpecs: AppSpec = {
+ name: "TON",
+ currency: getCryptoCurrencyById("ton"),
+ appQuery: {
+ model: DeviceModelId.nanoS,
+ appName: "TON",
+ },
+ genericDeviceAction: acceptTransaction,
+ testTimeout: 6 * 60 * 1000,
+ minViableAmount: MIN_SAFE,
+ transactionCheck: ({ maxSpendable }) => {
+ invariant(maxSpendable.gt(MIN_SAFE), "balance is too low");
+ },
+ mutations: [
+ {
+ name: "Send ~50%",
+ maxRun: 1,
+ testDestination,
+ transaction: ({ account, siblings, bridge, maxSpendable }) => {
+ invariant(maxSpendable.gt(MIN_SAFE), "balance is too low");
+
+ const updates: Array> = [
+ { recipient: pickSiblings(siblings).freshAddress },
+ { amount: maxSpendable.div(2).integerValue() },
+ ];
+ if (Math.random() < 0.5) updates.push({ comment: { isEncrypted: false, text: "LL Bot" } });
+
+ return {
+ transaction: bridge.createTransaction(account),
+ updates,
+ };
+ },
+
+ test: ({ accountBeforeTransaction, operation, account, transaction }) => {
+ // we don't know the exact amount in fees, so we accept +- 20% of expected fees
+ const baseAmount = accountBeforeTransaction.balance.minus(transaction.amount);
+ const maxBalance = baseAmount.minus(transaction.fees.multipliedBy(0.8).integerValue());
+ const minBalance = baseAmount.minus(transaction.fees.multipliedBy(1.2).integerValue());
+ botTest("account spendable balance decreased with operation", () => {
+ expect(account.spendableBalance.lte(maxBalance)).toBe(true);
+ expect(account.spendableBalance.gte(minBalance)).toBe(true);
+ });
+
+ botTest("operation comment", () =>
+ expect(operation.extra).toMatchObject({
+ comment: transaction.comment,
+ }),
+ );
+ },
+ },
+ {
+ name: "Transfer Max",
+ maxRun: 1,
+ transaction: ({ account, siblings, bridge }) => {
+ const updates: Array> = [
+ { recipient: pickSiblings(siblings).freshAddress },
+ { useAllAmount: true },
+ ];
+ if (Math.random() < 0.5) updates.push({ comment: { isEncrypted: false, text: "LL Bot" } });
+
+ return {
+ transaction: bridge.createTransaction(account),
+ updates,
+ };
+ },
+ testDestination,
+ test: ({ account, transaction, operation }) => {
+ botTest("account spendable balance is zero", () =>
+ expect(account.spendableBalance.toFixed()).toBe("0"),
+ );
+
+ botTest("operation comment", () =>
+ expect(operation.extra).toMatchObject({
+ comment: transaction.comment,
+ }),
+ );
+ },
+ },
+ ],
+};
+
+export default {
+ tonSpecs,
+};
diff --git a/libs/coin-modules/coin-ton/src/speculos-deviceActions.ts b/libs/coin-modules/coin-ton/src/speculos-deviceActions.ts
new file mode 100644
index 000000000000..586b926c6f45
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/speculos-deviceActions.ts
@@ -0,0 +1,38 @@
+import {
+ SpeculosButton,
+ deviceActionFlow,
+ formatDeviceAmount,
+} from "@ledgerhq/coin-framework/bot/specs";
+import type { DeviceAction, State } from "@ledgerhq/coin-framework/bot/types";
+import type { Transaction } from "./types";
+
+export const acceptTransaction: DeviceAction> = deviceActionFlow({
+ steps: [
+ {
+ title: "Review",
+ button: SpeculosButton.RIGHT,
+ },
+ {
+ title: "To",
+ button: SpeculosButton.RIGHT,
+ expectedValue: ({ transaction }) => transaction.recipient,
+ },
+ {
+ title: "Amount",
+ button: SpeculosButton.RIGHT,
+ expectedValue: ({ account, transaction }) =>
+ transaction.useAllAmount
+ ? "ALL YOUR TONs"
+ : formatDeviceAmount(account.currency, transaction.amount),
+ },
+ {
+ title: "Comment",
+ button: SpeculosButton.RIGHT,
+ expectedValue: ({ transaction }) => transaction.comment.text,
+ },
+ {
+ title: "Approve",
+ button: SpeculosButton.BOTH,
+ },
+ ],
+});
diff --git a/libs/coin-modules/coin-ton/src/synchronisation.ts b/libs/coin-modules/coin-ton/src/synchronisation.ts
new file mode 100644
index 000000000000..d91e15558143
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/synchronisation.ts
@@ -0,0 +1,91 @@
+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 { 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;
+
+ const publicKey = reconciliatePubkey(rest?.publicKey, initialAccount);
+
+ const blockHeight = await fetchLastBlockNumber();
+ 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 Promise.all([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 Promise.all([getTransactions(address, oldOps[0].extra.lt)]);
+ newTxs.transactions.push(...tmpTxs.transactions);
+ newTxs.address_book = { ...newTxs.address_book, ...tmpTxs.address_book };
+ }
+ }
+ }
+
+ const newOps = flatMap(newTxs.transactions, mapTxToOps(accountId, address, newTxs.address_book));
+ const operations = mergeOps(oldOps, newOps);
+
+ const toReturn = {
+ id: accountId,
+ balance: new BigNumber(balance),
+ spendableBalance: new BigNumber(balance),
+ operations,
+ operationsCount: operations.length,
+ blockHeight,
+ xpub: publicKey,
+ lastSyncDate: new Date(),
+ } as Partial;
+ return toReturn;
+};
+
+const postSync = (initial: Account, synced: Account): Account => {
+ const initialPendingOperations = initial.pendingOperations || [];
+ const { operations } = synced;
+ const pendingOperations = initialPendingOperations.filter(
+ op => !operations.some(o => o.id === op.id),
+ );
+ // Set of hashes from the pending operations of the main account
+ const coinPendingOperationsHashes = new Set();
+ for (const op of pendingOperations) {
+ coinPendingOperationsHashes.add(op.hash);
+ }
+
+ return {
+ ...synced,
+ pendingOperations,
+ };
+};
+
+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");
+}
+
+export const sync = makeSync({ getAccountShape, postSync });
diff --git a/libs/coin-modules/coin-ton/src/transaction.ts b/libs/coin-modules/coin-ton/src/transaction.ts
new file mode 100644
index 000000000000..953290067e47
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/transaction.ts
@@ -0,0 +1,61 @@
+import { getAccountCurrency } from "@ledgerhq/coin-framework/account/index";
+import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
+import { formatTransactionStatus } from "@ledgerhq/coin-framework/formatters";
+import {
+ fromTransactionCommonRaw,
+ fromTransactionStatusRawCommon as fromTransactionStatusRaw,
+ toTransactionCommonRaw,
+ toTransactionStatusRawCommon as toTransactionStatusRaw,
+} from "@ledgerhq/coin-framework/serialization";
+import type { Account } from "@ledgerhq/types-live";
+import BigNumber from "bignumber.js";
+import type { Transaction, TransactionRaw } from "./types";
+
+export const formatTransaction = (
+ { recipient, useAllAmount, amount }: Transaction,
+ account: Account,
+): string => `
+SEND ${
+ useAllAmount
+ ? "MAX"
+ : amount.isZero()
+ ? ""
+ : " " +
+ formatCurrencyUnit(getAccountCurrency(account).units[0], 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 = (transaction: Transaction): TransactionRaw => {
+ const common = toTransactionCommonRaw(transaction);
+
+ return {
+ ...common,
+ family: transaction.family,
+ amount: transaction.amount.toFixed(),
+ fees: transaction.fees.toFixed(),
+ comment: transaction.comment,
+ };
+};
+
+export default {
+ formatTransaction,
+ fromTransactionRaw,
+ toTransactionRaw,
+ fromTransactionStatusRaw,
+ toTransactionStatusRaw,
+ formatTransactionStatus,
+};
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..a855506654aa
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/types.ts
@@ -0,0 +1,76 @@
+import {
+ Operation,
+ TransactionCommon,
+ TransactionCommonRaw,
+ TransactionStatusCommon,
+ TransactionStatusCommonRaw,
+} from "@ledgerhq/types-live";
+import { Address, SendMode, StateInit } from "@ton/core";
+import { Cell } from "@ton/ton";
+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 type TonPayloadJettonTransfer = {
+ type: "jetton-transfer";
+ queryId: bigint | null;
+ amount: bigint;
+ destination: Address;
+ responseDestination: Address;
+ customPayload: TonCell | null;
+ forwardAmount: bigint;
+ forwardPayload: TonCell | null;
+};
+
+export type TonPayloadNftTransfer = {
+ type: "nft-transfer";
+ queryId: bigint | null;
+ newOwner: Address;
+ responseDestination: Address;
+ customPayload: TonCell | null;
+ forwardAmount: bigint;
+ forwardPayload: TonCell | null;
+};
+
+export type TonPayloadComment = {
+ type: "comment";
+ text: string;
+};
+
+export type TonPayloadFormat = TonPayloadComment | TonPayloadJettonTransfer | TonPayloadNftTransfer;
+
+export interface TonTransaction {
+ to: Address;
+ sendMode: SendMode;
+ seqno: number;
+ timeout: number;
+ bounce: boolean;
+ amount: bigint;
+ stateInit?: StateInit;
+ payload?: TonPayloadFormat;
+}
+
+export interface TonCell extends Cell {}
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..2b9c4b693b11
--- /dev/null
+++ b/libs/coin-modules/coin-ton/src/utils.ts
@@ -0,0 +1,120 @@
+import { decodeAccountId } from "@ledgerhq/coin-framework/account/index";
+import { Account } from "@ledgerhq/types-live";
+import { SendMode, Address as TonAddress, WalletContractV4, comment, internal } from "@ton/ton";
+import BigNumber from "bignumber.js";
+import { estimateFee } from "./bridge/bridgeHelpers/api";
+import { TonComment, TonTransaction, Transaction } from "./types";
+
+export const isAddressValid = (recipient: string) => {
+ try {
+ return Boolean(
+ (TonAddress.isRaw(recipient) || TonAddress.isFriendly(recipient)) &&
+ TonAddress.parse(recipient),
+ );
+ } catch {
+ return false;
+ }
+};
+
+export const addressesAreEqual = (addr1: string, addr2: string) => {
+ try {
+ return (
+ isAddressValid(addr1) &&
+ isAddressValid(addr2) &&
+ TonAddress.parse(addr1).equals(TonAddress.parse(addr2))
+ );
+ } catch {
+ return false;
+ }
+};
+
+export const buildTonTransaction = (transaction: Transaction, seqno: number): TonTransaction => {
+ const { useAllAmount, amount, comment, recipient } = transaction;
+ let recipientParsed = recipient;
+ // if recipient is not valid calculate fees with empty address
+ // we handle invalid addresses in account bridge
+ try {
+ TonAddress.parse(recipientParsed);
+ } catch {
+ recipientParsed = new TonAddress(0, Buffer.alloc(32)).toRawString();
+ }
+
+ const finalAmount = useAllAmount ? BigInt(0) : BigInt(amount.toFixed());
+
+ const tonTransaction: TonTransaction = {
+ to: TonAddress.parse(recipientParsed),
+ seqno,
+ amount: finalAmount,
+ bounce: TonAddress.isFriendly(recipientParsed)
+ ? TonAddress.parseFriendly(recipientParsed).isBounceable
+ : true,
+ timeout: getTransferExpirationTime(),
+ sendMode: useAllAmount
+ ? SendMode.CARRY_ALL_REMAINING_BALANCE
+ : SendMode.IGNORE_ERRORS + SendMode.PAY_GAS_SEPARATELY,
+ };
+
+ if (comment.text.length) {
+ tonTransaction.payload = { type: "comment", text: comment.text };
+ }
+
+ return tonTransaction;
+};
+
+// 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 (
+ account: Account,
+ needsInit: boolean,
+ tx: TonTransaction,
+) => {
+ const { xpubOrAddress: pubKey } = decodeAccountId(account.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(
+ account.freshAddress,
+ 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 c9481c5d4ce0..007c5d0c2bfc 100644
--- a/libs/ledger-live-common/package.json
+++ b/libs/ledger-live-common/package.json
@@ -143,6 +143,7 @@
"@ledgerhq/coin-evm": "workspace:^",
"@ledgerhq/coin-framework": "workspace:^",
"@ledgerhq/coin-near": "workspace:^",
+ "@ledgerhq/coin-ton": "workspace:^",
"@ledgerhq/coin-polkadot": "workspace:^",
"@ledgerhq/coin-icon": "workspace:^",
"@ledgerhq/coin-stellar": "workspace:^",
@@ -191,6 +192,9 @@
"@stacks/transactions": "6.11.0",
"@stricahq/typhonjs": "^1.2.6",
"@taquito/ledger-signer": "^20.0.0",
+ "@ton-community/ton-ledger": "^7.0.1",
+ "@ton/core": "^0.56.1",
+ "@ton/crypto": "^3.2.0",
"@types/bchaddrjs": "^0.4.0",
"@types/pako": "^2.0.0",
"@types/qs": "^6.9.7",
diff --git a/libs/ledger-live-common/scripts/sync-families-dispatch.mjs b/libs/ledger-live-common/scripts/sync-families-dispatch.mjs
index a0b8a5d7fa3f..087f8c32f6c5 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",
@@ -34,6 +34,7 @@ const familiesWPackage = [
"tron",
"xrp",
"icon",
+ "ton"
];
cd(path.join(__dirname, "..", "src"));
diff --git a/libs/ledger-live-common/src/__tests__/migration/account-migration.ts b/libs/ledger-live-common/src/__tests__/migration/account-migration.ts
index 9086272de931..c93fe36f8b2e 100644
--- a/libs/ledger-live-common/src/__tests__/migration/account-migration.ts
+++ b/libs/ledger-live-common/src/__tests__/migration/account-migration.ts
@@ -1,23 +1,23 @@
/* eslint-disable no-console */
-import { readFileSync, existsSync, writeFileSync } from "fs";
-import BigNumber from "bignumber.js";
+import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
import { CryptoCurrencyId } from "@ledgerhq/types-cryptoassets";
import { Account, AccountRaw } from "@ledgerhq/types-live";
-import { argv } from "yargs";
-import { LiveConfig } from "@ledgerhq/live-config/LiveConfig";
-import { promisify } from "util";
+import BigNumber from "bignumber.js";
import childProcess from "child_process";
+import { existsSync, readFileSync, writeFileSync } from "fs";
+import { promisify } from "util";
+import { argv } from "yargs";
+import { firstValueFrom, reduce } from "rxjs";
+import { encodeAccountId, fromAccountRaw, toAccountRaw } from "../../account";
+import { getAccountBridgeByFamily, getCurrencyBridge } from "../../bridge/impl";
+import { liveConfig } from "../../config/sharedConfig";
import {
findCryptoCurrencyById,
getCryptoCurrencyById,
setSupportedCurrencies,
} from "../../currencies";
-import { encodeAccountId, fromAccountRaw, toAccountRaw } from "../../account";
-import { firstValueFrom, reduce } from "rxjs";
-import { getAccountBridgeByFamily, getCurrencyBridge } from "../../bridge/impl";
import { MigrationAddress, migrationAddresses as defaultAddresses } from "./addresses";
-import { liveConfig } from "../../config/sharedConfig";
// mandatory to run the script
setSupportedCurrencies([
@@ -96,6 +96,7 @@ setSupportedCurrencies([
"lukso",
"filecoin",
"linea",
+ "ton",
]);
LiveConfig.setConfig(liveConfig);
diff --git a/libs/ledger-live-common/src/__tests__/test-helpers/environment.ts b/libs/ledger-live-common/src/__tests__/test-helpers/environment.ts
index 8a505e17d722..545f8f5bd3e6 100644
--- a/libs/ledger-live-common/src/__tests__/test-helpers/environment.ts
+++ b/libs/ledger-live-common/src/__tests__/test-helpers/environment.ts
@@ -100,6 +100,7 @@ setSupportedCurrencies([
"blast_sepolia",
"scroll",
"scroll_sepolia",
+ "ton",
]);
LiveConfig.setConfig(liveConfig);
diff --git a/libs/ledger-live-common/src/config/sharedConfig.ts b/libs/ledger-live-common/src/config/sharedConfig.ts
index bcaa6ba8eea4..d14a1ba5d274 100644
--- a/libs/ledger-live-common/src/config/sharedConfig.ts
+++ b/libs/ledger-live-common/src/config/sharedConfig.ts
@@ -1,4 +1,5 @@
import { ConfigSchema } from "@ledgerhq/live-config/LiveConfig";
+import { appConfig } from "../apps/config";
import { algorandConfig } from "../families/algorand/config";
import { bitcoinConfig } from "../families/bitcoin/config";
import { cardanoConfig } from "../families/cardano/config";
@@ -18,10 +19,10 @@ import { solanaConfig } from "../families/solana/config";
import { stacksConfig } from "../families/stacks/config";
import { stellarConfig } from "../families/stellar/config";
import { tezosConfig } from "../families/tezos/config";
+import { tonConfig } from "../families/ton/config";
import { tronConfig } from "../families/tron/config";
import { vechainConfig } from "../families/vechain/config";
import { iconConfig } from "../families/icon/config";
-import { appConfig } from "../apps/config";
const countervaluesConfig: ConfigSchema = {
config_countervalues_refreshRate: {
@@ -63,4 +64,5 @@ export const liveConfig: ConfigSchema = {
...tronConfig,
...vechainConfig,
...iconConfig,
+ ...tonConfig,
};
diff --git a/libs/ledger-live-common/src/families/ton/__snapshots__/bridge.integration.test.ts.snap b/libs/ledger-live-common/src/families/ton/__snapshots__/bridge.integration.test.ts.snap
new file mode 100644
index 000000000000..441fa5b3e922
--- /dev/null
+++ b/libs/ledger-live-common/src/families/ton/__snapshots__/bridge.integration.test.ts.snap
@@ -0,0 +1,198 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ton currency bridge scanAccounts ton seed 1 1`] = `
+[
+ {
+ "balance": "933174896",
+ "currencyId": "ton",
+ "derivationMode": "ton",
+ "freshAddress": "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ "freshAddressPath": "44'/607'/0'/0'/0'/0'",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "index": 0,
+ "operationsCount": 6,
+ "pendingOperations": [],
+ "seedIdentifier": "86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060",
+ "spendableBalance": "933174896",
+ "swapHistory": [],
+ "syncHash": undefined,
+ "used": true,
+ "xpub": "86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060",
+ },
+ {
+ "balance": "0",
+ "currencyId": "ton",
+ "derivationMode": "ton",
+ "freshAddress": "UQAui6M4jOYOezUGfmeONA22Ars9yjd34YIGdAR1Pcpp4sgR",
+ "freshAddressPath": "44'/607'/0'/0'/1'/0'",
+ "id": "js:2:ton:b5177c2b32f9d72fa8c673cc3d61acec6a9f68eb5e4945445fdbb48a45eb4887:ton",
+ "index": 1,
+ "operationsCount": 0,
+ "pendingOperations": [],
+ "seedIdentifier": "86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060",
+ "spendableBalance": "0",
+ "swapHistory": [],
+ "syncHash": undefined,
+ "used": false,
+ "xpub": "b5177c2b32f9d72fa8c673cc3d61acec6a9f68eb5e4945445fdbb48a45eb4887",
+ },
+]
+`;
+
+exports[`ton currency bridge scanAccounts ton seed 1 2`] = `
+[
+ [
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 37795019,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "bUaQn1mpH065e6GMtJAIGJ5YZiJ4g66oHKctDSmwqH4=",
+ "lt": "46398660000001",
+ },
+ "fee": "5547465",
+ "hasFailed": false,
+ "hash": "7ZIKj4q+KCfZ04dD7m7wI3Q/nVG6VoFujq61UtMQE3E=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-1KI+JhVbOM2sgDpvUILr6sEmQ6+Iz4twNIOG7lR2eg4=-OUT",
+ "recipients": [
+ "EQAG2WGLWM7dnTdopd2wQlbhsbJck3bOr1xtwzqZ6ldQwgSS",
+ ],
+ "senders": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "type": "OUT",
+ "value": "50000000",
+ },
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 37795026,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "zlj5W9f4F4rHDFHzdC3gSa+nKcgobho4MV8JzZWkzoE=",
+ "lt": "46398669000001",
+ },
+ "fee": "396411",
+ "hasFailed": false,
+ "hash": "4dDNdHb5JwpES6VYZS6ZyKSDnNNk0i37CLF6sYqD7oc=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-4dDNdHb5JwpES6VYZS6ZyKSDnNNk0i37CLF6sYqD7oc=-IN",
+ "recipients": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "senders": [
+ "EQAnDEE6JghjaRCaY5qWTeV2AalOgyU9ohZ37pmUl-t0toX7",
+ ],
+ "type": "IN",
+ "value": "21509565",
+ },
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 37795081,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "NqRiKhgFkHGL4z75j1M70KryRr/M5JosW2Zw2pxrciU=",
+ "lt": "46398735000001",
+ },
+ "fee": "2474733",
+ "hasFailed": false,
+ "hash": "URAvSSvE38mTydf61TAZSVO8b2F5RRwgL61gjX77YT4=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-cva3yUb4cC+AfML9eidygO1PeYVYqLYpcprXEiw3Nj0=-OUT",
+ "recipients": [
+ "EQAG2WGLWM7dnTdopd2wQlbhsbJck3bOr1xtwzqZ6ldQwgSS",
+ ],
+ "senders": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "type": "OUT",
+ "value": "50000000",
+ },
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 37795087,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "3QMpehQMIEz1L9i2RR8NlQ8MEhtIW8OlN9DBmm5PtyM=",
+ "lt": "46398745000001",
+ },
+ "fee": "396410",
+ "hasFailed": false,
+ "hash": "ExoMI/oYFVbPvh+Q52cf2Pyq0aL4jSpuwen8nmOvGBo=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-ExoMI/oYFVbPvh+Q52cf2Pyq0aL4jSpuwen8nmOvGBo=-IN",
+ "recipients": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "senders": [
+ "EQAnDEE6JghjaRCaY5qWTeV2AalOgyU9ohZ37pmUl-t0toX7",
+ ],
+ "type": "IN",
+ "value": "21509565",
+ },
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 37794992,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "XxJMiFGk4BHIwNWy1w4ZdenPc0Bh2No+tpUEIi6sycs=",
+ "lt": "46398628000001",
+ },
+ "fee": "84142",
+ "hasFailed": false,
+ "hash": "f73qoft8iF7ANKH32Mq0cRGO9STLYlhuOFVXPQE1jI0=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-f73qoft8iF7ANKH32Mq0cRGO9STLYlhuOFVXPQE1jI0=-IN",
+ "recipients": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "senders": [
+ "EQAG2WGLWM7dnTdopd2wQlbhsbJck3bOr1xtwzqZ6ldQwgSS",
+ ],
+ "type": "IN",
+ "value": "1",
+ },
+ {
+ "accountId": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton",
+ "blockHash": null,
+ "blockHeight": 35650189,
+ "extra": {
+ "comment": {
+ "isEncrypted": false,
+ "text": "",
+ },
+ "explorerHash": "mzCqNBjsEDmpgq5VMfNfNqnslW64f/B1seatUCej9TY=",
+ "lt": "44161792000003",
+ },
+ "fee": "0",
+ "hasFailed": false,
+ "hash": "V0y/lJDCYzAgmzNVA+dapRQemNP+AJpVhCxBhs7eNfQ=",
+ "id": "js:2:ton:86196cb40cd25e9e696bc808e3f2c074ce0b39f2a2a9d482a68eafef86e4a060:ton-V0y/lJDCYzAgmzNVA+dapRQemNP+AJpVhCxBhs7eNfQ=-IN",
+ "recipients": [
+ "UQCOvQLYvTcbi5tL9MaDNzuVl3-J3vATimNm9yO5XPafLfV4",
+ ],
+ "senders": [
+ "UQDzd8aeBOU-jqYw_ZSuZjceI5p-F4b7HMprAsUJAtRPbMol",
+ ],
+ "type": "IN",
+ "value": "1000000000",
+ },
+ ],
+ [],
+]
+`;
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
new file mode 100644
index 000000000000..8066c8a99701
--- /dev/null
+++ b/libs/ledger-live-common/src/families/ton/bridge.integration.test.ts
@@ -0,0 +1,5 @@
+import { dataset } from "@ledgerhq/coin-ton/__tests__/integration/bridge.integration.test";
+import { testBridge } from "../../__tests__/test-helpers/bridge";
+import "../../__tests__/test-helpers/setup";
+
+testBridge(dataset);
diff --git a/libs/ledger-live-common/src/families/ton/config.ts b/libs/ledger-live-common/src/families/ton/config.ts
new file mode 100644
index 000000000000..e6cabd6c8339
--- /dev/null
+++ b/libs/ledger-live-common/src/families/ton/config.ts
@@ -0,0 +1,15 @@
+import { CurrencyLiveConfigDefinition } from "../../config";
+
+const tonConfig: CurrencyLiveConfigDefinition = {
+ config_currency_ton: {
+ type: "object",
+ default: {
+ status: { type: "active" },
+ infra: {
+ API_TON_ENDPOINT: "https://ton.coin.ledger.com/api/v3",
+ },
+ },
+ },
+};
+
+export { tonConfig };
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..2527d74f8196
--- /dev/null
+++ b/libs/ledger-live-common/src/families/ton/setup.ts
@@ -0,0 +1,33 @@
+// 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 { TonCoinConfig } from "@ledgerhq/coin-ton/lib/config";
+import { TonSigner } from "@ledgerhq/coin-ton/lib/signer";
+import { Transaction } from "@ledgerhq/coin-ton/types";
+import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/currencies";
+import Transport from "@ledgerhq/hw-transport";
+import type { Bridge } from "@ledgerhq/types-live";
+import { TonTransport as Ton } from "@ton-community/ton-ledger";
+import { CreateSigner, createResolver, executeWithSigner } from "../../bridge/setup";
+import { getCurrencyConfiguration } from "../../config";
+import type { Resolver } from "../../hw/getAddress/types";
+
+const createSigner: CreateSigner = (transport: Transport) => new Ton(transport);
+
+const getCoinConfig: TonCoinConfig = () =>
+ getCurrencyConfiguration>(getCryptoCurrencyById("ton"));
+
+const bridge: Bridge = createBridges(executeWithSigner(createSigner), getCoinConfig);
+
+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
new file mode 100644
index 000000000000..ab1d666ee1d8
--- /dev/null
+++ b/libs/ledger-live-common/src/families/ton/types.ts
@@ -0,0 +1,2 @@
+// Encapsulate for LLD et LLM
+export * from "@ledgerhq/coin-ton/types";
diff --git a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts
index 0f665ac34d20..aa607d7f08bd 100644
--- a/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts
+++ b/libs/ledger-live-common/src/featureFlags/defaultFeatures.ts
@@ -77,6 +77,7 @@ export const CURRENCY_DEFAULT_FEATURES = {
currencyScroll: DEFAULT_FEATURE,
currencyScrollSepolia: DEFAULT_FEATURE,
currencyIcon: DEFAULT_FEATURE,
+ currencyTon: DEFAULT_FEATURE,
};
/**
diff --git a/libs/ledger-live-common/src/generated/bridge/js.ts b/libs/ledger-live-common/src/generated/bridge/js.ts
index aa3a6728c1b1..13ef473320ca 100644
--- a/libs/ledger-live-common/src/generated/bridge/js.ts
+++ b/libs/ledger-live-common/src/generated/bridge/js.ts
@@ -20,6 +20,7 @@ import { bridge as tezos } from "../../families/tezos/setup";
import { bridge as tron } from "../../families/tron/setup";
import { bridge as xrp } from "../../families/xrp/setup";
import { bridge as icon } from "../../families/icon/setup";
+import { bridge as ton } from "../../families/ton/setup";
export default {
casper,
@@ -44,4 +45,5 @@ export default {
tron,
xrp,
icon,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/cli-transaction.ts b/libs/ledger-live-common/src/generated/cli-transaction.ts
index 49054d96a1d3..493e4aa145ef 100644
--- a/libs/ledger-live-common/src/generated/cli-transaction.ts
+++ b/libs/ledger-live-common/src/generated/cli-transaction.ts
@@ -18,6 +18,7 @@ import { cliTools as tezos } from "../families/tezos/setup";
import { cliTools as tron } from "../families/tron/setup";
import { cliTools as xrp } from "../families/xrp/setup";
import { cliTools as icon } from "../families/icon/setup";
+import { cliTools as ton } from "../families/ton/setup";
export default {
celo,
@@ -40,4 +41,5 @@ export default {
tron,
xrp,
icon,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts
index eb4401b6930e..76571b1733c5 100644
--- a/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts
+++ b/libs/ledger-live-common/src/generated/deviceTransactionConfig.ts
@@ -19,6 +19,7 @@ import tezos from "@ledgerhq/coin-tezos/deviceTransactionConfig";
import tron from "@ledgerhq/coin-tron/deviceTransactionConfig";
import xrp from "@ledgerhq/coin-xrp/deviceTransactionConfig";
import icon from "@ledgerhq/coin-icon/deviceTransactionConfig";
+import ton from "@ledgerhq/coin-ton/deviceTransactionConfig";
export default {
casper,
@@ -42,6 +43,7 @@ export default {
tron,
xrp,
icon,
+ ton,
};
import { ExtraDeviceTransactionField as ExtraDeviceTransactionField_casper } from "../families/casper/deviceTransactionConfig";
import { ExtraDeviceTransactionField as ExtraDeviceTransactionField_cosmos } from "../families/cosmos/deviceTransactionConfig";
diff --git a/libs/ledger-live-common/src/generated/hw-getAddress.ts b/libs/ledger-live-common/src/generated/hw-getAddress.ts
index 27e83c7b0843..cad1a1f43e2c 100644
--- a/libs/ledger-live-common/src/generated/hw-getAddress.ts
+++ b/libs/ledger-live-common/src/generated/hw-getAddress.ts
@@ -20,6 +20,7 @@ import { resolver as tezos } from "../families/tezos/setup";
import { resolver as tron } from "../families/tron/setup";
import { resolver as xrp } from "../families/xrp/setup";
import { resolver as icon } from "../families/icon/setup";
+import { resolver as ton } from "../families/ton/setup";
export default {
casper,
@@ -44,4 +45,5 @@ export default {
tron,
xrp,
icon,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/hw-signMessage.ts b/libs/ledger-live-common/src/generated/hw-signMessage.ts
index 34c5e93b10be..13b1774984a0 100644
--- a/libs/ledger-live-common/src/generated/hw-signMessage.ts
+++ b/libs/ledger-live-common/src/generated/hw-signMessage.ts
@@ -5,6 +5,7 @@ import stacks from "../families/stacks/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,
@@ -14,4 +15,5 @@ export default {
vechain,
bitcoin,
evm,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/specs.ts b/libs/ledger-live-common/src/generated/specs.ts
index 668e76f64d3b..340f708d7435 100644
--- a/libs/ledger-live-common/src/generated/specs.ts
+++ b/libs/ledger-live-common/src/generated/specs.ts
@@ -20,6 +20,7 @@ import tezos from "@ledgerhq/coin-tezos/specs";
import tron from "@ledgerhq/coin-tron/specs";
import xrp from "@ledgerhq/coin-xrp/specs";
import icon from "@ledgerhq/coin-icon/specs";
+import ton from "@ledgerhq/coin-ton/specs";
export default {
casper,
@@ -44,4 +45,5 @@ export default {
tron,
xrp,
icon,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/transaction.ts b/libs/ledger-live-common/src/generated/transaction.ts
index 91a15376bdee..e6abcaa08017 100644
--- a/libs/ledger-live-common/src/generated/transaction.ts
+++ b/libs/ledger-live-common/src/generated/transaction.ts
@@ -20,6 +20,7 @@ import tezos from "@ledgerhq/coin-tezos/transaction";
import tron from "@ledgerhq/coin-tron/transaction";
import xrp from "@ledgerhq/coin-xrp/transaction";
import icon from "@ledgerhq/coin-icon/transaction";
+import ton from "@ledgerhq/coin-ton/transaction";
export default {
casper,
@@ -44,4 +45,5 @@ export default {
tron,
xrp,
icon,
+ ton,
};
diff --git a/libs/ledger-live-common/src/generated/types.ts b/libs/ledger-live-common/src/generated/types.ts
index 5dcedb91987b..abcd2e0a9510 100644
--- a/libs/ledger-live-common/src/generated/types.ts
+++ b/libs/ledger-live-common/src/generated/types.ts
@@ -112,6 +112,12 @@ import type {
TransactionStatus as tezosTransactionStatus,
TransactionStatusRaw as tezosTransactionStatusRaw,
} from "@ledgerhq/coin-tezos/types/index";
+import type {
+ Transaction as tonTransaction,
+ TransactionRaw as tonTransactionRaw,
+ TransactionStatus as tonTransactionStatus,
+ TransactionStatusRaw as tonTransactionStatusRaw,
+} from "@ledgerhq/coin-ton/types";
import type {
Transaction as tronTransaction,
TransactionRaw as tronTransactionRaw,
@@ -151,6 +157,7 @@ export type Transaction =
| stacksTransaction
| stellarTransaction
| tezosTransaction
+ | tonTransaction
| tronTransaction
| vechainTransaction
| xrpTransaction;
@@ -175,6 +182,7 @@ export type TransactionRaw =
| stacksTransactionRaw
| stellarTransactionRaw
| tezosTransactionRaw
+ | tonTransactionRaw
| tronTransactionRaw
| vechainTransactionRaw
| xrpTransactionRaw;
@@ -199,6 +207,7 @@ export type TransactionStatus =
| stacksTransactionStatus
| stellarTransactionStatus
| tezosTransactionStatus
+ | tonTransactionStatus
| tronTransactionStatus
| vechainTransactionStatus
| xrpTransactionStatus;
@@ -223,6 +232,7 @@ export type TransactionStatusRaw =
| stacksTransactionStatusRaw
| stellarTransactionStatusRaw
| tezosTransactionStatusRaw
+ | tonTransactionStatusRaw
| tronTransactionStatusRaw
| vechainTransactionStatusRaw
| xrpTransactionStatusRaw;
diff --git a/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts b/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts
index aca8413ca35a..3e224b5d7b17 100644
--- a/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts
+++ b/libs/ledgerjs/packages/cryptoassets/src/abandonseed.ts
@@ -104,6 +104,7 @@ const abandonSeedAddresses: Partial> = {
icon: "hxd3f4224ffb2cfd354f8db2eef39e12aadb7a4ebb",
icon_berlin_testnet: "hxd3f4224ffb2cfd354f8db2eef39e12aadb7a4ebb",
linea: EVM_DEAD_ADDRESS,
+ ton: "UQChCJybqqY3KOVE_6QCCVJDvXDMT-lPszwl1loaCEyVDfbq",
linea_sepolia: EVM_DEAD_ADDRESS,
blast: EVM_DEAD_ADDRESS,
blast_sepolia: EVM_DEAD_ADDRESS,
diff --git a/libs/ledgerjs/packages/cryptoassets/src/currencies.ts b/libs/ledgerjs/packages/cryptoassets/src/currencies.ts
index d2d5fe263602..e649a1ce3265 100644
--- a/libs/ledgerjs/packages/cryptoassets/src/currencies.ts
+++ b/libs/ledgerjs/packages/cryptoassets/src/currencies.ts
@@ -21,7 +21,7 @@
* if bitcoin family, supportsSegwit defines if it supports segwit.
*/
-import { CryptoCurrency, CoinType, Unit, CryptoCurrencyId } from "@ledgerhq/types-cryptoassets";
+import { CoinType, CryptoCurrency, CryptoCurrencyId, Unit } from "@ledgerhq/types-cryptoassets";
const makeTestnetUnit = u => ({ ...u, code: `𝚝${u.code}` });
@@ -2832,6 +2832,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://tonscan.org/tx/$hash",
+ address: "https://tonscan.org/address/$address",
+ },
+ ],
+ },
tron: {
type: "CryptoCurrency",
id: "tron",
diff --git a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts
index 0b94eb67f71a..4ed95df062ac 100644
--- a/libs/ledgerjs/packages/cryptoassets/src/tokens.ts
+++ b/libs/ledgerjs/packages/cryptoassets/src/tokens.ts
@@ -3,11 +3,11 @@ import { findCryptoCurrencyById, getCryptoCurrencyById } from "./currencies";
import asatokens, { AlgorandASAToken } from "./data/asa";
import bep20tokens, { BEP20Token } from "./data/bep20";
import cardanoNativeTokens, { CardanoNativeToken } from "./data/cardanoNative";
+import casperTokens, { CasperToken } from "./data/casper";
import erc20tokens, { ERC20Token } from "./data/erc20";
import esdttokens, { ElrondESDTToken } from "./data/esdt";
import polygonTokens, { PolygonERC20Token } from "./data/polygon-erc20";
import stellarTokens, { StellarToken } from "./data/stellar";
-import casperTokens, { CasperToken } from "./data/casper";
import trc10tokens, { TRC10Token } from "./data/trc10";
import trc20tokens, { TRC20Token } from "./data/trc20";
import vechainTokens, { vip180Token } from "./data/vip180";
@@ -89,7 +89,6 @@ export function listTokensForCryptoCurrency(
options?: Partial,
): TokenCurrency[] {
const { withDelisted } = { ...defaultTokenListOptions, ...options };
-
if (withDelisted) {
return tokensByCryptoCurrencyWithDelisted[currency.id] || emptyArray;
}
diff --git a/libs/ledgerjs/packages/types-cryptoassets/src/index.ts b/libs/ledgerjs/packages/types-cryptoassets/src/index.ts
index eb96fecb7b9a..680a14b72a40 100644
--- a/libs/ledgerjs/packages/types-cryptoassets/src/index.ts
+++ b/libs/ledgerjs/packages/types-cryptoassets/src/index.ts
@@ -107,6 +107,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/derivation.ts b/libs/ledgerjs/packages/types-live/src/derivation.ts
index 34f3ffc1e135..54b69f1a3770 100644
--- a/libs/ledgerjs/packages/types-live/src/derivation.ts
+++ b/libs/ledgerjs/packages/types-live/src/derivation.ts
@@ -35,4 +35,5 @@ export type DerivationMode =
| "vechain"
| "internet_computer"
| "stacks_wallet"
- | "icon";
+ | "icon"
+ | "ton";
diff --git a/libs/ledgerjs/packages/types-live/src/feature.ts b/libs/ledgerjs/packages/types-live/src/feature.ts
index df12b9febb34..c2c8687ee3bd 100644
--- a/libs/ledgerjs/packages/types-live/src/feature.ts
+++ b/libs/ledgerjs/packages/types-live/src/feature.ts
@@ -118,6 +118,7 @@ export type CurrencyFeatures = {
currencyScroll: DefaultFeature;
currencyScrollSepolia: DefaultFeature;
currencyIcon: 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 @@
+
+
diff --git a/libs/ui/packages/crypto-icons/src/svg/sABI.svg b/libs/ui/packages/crypto-icons/src/svg/sABI.svg
index f4c2669d3d6f..79c304b1d269 100644
--- a/libs/ui/packages/crypto-icons/src/svg/sABI.svg
+++ b/libs/ui/packages/crypto-icons/src/svg/sABI.svg
@@ -1,3 +1,3 @@
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 14f0d7b1c020..91ac3c6d9004 100644
--- a/package.json
+++ b/package.json
@@ -69,6 +69,7 @@
"coin:tester": "pnpm --filter coin-tester",
"coin:near": "pnpm --filter coin-near",
"coin:polkadot": "pnpm --filter coin-polkadot",
+ "coin:ton": "pnpm --filter coin-ton",
"coin:solana": "pnpm --filter coin-solana",
"coin:icon": "pnpm --filter coin-icon",
"coin:stellar": "pnpm --filter coin-stellar",
@@ -199,7 +200,9 @@
"react-native-video@5.2.1": "patches/react-native-video@5.2.1.patch",
"@hashgraph/sdk@2.14.2": "patches/@hashgraph__sdk@2.14.2.patch",
"@changesets/get-github-info@0.6.0": "patches/@changesets__get-github-info@0.6.0.patch",
- "react-native-webview@13.10.3": "patches/react-native-webview@13.10.3.patch"
+ "react-native-webview@13.10.3": "patches/react-native-webview@13.10.3.patch",
+ "buffer@6.0.3": "patches/buffer@6.0.3.patch",
+ "react-native-fast-pbkdf2@0.3.1": "patches/react-native-fast-pbkdf2@0.3.1.patch"
},
"packageExtensions": {
"eslint-config-next@*": {
diff --git a/patches/buffer@6.0.3.patch b/patches/buffer@6.0.3.patch
new file mode 100644
index 000000000000..d4a0a4f885fd
--- /dev/null
+++ b/patches/buffer@6.0.3.patch
@@ -0,0 +1,31 @@
+diff --git a/index.d.ts b/index.d.ts
+index 07096a2f725aaccf84776b5e8158ac95038484cf..e461a2bdf35518aaaabb938384d985d0a594b28a 100644
+--- a/index.d.ts
++++ b/index.d.ts
+@@ -7,6 +7,7 @@ export class Buffer extends Uint8Array {
+ compare(otherBuffer: Uint8Array, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): number;
+ copy(targetBuffer: Buffer, targetStart?: number, sourceStart?: number, sourceEnd?: number): number;
+ slice(start?: number, end?: number): Buffer;
++ subarray(start?: number, end?: number): Buffer;
+ writeUIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number;
+ writeUIntBE(value: number, offset: number, byteLength: number, noAssert?: boolean): number;
+ writeIntLE(value: number, offset: number, byteLength: number, noAssert?: boolean): number;
+diff --git a/index.js b/index.js
+index 7a0e9c2a123bc9d26c20bb3de4a3c4e49b24ee40..748adf8821db72f8277d6689f00f758760bf94df 100644
+--- a/index.js
++++ b/index.js
+@@ -1119,6 +1119,14 @@ Buffer.prototype.slice = function slice (start, end) {
+ return newBuf
+ }
+
++// got from https://github.com/craftzdog/react-native-buffer/commit/1adab65d393ba30829e35ca83d4304d1d1303749
++Buffer.prototype.subarray = function subarray (start, end) {
++ const newBuf = Uint8Array.prototype.subarray.call(this, start, end)
++ Object.setPrototypeOf(newBuf, Buffer.prototype)
++
++ return newBuf
++}
++
+ /*
+ * Need to make sure that buffer isn't trying to write out of bounds.
+ */
diff --git a/patches/react-native-fast-pbkdf2@0.3.1.patch b/patches/react-native-fast-pbkdf2@0.3.1.patch
new file mode 100644
index 000000000000..a6390e95ef61
--- /dev/null
+++ b/patches/react-native-fast-pbkdf2@0.3.1.patch
@@ -0,0 +1,18 @@
+diff --git a/android/build.gradle b/android/build.gradle
+index a022b38..2102b47 100644
+--- a/android/build.gradle
++++ b/android/build.gradle
+@@ -18,11 +18,11 @@ def safeExtGet(prop, fallback) {
+ }
+
+ android {
+- compileSdkVersion safeExtGet('Pbkdf2_compileSdkVersion', 29)
++ compileSdkVersion safeExtGet('Pbkdf2_compileSdkVersion', 30)
+ buildToolsVersion safeExtGet('Pbkdf2_buildToolsVersion', '29.0.2')
+ defaultConfig {
+ minSdkVersion safeExtGet('Pbkdf2_minSdkVersion', 16)
+- targetSdkVersion safeExtGet('Pbkdf2_targetSdkVersion', 29)
++ targetSdkVersion safeExtGet('Pbkdf2_targetSdkVersion', 30)
+ versionCode 1
+ versionName "1.0"
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index dca73515a464..1a2f45533216 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -27,12 +27,18 @@ patchedDependencies:
asyncstorage-down@4.2.0:
hash: 2npkndps6fxdasqj3xzcrnnfbe
path: patches/asyncstorage-down@4.2.0.patch
+ buffer@6.0.3:
+ hash: 2xnca52oxhztvr7iaoovwclcze
+ path: patches/buffer@6.0.3.patch
detox@20.23.0:
hash: yanhspfjw3apvvf4gev5ovygia
path: patches/detox@20.23.0.patch
react-native-fast-crypto@2.2.0:
hash: jdmv3zyvsaug2f6l23zgrmwdli
path: patches/react-native-fast-crypto@2.2.0.patch
+ react-native-fast-pbkdf2@0.3.1:
+ hash: ykj6nejfmobcfk2ww7pwsfjwje
+ path: patches/react-native-fast-pbkdf2@0.3.1.patch
react-native-image-crop-tools@1.6.4:
hash: p6wzwon3gzjcspjalwhmuroog4
path: patches/react-native-image-crop-tools@1.6.4.patch
@@ -951,7 +957,7 @@ importers:
version: 9.1.2
buffer:
specifier: 6.0.3
- version: 6.0.3
+ version: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
color:
specifier: ^4.0.0
version: 4.2.3
@@ -994,6 +1000,9 @@ importers:
expo-modules-core:
specifier: ^1.11.8
version: 1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
+ expo-random:
+ specifier: ^13.6.0
+ version: 13.6.0(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(expo@50.0.14(@babel/core@7.24.3)(@react-native/babel-preset@0.73.19(@babel/core@7.24.3))(expo-modules-autolinking@1.10.3(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
fuse.js:
specifier: ^6.4.6
version: 6.6.2
@@ -1072,6 +1081,9 @@ importers:
react-native-fast-image:
specifier: ^8.5.11
version: 8.6.3(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
+ react-native-fast-pbkdf2:
+ specifier: ^0.3.1
+ version: 0.3.1(patch_hash=ykj6nejfmobcfk2ww7pwsfjwje)(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
react-native-gesture-handler:
specifier: ^2.9.0
version: 2.16.0(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
@@ -2605,6 +2617,85 @@ importers:
specifier: ^29.1.4
version: 29.1.5(jest@29.7.0)(typescript@5.4.3)
+ libs/coin-modules/coin-ton:
+ dependencies:
+ '@ledgerhq/coin-framework':
+ specifier: workspace:^
+ version: link:../../coin-framework
+ '@ledgerhq/cryptoassets':
+ specifier: workspace:^
+ version: link:../../ledgerjs/packages/cryptoassets
+ '@ledgerhq/devices':
+ specifier: workspace:*
+ version: link:../../ledgerjs/packages/devices
+ '@ledgerhq/errors':
+ specifier: workspace:^
+ version: link:../../ledgerjs/packages/errors
+ '@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/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.2(@ton/core@0.56.3(@ton/crypto@3.2.0))(@ton/crypto@3.2.0)
+ bignumber.js:
+ specifier: ^9.1.2
+ version: 9.1.2
+ expect:
+ specifier: ^27.4.6
+ version: 27.5.1
+ imurmurhash:
+ specifier: ^0.1.4
+ version: 0.1.4
+ invariant:
+ specifier: ^2.2.2
+ version: 2.2.4
+ lodash:
+ specifier: ^4.17.21
+ version: 4.17.21
+ msw:
+ specifier: ^2.0.11
+ version: 2.3.1(typescript@5.4.3)
+ rxjs:
+ specifier: ^7.8.1
+ version: 7.8.1
+ devDependencies:
+ '@types/imurmurhash':
+ specifier: ^0.1.4
+ version: 0.1.4
+ '@types/invariant':
+ specifier: ^2.2.2
+ version: 2.2.37
+ '@types/jest':
+ specifier: ^29.5.10
+ version: 29.5.12
+ '@types/lodash':
+ specifier: ^4.14.191
+ version: 4.17.4
+ jest:
+ specifier: ^29.7.0
+ version: 29.7.0
+ ts-jest:
+ specifier: ^29.1.1
+ version: 29.1.5(jest@29.7.0)(typescript@5.4.3)
+
libs/coin-modules/coin-tron:
dependencies:
'@ledgerhq/coin-framework':
@@ -3164,6 +3255,9 @@ importers:
'@ledgerhq/coin-tezos':
specifier: workspace:^
version: link:../coin-modules/coin-tezos
+ '@ledgerhq/coin-ton':
+ specifier: workspace:^
+ version: link:../coin-modules/coin-ton
'@ledgerhq/coin-tron':
specifier: workspace:^
version: link:../coin-modules/coin-tron
@@ -3293,6 +3387,15 @@ importers:
'@taquito/ledger-signer':
specifier: ^20.0.0
version: 20.0.0
+ '@ton-community/ton-ledger':
+ specifier: ^7.0.1
+ version: 7.0.1(@ton/core@0.56.3(@ton/crypto@3.2.0))
+ '@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
'@types/bchaddrjs':
specifier: ^0.4.0
version: 0.4.3
@@ -6600,7 +6703,7 @@ importers:
version: 9.1.2
buffer:
specifier: ^6.0.3
- version: 6.0.3
+ version: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
react:
specifier: ^18.2.0
version: 18.2.0
@@ -13713,6 +13816,28 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
+ '@ton-community/ton-ledger@7.0.1':
+ resolution: {integrity: sha512-4QDjG9D/c3vKyApKa36KfqyE0UcPn/ExSJvCb8Cd03BdhMiZQNvm7tJsKg12lslPCIbl1dW9ZC6fg1RvqAAqnQ==}
+ peerDependencies:
+ '@ton/core': '>=0.52.2'
+
+ '@ton/core@0.56.3':
+ resolution: {integrity: sha512-HVkalfqw8zqLLPehtq0CNhu5KjVzc7IrbDwDHPjGoOSXmnqSobiWj8a5F+YuWnZnEbQKtrnMGNOOjVw4LG37rg==}
+ peerDependencies:
+ '@ton/crypto': '>=3.2.0'
+
+ '@ton/crypto-primitives@2.0.0':
+ resolution: {integrity: sha512-wttiNClmGbI6Dfy/8oyNnsIV0b/qYkCJz4Gn4eP62lJZzMtVQ94Ko7nikDX1EfYHkLI1xpOitWpW+8ZuG6XtDg==}
+
+ '@ton/crypto@3.2.0':
+ resolution: {integrity: sha512-50RkwReEuV2FkxSZ8ht/x9+n0ZGtwRKGsJ0ay4I/HFhkYVG/awIIBQeH0W4j8d5lADdO5h01UtX8PJ8AjiejjA==}
+
+ '@ton/ton@13.11.2':
+ resolution: {integrity: sha512-EPqW+ZTe0MmfqguJEIGMuAqTAFRKMEce95HlDx8h6CGn2y3jiMgV1/oO+WpDIOiX+1wnTu+xtajk8JTWr8nKRQ==}
+ peerDependencies:
+ '@ton/core': '>=0.56.0'
+ '@ton/crypto': '>=3.2.0'
+
'@tootallnate/once@1.1.2':
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
engines: {node: '>= 6'}
@@ -17446,6 +17571,9 @@ packages:
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
+ dataloader@2.2.2:
+ resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==}
+
date-fns@1.30.1:
resolution: {integrity: sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw==}
@@ -19117,6 +19245,24 @@ packages:
react-native:
optional: true
+ expo-random@13.6.0:
+ resolution: {integrity: sha512-c4Ikio+a2sUyJC0386K6JplqjVDelsyqQfjiy4yCx+0epEu44AP99ipF+HsmZVOvsWsWkd/lkpq5kGnJON5EfA==}
+ peerDependencies:
+ expo: '*'
+ expo-constants: '*'
+ expo-modules-core: '*'
+ react: '*'
+ react-native: '*'
+ peerDependenciesMeta:
+ expo-constants:
+ optional: true
+ expo-modules-core:
+ optional: true
+ react:
+ optional: true
+ react-native:
+ optional: true
+
expo@49.0.23:
resolution: {integrity: sha512-mFdBpWisPXBuocRGywC14nDai5vSUmvEyQpwvKH/xUo+m5/TUvfqV6YIewFpW22zn5WFGFiuJPhzNrqhBBinIw==}
hasBin: true
@@ -21792,6 +21938,9 @@ packages:
jsqr@1.4.0:
resolution: {integrity: sha512-dxLob7q65Xg2DvstYkRpkYtmKm2sPJ9oFhrhmudT1dZvNFFTlroai3AWSpLey/w5vMcLBXRgOJsbXpdN9HzU/A==}
+ jssha@3.2.0:
+ resolution: {integrity: sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==}
+
jsx-ast-utils@3.3.5:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
@@ -25261,6 +25410,12 @@ packages:
react: ^17 || ^18
react-native: '>=0.60.0'
+ react-native-fast-pbkdf2@0.3.1:
+ resolution: {integrity: sha512-G+1EdQs+w7RhgZKXbodbGPy6zKxX3qdSe1YSh9B7x6qZLTd3dDI15R33faWoiQjrESepbfvX3M64UevBT6SDDA==}
+ peerDependencies:
+ react: '*'
+ react-native: '*'
+
react-native-flipper-performance-plugin@0.4.0:
resolution: {integrity: sha512-D9Z5VrktJjUenYB4X9qfKcqg9YSyQWOD7fKoxkzXoKxbyalGoWib79iD3OBHOBZEsk5RbQ006zYL7VQwA3D4Wg==}
@@ -27213,6 +27368,9 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
+ symbol.inspect@1.0.1:
+ resolution: {integrity: sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==}
+
symbol.prototype.description@1.0.6:
resolution: {integrity: sha512-VgVgtEabORsQtmuindtO7v8fF+bsKxUkvEMFj+ecBK6bomrwv5JUSWdMoC3ypa9+Jaqp/wOzkWk4f6I+p5GzyA==}
engines: {node: '>= 0.4'}
@@ -27343,6 +27501,9 @@ packages:
engines: {node: '>=10'}
hasBin: true
+ teslabot@1.5.0:
+ resolution: {integrity: sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg==}
+
test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
engines: {node: '>=8'}
@@ -32345,7 +32506,7 @@ snapshots:
'@noble/hashes': 1.4.0
base64-arraybuffer: 0.2.0
borc: 2.1.2
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
simple-cbor: 0.4.1
'@dfinity/candid@0.21.4(@dfinity/principal@0.15.7(@types/node@20.12.12)(typescript@5.1.3))':
@@ -32433,7 +32594,7 @@ snapshots:
axios: 0.24.0
bech32: 1.1.4
bignumber.js: 9.0.1
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
json-bigint: 1.0.0
'@elrondnetwork/erdjs@11.0.0':
@@ -32442,7 +32603,7 @@ snapshots:
bech32: 1.1.4
bignumber.js: 9.0.1
blake2b: 2.1.3
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
json-duplicate-key-handle: 1.0.0
keccak: 3.0.2
protobufjs: 6.11.3
@@ -35030,7 +35191,7 @@ snapshots:
dependencies:
'@jest/types': 27.5.1
'@sinonjs/fake-timers': 8.1.0
- '@types/node': 20.12.12
+ '@types/node': 20.14.10
jest-message-util: 27.5.1
jest-mock: 27.5.1
jest-util: 27.5.1
@@ -35341,7 +35502,7 @@ snapshots:
dependencies:
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
- '@types/node': 20.12.12
+ '@types/node': 20.14.10
'@types/yargs': 16.0.9
chalk: 4.1.2
@@ -35404,7 +35565,7 @@ snapshots:
'@keplr-wallet/unit': 0.9.12
axios: 0.21.4
bech32: 1.1.4
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
long: 4.0.0
protobufjs: 6.11.4
@@ -35412,7 +35573,7 @@ snapshots:
dependencies:
bip32: 2.0.6
bip39: 3.1.0
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
crypto-js: 4.2.0
elliptic: 6.5.5
sha.js: 2.4.11
@@ -39131,14 +39292,14 @@ snapshots:
'@solana/buffer-layout@4.0.1':
dependencies:
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
'@solana/spl-token@0.3.8(@solana/web3.js@1.77.3)':
dependencies:
'@solana/buffer-layout': 4.0.1
'@solana/buffer-layout-utils': 0.2.0
'@solana/web3.js': 1.77.3
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
transitivePeerDependencies:
- bufferutil
- encoding
@@ -39155,7 +39316,7 @@ snapshots:
bn.js: 5.2.1
borsh: 0.7.0
bs58: 4.0.1
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
fast-stable-stringify: 1.0.0
jayson: 4.1.0
node-fetch: 2.7.0
@@ -39203,7 +39364,7 @@ snapshots:
dependencies:
'@types/bn.js': 5.1.5
'@types/node': 18.19.39
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
'@stacks/common@6.13.0':
dependencies:
@@ -39876,7 +40037,7 @@ snapshots:
'@stellar/js-xdr': 3.1.1
base32.js: 0.1.0
bignumber.js: 9.1.2
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
sha.js: 2.4.11
tweetnacl: 1.0.3
optionalDependencies:
@@ -40706,7 +40867,7 @@ snapshots:
'@storybook/core-common': 7.6.17
'@storybook/node-logger': 7.6.17
'@storybook/types': 7.6.17
- '@types/node': 18.19.26
+ '@types/node': 18.19.39
ts-dedent: 2.2.0
transitivePeerDependencies:
- encoding
@@ -40934,7 +41095,7 @@ snapshots:
endent: 2.1.0
find-cache-dir: 3.3.2
flat-cache: 3.2.0
- micromatch: 4.0.5
+ micromatch: 4.0.7
react-docgen-typescript: 2.2.2(typescript@5.4.3)
tslib: 2.6.2
typescript: 5.4.3
@@ -40948,7 +41109,7 @@ snapshots:
endent: 2.1.0
find-cache-dir: 3.3.2
flat-cache: 3.2.0
- micromatch: 4.0.5
+ micromatch: 4.0.7
react-docgen-typescript: 2.2.2(typescript@5.4.3)
tslib: 2.6.2
typescript: 5.4.3
@@ -41210,7 +41371,7 @@ snapshots:
dependencies:
blakejs: 1.2.1
bn.js: 5.2.1
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
chai: 4.4.1
elliptic: 6.5.4
hash.js: 1.1.7
@@ -41219,7 +41380,7 @@ snapshots:
'@stricahq/cbors@1.0.2':
dependencies:
bignumber.js: 9.1.2
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
'@stricahq/typhonjs@1.2.8':
dependencies:
@@ -41228,7 +41389,7 @@ snapshots:
bignumber.js: 9.1.2
blakejs: 1.2.1
bs58: 5.0.0
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
lodash: 4.17.21
'@styled-system/background@5.1.2':
@@ -41514,7 +41675,7 @@ snapshots:
'@taquito/core': 20.0.0
'@taquito/taquito': 20.0.0
'@taquito/utils': 20.0.0
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
transitivePeerDependencies:
- encoding
@@ -41570,7 +41731,7 @@ snapshots:
bignumber.js: 9.1.2
blakejs: 1.2.1
bs58check: 3.0.1
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
elliptic: 6.5.5
typedarray-to-buffer: 4.0.0
@@ -41687,6 +41848,38 @@ snapshots:
react-dom: 18.2.0(react@18.2.0)
tippy.js: 6.3.7
+ '@ton-community/ton-ledger@7.0.1(@ton/core@0.56.3(@ton/crypto@3.2.0))':
+ dependencies:
+ '@ledgerhq/hw-transport': 6.30.6
+ '@ton/core': 0.56.3(@ton/crypto@3.2.0)
+ '@ton/crypto': 3.2.0
+ teslabot: 1.5.0
+
+ '@ton/core@0.56.3(@ton/crypto@3.2.0)':
+ dependencies:
+ '@ton/crypto': 3.2.0
+ symbol.inspect: 1.0.1
+
+ '@ton/crypto-primitives@2.0.0':
+ dependencies:
+ jssha: 3.2.0
+
+ '@ton/crypto@3.2.0':
+ dependencies:
+ '@ton/crypto-primitives': 2.0.0
+ jssha: 3.2.0
+ tweetnacl: 1.0.3
+
+ '@ton/ton@13.11.2(@ton/core@0.56.3(@ton/crypto@3.2.0))(@ton/crypto@3.2.0)':
+ dependencies:
+ '@ton/core': 0.56.3(@ton/crypto@3.2.0)
+ '@ton/crypto': 3.2.0
+ axios: 1.6.8
+ dataloader: 2.2.2
+ symbol.inspect: 1.0.1
+ teslabot: 1.5.0
+ zod: 3.22.4
+
'@tootallnate/once@1.1.2': {}
'@tootallnate/once@2.0.0': {}
@@ -41839,7 +42032,7 @@ snapshots:
'@types/cross-spawn@6.0.6':
dependencies:
- '@types/node': 20.12.12
+ '@types/node': 20.14.10
'@types/crypto-js@4.2.2': {}
@@ -42514,7 +42707,7 @@ snapshots:
graphemer: 1.4.0
ignore: 5.3.1
natural-compare-lite: 1.4.0
- semver: 7.5.4
+ semver: 7.6.2
tsutils: 3.21.0(typescript@5.4.3)
optionalDependencies:
typescript: 5.4.3
@@ -43259,7 +43452,7 @@ snapshots:
'@vue/cli-plugin-typescript': 5.0.8(@vue/cli-service@5.0.8(lodash@4.17.21)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vue-template-compiler@2.7.16)(vue@2.7.16))(eslint@8.57.0)(typescript@5.1.3)(vue-template-compiler@2.7.16)(vue@2.7.16)
'@vue/cli-service': 5.0.8(lodash@4.17.21)(prettier@3.2.5)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vue-template-compiler@2.7.16)(vue@2.7.16)
bech32: 1.1.4
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
crypto-browserify: 3.12.0
ripemd160: 2.0.2
stream-browserify: 3.0.0
@@ -43532,7 +43725,7 @@ snapshots:
algosdk@1.13.1:
dependencies:
algo-msgpack-with-bigint: 2.1.1
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
hi-base32: 0.5.1
js-sha256: 0.9.0
js-sha3: 0.8.0
@@ -44497,7 +44690,7 @@ snapshots:
bchaddrjs@0.5.2:
dependencies:
bs58check: 2.1.2
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
cashaddrjs: 0.4.4
stream-browserify: 3.0.0
@@ -44894,7 +45087,7 @@ snapshots:
base64-js: 1.5.1
ieee754: 1.2.1
- buffer@6.0.3:
+ buffer@6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze):
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
@@ -45658,7 +45851,7 @@ snapshots:
dot-prop: 8.0.2
env-paths: 3.0.0
json-schema-typed: 8.0.1
- semver: 7.5.4
+ semver: 7.6.2
uint8array-extras: 0.3.0
config-file-ts@0.2.6:
@@ -46587,6 +46780,8 @@ snapshots:
dataloader@1.4.0: {}
+ dataloader@2.2.2: {}
+
date-fns@1.30.1: {}
date-fns@2.30.0:
@@ -48877,6 +49072,15 @@ snapshots:
react: 18.2.0
react-native: 0.73.6(react@18.2.0)
+ expo-random@13.6.0(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(expo@50.0.14(@babel/core@7.24.3)(@react-native/babel-preset@0.73.19(@babel/core@7.24.3))(expo-modules-autolinking@1.10.3(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0):
+ dependencies:
+ base64-js: 1.5.1
+ expo: 50.0.14(@babel/core@7.24.3)(@react-native/babel-preset@0.73.19(@babel/core@7.24.3))(expo-modules-autolinking@1.10.3(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(expo-modules-core@1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0))(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
+ optionalDependencies:
+ expo-modules-core: 1.11.12(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0)
+ react: 18.2.0
+ react-native: 0.73.6(@babel/core@7.24.3)(react@18.2.0)
+
expo@49.0.23(@babel/core@7.24.3)(@expo/metro-config@0.10.7)(expo-modules-core@1.5.11)(glob@7.2.3)(metro-core@0.80.8)(metro@0.80.8)(minimatch@5.1.6)(react-native@0.73.6(@babel/core@7.24.3)(@babel/preset-env@7.24.3(@babel/core@7.24.3))(metro-resolver@0.80.8)(metro-transform-worker@0.80.8)(react@18.2.0))(react@18.2.0):
dependencies:
'@babel/runtime': 7.24.1
@@ -49058,7 +49262,7 @@ snapshots:
'@nodelib/fs.walk': 1.2.8
glob-parent: 5.1.2
merge2: 1.4.1
- micromatch: 4.0.5
+ micromatch: 4.0.7
fast-json-parse@1.0.3: {}
@@ -49421,7 +49625,7 @@ snapshots:
memfs: 3.5.3
minimatch: 3.1.2
schema-utils: 2.7.0
- semver: 7.6.2
+ semver: 7.5.4
tapable: 1.1.3
typescript: 5.4.3
webpack: 5.89.0
@@ -49801,7 +50005,7 @@ snapshots:
es6-error: 4.1.1
matcher: 3.0.0
roarr: 2.15.4
- semver: 7.5.4
+ semver: 7.6.2
serialize-error: 7.0.1
optional: true
@@ -50445,7 +50649,7 @@ snapshots:
http-proxy: 1.18.1
is-glob: 4.0.3
is-plain-obj: 3.0.0
- micromatch: 4.0.5
+ micromatch: 4.0.7
optionalDependencies:
'@types/express': 4.17.21
@@ -50529,7 +50733,7 @@ snapshots:
'@babel/runtime-corejs3': 7.24.7
bignumber.js: 9.1.2
bip66: 1.1.5
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
bufferutil: 4.0.8
core-js: 3.36.1
crypto-browserify: 3.12.0
@@ -52374,7 +52578,7 @@ snapshots:
'@types/stack-utils': 2.0.3
chalk: 4.1.2
graceful-fs: 4.2.11
- micromatch: 4.0.5
+ micromatch: 4.0.7
pretty-format: 27.5.1
slash: 3.0.0
stack-utils: 2.0.6
@@ -52424,7 +52628,7 @@ snapshots:
jest-mock@27.5.1:
dependencies:
'@jest/types': 27.5.1
- '@types/node': 20.12.12
+ '@types/node': 20.14.10
jest-mock@28.1.3:
dependencies:
@@ -52719,7 +52923,7 @@ snapshots:
jest-util: 27.5.1
natural-compare: 1.4.0
pretty-format: 27.5.1
- semver: 7.5.4
+ semver: 7.6.2
transitivePeerDependencies:
- metro
- supports-color
@@ -53233,7 +53437,7 @@ snapshots:
chalk: 4.1.2
flow-parser: 0.232.0
graceful-fs: 4.2.11
- micromatch: 4.0.5
+ micromatch: 4.0.7
neo-async: 2.6.2
node-dir: 0.1.17
recast: 0.23.6
@@ -53506,6 +53710,8 @@ snapshots:
jsqr@1.4.0: {}
+ jssha@3.2.0: {}
+
jsx-ast-utils@3.3.5:
dependencies:
array-includes: 3.1.8
@@ -55993,7 +56199,7 @@ snapshots:
assert: 1.5.1
base-64: 0.1.0
browserify-zlib: 0.2.0
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
console-browserify: 1.2.0
constants-browserify: 1.0.0
domain-browser: 1.2.0
@@ -57050,7 +57256,7 @@ snapshots:
postcss-flexbugs-fixes: 5.0.2(postcss@8.4.32)
postcss-normalize: 10.0.1(browserslist@4.23.0)(postcss@8.4.32)
postcss-preset-env: 7.8.3(postcss@8.4.32)
- semver: 7.5.4
+ semver: 7.6.2
webpack: 5.89.0
transitivePeerDependencies:
- browserslist
@@ -57063,7 +57269,7 @@ snapshots:
postcss-flexbugs-fixes: 5.0.2(postcss@8.4.38)
postcss-normalize: 10.0.1(browserslist@4.23.0)(postcss@8.4.38)
postcss-preset-env: 7.8.3(postcss@8.4.38)
- semver: 7.5.4
+ semver: 7.6.2
webpack: 5.89.0
transitivePeerDependencies:
- browserslist
@@ -57833,7 +58039,7 @@ snapshots:
qrloop@1.4.1:
dependencies:
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
md5: 2.3.0
qs@6.11.0:
@@ -58207,6 +58413,11 @@ snapshots:
react: 18.2.0
react-native: 0.73.6(@babel/core@7.24.3)(react@18.2.0)
+ react-native-fast-pbkdf2@0.3.1(patch_hash=ykj6nejfmobcfk2ww7pwsfjwje)(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0):
+ dependencies:
+ react: 18.2.0
+ react-native: 0.73.6(@babel/core@7.24.3)(react@18.2.0)
+
react-native-flipper-performance-plugin@0.4.0: {}
react-native-flipper@0.163.0(react-native@0.73.6(@babel/core@7.24.3)(react@18.2.0))(react@18.2.0):
@@ -58996,7 +59207,7 @@ snapshots:
readable-stream@4.5.2:
dependencies:
abort-controller: 3.0.0
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
events: 3.3.0
process: 0.11.10
string_decoder: 1.3.0
@@ -59501,7 +59712,7 @@ snapshots:
dependencies:
assert: 2.1.0
big-integer: 1.6.52
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
create-hash: 1.2.0
decimal.js: 10.4.3
ripple-address-codec: 4.3.1
@@ -59929,7 +60140,7 @@ snapshots:
sha3@2.1.4:
dependencies:
- buffer: 6.0.3
+ buffer: 6.0.3(patch_hash=2xnca52oxhztvr7iaoovwclcze)
shaka-player@2.5.23:
dependencies:
@@ -60987,6 +61198,8 @@ snapshots:
symbol-tree@3.2.4: {}
+ symbol.inspect@1.0.1: {}
+
symbol.prototype.description@1.0.6:
dependencies:
call-bind: 1.0.7
@@ -61272,6 +61485,8 @@ snapshots:
commander: 2.20.3
source-map-support: 0.5.21
+ teslabot@1.5.0: {}
+
test-exclude@6.0.0:
dependencies:
'@istanbuljs/schema': 0.1.3
diff --git a/tools/actions/turbo-affected/build/main.js b/tools/actions/turbo-affected/build/main.js
index 128d5978a024..ecb3fc14aa64 100644
--- a/tools/actions/turbo-affected/build/main.js
+++ b/tools/actions/turbo-affected/build/main.js
@@ -19363,13 +19363,13 @@ var package_default = {
"coin:cardano": "pnpm --filter coin-cardano",
"coin:evm": "pnpm --filter coin-evm",
"coin:framework": "pnpm --filter coin-framework",
- "coin:tester": "pnpm --filter coin-tester",
"coin:near": "pnpm --filter coin-near",
"coin:polkadot": "pnpm --filter coin-polkadot",
"coin:solana": "pnpm --filter coin-solana",
"coin:tezos": "pnpm --filter coin-tezos",
"coin:tron": "pnpm --filter coin-tron",
"coin:xrp": "pnpm --filter coin-xrp",
+ "coin:ton": "pnpm --filter coin-ton",
"evm-tools": "pnpm --filter evm-tools",
domain: "pnpm --filter domain-service",
doc: "pnpm --filter docs",