diff --git a/apps/desktop/.env.example b/apps/desktop/.env.example index 1f6d9cbc6..b4802c19f 100644 --- a/apps/desktop/.env.example +++ b/apps/desktop/.env.example @@ -2,3 +2,4 @@ REACT_APP_TONCONSOLE_API=https://dev-pro.tonconsole.com REACT_APP_TG_BOT_ID=6345183204 REACT_APP_TG_BOT_ORIGIN=https://tonkeeper.com REACT_APP_STONFI_REFERRAL_ADDRESS=UQCthC8ICK7K8Hkfm9smblLFroKrYrEMwZuoD4Nbm5LswUnc +REACT_APP_TRON_API_KEY= diff --git a/apps/desktop/src/app/App.tsx b/apps/desktop/src/app/App.tsx index e9cbb08cc..6077c44c7 100644 --- a/apps/desktop/src/app/App.tsx +++ b/apps/desktop/src/app/App.tsx @@ -89,6 +89,7 @@ import { useGlobalPreferencesQuery } from '@tonkeeper/uikit/dist/state/global-pr import { DesktopManageMultisigsPage } from '@tonkeeper/uikit/dist/desktop-pages/manage-multisig-wallets/DesktopManageMultisigs'; import { useGlobalSetup } from '@tonkeeper/uikit/dist/state/globalSetup'; import { DesktopMultisigOrdersPage } from '@tonkeeper/uikit/dist/desktop-pages/multisig-orders/DesktopMultisigOrders'; +import { TronApi } from '@tonkeeper/core/dist/tronApi'; const queryClient = new QueryClient({ defaultOptions: { @@ -134,6 +135,7 @@ const langs = 'en,zh_TW,zh_CN,id,ru,it,es,uk,tr,bg,uz,bn'; declare const REACT_APP_TONCONSOLE_API: string; declare const REACT_APP_TG_BOT_ID: string; declare const REACT_APP_STONFI_REFERRAL_ADDRESS: string; +declare const REACT_APP_TRON_API_KEY: string; export const Providers = () => { const { t: tSimple, i18n } = useTranslation(); @@ -333,7 +335,8 @@ export const Loader: FC = () => { ios: false, env: { tgAuthBotId: REACT_APP_TG_BOT_ID, - stonfiReferralAddress: REACT_APP_STONFI_REFERRAL_ADDRESS + stonfiReferralAddress: REACT_APP_STONFI_REFERRAL_ADDRESS, + tronApiKey: REACT_APP_TRON_API_KEY }, defaultWalletVersion: WalletVersion.V5R1 }; diff --git a/apps/desktop/webpack.plugins.ts b/apps/desktop/webpack.plugins.ts index 754e4081c..58373f580 100644 --- a/apps/desktop/webpack.plugins.ts +++ b/apps/desktop/webpack.plugins.ts @@ -21,6 +21,7 @@ export const plugins = [ process.env.REACT_APP_STONFI_REFERRAL_ADDRESS ), REACT_APP_APTABASE: JSON.stringify(process.env.REACT_APP_APTABASE), - REACT_APP_APTABASE_HOST: JSON.stringify(process.env.REACT_APP_APTABASE_HOST) + REACT_APP_APTABASE_HOST: JSON.stringify(process.env.REACT_APP_APTABASE_HOST), + REACT_APP_TRON_API_KEY: JSON.stringify(process.env.REACT_APP_TRON_API_KEY) }) ]; diff --git a/apps/extension/config-overrides.js b/apps/extension/config-overrides.js index 3bd47fe59..ceb61b58f 100644 --- a/apps/extension/config-overrides.js +++ b/apps/extension/config-overrides.js @@ -9,7 +9,8 @@ module.exports = function override(config, env) { ...config.resolve.fallback, buffer: require.resolve('buffer'), crypto: require.resolve('crypto-browserify'), - stream: require.resolve('stream-browserify') + stream: require.resolve('stream-browserify'), + 'process/browser': require.resolve('process/browser') }; config.resolve.alias = { diff --git a/apps/extension/src/App.tsx b/apps/extension/src/App.tsx index 55445d47f..a0b95f893 100644 --- a/apps/extension/src/App.tsx +++ b/apps/extension/src/App.tsx @@ -238,7 +238,10 @@ export const Loader: FC = React.memo(() => { hideSigner: true, hideMam: true, hideMultisig: true, - defaultWalletVersion: WalletVersion.V5R1 + defaultWalletVersion: WalletVersion.V5R1, + env: { + tronApiKey: process.env.REACT_APP_TRON_API_KEY + } }; return ( diff --git a/apps/tablet/.env.example b/apps/tablet/.env.example index 86d0a079a..0bfa8bd08 100644 --- a/apps/tablet/.env.example +++ b/apps/tablet/.env.example @@ -5,3 +5,4 @@ VITE_APP_TONCONSOLE_HOST=https://pro.tonconsole.com VITE_APP_TG_BOT_ID=6345183204 VITE_APP_TG_BOT_ORIGIN=https://tonkeeper.com VITE_APP_STONFI_REFERRAL_ADDRESS=UQCthC8ICK7K8Hkfm9smblLFroKrYrEMwZuoD4Nbm5LswUnc +VITE_APP_TRON_API_KEY= diff --git a/apps/tablet/src/app/App.tsx b/apps/tablet/src/app/App.tsx index dbedaf775..c08f0a8fb 100644 --- a/apps/tablet/src/app/App.tsx +++ b/apps/tablet/src/app/App.tsx @@ -348,7 +348,8 @@ export const Loader: FC = () => { ios: false, env: { tgAuthBotId: import.meta.env.VITE_APP_TG_BOT_ID, - stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS + stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS, + tronApiKey: import.meta.env.VITE_APP_TRON_API_KEY }, defaultWalletVersion: WalletVersion.V5R1 }; diff --git a/apps/twa/src/App.tsx b/apps/twa/src/App.tsx index a18f95cad..3759fd019 100644 --- a/apps/twa/src/App.tsx +++ b/apps/twa/src/App.tsx @@ -284,7 +284,10 @@ export const Loader: FC<{ sdk: TwaAppSdk }> = ({ sdk }) => { hideMam: true, hideMultisig: true, defaultWalletVersion: WalletVersion.V5R1, - browserLength: 4 + browserLength: 4, + env: { + tronApiKey: import.meta.env.VITE_APP_TRON_API_KEY + } }; return ( diff --git a/apps/twa/src/components/transfer/SendNotifications.tsx b/apps/twa/src/components/transfer/SendNotifications.tsx index b598ebdd9..60a01b8f7 100644 --- a/apps/twa/src/components/transfer/SendNotifications.tsx +++ b/apps/twa/src/components/transfer/SendNotifications.tsx @@ -1,7 +1,6 @@ import { TransferInitParams } from '@tonkeeper/core/dist/AppSdk'; import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; -import { toTronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { jettonToTonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { RecipientData } from '@tonkeeper/core/dist/entries/send'; import { @@ -34,7 +33,7 @@ import { useAppSdk } from '@tonkeeper/uikit/dist/hooks/appSdk'; import { openIosKeyboard } from '@tonkeeper/uikit/dist/hooks/ios'; import { useTranslation } from '@tonkeeper/uikit/dist/hooks/translation'; import { useJettonList } from '@tonkeeper/uikit/dist/state/jetton'; -import { useTronBalances } from '@tonkeeper/uikit/dist/state/tron/tron'; +import { useActiveTronWallet, useTronBalances } from "@tonkeeper/uikit/dist/state/tron/tron"; import BigNumber from 'bignumber.js'; import { FC, PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { CSSTransition, TransitionGroup } from 'react-transition-group'; @@ -52,6 +51,8 @@ import { RecipientTwaHeaderBlock } from './SendNotificationHeader'; import { useAnalyticsTrack } from '@tonkeeper/uikit/dist/hooks/amplitude'; +import { TRON_USDT_ASSET } from "@tonkeeper/core/dist/entries/crypto/asset/constants"; +import { seeIfValidTronAddress } from "@tonkeeper/core/dist/utils/common"; const Body = styled.div` padding: 0 16px 16px; @@ -93,10 +94,10 @@ const SendContent: FC<{ } }, []); - const { data: tronBalances } = useTronBalances(); - const { mutateAsync: getAccountAsync, isLoading: isAccountLoading } = useGetToAccount(); + const activeTronWallet = useActiveTronWallet(); + const setRecipient = (value: RecipientData) => { if ( amountViewState?.token?.blockchain && @@ -106,8 +107,8 @@ const SendContent: FC<{ } _setRecipient(value); - if (tronBalances && value.address.blockchain === BLOCKCHAIN_NAME.TRON) { - setAmountViewState({ token: toTronAsset(tronBalances.balances[0]) }); + if (activeTronWallet && value.address.blockchain === BLOCKCHAIN_NAME.TRON) { + setAmountViewState({ token: TRON_USDT_ASSET }); } }; @@ -152,6 +153,9 @@ const SendContent: FC<{ }; const processTron = (address: string) => { + if (!activeTronWallet) { + return; + } const item = { address: address, blockchain: BLOCKCHAIN_NAME.TRON } as const; setRecipient({ @@ -228,10 +232,9 @@ const SendContent: FC<{ return; } - // TODO: ENABLE TRON - // if (seeIfValidTronAddress(signature)) { - // return processTron(signature); - // } + if (seeIfValidTronAddress(signature)) { + return processTron(signature); + } return sdk.uiEvents.emit('copy', { method: 'copy', @@ -258,6 +261,19 @@ const SendContent: FC<{ }); }, [amountViewState?.token, amountViewState?.coinValue]); + let acceptBlockchains: BLOCKCHAIN_NAME[] = []; + if (chain) { + if (chain === BLOCKCHAIN_NAME.TRON && !activeTronWallet) { + acceptBlockchains = [BLOCKCHAIN_NAME.TON]; + } else { + acceptBlockchains = [chain]; + } + } else { + acceptBlockchains = activeTronWallet + ? [BLOCKCHAIN_NAME.TON, BLOCKCHAIN_NAME.TRON] + : [BLOCKCHAIN_NAME.TON]; + } + return ( @@ -279,7 +295,7 @@ const SendContent: FC<{ onScan={onScan} keyboard="decimal" isExternalLoading={isAccountLoading} - acceptBlockchains={chain ? [chain] : undefined} + acceptBlockchains={acceptBlockchains} MainButton={RecipientTwaMainButton} HeaderBlock={() => } fitContent @@ -348,6 +364,13 @@ export const TwaSendNotification: FC = ({ children }) => { reset(); const transfer = options.params; setChain(chain); + + if (transfer.chain === BLOCKCHAIN_NAME.TRON) { + setOpen(true); + track('send_open', { from: transfer.from }); + return; + } + if (transfer.address) { getAccountAsync({ address: transfer.address }).then(account => { setTonTransfer(makeTransferInitData(transfer, account, jettons)); diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 3ed81b046..aa18e6b71 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -193,7 +193,8 @@ const Loader: FC = () => { hideMultisig: isMobile, env: { tgAuthBotId: import.meta.env.VITE_APP_TG_BOT_ID, - stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS + stonfiReferralAddress: import.meta.env.VITE_APP_STONFI_REFERRAL_ADDRESS, + tronApiKey: import.meta.env.VITE_APP_TRON_API_KEY } }; diff --git a/packages/core/package.json b/packages/core/package.json index cea6501d3..c4a47f66f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -9,7 +9,6 @@ "start": "tsc -w", "sdk": "npm-run-all -p generate:*", "generate:sdkV2": "rm -fr src/tonApiV2 && docker build -f resource/Dockerfile.apiV2 . -t tonapisdkv2 && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local tonapisdkv2", - "generate:tronApi": "rm -fr src/tronApi && docker build -f resource/Dockerfile.tronApi . -t tronapisdk && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local tronapisdk", "generate:tonConsoleApi": "rm -r ./src/tonConsoleApi || true && npx openapi-typescript-codegen --input 'https://raw.githubusercontent.com/tonkeeper/tonconsole_backend/refs/heads/main/swagger.yaml?token=GHSAT0AAAAAACJYQUOCGQXJBM2BFNOB6XREZ2ZZZCA' --output ./src/tonConsoleApi", "generate:swapsApi": "rm -r ./src/swapsApi || true && npx openapi-typescript-codegen --input 'https://raw.githubusercontent.com/tonkeeper/swaps-backend/master/swagger.yaml?token=GHSAT0AAAAAACJYQUODBKR67AB7WULZBFWEZSUUGFQ' --output ./src/swapsApi", "generate:batteryApi": "rm -fr ./src/batteryApi && docker build -f resource/Dockerfile.batteryApi . -t batteryapi && docker run --rm --user=$(id -u):$(id -g) -v \"$PWD\":/local batteryapi", @@ -27,13 +26,15 @@ "@ledgerhq/hw-transport-webusb": "^6.28.6", "@ton-community/ton-ledger": "^7.2.0-pre.3", "@ton-keychain/core": "^0.0.4", + "@ton-keychain/trx": "^0.0.7", "@ton/core": "0.56.0", "@ton/crypto": "3.2.0", "@ton/ton": "^15.1.0", "bignumber.js": "^9.1.1", "bip39": "^3.1.0", - "ethers": "^6.6.5", + "ethers": "^6.13.4", "query-string": "^8.1.0", + "tronweb": "^6.0.0", "tweetnacl": "^1.0.3" } } diff --git a/packages/core/resource/Dockerfile.tronApi b/packages/core/resource/Dockerfile.tronApi deleted file mode 100644 index 837ef89b4..000000000 --- a/packages/core/resource/Dockerfile.tronApi +++ /dev/null @@ -1,3 +0,0 @@ -FROM openapitools/openapi-generator-cli - -CMD ["generate", "-i", "https://raw.githubusercontent.com/tonkeeper/gasless-tron-protocol/main/api/openapi.yml", "-g", "typescript-fetch", "-o", "/local/src/tronApi", "-p", "supportsES6=true,withInterfaces=true"] \ No newline at end of file diff --git a/packages/core/src/AppSdk.ts b/packages/core/src/AppSdk.ts index 7c93a2655..85ebfddc6 100644 --- a/packages/core/src/AppSdk.ts +++ b/packages/core/src/AppSdk.ts @@ -16,6 +16,11 @@ export type TransferInitParams = chain: BLOCKCHAIN_NAME.TON; from: string; }) + | { + chain: BLOCKCHAIN_NAME.TRON; + from: string; + address?: string; + } | Record; export type ReceiveInitParams = { diff --git a/packages/core/src/entries/account.ts b/packages/core/src/entries/account.ts index 7dbe458e2..7878e47f5 100644 --- a/packages/core/src/entries/account.ts +++ b/packages/core/src/entries/account.ts @@ -16,6 +16,7 @@ import { } from './wallet'; import { assertUnreachable } from '../utils/types'; import { Network } from './network'; +import { TronWallet } from './tron/tron-wallet'; /** * @deprecated @@ -61,6 +62,11 @@ export class Clonable { } abstract class TonMnemonic extends Clonable implements IAccountVersionsEditable { + /** + * undefined for old wallets + */ + readonly tronWallet: TronWallet | undefined; + get allTonWallets() { return this.tonWallets; } @@ -69,6 +75,10 @@ abstract class TonMnemonic extends Clonable implements IAccountVersionsEditable return this.tonWallets.find(w => w.id === this.activeTonWalletId)!; } + get activeTronWallet() { + return this.tronWallet; + } + /** * @param id ton public key hex string without 0x corresponding to the mnemonic */ @@ -79,9 +89,21 @@ abstract class TonMnemonic extends Clonable implements IAccountVersionsEditable public auth: AuthPassword | AuthKeychain, public activeTonWalletId: WalletId, public tonWallets: TonWalletStandard[], - public mnemonicType?: MnemonicType + public mnemonicType?: MnemonicType, + networks?: { + tron: TronWallet; + } ) { super(); + this.tronWallet = networks?.tron; + } + + getTronWallet(id: WalletId) { + if (id === this.activeTronWallet?.id) { + return this.activeTronWallet; + } + + return undefined; } getTonWallet(id: WalletId) { @@ -117,10 +139,54 @@ abstract class TonMnemonic extends Clonable implements IAccountVersionsEditable } export class AccountTonMnemonic extends TonMnemonic { public readonly type = 'mnemonic'; + + static create(params: { + id: AccountId; + name: string; + emoji: string; + auth: AuthPassword | AuthKeychain; + activeTonWalletId: WalletId; + tonWallets: TonWalletStandard[]; + mnemonicType: MnemonicType; + networks?: { + tron: TronWallet; + }; + }) { + return new AccountTonMnemonic( + params.id, + params.name, + params.emoji, + params.auth, + params.activeTonWalletId, + params.tonWallets, + params.mnemonicType, + params.networks + ); + } } export class AccountTonTestnet extends TonMnemonic { public readonly type = 'testnet'; + + static create(params: { + id: AccountId; + name: string; + emoji: string; + auth: AuthPassword | AuthKeychain; + activeTonWalletId: WalletId; + tonWallets: TonWalletStandard[]; + mnemonicType: MnemonicType; + }) { + return new AccountTonTestnet( + params.id, + params.name, + params.emoji, + params.auth, + params.activeTonWalletId, + params.tonWallets, + params.mnemonicType + ); + } } export class AccountTonWatchOnly extends Clonable implements IAccount { @@ -389,6 +455,11 @@ export class AccountMAM extends Clonable implements IAccountTonWalletStandard { )!; } + get activeTronWallet() { + const activeDerivation = this.activeDerivation; + return activeDerivation.tronWallet; + } + /** * @param id index 0 derivation ton public key hex string without 0x */ @@ -422,6 +493,10 @@ export class AccountMAM extends Clonable implements IAccountTonWalletStandard { return this.allTonWallets.find(w => w.id === id); } + getTronWallet(id: WalletId) { + return this.derivations.map(d => d.tronWallet).find(item => item?.id === id); + } + getTonWalletsDerivation(id: WalletId) { return this.allAvailableDerivations.find(d => d.tonWallets.some(w => w.id === id)); } @@ -682,6 +757,42 @@ export function seeIfMainnnetAccount(account: Account): boolean { return network === Network.MAINNET; } +export function isAccountTronCompatible( + account: Account +): account is AccountTonMnemonic | AccountMAM { + switch (account.type) { + case 'mnemonic': + case 'mam': + return true; + case 'testnet': + case 'ton-only': + case 'ledger': + case 'watch-only': + case 'ton-multisig': + case 'keystone': + return false; + default: + return assertUnreachable(account); + } +} + +export function isAccountBip39(account: Account) { + switch (account.type) { + case 'testnet': + case 'mnemonic': + return account.mnemonicType === 'bip39'; + case 'mam': + case 'ton-only': + case 'ledger': + case 'watch-only': + case 'ton-multisig': + case 'keystone': + return false; + default: + return assertUnreachable(account); + } +} + export type AccountsState = Account[]; export const defaultAccountState: AccountsState = []; diff --git a/packages/core/src/entries/apis.ts b/packages/core/src/entries/apis.ts index 06c4e51ab..856c33c25 100644 --- a/packages/core/src/entries/apis.ts +++ b/packages/core/src/entries/apis.ts @@ -1,7 +1,5 @@ import { Configuration as TonV2Configuration } from '../tonApiV2'; -import { Configuration as TronConfiguration } from '../tronApi'; export interface APIConfig { tonApiV2: TonV2Configuration; - tronApi: TronConfiguration; } diff --git a/packages/core/src/entries/crypto/asset/constants.ts b/packages/core/src/entries/crypto/asset/constants.ts index 48a48856b..a6edbbfed 100644 --- a/packages/core/src/entries/crypto/asset/constants.ts +++ b/packages/core/src/entries/crypto/asset/constants.ts @@ -1,28 +1,29 @@ -import { TronBalance } from '../../../tronApi'; import { BLOCKCHAIN_NAME } from '../../crypto'; import { packAssetId } from './basic-asset'; import { TonAsset } from './ton-asset'; import { TronAsset } from './tron-asset'; import { Address } from '@ton/core'; +const usdtAddress = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; + export const TRON_USDT_ASSET: TronAsset = { - id: packAssetId(BLOCKCHAIN_NAME.TRON, 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'), + id: packAssetId(BLOCKCHAIN_NAME.TRON, usdtAddress), symbol: 'USDT', name: 'Tether USDT', decimals: 6, - address: 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t', - blockchain: BLOCKCHAIN_NAME.TRON + address: usdtAddress, + blockchain: BLOCKCHAIN_NAME.TRON, + image: 'https://wallet.tonkeeper.com/img/usdt-trc20.png' }; -export const toTronAsset = (balance: TronBalance): TronAsset => { - return { - id: packAssetId(BLOCKCHAIN_NAME.TRON, balance.token.address), - symbol: balance.token.symbol, - name: balance.token.name, - decimals: balance.token.decimals, - address: balance.token.address, - blockchain: BLOCKCHAIN_NAME.TRON - }; +export const TRON_TRX_ASSET: TronAsset = { + id: packAssetId(BLOCKCHAIN_NAME.TRON, 'TRX'), + symbol: 'TRX', + name: 'TRX', + decimals: 6, + address: 'TRX', + blockchain: BLOCKCHAIN_NAME.TRON, + image: 'https://wallet.tonkeeper.com/img/trx.svg' }; export const TON_ASSET: TonAsset = { diff --git a/packages/core/src/entries/crypto/asset/ton-asset.ts b/packages/core/src/entries/crypto/asset/ton-asset.ts index 56c88cb31..662afd008 100644 --- a/packages/core/src/entries/crypto/asset/ton-asset.ts +++ b/packages/core/src/entries/crypto/asset/ton-asset.ts @@ -4,6 +4,7 @@ import { BLOCKCHAIN_NAME } from '../../crypto'; import { BasicAsset, packAssetId } from './basic-asset'; import { TON_ASSET } from './constants'; import { AssetAmount } from './asset-amount'; +import { TronAsset } from './tron-asset'; export type TonAssetAddress = Address | 'TON'; export function isTon(address: TonAssetAddress): address is 'TON' { @@ -25,6 +26,10 @@ export function tonAssetAddressFromString(address: string): TonAsset['address'] return address === 'TON' ? address : Address.parse(address); } +export function assetAddressToString(address: TonAsset['address'] | TronAsset['address']): string { + return typeof address === 'string' ? address : address.toRawString(); +} + export function jettonToTonAsset(address: string, jettons: JettonsBalances): TonAsset { if (address === 'TON') { return TON_ASSET; diff --git a/packages/core/src/entries/network.ts b/packages/core/src/entries/network.ts index 220de1f9c..d28148162 100644 --- a/packages/core/src/entries/network.ts +++ b/packages/core/src/entries/network.ts @@ -1,8 +1,6 @@ import { Configuration as ConfigurationV2 } from '../tonApiV2'; import { OpenAPI as TonConsoleApi } from '../tonConsoleApi'; import { TonendpointConfig } from '../tonkeeperApi/tonendpoint'; -import { Configuration as TronConfiguration } from '../tronApi'; -import { TronApi, TronChain } from './tron'; export enum Network { MAINNET = -239, @@ -25,12 +23,6 @@ export const getTonClientV2 = (config: TonendpointConfig, current?: Network) => }); }; -const getTronClient = (current?: Network) => { - return new TronConfiguration({ - basePath: TronApi[current === Network.MAINNET ? TronChain.MAINNET : TronChain.NILE] - }); -}; - export const getApiConfig = (config: TonendpointConfig, network: Network, TonConsoleBase = '') => { // Global config if (TonConsoleBase) { @@ -39,7 +31,6 @@ export const getApiConfig = (config: TonendpointConfig, network: Network, TonCon } return { - tonApiV2: getTonClientV2(config, network), - tronApi: getTronClient(network) + tonApiV2: getTonClientV2(config, network) }; }; diff --git a/packages/core/src/entries/signer.ts b/packages/core/src/entries/signer.ts index 1e500d52a..7c6e31e3f 100644 --- a/packages/core/src/entries/signer.ts +++ b/packages/core/src/entries/signer.ts @@ -1,5 +1,6 @@ import { Cell } from '@ton/core'; import { LedgerTransaction } from '../service/ledger/connector'; +import type { SignedTransaction, Transaction } from 'tronweb/src/types/Transaction'; export type BaseSigner = (message: Cell) => Promise; @@ -10,3 +11,5 @@ export type LedgerSigner = ((messages: LedgerTransaction[]) => Promise) }; export type Signer = CellSigner | LedgerSigner; + +export type TronSigner = (tx: Transaction) => Promise; diff --git a/packages/core/src/entries/tron/tron-wallet.ts b/packages/core/src/entries/tron/tron-wallet.ts new file mode 100644 index 000000000..e61b82a87 --- /dev/null +++ b/packages/core/src/entries/tron/tron-wallet.ts @@ -0,0 +1,4 @@ +export type TronWallet = { + address: string; + id: string; +}; diff --git a/packages/core/src/entries/wallet.ts b/packages/core/src/entries/wallet.ts index 3000fcabd..fa66a053d 100644 --- a/packages/core/src/entries/wallet.ts +++ b/packages/core/src/entries/wallet.ts @@ -2,6 +2,7 @@ import { Language } from './language'; import { Network } from './network'; import { DeprecatedAuthState } from './password'; import { WalletProxy } from './proxy'; +import { TronWallet } from './tron/tron-wallet'; export enum WalletVersion { V3R1 = 0, @@ -108,8 +109,6 @@ export interface DeprecatedWalletState { theme?: string; proxy?: WalletProxy; - - tron?: TronWalletStorage; } export type WalletId = string; @@ -129,7 +128,10 @@ export type DerivationItem = { index: number; activeTonWalletId: WalletId; tonWallets: TonWalletStandard[]; - // tronWallets: never; + /** + * undefined for old wallets + */ + tronWallet?: TronWallet; }; export type DerivationItemNamed = DerivationItem & { @@ -168,14 +170,3 @@ export const defaultPreferencesConfig: TonWalletConfig = { enabledForNfts: true } }; - -export interface TronWalletStorage { - ownerWalletAddress: string; - walletByChain: Record; -} - -export interface TronWalletState { - ownerWalletAddress: string; - chainId: string; - walletAddress: string; -} diff --git a/packages/core/src/service/devStorage.ts b/packages/core/src/service/devStorage.ts index f9ae38789..26641df30 100644 --- a/packages/core/src/service/devStorage.ts +++ b/packages/core/src/service/devStorage.ts @@ -5,11 +5,13 @@ import { AppKey } from '../Keys'; export interface DevSettings { tonNetwork: Network; twoFAEnabled: boolean; + tronEnabled: boolean; } const defaultDevSettings: DevSettings = { tonNetwork: Network.MAINNET, - twoFAEnabled: false + twoFAEnabled: false, + tronEnabled: false }; export const getDevSettings = async (storage: IStorage) => { diff --git a/packages/core/src/service/suggestionService.ts b/packages/core/src/service/suggestionService.ts index 60e11b241..66ae5473d 100644 --- a/packages/core/src/service/suggestionService.ts +++ b/packages/core/src/service/suggestionService.ts @@ -41,36 +41,6 @@ export const deleteFavoriteSuggestion = async ( storage.set(`${AppKey.FAVOURITES}_${publicKey}`, items); }; -/*const getTronSuggestionsList = async ( - api: APIConfig, - wallet: DeprecatedWalletState, - seeIfAddressIsAdded: (list: LatestSuggestion[], address: string) => boolean -) => { - const list = [] as LatestSuggestion[]; - - if (wallet.tron) { - const tronItems = await new TronApi(api.tronApi).getTransactions({ - ownerAddress: wallet.tron.ownerWalletAddress, - limit: 100 - }); - - tronItems.events.forEach(event => { - event.actions.forEach(({ sendTRC20 }) => { - if (sendTRC20 && !seeIfAddressIsAdded(list, sendTRC20.recipient)) { - list.push({ - isFavorite: false, - timestamp: event.timestamp, - address: sendTRC20.recipient, - blockchain: BLOCKCHAIN_NAME.TRON - }); - } - }); - }); - } - - return list; -};*/ - const getTonSuggestionsList = async ( api: APIConfig, wallet: TonWalletStandard, @@ -128,8 +98,8 @@ export const getSuggestionsList = async ( switch (name) { case BLOCKCHAIN_NAME.TON: return getTonSuggestionsList(api, wallet, seeIfAddressIsAdded); - /* case BLOCKCHAIN_NAME.TRON: - return getTronSuggestionsList(api, wallet, seeIfAddressIsAdded);*/ + case BLOCKCHAIN_NAME.TRON: + return []; default: throw new Error('Unexpected chain'); } diff --git a/packages/core/src/service/tron-blockchain/tron-sender.ts b/packages/core/src/service/tron-blockchain/tron-sender.ts new file mode 100644 index 000000000..280fd3946 --- /dev/null +++ b/packages/core/src/service/tron-blockchain/tron-sender.ts @@ -0,0 +1,84 @@ +import { ethers } from 'ethers'; +import { TronApi } from '../../tronApi'; +import { TRON_TRX_ASSET, TRON_USDT_ASSET } from '../../entries/crypto/asset/constants'; +import { AssetAmount } from '../../entries/crypto/asset/asset-amount'; +import { TronAsset } from '../../entries/crypto/asset/tron-asset'; +import BigNumber from 'bignumber.js'; +import { TronSigner } from '../../entries/signer'; +import { TronWallet } from '../../entries/tron/tron-wallet'; +import { TronAddressUtils } from '@ton-keychain/trx'; +import { TronWeb } from 'tronweb'; +const AbiCoder = ethers.AbiCoder; + +const toHexAddress = (base58: string) => { + return ethers.getAddress(TronAddressUtils.base58ToHex(base58)); +}; + +export class TronSender { + private static transferSelector = 'transfer(address,uint256)'; + + constructor( + private tronApi: TronApi, + private walletInfo: TronWallet, + private tronSigner: TronSigner + ) {} + + async send(to: string, assetAmount: AssetAmount) { + if (assetAmount.asset.id !== TRON_USDT_ASSET.id) { + throw new Error(`Unsupported tron asset ${assetAmount.asset.symbol}`); + } + + const tronWeb = new TronWeb({ + fullHost: this.tronApi.baseURL + }); + + const functionSelector = 'transfer(address,uint256)'; + const parameter = [ + { type: 'address', value: to }, + { type: 'uint256', value: assetAmount.weiAmount.toFixed(0) } + ]; + const tx = await tronWeb.transactionBuilder.triggerSmartContract( + assetAmount.asset.address, + functionSelector, + {}, + parameter, + this.walletInfo.address + ); + + const signedTx = await this.tronSigner(tx.transaction); + const result = await tronWeb.trx.sendRawTransaction(signedTx); + + if (!result.result) { + throw new Error('err'); + } + } + + async estimate(to: string, assetAmount: AssetAmount) { + if (assetAmount.asset.id !== TRON_USDT_ASSET.id) { + throw new Error(`Unsupported tron asset ${assetAmount.asset.symbol}`); + } + + const estimatedGas = await this.tronApi.estimateEnergy({ + from: this.walletInfo.address, + contractAddress: assetAmount.asset.address, + selector: TronSender.transferSelector, + data: new AbiCoder() + .encode( + ['address', 'uint256'], + [toHexAddress(to), assetAmount.weiAmount.toString()] + ) + .replace(/^(0x)/, '') + }); + + const gasPrice = parseInt(await this.tronApi.rpc('eth_gasPrice')); + + const trxToBurn = estimatedGas * gasPrice; + + return { + extra: new AssetAmount({ + weiAmount: new BigNumber(trxToBurn.toString()), + asset: TRON_TRX_ASSET + }) + }; + } +} diff --git a/packages/core/src/service/tron-blockchain/types.ts b/packages/core/src/service/tron-blockchain/types.ts new file mode 100644 index 000000000..a0f4d4095 --- /dev/null +++ b/packages/core/src/service/tron-blockchain/types.ts @@ -0,0 +1,3 @@ +import { Wallet } from 'ethers'; + +export type TronEthersWallet = Wallet; diff --git a/packages/core/src/service/tron/addressCalculation.ts b/packages/core/src/service/tron/addressCalculation.ts deleted file mode 100644 index 3298ded25..000000000 --- a/packages/core/src/service/tron/addressCalculation.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { BytesLike, concat, dataSlice, getAddress, getBytes, keccak256 } from 'ethers'; -import { TronAddress } from './tronUtils'; - -const base58AddressToEth = (address: string) => { - return '0x' + TronAddress.base58ToHex(address).slice(2); -}; - -const addressToSalt = (address: string) => { - return '0x' + '0'.repeat(24) + base58AddressToEth(address).slice(2); -}; - -const buildBytecodeHash = (implementationAddress: string) => { - return keccak256( - concat([ - '0x3d602d80600a3d3981f3363d3d373d3d3d363d73', - base58AddressToEth(implementationAddress), - '0x5af43d82803e903d91602b57fd5bf3' - ]) - ); -}; - -function getCreate2Address(_from: string, _salt: BytesLike, _initCodeHash: BytesLike): string { - const from = getAddress(_from); - const salt = getBytes(_salt, 'salt'); - const initCodeHash = getBytes(_initCodeHash, 'initCodeHash'); - - return dataSlice(keccak256(concat(['0x41', from, salt, initCodeHash])), 12); -} - -export const calculateCreate2 = ({ - factoryAddress, - ownerAddress, - implementationAddress -}: { - factoryAddress: string; - implementationAddress: string; - ownerAddress: string; -}) => { - const byteCode = buildBytecodeHash(implementationAddress); - const saltHex = addressToSalt(ownerAddress); - - const predictedAddress = getCreate2Address( - base58AddressToEth(factoryAddress), - saltHex, - byteCode - ); - return TronAddress.hexToBase58(predictedAddress); -}; diff --git a/packages/core/src/service/tron/tronEncoding.ts b/packages/core/src/service/tron/tronEncoding.ts deleted file mode 100644 index 0f09477a7..000000000 --- a/packages/core/src/service/tron/tronEncoding.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { RequestData } from '../../tronApi'; -import { TronAddress, encodePackedBytes, encodeTronParams, keccak256 } from './tronUtils'; - -const DomainAbi = ['bytes32', 'bytes32', 'bytes32', 'uint256', 'address', 'bytes32']; -const RequestAbi = [ - 'bytes32', - 'address', - 'address', - 'uint256', - 'uint256', - 'uint32', - 'tuple(address,uint256,bytes)[]' -]; - -export function hashRequest(request: RequestData, contractAddress: string, chainId: string) { - const REQUEST_TYPEHASH = keccak256( - 'Request(address feeReceiver,address feeToken,uint256 fee,uint256 deadline,uint32 nonce,Message[] messages)' - ); - const structHash = keccak256( - encodeTronParams(RequestAbi, [ - REQUEST_TYPEHASH, - request.feeReceiver, - request.feeToken, - request.fee, - request.deadline, - request.nonce, - request.messages.map(m => [ - '0x' + TronAddress.base58ToHex(m.to).slice(2), - m.value, - m.data - ]) - ]) - ); - - const domain = domainHash(contractAddress, chainId); - - return keccak256(encodePackedBytes(['0x1901', domain, structHash])); -} - -function domainHash(contractAddress: string, chainId: string) { - const TIP712_DOMAIN_TYPEHASH = keccak256( - 'EIP712Domain(string name,string version,uint256 chainId,address verifyingContract,bytes32 salt)' - ); - const NAME = keccak256('TONKEEPER'); - const VERSION = keccak256('1'); - const SALT = keccak256('TRON_WALLET'); - return keccak256( - encodeTronParams(DomainAbi, [ - TIP712_DOMAIN_TYPEHASH, - NAME, - VERSION, - chainId, - contractAddress, - SALT - ]) - ); -} diff --git a/packages/core/src/service/tron/tronService.ts b/packages/core/src/service/tron/tronService.ts deleted file mode 100644 index 118ec7200..000000000 --- a/packages/core/src/service/tron/tronService.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { hmac_sha512, mnemonicToPrivateKey } from '@ton/crypto'; -import { ethers } from 'ethers'; -import { Network } from '../../entries/network'; -import { Factories, TronChain, WalletImplementations } from '../../entries/tron'; -import { TronWalletState, TronWalletStorage } from '../../entries/wallet'; -import { calculateCreate2 } from './addressCalculation'; -import { TronAddress } from './tronUtils'; - -export const getPrivateKey = async (tonMnemonic: string[]): Promise => { - // TON-compatible seed - const pair = await mnemonicToPrivateKey(tonMnemonic); - const seed = pair.secretKey.slice(0, 32); - - // Sub-protocol derivation for ETH-derived keys: - // Note that tonweb's definition of hmacSha512 takes in hex-encoded strings - const tronSeed = await hmac_sha512(/*key*/ seed, /*data*/ 'BIP32'); - - // Plug into BIP39 with TRON path m/44'/195'/0'/0/0: - const TRON_BIP39_PATH_INDEX_0 = "m/44'/195'/0'/0/0"; - const account = ethers.HDNodeWallet.fromSeed(tronSeed).derivePath(TRON_BIP39_PATH_INDEX_0); - return account.privateKey.slice(2); // note: this is hex-encoded, remove 0x -}; - -const getOwnerAddress = async (mnemonic: string[]): Promise => { - const wallet = new ethers.Wallet(await getPrivateKey(mnemonic)); - return TronAddress.hexToBase58(wallet.address); -}; - -/*export const getTronWallet = async ( - tronApi: Configuration, - mnemonic: string[], - wallet: DeprecatedWalletState -): Promise => { - const ownerWalletAddress = await getOwnerAddress(mnemonic); - - const tronWallet = await new TronApi(tronApi).getWallet({ - ownerAddress: ownerWalletAddress - }); - - return { - ownerWalletAddress, - walletByChain: { - ...(wallet.tron?.walletByChain ?? {}), - [tronWallet.chainId]: tronWallet.address - } - }; -}; - -export const importTronWallet = async ( - storage: IStorage, - tronApi: Configuration, - wallet: DeprecatedWalletState, - mnemonic: string[] -): Promise => { - const tron = await getTronWallet(tronApi, mnemonic, wallet); - - const updated = { ...wallet, tron }; - - // await setWalletState(storage, updated); - - return tron; -};*/ - -export const getTronWalletState = (tron: TronWalletStorage, network?: Network): TronWalletState => { - const chainId = network !== Network.TESTNET ? TronChain.MAINNET : TronChain.NILE; - - if (tron.walletByChain[chainId]) { - return { - ownerWalletAddress: tron.ownerWalletAddress, - chainId: chainId, - walletAddress: tron.walletByChain[chainId] - }; - } - - const predictedAddress = calculateCreate2({ - factoryAddress: Factories[chainId], - ownerAddress: tron.ownerWalletAddress, - implementationAddress: WalletImplementations[chainId] - }); - - return { - ownerWalletAddress: tron.ownerWalletAddress, - chainId: chainId, - walletAddress: predictedAddress - }; -}; diff --git a/packages/core/src/service/tron/tronTransferService.ts b/packages/core/src/service/tron/tronTransferService.ts deleted file mode 100644 index 180a98dd7..000000000 --- a/packages/core/src/service/tron/tronTransferService.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { ethers } from 'ethers'; -import { AssetAmount } from '../../entries/crypto/asset/asset-amount'; -import { TronAsset } from '../../entries/crypto/asset/tron-asset'; -import { TronRecipient } from '../../entries/send'; -import { TronWalletStorage } from '../../entries/wallet'; -import { - Configuration, - EstimatePayload, - PublishPayload, - RequestData, - TronApi -} from '../../tronApi'; -import { hashRequest } from './tronEncoding'; -import { getPrivateKey } from './tronService'; - -async function getEstimatePayload({ - tronApi, - tron, - recipient, - amount -}: { - tronApi: Configuration; - tron: TronWalletStorage; - recipient: TronRecipient; - amount: AssetAmount; -}): Promise { - return new TronApi(tronApi).getEstimation({ - ownerAddress: tron.ownerWalletAddress, - getEstimationRequest: { - lifeTime: Math.floor(Date.now() / 1000) + 600, - messages: [ - { - to: recipient.address, - value: amount.stringWeiAmount, - assetAddress: amount.asset.address - } - ] - } - }); -} - -export async function sendTronTransfer( - { - tronApi, - tron, - request - }: { - tronApi: Configuration; - tron: TronWalletStorage; - request: RequestData; - }, - { mnemonic }: { mnemonic: string[] } -): Promise { - const settings = await new TronApi(tronApi).getSettings(); - - const hash = hashRequest(request, settings.walletImplementation, settings.chainId); - - const privateKey = await getPrivateKey(mnemonic); - const signingKey = new ethers.SigningKey('0x' + privateKey); - const signature = signingKey.sign(hash).serialized; - - return new TronApi(tronApi).publishTransaction({ - ownerAddress: tron.ownerWalletAddress, - publishTransactionRequest: { - request, - signature, - hash - } - }); -} - -/*async function estimateTronFee({ - wallet, - tronApi, - address, - amount -}: { - wallet: DeprecatedWalletState; - tronApi: Configuration; - address: TronRecipient; - amount: AssetAmount; -}) { - const payload = await getEstimatePayload({ - tron: wallet.tron!, - tronApi, - recipient: address, - amount: new AssetAmount({ asset: amount.asset, weiAmount: new BigNumber('1') }) - }); - - return payload.request.fee; -}*/ - -/* -export async function estimateTron({ - recipient, - amount, - isMax, - tronApi, - wallet, - balances -}: { - recipient: RecipientData; - amount: AssetAmount; - isMax: boolean; - tronApi: Configuration; - wallet: DeprecatedWalletState; - balances: TronBalances | undefined; -}): Promise> { - if (isMax) { - const fee = await estimateTronFee({ - wallet, - tronApi, - address: recipient.address as TronRecipient, - amount - }); - - amount = new AssetAmount({ asset: amount.asset, weiAmount: amount.weiAmount.minus(fee) }); - } - - const payload = await getEstimatePayload({ - tron: wallet.tron!, - tronApi, - recipient: recipient.address as TronRecipient, - amount: amount - }); - - if (payload.internalMsgs.some(item => item === false)) { - throw new Error('Estimation fail.'); - } - - const feeToken = balances?.balances.find( - item => item.token.address === payload.request.feeToken - ); - if (!feeToken) { - throw new Error(`Unexpected feeToken, token's address is ${payload.request.feeToken}`); - } - - const fee = new AssetAmount({ - asset: toTronAsset(feeToken), - weiAmount: payload.request.fee - }); - - return { fee, payload }; -} -*/ diff --git a/packages/core/src/service/tron/tronUtils.ts b/packages/core/src/service/tron/tronUtils.ts deleted file mode 100644 index 92dbb5a05..000000000 --- a/packages/core/src/service/tron/tronUtils.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { AbiCoder, decodeBase58, encodeBase58, ethers, sha256 } from 'ethers'; -export const ADDRESS_PREFIX_REGEX = /^(41)/; - -export const TronAddress = { - hexToBase58(address: string): string { - const tronAddressPayload = '0x41' + address.slice(2); - const checkSumTail = sha256(sha256(tronAddressPayload)).slice(2, 10); - return encodeBase58(tronAddressPayload + checkSumTail); - }, - base58ToHex(address: string): string { - const decoded = decodeBase58(address).toString(16); - return decoded.slice(0, -8); - } -}; - -export function keccak256(value: string) { - if (!value.startsWith('0x')) { - return ethers.keccak256(ethers.toUtf8Bytes(value)); - } - return ethers.keccak256(value); -} - -export function encodeTronParams(types: string[], values: unknown[]) { - for (let i = 0; i < types.length; i++) { - if (types[i] === 'address') { - values[i] = TronAddress.base58ToHex(values[i] as string).replace( - ADDRESS_PREFIX_REGEX, - '0x' - ); - } - } - - return new AbiCoder().encode(types, values); -} - -export function encodePackedBytes(values: string[]) { - return '0x' + values.map(x => x.replace('0x', '')).join(''); -} diff --git a/packages/core/src/service/walletService.ts b/packages/core/src/service/walletService.ts index 0f6d95152..bb222d26c 100644 --- a/packages/core/src/service/walletService.ts +++ b/packages/core/src/service/walletService.ts @@ -33,6 +33,9 @@ import { walletContract } from './wallet/contractService'; import { TonKeychainRoot, KeychainTonAccount } from '@ton-keychain/core'; import { mnemonicToKeypair } from './mnemonicService'; import { FiatCurrencies } from '../entries/fiat'; +import { KeychainTrxAccountsProvider, TronAddressUtils } from '@ton-keychain/trx'; +import { TronWallet } from '../entries/tron/tron-wallet'; +import { ethers } from 'ethers'; export const createMultisigTonAccount = async ( storage: IStorage, @@ -116,6 +119,43 @@ interface CreateWalletContext { defaultWalletVersion: WalletVersion; } +export const tronWalletByTonMnemonic = async ( + mnemonic: string[], + mnemonicType: MnemonicType = 'ton' +): Promise => { + if (mnemonicType === 'ton') { + const tonAccount = await KeychainTonAccount.fromMnemonic(mnemonic); + const trxProvider = KeychainTrxAccountsProvider.fromEntropy(tonAccount.entropy); + const trxAccount = trxProvider.getAccount(); + + return { id: trxAccount.address, address: trxAccount.address }; + } else { + const node = ethers.HDNodeWallet.fromPhrase( + mnemonic.join(' '), + undefined, + "m/44'/195'/0'/0" + ); + const address = TronAddressUtils.hexToBase58(node.deriveChild(0).address); + return { + id: address, + address + }; + } +}; + +export const tonMnemonicToTronMnemonic = async ( + mnemonic: string[], + mnemonicType: MnemonicType = 'ton' +): Promise => { + if (mnemonicType === 'ton') { + const tonAccount = await KeychainTonAccount.fromMnemonic(mnemonic); + const trxProvider = KeychainTrxAccountsProvider.fromEntropy(tonAccount.entropy); + return trxProvider.mnemonics; + } else { + return mnemonic; + } +}; + export const getContextApiByNetwork = (context: CreateWalletContext, network: Network) => { const api = network === Network.TESTNET ? context.testnetApi : context.mainnetApi; return api; @@ -164,7 +204,9 @@ const createPayloadOfStandardTonAccount = async ( const walletIdToActivate = wallets.slice().sort(sortWalletsByVersion)[0].id; - return { name, emoji, publicKey, walletAuth, walletIdToActivate, wallets }; + const tronWallet = await tronWalletByTonMnemonic(mnemonic, mnemonicType); + + return { name, emoji, publicKey, walletAuth, walletIdToActivate, wallets, tronWallet }; }; export const mayBeCreateAccountId = ( @@ -193,9 +235,10 @@ export const createStandardTonAccountByMnemonic = async ( options: { versions?: WalletVersion[]; auth: AuthPassword | Omit; + generateTronWallet?: boolean; } ) => { - const { name, emoji, publicKey, walletAuth, walletIdToActivate, wallets } = + const { name, emoji, publicKey, walletAuth, walletIdToActivate, wallets, tronWallet } = await createPayloadOfStandardTonAccount( appContext, storage, @@ -205,14 +248,42 @@ export const createStandardTonAccountByMnemonic = async ( Network.MAINNET ); - return new AccountTonMnemonic( - createAccountId(Network.MAINNET, publicKey), + return AccountTonMnemonic.create({ + id: createAccountId(Network.MAINNET, publicKey), name, emoji, - walletAuth, - walletIdToActivate, - wallets, - mnemonicType + auth: walletAuth, + activeTonWalletId: walletIdToActivate, + tonWallets: wallets, + mnemonicType, + networks: options?.generateTronWallet + ? { + tron: tronWallet + } + : undefined + }); +}; + +export const standardTonAccountToAccountWithTron = async ( + account: AccountTonMnemonic, + getAccountMnemonic: () => Promise +) => { + const tronWallet = await tronWalletByTonMnemonic( + await getAccountMnemonic(), + account.mnemonicType + ); + + return new AccountTonMnemonic( + account.id, + account.name, + account.emoji, + account.auth, + account.activeTonWalletId, + account.tonWallets, + account.mnemonicType, + { + tron: tronWallet + } ); }; @@ -236,15 +307,15 @@ export const createStandardTestnetAccountByMnemonic = async ( Network.TESTNET ); - return new AccountTonTestnet( - createAccountId(Network.TESTNET, publicKey), + return AccountTonTestnet.create({ + id: createAccountId(Network.TESTNET, publicKey), name, emoji, - walletAuth, - walletIdToActivate, - wallets, + auth: walletAuth, + activeTonWalletId: walletIdToActivate, + tonWallets: wallets, mnemonicType - ); + }); }; const versionMap: Record = { @@ -481,6 +552,7 @@ export const createMAMAccountByMnemonic = async ( options: { selectedDerivations?: number[]; auth: AuthPassword | Omit; + generateTronWallet?: boolean; } ) => { const rootAccount = await TonKeychainRoot.fromMnemonic(rootMnemonic, { @@ -523,8 +595,8 @@ export const createMAMAccountByMnemonic = async ( rootAccount.id ); - const namedDerivations: { item: DerivationItemNamed; shouldAdd: boolean }[] = - childTonWallets.map(w => { + const namedDerivations: { item: DerivationItemNamed; shouldAdd: boolean }[] = await Promise.all( + childTonWallets.map(async w => { const tonWallet = walletContract( w.tonAccount.publicKey, appContext.defaultWalletVersion, @@ -548,11 +620,15 @@ export const createMAMAccountByMnemonic = async ( emoji, index: w.derivationIndex, tonWallets, - activeTonWalletId: tonWallets[0].id + activeTonWalletId: tonWallets[0].id, + tronWallet: options?.generateTronWallet + ? await tronWalletByTonMnemonic(w.tonAccount.mnemonics) + : undefined }, shouldAdd: w.shouldAdd }; - }); + }) + ); const addedDerivationIndexes = namedDerivations.filter(d => d.shouldAdd).map(d => d.item.index); if (addedDerivationIndexes.length === 0) { @@ -570,6 +646,35 @@ export const createMAMAccountByMnemonic = async ( ); }; +export const mamAccountToMamAccountWithTron = async ( + account: AccountMAM, + getAccountMnemonic: () => Promise +) => { + const rootAccount = await TonKeychainRoot.fromMnemonic(await getAccountMnemonic()); + + const derivations = await Promise.all( + account.allAvailableDerivations.map(async d => { + const tonAccount = await rootAccount.getTonAccount(d.index); + const tronWallet = await tronWalletByTonMnemonic(tonAccount.mnemonics); + + return { + ...d, + tronWallet + }; + }) + ); + + return new AccountMAM( + account.id, + account.name, + account.emoji, + account.auth, + account.activeDerivationIndex, + account.addedDerivationsIndexes, + derivations + ); +}; + export function getFallbackAccountEmoji(publicKeyOrBase64: string) { let index; if (/^[0-9A-Fa-f]+$/g.test(publicKeyOrBase64)) { diff --git a/packages/core/src/tonkeeperApi/tonendpoint.ts b/packages/core/src/tonkeeperApi/tonendpoint.ts index 87c9ffcdf..fbc9585c3 100644 --- a/packages/core/src/tonkeeperApi/tonendpoint.ts +++ b/packages/core/src/tonkeeperApi/tonendpoint.ts @@ -85,12 +85,14 @@ export interface TonendpointConfig { * "secret" flag name to determine if the app is on ios review */ tablet_enable_additional_security?: boolean; - isOnReview?: boolean; + '2fa_public_key'?: string; '2fa_api_url'?: string; '2fa_tg_confirm_send_message_ttl_seconds'?: number; '2fa_tg_linked_ttl_seconds'?: number; '2fa_bot_url'?: string; + + tron_api_url?: string; } const defaultTonendpoint = 'https://api.tonkeeper.com'; // 'http://localhost:1339'; diff --git a/packages/core/src/tronApi/.openapi-generator-ignore b/packages/core/src/tronApi/.openapi-generator-ignore deleted file mode 100644 index 7484ee590..000000000 --- a/packages/core/src/tronApi/.openapi-generator-ignore +++ /dev/null @@ -1,23 +0,0 @@ -# OpenAPI Generator Ignore -# Generated by openapi-generator https://github.com/openapitools/openapi-generator - -# Use this file to prevent files from being overwritten by the generator. -# The patterns follow closely to .gitignore or .dockerignore. - -# As an example, the C# client generator defines ApiClient.cs. -# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: -#ApiClient.cs - -# You can match any string of characters against a directory, file or extension with a single asterisk (*): -#foo/*/qux -# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux - -# You can recursively match patterns against a directory, file or extension with a double asterisk (**): -#foo/**/qux -# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux - -# You can also negate patterns with an exclamation (!). -# For example, you can ignore all files in a docs folder with the file extension .md: -#docs/*.md -# Then explicitly reverse the ignore rule for a single file: -#!docs/README.md diff --git a/packages/core/src/tronApi/.openapi-generator/FILES b/packages/core/src/tronApi/.openapi-generator/FILES deleted file mode 100644 index 53edd2d1b..000000000 --- a/packages/core/src/tronApi/.openapi-generator/FILES +++ /dev/null @@ -1,27 +0,0 @@ -.openapi-generator-ignore -apis/TronApi.ts -apis/index.ts -index.ts -models/ContractDeployAction.ts -models/EstimatePayload.ts -models/FromMessage.ts -models/GetEstimationRequest.ts -models/GetSettings401Response.ts -models/ModelError.ts -models/PublishPayload.ts -models/PublishTransactionRequest.ts -models/ReceiveTRC20Action.ts -models/RequestData.ts -models/RequestMessage.ts -models/SendTRC20Action.ts -models/TronAction.ts -models/TronBalance.ts -models/TronBalances.ts -models/TronEvent.ts -models/TronEvents.ts -models/TronFee.ts -models/TronSettings.ts -models/TronToken.ts -models/TronWallet.ts -models/index.ts -runtime.ts diff --git a/packages/core/src/tronApi/.openapi-generator/VERSION b/packages/core/src/tronApi/.openapi-generator/VERSION deleted file mode 100644 index 44bad91b1..000000000 --- a/packages/core/src/tronApi/.openapi-generator/VERSION +++ /dev/null @@ -1 +0,0 @@ -7.0.1-SNAPSHOT \ No newline at end of file diff --git a/packages/core/src/tronApi/apis/TronApi.ts b/packages/core/src/tronApi/apis/TronApi.ts deleted file mode 100644 index bb20cc840..000000000 --- a/packages/core/src/tronApi/apis/TronApi.ts +++ /dev/null @@ -1,441 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import * as runtime from '../runtime'; -import type { - EstimatePayload, - GetEstimationRequest, - GetSettings401Response, - PublishPayload, - PublishTransactionRequest, - TronBalances, - TronEvent, - TronEvents, - TronSettings, - TronWallet, -} from '../models/index'; -import { - EstimatePayloadFromJSON, - EstimatePayloadToJSON, - GetEstimationRequestFromJSON, - GetEstimationRequestToJSON, - GetSettings401ResponseFromJSON, - GetSettings401ResponseToJSON, - PublishPayloadFromJSON, - PublishPayloadToJSON, - PublishTransactionRequestFromJSON, - PublishTransactionRequestToJSON, - TronBalancesFromJSON, - TronBalancesToJSON, - TronEventFromJSON, - TronEventToJSON, - TronEventsFromJSON, - TronEventsToJSON, - TronSettingsFromJSON, - TronSettingsToJSON, - TronWalletFromJSON, - TronWalletToJSON, -} from '../models/index'; - -export interface GetEstimationOperationRequest { - ownerAddress: string; - getEstimationRequest: GetEstimationRequest; -} - -export interface GetTransactionRequest { - transactionHash: string; -} - -export interface GetTransactionsRequest { - ownerAddress: string; - fingerprint?: string; - limit?: number; - offset?: number; - minTimestamp?: number; - maxTimestamp?: number; -} - -export interface GetWalletRequest { - ownerAddress: string; -} - -export interface GetWalletBalancesRequest { - walletAddress: string; -} - -export interface PublishTransactionOperationRequest { - ownerAddress: string; - publishTransactionRequest: PublishTransactionRequest; -} - -/** - * TronApi - interface - * - * @export - * @interface TronApiInterface - */ -export interface TronApiInterface { - /** - * Get transaction estimate - * @param {string} ownerAddress owner address - * @param {GetEstimationRequest} getEstimationRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getEstimationRaw(requestParameters: GetEstimationOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get transaction estimate - */ - getEstimation(requestParameters: GetEstimationOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Get service configuration - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getSettingsRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get service configuration - */ - getSettings(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Get transaction data - * @param {string} transactionHash transaction hash - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getTransactionRaw(requestParameters: GetTransactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get transaction data - */ - getTransaction(requestParameters: GetTransactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Get list of transactions - * @param {string} ownerAddress owner address - * @param {string} [fingerprint] - * @param {number} [limit] - * @param {number} [offset] - * @param {number} [minTimestamp] - * @param {number} [maxTimestamp] - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getTransactionsRaw(requestParameters: GetTransactionsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get list of transactions - */ - getTransactions(requestParameters: GetTransactionsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Get user wallet address data - * @param {string} ownerAddress owner address - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getWalletRaw(requestParameters: GetWalletRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get user wallet address data - */ - getWallet(requestParameters: GetWalletRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Get wallet balance - * @param {string} walletAddress wallet address - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - getWalletBalancesRaw(requestParameters: GetWalletBalancesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Get wallet balance - */ - getWalletBalances(requestParameters: GetWalletBalancesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - - /** - * Publish transaction - * @param {string} ownerAddress owner address - * @param {PublishTransactionRequest} publishTransactionRequest - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof TronApiInterface - */ - publishTransactionRaw(requestParameters: PublishTransactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise>; - - /** - * Publish transaction - */ - publishTransaction(requestParameters: PublishTransactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise; - -} - -/** - * - */ -export class TronApi extends runtime.BaseAPI implements TronApiInterface { - - /** - * Get transaction estimate - */ - async getEstimationRaw(requestParameters: GetEstimationOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.ownerAddress === null || requestParameters.ownerAddress === undefined) { - throw new runtime.RequiredError('ownerAddress','Required parameter requestParameters.ownerAddress was null or undefined when calling getEstimation.'); - } - - if (requestParameters.getEstimationRequest === null || requestParameters.getEstimationRequest === undefined) { - throw new runtime.RequiredError('getEstimationRequest','Required parameter requestParameters.getEstimationRequest was null or undefined when calling getEstimation.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - const response = await this.request({ - path: `/api/v2/wallet/{owner_address}/estimate`.replace(`{${"owner_address"}}`, encodeURIComponent(String(requestParameters.ownerAddress))), - method: 'POST', - headers: headerParameters, - query: queryParameters, - body: GetEstimationRequestToJSON(requestParameters.getEstimationRequest), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => EstimatePayloadFromJSON(jsonValue)); - } - - /** - * Get transaction estimate - */ - async getEstimation(requestParameters: GetEstimationOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getEstimationRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * Get service configuration - */ - async getSettingsRaw(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - const response = await this.request({ - path: `/api/v2/settings`, - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TronSettingsFromJSON(jsonValue)); - } - - /** - * Get service configuration - */ - async getSettings(initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getSettingsRaw(initOverrides); - return await response.value(); - } - - /** - * Get transaction data - */ - async getTransactionRaw(requestParameters: GetTransactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.transactionHash === null || requestParameters.transactionHash === undefined) { - throw new runtime.RequiredError('transactionHash','Required parameter requestParameters.transactionHash was null or undefined when calling getTransaction.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - const response = await this.request({ - path: `/api/v2/transactions/{transaction_hash}`.replace(`{${"transaction_hash"}}`, encodeURIComponent(String(requestParameters.transactionHash))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TronEventFromJSON(jsonValue)); - } - - /** - * Get transaction data - */ - async getTransaction(requestParameters: GetTransactionRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getTransactionRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * Get list of transactions - */ - async getTransactionsRaw(requestParameters: GetTransactionsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.ownerAddress === null || requestParameters.ownerAddress === undefined) { - throw new runtime.RequiredError('ownerAddress','Required parameter requestParameters.ownerAddress was null or undefined when calling getTransactions.'); - } - - const queryParameters: any = {}; - - if (requestParameters.fingerprint !== undefined) { - queryParameters['fingerprint'] = requestParameters.fingerprint; - } - - if (requestParameters.limit !== undefined) { - queryParameters['limit'] = requestParameters.limit; - } - - if (requestParameters.offset !== undefined) { - queryParameters['offset'] = requestParameters.offset; - } - - if (requestParameters.minTimestamp !== undefined) { - queryParameters['min_timestamp'] = requestParameters.minTimestamp; - } - - if (requestParameters.maxTimestamp !== undefined) { - queryParameters['max_timestamp'] = requestParameters.maxTimestamp; - } - - const headerParameters: runtime.HTTPHeaders = {}; - - const response = await this.request({ - path: `/api/v2/wallet/{owner_address}/transactions`.replace(`{${"owner_address"}}`, encodeURIComponent(String(requestParameters.ownerAddress))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TronEventsFromJSON(jsonValue)); - } - - /** - * Get list of transactions - */ - async getTransactions(requestParameters: GetTransactionsRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getTransactionsRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * Get user wallet address data - */ - async getWalletRaw(requestParameters: GetWalletRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.ownerAddress === null || requestParameters.ownerAddress === undefined) { - throw new runtime.RequiredError('ownerAddress','Required parameter requestParameters.ownerAddress was null or undefined when calling getWallet.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - const response = await this.request({ - path: `/api/v2/wallet/{owner_address}`.replace(`{${"owner_address"}}`, encodeURIComponent(String(requestParameters.ownerAddress))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TronWalletFromJSON(jsonValue)); - } - - /** - * Get user wallet address data - */ - async getWallet(requestParameters: GetWalletRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getWalletRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * Get wallet balance - */ - async getWalletBalancesRaw(requestParameters: GetWalletBalancesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.walletAddress === null || requestParameters.walletAddress === undefined) { - throw new runtime.RequiredError('walletAddress','Required parameter requestParameters.walletAddress was null or undefined when calling getWalletBalances.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - const response = await this.request({ - path: `/api/v2/balance/{wallet_address}`.replace(`{${"wallet_address"}}`, encodeURIComponent(String(requestParameters.walletAddress))), - method: 'GET', - headers: headerParameters, - query: queryParameters, - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => TronBalancesFromJSON(jsonValue)); - } - - /** - * Get wallet balance - */ - async getWalletBalances(requestParameters: GetWalletBalancesRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.getWalletBalancesRaw(requestParameters, initOverrides); - return await response.value(); - } - - /** - * Publish transaction - */ - async publishTransactionRaw(requestParameters: PublishTransactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise> { - if (requestParameters.ownerAddress === null || requestParameters.ownerAddress === undefined) { - throw new runtime.RequiredError('ownerAddress','Required parameter requestParameters.ownerAddress was null or undefined when calling publishTransaction.'); - } - - if (requestParameters.publishTransactionRequest === null || requestParameters.publishTransactionRequest === undefined) { - throw new runtime.RequiredError('publishTransactionRequest','Required parameter requestParameters.publishTransactionRequest was null or undefined when calling publishTransaction.'); - } - - const queryParameters: any = {}; - - const headerParameters: runtime.HTTPHeaders = {}; - - headerParameters['Content-Type'] = 'application/json'; - - const response = await this.request({ - path: `/api/v2/wallet/{owner_address}/publish`.replace(`{${"owner_address"}}`, encodeURIComponent(String(requestParameters.ownerAddress))), - method: 'POST', - headers: headerParameters, - query: queryParameters, - body: PublishTransactionRequestToJSON(requestParameters.publishTransactionRequest), - }, initOverrides); - - return new runtime.JSONApiResponse(response, (jsonValue) => PublishPayloadFromJSON(jsonValue)); - } - - /** - * Publish transaction - */ - async publishTransaction(requestParameters: PublishTransactionOperationRequest, initOverrides?: RequestInit | runtime.InitOverrideFunction): Promise { - const response = await this.publishTransactionRaw(requestParameters, initOverrides); - return await response.value(); - } - -} diff --git a/packages/core/src/tronApi/apis/index.ts b/packages/core/src/tronApi/apis/index.ts deleted file mode 100644 index 5fdc9eb15..000000000 --- a/packages/core/src/tronApi/apis/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -export * from './TronApi'; diff --git a/packages/core/src/tronApi/index.ts b/packages/core/src/tronApi/index.ts index bebe8bbbe..d6ba94a59 100644 --- a/packages/core/src/tronApi/index.ts +++ b/packages/core/src/tronApi/index.ts @@ -1,5 +1,283 @@ -/* tslint:disable */ -/* eslint-disable */ -export * from './runtime'; -export * from './apis/index'; -export * from './models/index'; +import BigNumber from 'bignumber.js'; +import { TRON_USDT_ASSET } from '../entries/crypto/asset/constants'; +import { TronWeb } from 'tronweb'; +import { TronAsset } from '../entries/crypto/asset/tron-asset'; +import { AssetAmount } from '../entries/crypto/asset/asset-amount'; +import { notNullish } from '../utils/types'; + +const removeTrailingSlash = (str: string) => str.replace(/\/$/, ''); + +type TronTokenDTO = { + transaction_id: string; // no 0x + token_info: { + address: string; + }; + block_timestamp: number; // ms + from: string; + to: string; + type: 'Transfer'; + value: string; +}; + +export type TronHistoryItemTransferAsset = { + type: 'asset-transfer'; + assetAmount: AssetAmount; + timestamp: number; + transactionHash: string; + from: string; + to: string; + isScam: boolean; + isFailed: boolean; +}; +export type TronHistoryItem = TronHistoryItemTransferAsset; + +export class TronApi { + public readonly baseURL: string; + + public get headers() { + return { + ...(this.apiKey && { + 'TRON-PRO-API-KEY': this.apiKey + }), + 'Content-Type': 'application/json' + }; + } + + constructor(baseURL: string, private readonly apiKey?: string) { + this.baseURL = removeTrailingSlash(baseURL); + } + + async getBalances(address: string) { + const res = await ( + await fetch(`${this.baseURL}/v1/accounts/${address}`, { + headers: this.headers + }) + ).json(); + + if (!res?.success || !res?.data) { + throw new Error('Fetch tron balances failed'); + } + + const info = res?.data?.[0]; + if (!info) { + return { + trx: '0', + usdt: await this.getUSDTBalance(address) + }; + } + + let trx = '0'; + let usdt = '0'; + if (info.balance !== undefined) { + const trxBalance = parseInt(info.balance); + if (isFinite(trxBalance)) { + trx = new BigNumber(trxBalance).toFixed(0); + } else { + throw new Error('Invalid tron balance'); + } + } + + if (info.trc20 && Array.isArray(info.trc20)) { + const usdtBalance = info.trc20.find( + (obj: Record) => TRON_USDT_ASSET.address in obj + )?.[TRON_USDT_ASSET.address]; + + if (usdtBalance !== undefined) { + const parsed = parseInt(usdtBalance); + if (isFinite(parsed)) { + usdt = new BigNumber(parsed).toFixed(0); + } else { + throw new Error('Invalid tron usdt balance'); + } + } + } + + return { + trx, + usdt + }; + } + + async estimateEnergy(params: { + from: string; + contractAddress: string; + selector: string; + data: string; + }) { + try { + const response = await ( + await fetch(`${this.baseURL}/wallet/triggerconstantcontract`, { + method: 'POST', + headers: this.headers, + body: JSON.stringify({ + owner_address: params.from, + contract_address: params.contractAddress, + function_selector: params.selector, + parameter: params.data, + visible: true + }) + }) + ).json(); + + if (response.result.result !== true) { + throw new Error('Estimating energy error'); + } + + if (!('energy_used' in response)) { + throw new Error('Estimating energy error'); + } + const energy = Number.parseInt(response.energy_used); + + if (!isFinite(energy)) { + throw new Error('Estimating energy error'); + } + + return energy; + } catch (error) { + console.error('Error estimating energy:', error); + throw error; + } + } + + async getUSDTBalance(of: string) { + try { + const abi = [ + { + outputs: [{ type: 'uint256' }], + constant: true, + inputs: [{ name: 'who', type: 'address' }], + name: 'balanceOf', + stateMutability: 'View', + type: 'Function' + }, + { + outputs: [{ type: 'bool' }], + inputs: [ + { name: '_to', type: 'address' }, + { name: '_value', type: 'uint256' } + ], + name: 'transfer', + stateMutability: 'Nonpayable', + type: 'Function' + } + ]; + const contract = new TronWeb({ fullHost: this.baseURL }).contract( + abi, + TRON_USDT_ASSET.address + ); + const res = await contract.balanceOf(of).call({ from: of }); + + if (typeof res !== 'bigint' && typeof res !== 'number' && typeof res !== 'string') { + throw new Error('Error fetching usdt balance'); + } + + const parsed = new BigNumber(res.toString()); + if (!parsed.isFinite()) { + throw new Error('Error fetching usdt balance'); + } + + return parsed.toFixed(0); + } catch (error) { + console.error('Error estimating energy:', error); + return '0'; + } + } + + async rpc(method: string, params: string[] = []) { + try { + const response = await ( + await fetch(`${this.baseURL}/jsonrpc`, { + method: 'POST', + headers: this.headers, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method, + params + }) + }) + ).json(); + + if ('error' in response) { + throw new Error(response.error); + } + + if (!('result' in response)) { + throw new Error('RPC error'); + } + + return response.result; + } catch (error) { + console.error('Error estimating energy:', error); + throw error; + } + } + + async getTransfersHistory( + address: string, + options?: { + limit?: number; + maxTimestamp?: number; + onlyInitiator?: boolean; + filterSpam?: boolean; + } + ): Promise { + const url = new URL(`${this.baseURL}/v1/accounts/${address}/transactions/trc20`); + + if (options?.limit !== undefined) { + url.searchParams.set('limit', options.limit.toString()); + } + + if (options?.maxTimestamp !== undefined) { + url.searchParams.set('max_timestamp', options.maxTimestamp.toString()); + } + + if (options?.onlyInitiator === true) { + url.searchParams.set('only_from', 'true'); + } + + const response = await ( + await fetch(url, { + method: 'GET', + headers: this.headers + }) + ).json(); + + if (!response?.success || !response?.data || !Array.isArray(response.data)) { + throw new Error('Error fetching transfers history'); + } + + return response.data + .map((item: TronTokenDTO) => { + if (item.type !== 'Transfer') { + return null; + } + if (item.token_info?.address !== TRON_USDT_ASSET.address) { + return null; + } + + const assetAmount = new AssetAmount({ + weiAmount: item.value, + asset: TRON_USDT_ASSET + }); + + const isScam = assetAmount.relativeAmount.lt(1); + + if (options?.filterSpam && isScam) { + return null; + } + + return { + type: 'asset-transfer', + assetAmount, + timestamp: item.block_timestamp, + transactionHash: item.transaction_id, + from: item.from, + to: item.to, + isScam, + isFailed: false // TODO + } satisfies TronHistoryItemTransferAsset; + }) + .filter(notNullish); + } +} diff --git a/packages/core/src/tronApi/models/ContractDeployAction.ts b/packages/core/src/tronApi/models/ContractDeployAction.ts deleted file mode 100644 index 844c45e9a..000000000 --- a/packages/core/src/tronApi/models/ContractDeployAction.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface ContractDeployAction - */ -export interface ContractDeployAction { - /** - * - * @type {string} - * @memberof ContractDeployAction - */ - ownerAddress: string; -} - -/** - * Check if a given object implements the ContractDeployAction interface. - */ -export function instanceOfContractDeployAction(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "ownerAddress" in value; - - return isInstance; -} - -export function ContractDeployActionFromJSON(json: any): ContractDeployAction { - return ContractDeployActionFromJSONTyped(json, false); -} - -export function ContractDeployActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ContractDeployAction { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'ownerAddress': json['ownerAddress'], - }; -} - -export function ContractDeployActionToJSON(value?: ContractDeployAction | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'ownerAddress': value.ownerAddress, - }; -} - diff --git a/packages/core/src/tronApi/models/EstimatePayload.ts b/packages/core/src/tronApi/models/EstimatePayload.ts deleted file mode 100644 index 6a5f31d3f..000000000 --- a/packages/core/src/tronApi/models/EstimatePayload.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { RequestData } from './RequestData'; -import { - RequestDataFromJSON, - RequestDataFromJSONTyped, - RequestDataToJSON, -} from './RequestData'; - -/** - * - * @export - * @interface EstimatePayload - */ -export interface EstimatePayload { - /** - * - * @type {string} - * @memberof EstimatePayload - */ - hash: string; - /** - * - * @type {RequestData} - * @memberof EstimatePayload - */ - request: RequestData; - /** - * - * @type {Array} - * @memberof EstimatePayload - */ - internalMsgs: Array; -} - -/** - * Check if a given object implements the EstimatePayload interface. - */ -export function instanceOfEstimatePayload(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "hash" in value; - isInstance = isInstance && "request" in value; - isInstance = isInstance && "internalMsgs" in value; - - return isInstance; -} - -export function EstimatePayloadFromJSON(json: any): EstimatePayload { - return EstimatePayloadFromJSONTyped(json, false); -} - -export function EstimatePayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): EstimatePayload { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'hash': json['hash'], - 'request': RequestDataFromJSON(json['request']), - 'internalMsgs': json['internalMsgs'], - }; -} - -export function EstimatePayloadToJSON(value?: EstimatePayload | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'hash': value.hash, - 'request': RequestDataToJSON(value.request), - 'internalMsgs': value.internalMsgs, - }; -} - diff --git a/packages/core/src/tronApi/models/FromMessage.ts b/packages/core/src/tronApi/models/FromMessage.ts deleted file mode 100644 index 803f67a46..000000000 --- a/packages/core/src/tronApi/models/FromMessage.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface FromMessage - */ -export interface FromMessage { - /** - * - * @type {string} - * @memberof FromMessage - */ - to: string; - /** - * - * @type {string} - * @memberof FromMessage - */ - value: string; - /** - * - * @type {string} - * @memberof FromMessage - */ - assetAddress?: string; -} - -/** - * Check if a given object implements the FromMessage interface. - */ -export function instanceOfFromMessage(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "to" in value; - isInstance = isInstance && "value" in value; - - return isInstance; -} - -export function FromMessageFromJSON(json: any): FromMessage { - return FromMessageFromJSONTyped(json, false); -} - -export function FromMessageFromJSONTyped(json: any, ignoreDiscriminator: boolean): FromMessage { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'to': json['to'], - 'value': json['value'], - 'assetAddress': !exists(json, 'assetAddress') ? undefined : json['assetAddress'], - }; -} - -export function FromMessageToJSON(value?: FromMessage | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'to': value.to, - 'value': value.value, - 'assetAddress': value.assetAddress, - }; -} - diff --git a/packages/core/src/tronApi/models/GetEstimationRequest.ts b/packages/core/src/tronApi/models/GetEstimationRequest.ts deleted file mode 100644 index 9100b659b..000000000 --- a/packages/core/src/tronApi/models/GetEstimationRequest.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { FromMessage } from './FromMessage'; -import { - FromMessageFromJSON, - FromMessageFromJSONTyped, - FromMessageToJSON, -} from './FromMessage'; - -/** - * - * @export - * @interface GetEstimationRequest - */ -export interface GetEstimationRequest { - /** - * - * @type {number} - * @memberof GetEstimationRequest - */ - lifeTime: number; - /** - * - * @type {Array} - * @memberof GetEstimationRequest - */ - messages: Array; -} - -/** - * Check if a given object implements the GetEstimationRequest interface. - */ -export function instanceOfGetEstimationRequest(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "lifeTime" in value; - isInstance = isInstance && "messages" in value; - - return isInstance; -} - -export function GetEstimationRequestFromJSON(json: any): GetEstimationRequest { - return GetEstimationRequestFromJSONTyped(json, false); -} - -export function GetEstimationRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): GetEstimationRequest { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'lifeTime': json['lifeTime'], - 'messages': ((json['messages'] as Array).map(FromMessageFromJSON)), - }; -} - -export function GetEstimationRequestToJSON(value?: GetEstimationRequest | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'lifeTime': value.lifeTime, - 'messages': ((value.messages as Array).map(FromMessageToJSON)), - }; -} - diff --git a/packages/core/src/tronApi/models/GetSettings401Response.ts b/packages/core/src/tronApi/models/GetSettings401Response.ts deleted file mode 100644 index f80de81ee..000000000 --- a/packages/core/src/tronApi/models/GetSettings401Response.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface GetSettings401Response - */ -export interface GetSettings401Response { - /** - * - * @type {string} - * @memberof GetSettings401Response - */ - error: string; -} - -/** - * Check if a given object implements the GetSettings401Response interface. - */ -export function instanceOfGetSettings401Response(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "error" in value; - - return isInstance; -} - -export function GetSettings401ResponseFromJSON(json: any): GetSettings401Response { - return GetSettings401ResponseFromJSONTyped(json, false); -} - -export function GetSettings401ResponseFromJSONTyped(json: any, ignoreDiscriminator: boolean): GetSettings401Response { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'error': json['error'], - }; -} - -export function GetSettings401ResponseToJSON(value?: GetSettings401Response | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'error': value.error, - }; -} - diff --git a/packages/core/src/tronApi/models/ModelError.ts b/packages/core/src/tronApi/models/ModelError.ts deleted file mode 100644 index 7d8f981c3..000000000 --- a/packages/core/src/tronApi/models/ModelError.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface ModelError - */ -export interface ModelError { - /** - * - * @type {string} - * @memberof ModelError - */ - error: string; -} - -/** - * Check if a given object implements the ModelError interface. - */ -export function instanceOfModelError(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "error" in value; - - return isInstance; -} - -export function ModelErrorFromJSON(json: any): ModelError { - return ModelErrorFromJSONTyped(json, false); -} - -export function ModelErrorFromJSONTyped(json: any, ignoreDiscriminator: boolean): ModelError { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'error': json['error'], - }; -} - -export function ModelErrorToJSON(value?: ModelError | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'error': value.error, - }; -} - diff --git a/packages/core/src/tronApi/models/PublishPayload.ts b/packages/core/src/tronApi/models/PublishPayload.ts deleted file mode 100644 index 7fb41c856..000000000 --- a/packages/core/src/tronApi/models/PublishPayload.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface PublishPayload - */ -export interface PublishPayload { - /** - * - * @type {string} - * @memberof PublishPayload - */ - transactionHash: string; -} - -/** - * Check if a given object implements the PublishPayload interface. - */ -export function instanceOfPublishPayload(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "transactionHash" in value; - - return isInstance; -} - -export function PublishPayloadFromJSON(json: any): PublishPayload { - return PublishPayloadFromJSONTyped(json, false); -} - -export function PublishPayloadFromJSONTyped(json: any, ignoreDiscriminator: boolean): PublishPayload { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'transactionHash': json['transactionHash'], - }; -} - -export function PublishPayloadToJSON(value?: PublishPayload | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'transactionHash': value.transactionHash, - }; -} - diff --git a/packages/core/src/tronApi/models/PublishTransactionRequest.ts b/packages/core/src/tronApi/models/PublishTransactionRequest.ts deleted file mode 100644 index ae05f171a..000000000 --- a/packages/core/src/tronApi/models/PublishTransactionRequest.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { RequestData } from './RequestData'; -import { - RequestDataFromJSON, - RequestDataFromJSONTyped, - RequestDataToJSON, -} from './RequestData'; - -/** - * - * @export - * @interface PublishTransactionRequest - */ -export interface PublishTransactionRequest { - /** - * - * @type {RequestData} - * @memberof PublishTransactionRequest - */ - request: RequestData; - /** - * - * @type {string} - * @memberof PublishTransactionRequest - */ - hash: string; - /** - * - * @type {string} - * @memberof PublishTransactionRequest - */ - signature: string; -} - -/** - * Check if a given object implements the PublishTransactionRequest interface. - */ -export function instanceOfPublishTransactionRequest(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "request" in value; - isInstance = isInstance && "hash" in value; - isInstance = isInstance && "signature" in value; - - return isInstance; -} - -export function PublishTransactionRequestFromJSON(json: any): PublishTransactionRequest { - return PublishTransactionRequestFromJSONTyped(json, false); -} - -export function PublishTransactionRequestFromJSONTyped(json: any, ignoreDiscriminator: boolean): PublishTransactionRequest { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'request': RequestDataFromJSON(json['request']), - 'hash': json['hash'], - 'signature': json['signature'], - }; -} - -export function PublishTransactionRequestToJSON(value?: PublishTransactionRequest | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'request': RequestDataToJSON(value.request), - 'hash': value.hash, - 'signature': value.signature, - }; -} - diff --git a/packages/core/src/tronApi/models/ReceiveTRC20Action.ts b/packages/core/src/tronApi/models/ReceiveTRC20Action.ts deleted file mode 100644 index e491858c5..000000000 --- a/packages/core/src/tronApi/models/ReceiveTRC20Action.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronToken } from './TronToken'; -import { - TronTokenFromJSON, - TronTokenFromJSONTyped, - TronTokenToJSON, -} from './TronToken'; - -/** - * - * @export - * @interface ReceiveTRC20Action - */ -export interface ReceiveTRC20Action { - /** - * - * @type {string} - * @memberof ReceiveTRC20Action - */ - sender: string; - /** - * amount in quanta of tokens - * @type {string} - * @memberof ReceiveTRC20Action - */ - amount: string; - /** - * - * @type {TronToken} - * @memberof ReceiveTRC20Action - */ - token: TronToken; -} - -/** - * Check if a given object implements the ReceiveTRC20Action interface. - */ -export function instanceOfReceiveTRC20Action(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "sender" in value; - isInstance = isInstance && "amount" in value; - isInstance = isInstance && "token" in value; - - return isInstance; -} - -export function ReceiveTRC20ActionFromJSON(json: any): ReceiveTRC20Action { - return ReceiveTRC20ActionFromJSONTyped(json, false); -} - -export function ReceiveTRC20ActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): ReceiveTRC20Action { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'sender': json['sender'], - 'amount': json['amount'], - 'token': TronTokenFromJSON(json['token']), - }; -} - -export function ReceiveTRC20ActionToJSON(value?: ReceiveTRC20Action | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'sender': value.sender, - 'amount': value.amount, - 'token': TronTokenToJSON(value.token), - }; -} - diff --git a/packages/core/src/tronApi/models/RequestData.ts b/packages/core/src/tronApi/models/RequestData.ts deleted file mode 100644 index 958a746fc..000000000 --- a/packages/core/src/tronApi/models/RequestData.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { RequestMessage } from './RequestMessage'; -import { - RequestMessageFromJSON, - RequestMessageFromJSONTyped, - RequestMessageToJSON, -} from './RequestMessage'; - -/** - * - * @export - * @interface RequestData - */ -export interface RequestData { - /** - * - * @type {string} - * @memberof RequestData - */ - fee: string; - /** - * - * @type {string} - * @memberof RequestData - */ - feeToken: string; - /** - * - * @type {string} - * @memberof RequestData - */ - feeReceiver: string; - /** - * - * @type {number} - * @memberof RequestData - */ - deadline: number; - /** - * - * @type {number} - * @memberof RequestData - */ - nonce: number; - /** - * - * @type {Array} - * @memberof RequestData - */ - messages: Array; -} - -/** - * Check if a given object implements the RequestData interface. - */ -export function instanceOfRequestData(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "fee" in value; - isInstance = isInstance && "feeToken" in value; - isInstance = isInstance && "feeReceiver" in value; - isInstance = isInstance && "deadline" in value; - isInstance = isInstance && "nonce" in value; - isInstance = isInstance && "messages" in value; - - return isInstance; -} - -export function RequestDataFromJSON(json: any): RequestData { - return RequestDataFromJSONTyped(json, false); -} - -export function RequestDataFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestData { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'fee': json['fee'], - 'feeToken': json['feeToken'], - 'feeReceiver': json['feeReceiver'], - 'deadline': json['deadline'], - 'nonce': json['nonce'], - 'messages': ((json['messages'] as Array).map(RequestMessageFromJSON)), - }; -} - -export function RequestDataToJSON(value?: RequestData | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'fee': value.fee, - 'feeToken': value.feeToken, - 'feeReceiver': value.feeReceiver, - 'deadline': value.deadline, - 'nonce': value.nonce, - 'messages': ((value.messages as Array).map(RequestMessageToJSON)), - }; -} - diff --git a/packages/core/src/tronApi/models/RequestMessage.ts b/packages/core/src/tronApi/models/RequestMessage.ts deleted file mode 100644 index bde29ca00..000000000 --- a/packages/core/src/tronApi/models/RequestMessage.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface RequestMessage - */ -export interface RequestMessage { - /** - * - * @type {string} - * @memberof RequestMessage - */ - to: string; - /** - * - * @type {string} - * @memberof RequestMessage - */ - value: string; - /** - * - * @type {string} - * @memberof RequestMessage - */ - data: string; -} - -/** - * Check if a given object implements the RequestMessage interface. - */ -export function instanceOfRequestMessage(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "to" in value; - isInstance = isInstance && "value" in value; - isInstance = isInstance && "data" in value; - - return isInstance; -} - -export function RequestMessageFromJSON(json: any): RequestMessage { - return RequestMessageFromJSONTyped(json, false); -} - -export function RequestMessageFromJSONTyped(json: any, ignoreDiscriminator: boolean): RequestMessage { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'to': json['to'], - 'value': json['value'], - 'data': json['data'], - }; -} - -export function RequestMessageToJSON(value?: RequestMessage | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'to': value.to, - 'value': value.value, - 'data': value.data, - }; -} - diff --git a/packages/core/src/tronApi/models/SendTRC20Action.ts b/packages/core/src/tronApi/models/SendTRC20Action.ts deleted file mode 100644 index 05ab6c0d1..000000000 --- a/packages/core/src/tronApi/models/SendTRC20Action.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronToken } from './TronToken'; -import { - TronTokenFromJSON, - TronTokenFromJSONTyped, - TronTokenToJSON, -} from './TronToken'; - -/** - * - * @export - * @interface SendTRC20Action - */ -export interface SendTRC20Action { - /** - * - * @type {string} - * @memberof SendTRC20Action - */ - recipient: string; - /** - * amount in quanta of tokens - * @type {string} - * @memberof SendTRC20Action - */ - amount: string; - /** - * - * @type {TronToken} - * @memberof SendTRC20Action - */ - token: TronToken; -} - -/** - * Check if a given object implements the SendTRC20Action interface. - */ -export function instanceOfSendTRC20Action(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "recipient" in value; - isInstance = isInstance && "amount" in value; - isInstance = isInstance && "token" in value; - - return isInstance; -} - -export function SendTRC20ActionFromJSON(json: any): SendTRC20Action { - return SendTRC20ActionFromJSONTyped(json, false); -} - -export function SendTRC20ActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): SendTRC20Action { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'recipient': json['recipient'], - 'amount': json['amount'], - 'token': TronTokenFromJSON(json['token']), - }; -} - -export function SendTRC20ActionToJSON(value?: SendTRC20Action | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'recipient': value.recipient, - 'amount': value.amount, - 'token': TronTokenToJSON(value.token), - }; -} - diff --git a/packages/core/src/tronApi/models/TronAction.ts b/packages/core/src/tronApi/models/TronAction.ts deleted file mode 100644 index ced8e8d55..000000000 --- a/packages/core/src/tronApi/models/TronAction.ts +++ /dev/null @@ -1,140 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { ContractDeployAction } from './ContractDeployAction'; -import { - ContractDeployActionFromJSON, - ContractDeployActionFromJSONTyped, - ContractDeployActionToJSON, -} from './ContractDeployAction'; -import type { ReceiveTRC20Action } from './ReceiveTRC20Action'; -import { - ReceiveTRC20ActionFromJSON, - ReceiveTRC20ActionFromJSONTyped, - ReceiveTRC20ActionToJSON, -} from './ReceiveTRC20Action'; -import type { SendTRC20Action } from './SendTRC20Action'; -import { - SendTRC20ActionFromJSON, - SendTRC20ActionFromJSONTyped, - SendTRC20ActionToJSON, -} from './SendTRC20Action'; - -/** - * - * @export - * @interface TronAction - */ -export interface TronAction { - /** - * - * @type {string} - * @memberof TronAction - */ - type: TronActionTypeEnum; - /** - * - * @type {string} - * @memberof TronAction - */ - status: TronActionStatusEnum; - /** - * - * @type {ReceiveTRC20Action} - * @memberof TronAction - */ - receiveTRC20?: ReceiveTRC20Action; - /** - * - * @type {SendTRC20Action} - * @memberof TronAction - */ - sendTRC20?: SendTRC20Action; - /** - * - * @type {ContractDeployAction} - * @memberof TronAction - */ - contractDeploy?: ContractDeployAction; -} - - -/** - * @export - */ -export const TronActionTypeEnum = { - ReceiveTrc20: 'ReceiveTRC20', - SendTrc20: 'SendTRC20', - ContractDeploy: 'ContractDeploy' -} as const; -export type TronActionTypeEnum = typeof TronActionTypeEnum[keyof typeof TronActionTypeEnum]; - -/** - * @export - */ -export const TronActionStatusEnum = { - Ok: 'ok', - Failed: 'failed', - Pending: 'pending' -} as const; -export type TronActionStatusEnum = typeof TronActionStatusEnum[keyof typeof TronActionStatusEnum]; - - -/** - * Check if a given object implements the TronAction interface. - */ -export function instanceOfTronAction(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "type" in value; - isInstance = isInstance && "status" in value; - - return isInstance; -} - -export function TronActionFromJSON(json: any): TronAction { - return TronActionFromJSONTyped(json, false); -} - -export function TronActionFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronAction { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'type': json['type'], - 'status': json['status'], - 'receiveTRC20': !exists(json, 'receiveTRC20') ? undefined : ReceiveTRC20ActionFromJSON(json['receiveTRC20']), - 'sendTRC20': !exists(json, 'sendTRC20') ? undefined : SendTRC20ActionFromJSON(json['sendTRC20']), - 'contractDeploy': !exists(json, 'contractDeploy') ? undefined : ContractDeployActionFromJSON(json['contractDeploy']), - }; -} - -export function TronActionToJSON(value?: TronAction | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'type': value.type, - 'status': value.status, - 'receiveTRC20': ReceiveTRC20ActionToJSON(value.receiveTRC20), - 'sendTRC20': SendTRC20ActionToJSON(value.sendTRC20), - 'contractDeploy': ContractDeployActionToJSON(value.contractDeploy), - }; -} - diff --git a/packages/core/src/tronApi/models/TronBalance.ts b/packages/core/src/tronApi/models/TronBalance.ts deleted file mode 100644 index 25fc2ad2c..000000000 --- a/packages/core/src/tronApi/models/TronBalance.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronToken } from './TronToken'; -import { - TronTokenFromJSON, - TronTokenFromJSONTyped, - TronTokenToJSON, -} from './TronToken'; - -/** - * - * @export - * @interface TronBalance - */ -export interface TronBalance { - /** - * - * @type {TronToken} - * @memberof TronBalance - */ - token: TronToken; - /** - * - * @type {string} - * @memberof TronBalance - */ - weiAmount: string; -} - -/** - * Check if a given object implements the TronBalance interface. - */ -export function instanceOfTronBalance(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "token" in value; - isInstance = isInstance && "weiAmount" in value; - - return isInstance; -} - -export function TronBalanceFromJSON(json: any): TronBalance { - return TronBalanceFromJSONTyped(json, false); -} - -export function TronBalanceFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronBalance { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'token': TronTokenFromJSON(json['token']), - 'weiAmount': json['weiAmount'], - }; -} - -export function TronBalanceToJSON(value?: TronBalance | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'token': TronTokenToJSON(value.token), - 'weiAmount': value.weiAmount, - }; -} - diff --git a/packages/core/src/tronApi/models/TronBalances.ts b/packages/core/src/tronApi/models/TronBalances.ts deleted file mode 100644 index 4c9a03cee..000000000 --- a/packages/core/src/tronApi/models/TronBalances.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronBalance } from './TronBalance'; -import { - TronBalanceFromJSON, - TronBalanceFromJSONTyped, - TronBalanceToJSON, -} from './TronBalance'; - -/** - * - * @export - * @interface TronBalances - */ -export interface TronBalances { - /** - * - * @type {Array} - * @memberof TronBalances - */ - balances: Array; -} - -/** - * Check if a given object implements the TronBalances interface. - */ -export function instanceOfTronBalances(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "balances" in value; - - return isInstance; -} - -export function TronBalancesFromJSON(json: any): TronBalances { - return TronBalancesFromJSONTyped(json, false); -} - -export function TronBalancesFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronBalances { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'balances': ((json['balances'] as Array).map(TronBalanceFromJSON)), - }; -} - -export function TronBalancesToJSON(value?: TronBalances | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'balances': ((value.balances as Array).map(TronBalanceToJSON)), - }; -} - diff --git a/packages/core/src/tronApi/models/TronEvent.ts b/packages/core/src/tronApi/models/TronEvent.ts deleted file mode 100644 index 2b7edd718..000000000 --- a/packages/core/src/tronApi/models/TronEvent.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronAction } from './TronAction'; -import { - TronActionFromJSON, - TronActionFromJSONTyped, - TronActionToJSON, -} from './TronAction'; -import type { TronFee } from './TronFee'; -import { - TronFeeFromJSON, - TronFeeFromJSONTyped, - TronFeeToJSON, -} from './TronFee'; - -/** - * - * @export - * @interface TronEvent - */ -export interface TronEvent { - /** - * - * @type {string} - * @memberof TronEvent - */ - txHash: string; - /** - * - * @type {number} - * @memberof TronEvent - */ - timestamp: number; - /** - * - * @type {Array} - * @memberof TronEvent - */ - actions: Array; - /** - * - * @type {TronFee} - * @memberof TronEvent - */ - fees?: TronFee; - /** - * Event is not finished yet. Transactions still happening - * @type {boolean} - * @memberof TronEvent - */ - inProgress: boolean; -} - -/** - * Check if a given object implements the TronEvent interface. - */ -export function instanceOfTronEvent(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "txHash" in value; - isInstance = isInstance && "timestamp" in value; - isInstance = isInstance && "actions" in value; - isInstance = isInstance && "inProgress" in value; - - return isInstance; -} - -export function TronEventFromJSON(json: any): TronEvent { - return TronEventFromJSONTyped(json, false); -} - -export function TronEventFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronEvent { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'txHash': json['txHash'], - 'timestamp': json['timestamp'], - 'actions': ((json['actions'] as Array).map(TronActionFromJSON)), - 'fees': !exists(json, 'fees') ? undefined : TronFeeFromJSON(json['fees']), - 'inProgress': json['inProgress'], - }; -} - -export function TronEventToJSON(value?: TronEvent | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'txHash': value.txHash, - 'timestamp': value.timestamp, - 'actions': ((value.actions as Array).map(TronActionToJSON)), - 'fees': TronFeeToJSON(value.fees), - 'inProgress': value.inProgress, - }; -} - diff --git a/packages/core/src/tronApi/models/TronEvents.ts b/packages/core/src/tronApi/models/TronEvents.ts deleted file mode 100644 index 51f073096..000000000 --- a/packages/core/src/tronApi/models/TronEvents.ts +++ /dev/null @@ -1,81 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronEvent } from './TronEvent'; -import { - TronEventFromJSON, - TronEventFromJSONTyped, - TronEventToJSON, -} from './TronEvent'; - -/** - * - * @export - * @interface TronEvents - */ -export interface TronEvents { - /** - * - * @type {Array} - * @memberof TronEvents - */ - events: Array; - /** - * - * @type {string} - * @memberof TronEvents - */ - fingerprint?: string; -} - -/** - * Check if a given object implements the TronEvents interface. - */ -export function instanceOfTronEvents(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "events" in value; - - return isInstance; -} - -export function TronEventsFromJSON(json: any): TronEvents { - return TronEventsFromJSONTyped(json, false); -} - -export function TronEventsFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronEvents { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'events': ((json['events'] as Array).map(TronEventFromJSON)), - 'fingerprint': !exists(json, 'fingerprint') ? undefined : json['fingerprint'], - }; -} - -export function TronEventsToJSON(value?: TronEvents | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'events': ((value.events as Array).map(TronEventToJSON)), - 'fingerprint': value.fingerprint, - }; -} - diff --git a/packages/core/src/tronApi/models/TronFee.ts b/packages/core/src/tronApi/models/TronFee.ts deleted file mode 100644 index 4194c32ca..000000000 --- a/packages/core/src/tronApi/models/TronFee.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronToken } from './TronToken'; -import { - TronTokenFromJSON, - TronTokenFromJSONTyped, - TronTokenToJSON, -} from './TronToken'; - -/** - * - * @export - * @interface TronFee - */ -export interface TronFee { - /** - * - * @type {string} - * @memberof TronFee - */ - amount: string; - /** - * - * @type {TronToken} - * @memberof TronFee - */ - token: TronToken; -} - -/** - * Check if a given object implements the TronFee interface. - */ -export function instanceOfTronFee(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "amount" in value; - isInstance = isInstance && "token" in value; - - return isInstance; -} - -export function TronFeeFromJSON(json: any): TronFee { - return TronFeeFromJSONTyped(json, false); -} - -export function TronFeeFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronFee { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'amount': json['amount'], - 'token': TronTokenFromJSON(json['token']), - }; -} - -export function TronFeeToJSON(value?: TronFee | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'amount': value.amount, - 'token': TronTokenToJSON(value.token), - }; -} - diff --git a/packages/core/src/tronApi/models/TronSettings.ts b/packages/core/src/tronApi/models/TronSettings.ts deleted file mode 100644 index e7253ac21..000000000 --- a/packages/core/src/tronApi/models/TronSettings.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -import type { TronToken } from './TronToken'; -import { - TronTokenFromJSON, - TronTokenFromJSONTyped, - TronTokenToJSON, -} from './TronToken'; - -/** - * - * @export - * @interface TronSettings - */ -export interface TronSettings { - /** - * - * @type {string} - * @memberof TronSettings - */ - walletImplementation: string; - /** - * - * @type {string} - * @memberof TronSettings - */ - chainId: string; - /** - * - * @type {Array} - * @memberof TronSettings - */ - tokens: Array; -} - -/** - * Check if a given object implements the TronSettings interface. - */ -export function instanceOfTronSettings(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "walletImplementation" in value; - isInstance = isInstance && "chainId" in value; - isInstance = isInstance && "tokens" in value; - - return isInstance; -} - -export function TronSettingsFromJSON(json: any): TronSettings { - return TronSettingsFromJSONTyped(json, false); -} - -export function TronSettingsFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronSettings { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'walletImplementation': json['walletImplementation'], - 'chainId': json['chainId'], - 'tokens': ((json['tokens'] as Array).map(TronTokenFromJSON)), - }; -} - -export function TronSettingsToJSON(value?: TronSettings | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'walletImplementation': value.walletImplementation, - 'chainId': value.chainId, - 'tokens': ((value.tokens as Array).map(TronTokenToJSON)), - }; -} - diff --git a/packages/core/src/tronApi/models/TronToken.ts b/packages/core/src/tronApi/models/TronToken.ts deleted file mode 100644 index 29189a619..000000000 --- a/packages/core/src/tronApi/models/TronToken.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface TronToken - */ -export interface TronToken { - /** - * - * @type {string} - * @memberof TronToken - */ - image: string; - /** - * - * @type {string} - * @memberof TronToken - */ - name: string; - /** - * - * @type {string} - * @memberof TronToken - */ - symbol: string; - /** - * - * @type {string} - * @memberof TronToken - */ - address: string; - /** - * - * @type {number} - * @memberof TronToken - */ - decimals: number; -} - -/** - * Check if a given object implements the TronToken interface. - */ -export function instanceOfTronToken(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "image" in value; - isInstance = isInstance && "name" in value; - isInstance = isInstance && "symbol" in value; - isInstance = isInstance && "address" in value; - isInstance = isInstance && "decimals" in value; - - return isInstance; -} - -export function TronTokenFromJSON(json: any): TronToken { - return TronTokenFromJSONTyped(json, false); -} - -export function TronTokenFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronToken { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'image': json['image'], - 'name': json['name'], - 'symbol': json['symbol'], - 'address': json['address'], - 'decimals': json['decimals'], - }; -} - -export function TronTokenToJSON(value?: TronToken | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'image': value.image, - 'name': value.name, - 'symbol': value.symbol, - 'address': value.address, - 'decimals': value.decimals, - }; -} - diff --git a/packages/core/src/tronApi/models/TronWallet.ts b/packages/core/src/tronApi/models/TronWallet.ts deleted file mode 100644 index 8320bfd86..000000000 --- a/packages/core/src/tronApi/models/TronWallet.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - -import { exists, mapValues } from '../runtime'; -/** - * - * @export - * @interface TronWallet - */ -export interface TronWallet { - /** - * - * @type {string} - * @memberof TronWallet - */ - address: string; - /** - * - * @type {string} - * @memberof TronWallet - */ - chainId: string; - /** - * - * @type {number} - * @memberof TronWallet - */ - nonce: number; -} - -/** - * Check if a given object implements the TronWallet interface. - */ -export function instanceOfTronWallet(value: object): boolean { - let isInstance = true; - isInstance = isInstance && "address" in value; - isInstance = isInstance && "chainId" in value; - isInstance = isInstance && "nonce" in value; - - return isInstance; -} - -export function TronWalletFromJSON(json: any): TronWallet { - return TronWalletFromJSONTyped(json, false); -} - -export function TronWalletFromJSONTyped(json: any, ignoreDiscriminator: boolean): TronWallet { - if ((json === undefined) || (json === null)) { - return json; - } - return { - - 'address': json['address'], - 'chainId': json['chainId'], - 'nonce': json['nonce'], - }; -} - -export function TronWalletToJSON(value?: TronWallet | null): any { - if (value === undefined) { - return undefined; - } - if (value === null) { - return null; - } - return { - - 'address': value.address, - 'chainId': value.chainId, - 'nonce': value.nonce, - }; -} - diff --git a/packages/core/src/tronApi/models/index.ts b/packages/core/src/tronApi/models/index.ts deleted file mode 100644 index 388ebbf89..000000000 --- a/packages/core/src/tronApi/models/index.ts +++ /dev/null @@ -1,23 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -export * from './ContractDeployAction'; -export * from './EstimatePayload'; -export * from './FromMessage'; -export * from './GetEstimationRequest'; -export * from './GetSettings401Response'; -export * from './ModelError'; -export * from './PublishPayload'; -export * from './PublishTransactionRequest'; -export * from './ReceiveTRC20Action'; -export * from './RequestData'; -export * from './RequestMessage'; -export * from './SendTRC20Action'; -export * from './TronAction'; -export * from './TronBalance'; -export * from './TronBalances'; -export * from './TronEvent'; -export * from './TronEvents'; -export * from './TronFee'; -export * from './TronSettings'; -export * from './TronToken'; -export * from './TronWallet'; diff --git a/packages/core/src/tronApi/runtime.ts b/packages/core/src/tronApi/runtime.ts deleted file mode 100644 index a98b04582..000000000 --- a/packages/core/src/tronApi/runtime.ts +++ /dev/null @@ -1,431 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * gasless TRON service REST api - * Service to publish TRON transactions - * - * The version of the OpenAPI document: 2.0.0 - * Contact: support@tonkeeper.com - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -export const BASE_PATH = "https://tron.tonkeeper.com".replace(/\/+$/, ""); - -export interface ConfigurationParameters { - basePath?: string; // override base path - fetchApi?: FetchAPI; // override for fetch implementation - middleware?: Middleware[]; // middleware to apply before/after fetch requests - queryParamsStringify?: (params: HTTPQuery) => string; // stringify function for query strings - username?: string; // parameter for basic security - password?: string; // parameter for basic security - apiKey?: string | ((name: string) => string); // parameter for apiKey security - accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string | Promise); // parameter for oauth2 security - headers?: HTTPHeaders; //header params we want to use on every request - credentials?: RequestCredentials; //value for the credentials param we want to use on each request -} - -export class Configuration { - constructor(private configuration: ConfigurationParameters = {}) {} - - set config(configuration: Configuration) { - this.configuration = configuration; - } - - get basePath(): string { - return this.configuration.basePath != null ? this.configuration.basePath : BASE_PATH; - } - - get fetchApi(): FetchAPI | undefined { - return this.configuration.fetchApi; - } - - get middleware(): Middleware[] { - return this.configuration.middleware || []; - } - - get queryParamsStringify(): (params: HTTPQuery) => string { - return this.configuration.queryParamsStringify || querystring; - } - - get username(): string | undefined { - return this.configuration.username; - } - - get password(): string | undefined { - return this.configuration.password; - } - - get apiKey(): ((name: string) => string) | undefined { - const apiKey = this.configuration.apiKey; - if (apiKey) { - return typeof apiKey === 'function' ? apiKey : () => apiKey; - } - return undefined; - } - - get accessToken(): ((name?: string, scopes?: string[]) => string | Promise) | undefined { - const accessToken = this.configuration.accessToken; - if (accessToken) { - return typeof accessToken === 'function' ? accessToken : async () => accessToken; - } - return undefined; - } - - get headers(): HTTPHeaders | undefined { - return this.configuration.headers; - } - - get credentials(): RequestCredentials | undefined { - return this.configuration.credentials; - } -} - -export const DefaultConfig = new Configuration(); - -/** - * This is the base class for all generated API classes. - */ -export class BaseAPI { - - private static readonly jsonRegex = new RegExp('^(:?application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(:?;.*)?$', 'i'); - private middleware: Middleware[]; - - constructor(protected configuration = DefaultConfig) { - this.middleware = configuration.middleware; - } - - withMiddleware(this: T, ...middlewares: Middleware[]) { - const next = this.clone(); - next.middleware = next.middleware.concat(...middlewares); - return next; - } - - withPreMiddleware(this: T, ...preMiddlewares: Array) { - const middlewares = preMiddlewares.map((pre) => ({ pre })); - return this.withMiddleware(...middlewares); - } - - withPostMiddleware(this: T, ...postMiddlewares: Array) { - const middlewares = postMiddlewares.map((post) => ({ post })); - return this.withMiddleware(...middlewares); - } - - /** - * Check if the given MIME is a JSON MIME. - * JSON MIME examples: - * application/json - * application/json; charset=UTF8 - * APPLICATION/JSON - * application/vnd.company+json - * @param mime - MIME (Multipurpose Internet Mail Extensions) - * @return True if the given MIME is JSON, false otherwise. - */ - protected isJsonMime(mime: string | null | undefined): boolean { - if (!mime) { - return false; - } - return BaseAPI.jsonRegex.test(mime); - } - - protected async request(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction): Promise { - const { url, init } = await this.createFetchParams(context, initOverrides); - const response = await this.fetchApi(url, init); - if (response && (response.status >= 200 && response.status < 300)) { - return response; - } - throw new ResponseError(response, 'Response returned an error code'); - } - - private async createFetchParams(context: RequestOpts, initOverrides?: RequestInit | InitOverrideFunction) { - let url = this.configuration.basePath + context.path; - if (context.query !== undefined && Object.keys(context.query).length !== 0) { - // only add the querystring to the URL if there are query parameters. - // this is done to avoid urls ending with a "?" character which buggy webservers - // do not handle correctly sometimes. - url += '?' + this.configuration.queryParamsStringify(context.query); - } - - const headers = Object.assign({}, this.configuration.headers, context.headers); - Object.keys(headers).forEach(key => headers[key] === undefined ? delete headers[key] : {}); - - const initOverrideFn = - typeof initOverrides === "function" - ? initOverrides - : async () => initOverrides; - - const initParams = { - method: context.method, - headers, - body: context.body, - credentials: this.configuration.credentials, - }; - - const overriddenInit: RequestInit = { - ...initParams, - ...(await initOverrideFn({ - init: initParams, - context, - })) - }; - - let body: any; - if (isFormData(overriddenInit.body) - || (overriddenInit.body instanceof URLSearchParams) - || isBlob(overriddenInit.body)) { - body = overriddenInit.body; - } else if (this.isJsonMime(headers['Content-Type'])) { - body = JSON.stringify(overriddenInit.body); - } else { - body = overriddenInit.body; - } - - const init: RequestInit = { - ...overriddenInit, - body - }; - - return { url, init }; - } - - private fetchApi = async (url: string, init: RequestInit) => { - let fetchParams = { url, init }; - for (const middleware of this.middleware) { - if (middleware.pre) { - fetchParams = await middleware.pre({ - fetch: this.fetchApi, - ...fetchParams, - }) || fetchParams; - } - } - let response: Response | undefined = undefined; - try { - response = await (this.configuration.fetchApi || fetch)(fetchParams.url, fetchParams.init); - } catch (e) { - for (const middleware of this.middleware) { - if (middleware.onError) { - response = await middleware.onError({ - fetch: this.fetchApi, - url: fetchParams.url, - init: fetchParams.init, - error: e, - response: response ? response.clone() : undefined, - }) || response; - } - } - if (response === undefined) { - if (e instanceof Error) { - throw new FetchError(e, 'The request failed and the interceptors did not return an alternative response'); - } else { - throw e; - } - } - } - for (const middleware of this.middleware) { - if (middleware.post) { - response = await middleware.post({ - fetch: this.fetchApi, - url: fetchParams.url, - init: fetchParams.init, - response: response.clone(), - }) || response; - } - } - return response; - } - - /** - * Create a shallow clone of `this` by constructing a new instance - * and then shallow cloning data members. - */ - private clone(this: T): T { - const constructor = this.constructor as any; - const next = new constructor(this.configuration); - next.middleware = this.middleware.slice(); - return next; - } -}; - -function isBlob(value: any): value is Blob { - return typeof Blob !== 'undefined' && value instanceof Blob; -} - -function isFormData(value: any): value is FormData { - return typeof FormData !== "undefined" && value instanceof FormData; -} - -export class ResponseError extends Error { - override name: "ResponseError" = "ResponseError"; - constructor(public response: Response, msg?: string) { - super(msg); - } -} - -export class FetchError extends Error { - override name: "FetchError" = "FetchError"; - constructor(public cause: Error, msg?: string) { - super(msg); - } -} - -export class RequiredError extends Error { - override name: "RequiredError" = "RequiredError"; - constructor(public field: string, msg?: string) { - super(msg); - } -} - -export const COLLECTION_FORMATS = { - csv: ",", - ssv: " ", - tsv: "\t", - pipes: "|", -}; - -export type FetchAPI = WindowOrWorkerGlobalScope['fetch']; - -export type Json = any; -export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'OPTIONS' | 'HEAD'; -export type HTTPHeaders = { [key: string]: string }; -export type HTTPQuery = { [key: string]: string | number | null | boolean | Array | Set | HTTPQuery }; -export type HTTPBody = Json | FormData | URLSearchParams; -export type HTTPRequestInit = { headers?: HTTPHeaders; method: HTTPMethod; credentials?: RequestCredentials; body?: HTTPBody }; -export type ModelPropertyNaming = 'camelCase' | 'snake_case' | 'PascalCase' | 'original'; - -export type InitOverrideFunction = (requestContext: { init: HTTPRequestInit, context: RequestOpts }) => Promise - -export interface FetchParams { - url: string; - init: RequestInit; -} - -export interface RequestOpts { - path: string; - method: HTTPMethod; - headers: HTTPHeaders; - query?: HTTPQuery; - body?: HTTPBody; -} - -export function exists(json: any, key: string) { - const value = json[key]; - return value !== null && value !== undefined; -} - -export function querystring(params: HTTPQuery, prefix: string = ''): string { - return Object.keys(params) - .map(key => querystringSingleKey(key, params[key], prefix)) - .filter(part => part.length > 0) - .join('&'); -} - -function querystringSingleKey(key: string, value: string | number | null | undefined | boolean | Array | Set | HTTPQuery, keyPrefix: string = ''): string { - const fullKey = keyPrefix + (keyPrefix.length ? `[${key}]` : key); - if (value instanceof Array) { - const multiValue = value.map(singleValue => encodeURIComponent(String(singleValue))) - .join(`&${encodeURIComponent(fullKey)}=`); - return `${encodeURIComponent(fullKey)}=${multiValue}`; - } - if (value instanceof Set) { - const valueAsArray = Array.from(value); - return querystringSingleKey(key, valueAsArray, keyPrefix); - } - if (value instanceof Date) { - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(value.toISOString())}`; - } - if (value instanceof Object) { - return querystring(value as HTTPQuery, fullKey); - } - return `${encodeURIComponent(fullKey)}=${encodeURIComponent(String(value))}`; -} - -export function mapValues(data: any, fn: (item: any) => any) { - return Object.keys(data).reduce( - (acc, key) => ({ ...acc, [key]: fn(data[key]) }), - {} - ); -} - -export function canConsumeForm(consumes: Consume[]): boolean { - for (const consume of consumes) { - if ('multipart/form-data' === consume.contentType) { - return true; - } - } - return false; -} - -export interface Consume { - contentType: string; -} - -export interface RequestContext { - fetch: FetchAPI; - url: string; - init: RequestInit; -} - -export interface ResponseContext { - fetch: FetchAPI; - url: string; - init: RequestInit; - response: Response; -} - -export interface ErrorContext { - fetch: FetchAPI; - url: string; - init: RequestInit; - error: unknown; - response?: Response; -} - -export interface Middleware { - pre?(context: RequestContext): Promise; - post?(context: ResponseContext): Promise; - onError?(context: ErrorContext): Promise; -} - -export interface ApiResponse { - raw: Response; - value(): Promise; -} - -export interface ResponseTransformer { - (json: any): T; -} - -export class JSONApiResponse { - constructor(public raw: Response, private transformer: ResponseTransformer = (jsonValue: any) => jsonValue) {} - - async value(): Promise { - return this.transformer(await this.raw.json()); - } -} - -export class VoidApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return undefined; - } -} - -export class BlobApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.blob(); - }; -} - -export class TextApiResponse { - constructor(public raw: Response) {} - - async value(): Promise { - return await this.raw.text(); - }; -} diff --git a/packages/core/src/utils/types.ts b/packages/core/src/utils/types.ts index 34da79241..ab846f93a 100644 --- a/packages/core/src/utils/types.ts +++ b/packages/core/src/utils/types.ts @@ -4,6 +4,10 @@ export function assertUnreachable(_: never): never { throw new Error("Didn't expect to get here"); } +export function assertUnreachableSoft(_: never): void { + console.error("Didn't expect to get here", _); +} + export type NonNullableFields = { [P in keyof T]: NonNullable; }; diff --git a/packages/uikit/package.json b/packages/uikit/package.json index 476184c80..ba57c24ea 100644 --- a/packages/uikit/package.json +++ b/packages/uikit/package.json @@ -21,6 +21,7 @@ "@tanstack/react-query": "4.3.4", "@tanstack/react-virtual": "^3.8.3", "@ton-keychain/core": "^0.0.4", + "@ton-keychain/trx": "^0.0.7", "@ton/core": "0.56.0", "@ton/crypto": "3.2.0", "@tonkeeper/core": "0.1.0", @@ -31,6 +32,7 @@ "country-flag-icons": "^1.5.7", "country-list-js": "^3.1.7", "dayjs": "^1.11.10", + "ethers": "^6.13.4", "framer-motion": "^11.3.28", "react": "^18.2.0", "react-beautiful-dnd": "^13.1.1", @@ -46,6 +48,7 @@ "recharts": "^2.12.3", "slick-carousel": "^1.8.1", "styled-components": "^6.1.1", + "tronweb": "^6.0.0", "tweetnacl": "^1.0.3", "uuid": "^9.0.0" }, diff --git a/packages/uikit/src/components/Tabs.tsx b/packages/uikit/src/components/Tabs.tsx index ffae951b0..658552f9d 100644 --- a/packages/uikit/src/components/Tabs.tsx +++ b/packages/uikit/src/components/Tabs.tsx @@ -4,26 +4,29 @@ import { Button } from './fields/Button'; const TabsBlock = styled.div` display: flex; - padding: 3px; + padding: 4px; position: relative; justify-content: center; gap: 3px; user-select: none; - border-radius: ${props => props.theme.cornerMedium}; + border-radius: ${props => props.theme.corner2xSmall}; background: ${props => props.theme.backgroundContent}; + width: fit-content; `; export function Tabs({ active, values, - setActive + setActive, + className }: { active: T; values: { name: string; id: T }[]; setActive: (value: T) => void; + className?: string; }) { return ( - + {values.map(item => ( + + ); -}); +}; + +export const TronAssets: FC<{ usdt: AssetAmount | null; className?: string }> = React.memo( + ({ usdt, className }) => { + if (!usdt) { + return ; + } + return ( + } + className={className} + /> + ); + } +); diff --git a/packages/uikit/src/components/jettons/Info.tsx b/packages/uikit/src/components/jettons/Info.tsx index e3d313b3d..d33e041e8 100644 --- a/packages/uikit/src/components/jettons/Info.tsx +++ b/packages/uikit/src/components/jettons/Info.tsx @@ -17,11 +17,11 @@ const Text = styled.div` flex-grow: 1; `; -const Image = styled.img` +const Image = styled.img<{ $noCorners?: boolean }>` width: 64px; height: 64px; flex-shrink: 0; - border-radius: 100%; + border-radius: ${p => (p.$noCorners ? '0' : '100%')}; `; interface CoinProps { @@ -30,6 +30,7 @@ interface CoinProps { price?: string; image?: string; description?: string; + imageNoCorners?: boolean; } export const CoinInfoSkeleton = () => { @@ -52,7 +53,7 @@ const Title = styled(H2)` margin-bottom: 2px; `; -export const CoinInfo: FC = ({ amount, symbol, price, image }) => { +export const CoinInfo: FC = ({ amount, symbol, price, image, imageNoCorners }) => { return ( @@ -61,7 +62,11 @@ export const CoinInfo: FC = ({ amount, symbol, price, image }) => { {price && {price}} - {image ? : } + {image ? ( + + ) : ( + + )} ); }; diff --git a/packages/uikit/src/components/modals/useSendTransferNotification.ts b/packages/uikit/src/components/modals/useSendTransferNotification.ts index ad6a7787d..231ff2fe7 100644 --- a/packages/uikit/src/components/modals/useSendTransferNotification.ts +++ b/packages/uikit/src/components/modals/useSendTransferNotification.ts @@ -1,17 +1,19 @@ import { useAppSdk } from '../../hooks/appSdk'; -import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; import { useCallback } from 'react'; -import { TonTransferParams } from '@tonkeeper/core/dist/service/deeplinkingService'; +import { TransferInitParams } from '@tonkeeper/core/dist/AppSdk'; export const useSendTransferNotification = () => { const sdk = useAppSdk(); const onOpen = useCallback( - (params?: TonTransferParams) => { + (params?: Omit) => { sdk.uiEvents.emit('transfer', { method: 'transfer', id: Date.now(), - params: { chain: BLOCKCHAIN_NAME.TON, ...params, from: 'wallet' } + params: { + ...params, + from: 'wallet' as const + } as TransferInitParams }); }, [sdk] diff --git a/packages/uikit/src/components/transfer/Confirm.tsx b/packages/uikit/src/components/transfer/Confirm.tsx index fa07dc4a0..622418af9 100644 --- a/packages/uikit/src/components/transfer/Confirm.tsx +++ b/packages/uikit/src/components/transfer/Confirm.tsx @@ -8,10 +8,11 @@ export const Info = styled.div` margin-bottom: 1rem; `; -export const Image = styled.img<{ full?: boolean }>` +export const Image = styled.img<{ full?: boolean; $noBorders?: boolean }>` width: 96px; height: 96px; - border-radius: ${props => (props.full ? props.theme.cornerFull : props.theme.cornerMedium)}; + border-radius: ${props => + props.$noBorders ? 'none' : props.full ? props.theme.cornerFull : props.theme.cornerMedium}; `; export const ImageMock = styled.div<{ full?: boolean }>` diff --git a/packages/uikit/src/components/transfer/ConfirmView.tsx b/packages/uikit/src/components/transfer/ConfirmView.tsx index b7b82a6e2..11f45a7d4 100644 --- a/packages/uikit/src/components/transfer/ConfirmView.tsx +++ b/packages/uikit/src/components/transfer/ConfirmView.tsx @@ -275,7 +275,11 @@ export const ConfirmViewHeading: FC - {icon ? : } + {icon ? ( + + ) : ( + + )} {t('confirm_sending_title')} {title} diff --git a/packages/uikit/src/components/transfer/RecipientView.tsx b/packages/uikit/src/components/transfer/RecipientView.tsx index d97533fc5..87196caf0 100644 --- a/packages/uikit/src/components/transfer/RecipientView.tsx +++ b/packages/uikit/src/components/transfer/RecipientView.tsx @@ -167,7 +167,7 @@ export const RecipientView: FC<{ } return null; - }, [recipient]); + }, [recipient, acceptBlockchains]); const isValidAddress = useMemo(() => { if (acceptBlockchains && acceptBlockchains.length === 1) { @@ -235,10 +235,13 @@ export const RecipientView: FC<{ let isValid; switch (isValidForBlockchain) { case BLOCKCHAIN_NAME.TON: - isValid = isMemoValid && toAccount; + isValid = + isMemoValid && toAccount && acceptBlockchains?.includes(BLOCKCHAIN_NAME.TON); break; case BLOCKCHAIN_NAME.TRON: - isValid = seeIfValidTronAddress(recipient.address); + isValid = + seeIfValidTronAddress(recipient.address) && + acceptBlockchains?.includes(BLOCKCHAIN_NAME.TRON); } if (isValid) { if (ios && keyboard) openIosKeyboard(keyboard); @@ -272,23 +275,6 @@ export const RecipientView: FC<{ } setAddress(item); ref.current?.focus(); - // if (ios && keyboard) openIosKeyboard(keyboard); - - // if (seeIfValidTronAddress(item.address)) { - // setRecipient({ - // address: { ...item, blockchain: BLOCKCHAIN_NAME.TRON }, - // done: false - // }); - // } else { - // const to = await getAccountAsync(item); - // if (to.memoRequired) return; - // setRecipient({ - // address: { ...item, blockchain: BLOCKCHAIN_NAME.TON }, - // toAccount: to, - // comment, - // done: false - // }); - // } }; return ( diff --git a/packages/uikit/src/components/transfer/SendNotifications.tsx b/packages/uikit/src/components/transfer/SendNotifications.tsx index 00a82d5ae..b7aabf4ee 100644 --- a/packages/uikit/src/components/transfer/SendNotifications.tsx +++ b/packages/uikit/src/components/transfer/SendNotifications.tsx @@ -1,12 +1,11 @@ import { TransferInitParams } from '@tonkeeper/core/dist/AppSdk'; import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; -import { toTronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { jettonToTonAsset, TonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { RecipientData, TonRecipientData } from '@tonkeeper/core/dist/entries/send'; import { - TonTransferParams, - parseTonTransferWithAddress + parseTonTransferWithAddress, + TonTransferParams } from '@tonkeeper/core/dist/service/deeplinkingService'; import { shiftedDecimals } from '@tonkeeper/core/dist/utils/balance'; import BigNumber from 'bignumber.js'; @@ -18,7 +17,7 @@ import { openIosKeyboard } from '../../hooks/ios'; import { useTranslation } from '../../hooks/translation'; import { useIsFullWidthMode } from '../../hooks/useIsFullWidthMode'; import { useJettonList } from '../../state/jetton'; -import { useTronBalances } from '../../state/tron/tron'; +import { useActiveTronWallet } from '../../state/tron/tron'; import { Notification, NotificationFooter, @@ -39,22 +38,24 @@ import { AmountState } from './amountView/amountState'; import { AmountHeaderBlock, AmountMainButton, + childFactoryCreator, ConfirmMainButton, + duration, InitTransferData, MainButton, - RecipientHeaderBlock, - Wrapper, - childFactoryCreator, - duration, + makeTransferInitAmountState, makeTransferInitData, + RecipientHeaderBlock, TransferViewHeaderBlock, - makeTransferInitAmountState + Wrapper } from './common'; import { MultisigOrderFormView } from './MultisigOrderFormView'; import { MultisigOrderLifetimeMinutes } from '../../libs/multisig'; import { useIsActiveAccountMultisig } from '../../state/multisig'; import { ConfirmMultisigNewTransferView } from './ConfirmMultisigNewTransferView'; import { useAnalyticsTrack } from '../../hooks/amplitude'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { seeIfValidTronAddress } from '@tonkeeper/core/dist/utils/common'; const SendContent: FC<{ onClose: () => void; @@ -102,7 +103,7 @@ const SendContent: FC<{ } }, []); - const { data: tronBalances } = useTronBalances(); + const activeTronWallet = useActiveTronWallet(); const { mutateAsync: getAccountAsync, isLoading: isAccountLoading } = useGetToAccount(); @@ -115,8 +116,8 @@ const SendContent: FC<{ } _setRecipient(value); - if (tronBalances && value.address.blockchain === BLOCKCHAIN_NAME.TRON) { - setAmountViewState({ token: toTronAsset(tronBalances.balances[0]) }); + if (activeTronWallet && value.address.blockchain === BLOCKCHAIN_NAME.TRON) { + setAmountViewState({ token: TRON_USDT_ASSET }); } }; @@ -153,6 +154,9 @@ const SendContent: FC<{ }; const processTron = (address: string) => { + if (!activeTronWallet) { + return; + } const item = { address: address, blockchain: BLOCKCHAIN_NAME.TRON } as const; setRecipient({ @@ -229,10 +233,9 @@ const SendContent: FC<{ return; } - // TODO: ENABLE TRON - // if (seeIfValidTronAddress(signature)) { - // return processTron(signature); - // } + if (seeIfValidTronAddress(signature)) { + return processTron(signature); + } return sdk.uiEvents.emit('copy', { method: 'copy', @@ -263,6 +266,19 @@ const SendContent: FC<{ }); }, [amountViewState?.token?.id, amountViewState?.coinValue]); + let acceptBlockchains: BLOCKCHAIN_NAME[] = []; + if (chain) { + if (chain === BLOCKCHAIN_NAME.TRON && !activeTronWallet) { + acceptBlockchains = [BLOCKCHAIN_NAME.TON]; + } else { + acceptBlockchains = [chain]; + } + } else { + acceptBlockchains = activeTronWallet + ? [BLOCKCHAIN_NAME.TON, BLOCKCHAIN_NAME.TRON] + : [BLOCKCHAIN_NAME.TON]; + } + return ( @@ -300,7 +316,7 @@ const SendContent: FC<{ onScan={onScan} keyboard="decimal" isExternalLoading={isAccountLoading} - acceptBlockchains={chain ? [chain] : undefined} + acceptBlockchains={acceptBlockchains} MainButton={MainButton} HeaderBlock={() => ( { const transfer = options.params; setChain(options.params.chain); + + if (transfer.chain === BLOCKCHAIN_NAME.TRON) { + setOpen(true); + track('send_open', { from: transfer.from }); + return; + } + if (transfer.address) { getAccountAsync({ address: transfer.address }).then(account => { setTonTransfer(makeTransferInitData(transfer, account, jettons)); diff --git a/packages/uikit/src/components/transfer/SuggestionList.tsx b/packages/uikit/src/components/transfer/SuggestionList.tsx index a03c650fd..59b7dee0d 100644 --- a/packages/uikit/src/components/transfer/SuggestionList.tsx +++ b/packages/uikit/src/components/transfer/SuggestionList.tsx @@ -14,7 +14,6 @@ import { import { toShortValue } from '@tonkeeper/core/dist/utils/common'; import { FC } from 'react'; import styled from 'styled-components'; -import { useAppContext } from '../../hooks/appContext'; import { useAppSdk } from '../../hooks/appSdk'; import { useTranslation } from '../../hooks/translation'; import { QueryKey } from '../../libs/queryKey'; @@ -237,6 +236,10 @@ export const SuggestionList: FC<{ const { t } = useTranslation(); const { data } = useLatestSuggestion(acceptBlockchains); + if (acceptBlockchains?.length === 1 && acceptBlockchains?.[0] === BLOCKCHAIN_NAME.TRON) { + return null; + } + if (!data) { return ( <> diff --git a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx index 3e5d2761f..5cd7f010f 100644 --- a/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx +++ b/packages/uikit/src/desktop-pages/coin/DesktopCoinPage.tsx @@ -1,10 +1,9 @@ import { BLOCKCHAIN_NAME, CryptoCurrency } from '@tonkeeper/core/dist/entries/crypto'; -import { tonAssetAddressFromString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { eqAddresses } from '@tonkeeper/core/dist/utils/address'; import { shiftedDecimals } from '@tonkeeper/core/dist/utils/balance'; import BigNumber from 'bignumber.js'; import { FC, useEffect, useMemo, useRef } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; +import { Navigate, useNavigate, useParams } from 'react-router-dom'; import styled from 'styled-components'; import { ArrowDownIcon, ArrowUpIcon, PlusIcon, SwapIcon } from '../../components/Icon'; import { Body2, Label2, Num3 } from '../../components/Text'; @@ -24,8 +23,7 @@ import { useFetchNext } from '../../hooks/useFetchNext'; import { AppRoute } from '../../libs/routes'; import { useFetchFilteredActivity, useScrollMonitor } from '../../state/activity'; import { useAssets } from '../../state/home'; -import { getMixedActivity } from '../../state/mixedActivity'; -import { toTokenRate, useRate } from '../../state/rates'; +import { toTokenRate, useRate, useUSDTRate } from '../../state/rates'; import { useAllSwapAssets } from '../../state/swap/useSwapAssets'; import { useSwapFromAsset } from '../../state/swap/useSwapForm'; import { useTonendpointBuyMethods } from '../../state/tonendpoint'; @@ -33,6 +31,15 @@ import { useActiveTonNetwork, useIsActiveWalletWatchOnly } from '../../state/wal import { OtherHistoryFilters } from '../../components/desktop/history/DesktopHistoryFilters'; import { Network } from '@tonkeeper/core/dist/entries/network'; import { HideOnReview } from '../../components/ios/HideOnReview'; +import { + TRON_TRX_ASSET, + TRON_USDT_ASSET +} from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { tonAssetAddressFromString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; +import { useActiveTronWallet, useTronBalances } from '../../state/tron/tron'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { BorderSmallResponsive } from '../../components/shared/Styles'; +import { useSendTransferNotification } from '../../components/modals/useSendTransferNotification'; export const DesktopCoinPage = () => { const navigate = useNavigate(); @@ -44,10 +51,19 @@ export const DesktopCoinPage = () => { } }, [name]); + const canUseTron = useActiveTronWallet(); + if (!name) return <>; const token = name === 'ton' ? CryptoCurrency.TON : name; + if (token === TRON_USDT_ASSET.id) { + if (!canUseTron) { + return ; + } + return ; + } + return ; }; @@ -166,6 +182,18 @@ const CoinInfoWrapper = styled.div` } `; +const TronCoinInfoWrapper = styled.div` + padding: 1rem 0; + display: flex; + + gap: 1rem; + + > img { + width: 56px; + height: 56px; + } +`; + const CoinInfoAmounts = styled.div` > * { display: block; @@ -264,21 +292,22 @@ const DesktopViewHeaderStyled = styled(DesktopViewHeader)` padding-right: 0; `; -export const CoinPage: FC<{ token: string }> = ({ token }) => { +const CoinPage: FC<{ token: string }> = ({ token }) => { const { t } = useTranslation(); const ref = useRef(null); - const { fetchNextPage, hasNextPage, isFetchingNextPage, data, refetch } = - useFetchFilteredActivity(token); + const { + fetchNextPage, + hasNextPage, + isFetchingNextPage, + data: activity, + refetch + } = useFetchFilteredActivity(token); useScrollMonitor(ref, 5000, refetch); useFetchNext(hasNextPage, isFetchingNextPage, fetchNextPage, true, ref); - const activity = useMemo(() => { - return getMixedActivity(data, undefined); - }, [data]); - const [assets] = useAssets(); const assetSymbol = useMemo(() => { if (!assets) { @@ -306,3 +335,171 @@ export const CoinPage: FC<{ token: string }> = ({ token }) => { ); }; + +export const TronUSDTPage = () => { + const { t } = useTranslation(); + const sdk = useAppSdk(); + + const asset = TRON_USDT_ASSET; + const { fiat } = useAppContext(); + const { data: balances } = useTronBalances(); + const { onOpen: sendTransfer } = useSendTransferNotification(); + + const usdtBalance = useMemo(() => { + if (balances === undefined) { + return undefined; + } + + if (balances === null) { + return new AssetAmount({ weiAmount: 0, asset: TRON_USDT_ASSET }); + } + + return balances.usdt; + }, [balances]); + + const trxBalance = useMemo(() => { + if (balances === undefined) { + return undefined; + } + + if (balances === null) { + return new AssetAmount({ weiAmount: 0, asset: TRON_TRX_ASSET }); + } + + return balances.trx; + }, [balances]); + + const ref = useRef(null); + const { + fetchNextPage, + hasNextPage, + isFetchingNextPage, + data: activity, + refetch + } = useFetchFilteredActivity(TRON_USDT_ASSET.address); + + useScrollMonitor(ref, 5000, refetch); + + useFetchNext(hasNextPage, isFetchingNextPage, fetchNextPage, true, ref); + + const { data: rate } = useUSDTRate(); + + return ( + + + {asset.symbol} + + + + {asset.symbol} + {usdtBalance !== undefined && ( + + {usdtBalance.stringAssetRelativeAmount} + + {formatFiatCurrency( + fiat, + usdtBalance.relativeAmount.multipliedBy(rate?.prices ?? 0) + )} + + + )} + + + sendTransfer({ chain: BLOCKCHAIN_NAME.TRON })} + disabled={usdtBalance?.weiAmount.isZero()} + > + + {t('wallet_send')} + + { + sdk.uiEvents.emit('receive', { + method: 'receive', + params: { + chain: BLOCKCHAIN_NAME.TRON, + jetton: asset.id + } + }); + }} + > + + {t('wallet_receive')} + + + + {!!trxBalance && ( + + )} + {t('page_header_history')} + + + + + ); +}; + +const TronTopUpUSDTWrapper = styled.div` + background-color: ${p => p.theme.backgroundContent}; + ${BorderSmallResponsive}; + padding: 16px 14px; + display: flex; + align-items: center; + justify-content: space-between; + margin: 16px; + + > ${Body2} { + color: ${p => p.theme.textSecondary}; + } +`; + +const TextContainer = styled.div` + > * { + display: block; + } + + > ${Body2} { + color: ${p => p.theme.textSecondary}; + } +`; + +const SmallDivider = styled.div` + width: 100%; + height: 1px; + background-color: ${p => p.theme.separatorCommon}; +`; + +const TronTopUpTRX: FC<{ relativeAssetBalance: string }> = ({ relativeAssetBalance }) => { + const { t } = useTranslation(); + const sdk = useAppSdk(); + + return ( + <> + + + {t('tron_top_up_trx_title')} + + {t('tron_top_up_trx_description', { balance: relativeAssetBalance })} + + + + + + + ); +}; diff --git a/packages/uikit/src/desktop-pages/history/DesktopHistoryPage.tsx b/packages/uikit/src/desktop-pages/history/DesktopHistoryPage.tsx index fb0df5732..6374d6495 100644 --- a/packages/uikit/src/desktop-pages/history/DesktopHistoryPage.tsx +++ b/packages/uikit/src/desktop-pages/history/DesktopHistoryPage.tsx @@ -1,9 +1,7 @@ -import { FC, Suspense, useCallback, useMemo, useRef } from 'react'; +import { FC, Suspense, useRef } from 'react'; import styled from 'styled-components'; import { ActivitySkeletonPage } from '../../components/Skeleton'; -import { useAppContext } from '../../hooks/appContext'; import { useFetchNext } from '../../hooks/useFetchNext'; -import { getMixedActivity } from '../../state/mixedActivity'; import EmptyActivity from '../../components/activity/EmptyActivity'; import { DesktopViewHeader, @@ -21,8 +19,6 @@ import { AssetHistoryFilter, OtherHistoryFilters } from '../../components/desktop/history/DesktopHistoryFilters'; -import { useQueryClient } from '@tanstack/react-query'; -import { QueryKey } from '../../libs/queryKey'; const HistoryPageWrapper = styled(DesktopViewPageLayout)` overflow: auto; @@ -85,29 +81,25 @@ export const DesktopHistoryPage: FC = () => { const { refetch, - isFetched: isTonFetched, - fetchNextPage: fetchTonNextPage, - hasNextPage: hasTonNextPage, - isFetchingNextPage: isTonFetchingNextPage, - data: tonEvents + isFetched: isActivityFetched, + fetchNextPage: fetchActivityNextPage, + hasNextPage: hasActivityNextPage, + isFetchingNextPage: isActivityFetchingNextPage, + data: activity } = useFetchFilteredActivity(); useScrollMonitor(ref, 5000, refetch); - const isFetchingNextPage = isTonFetchingNextPage; + const isFetchingNextPage = isActivityFetchingNextPage; - useFetchNext(hasTonNextPage, isFetchingNextPage, fetchTonNextPage, true, ref); - - const activity = useMemo(() => { - return getMixedActivity(tonEvents, undefined); - }, [tonEvents]); + useFetchNext(hasActivityNextPage, isFetchingNextPage, fetchActivityNextPage, true, ref); const onOpenExplorer = () => config.accountExplorer ? sdk.openPage(config.accountExplorer.replace('%s', formatAddress(wallet.rawAddress))) : undefined; - if (!isTonFetched!) { + if (!isActivityFetched!) { return ( @@ -129,7 +121,7 @@ export const DesktopHistoryPage: FC = () => { ); } - if (activity.length === 0) { + if (activity?.length === 0) { return ( }> diff --git a/packages/uikit/src/desktop-pages/tokens/DesktopTokens.tsx b/packages/uikit/src/desktop-pages/tokens/DesktopTokens.tsx index fd4ea4218..f1b0e954c 100644 --- a/packages/uikit/src/desktop-pages/tokens/DesktopTokens.tsx +++ b/packages/uikit/src/desktop-pages/tokens/DesktopTokens.tsx @@ -10,12 +10,15 @@ import { DesktopViewPageLayout } from '../../components/desktop/DesktopViewLayout'; import { TokensPieChart } from '../../components/desktop/tokens/TokensPieChart'; -import { JettonAsset, TonAsset } from '../../components/home/Jettons'; +import { AnyChainAsset, TonAsset } from '../../components/home/Jettons'; import { useTranslation } from '../../hooks/translation'; -import { useAssets } from '../../state/home'; +import { useAllChainsAssets } from '../../state/home'; import { useMutateUserUIPreferences, useUserUIPreferences } from '../../state/theme'; import { useAssetsDistribution } from '../../state/asset'; +import { TronAssets } from '../../components/home/TronAssets'; +import { useActiveTronWallet, useCanUseTronForActiveWallet } from '../../state/tron/tron'; +import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; const DesktopAssetStylesOverride = css` background-color: transparent; @@ -33,7 +36,13 @@ const TonAssetStyled = styled(TonAsset)` ${DesktopAssetStylesOverride} `; -const JettonAssetStyled = styled(JettonAsset)` +const TronAssetsStyled = styled(TronAssets)` + margin: 0 -16px; + + ${DesktopAssetStylesOverride} +`; + +const AnyChainAssetStyled = styled(AnyChainAsset)` ${DesktopAssetStylesOverride} `; @@ -73,7 +82,13 @@ const Divider = styled.div` const itemSize = 77; const DesktopTokensPayload = () => { - const [assets] = useAssets(); + const { assets: allAssets } = useAllChainsAssets() ?? []; + const [tonAssetAmount, assets] = useMemo(() => { + return [ + allAssets?.find(item => item.asset.id === TON_ASSET.id), + allAssets?.filter(item => item.asset.id !== TON_ASSET.id) + ]; + }, [allAssets]); const { t } = useTranslation(); const { data: distribution } = useAssetsDistribution(); const { data: uiPreferences } = useUserUIPreferences(); @@ -82,6 +97,9 @@ const DesktopTokensPayload = () => { const tonRef = useRef(null); const containerRef = useRef(null); + const tronWallet = useActiveTronWallet(); + const canUseTron = useCanUseTronForActiveWallet(); + useLayoutEffect(() => { if (uiPreferences?.showTokensChart !== undefined) { setShowChart(uiPreferences.showTokensChart); @@ -95,16 +113,15 @@ const DesktopTokensPayload = () => { setShowChart(!showChart); }; - const sortedAssets = useMemo(() => { - return assets?.ton?.jettons?.balances ?? []; - }, [assets]); + const virtualScrollPaddingBase = canUseTron && !tronWallet ? 2 * itemSize : itemSize; const rowVirtualizer = useVirtualizer({ - count: sortedAssets.length, + count: assets?.length ?? 0, getScrollElement: () => containerRef.current, estimateSize: () => itemSize, - getItemKey: index => sortedAssets[index].jetton.address, - paddingStart: canShowChart && showChart ? 192 + itemSize : itemSize + getItemKey: index => assets![index].asset.id, + paddingStart: + canShowChart && showChart ? 192 + virtualScrollPaddingBase : virtualScrollPaddingBase }); const onTokenClick = useCallback( @@ -117,14 +134,14 @@ const DesktopTokensPayload = () => { return rowVirtualizer.scrollToOffset(containerRef.current!.scrollHeight); } - const index = sortedAssets.findIndex(item => item.jetton.address === address); + const index = assets!.findIndex(item => item.asset.address === address); if (index !== undefined) { rowVirtualizer.scrollToOffset( (tonRef.current?.offsetTop ?? 0) + (index + 1) * itemSize ); } }, - [sortedAssets, rowVirtualizer, rowVirtualizer.elementsCache] + [assets, rowVirtualizer, rowVirtualizer.elementsCache] ); return ( @@ -150,7 +167,7 @@ const DesktopTokensPayload = () => { overflow: 'hidden' }} > - {sortedAssets && assets && distribution && uiPreferences && ( + {tonAssetAmount && assets && distribution && uiPreferences && ( <> {canShowChart && showChart && ( { )} - + + {canUseTron && !tronWallet && ( + <> + + + + )} {rowVirtualizer.getVirtualItems().map(virtualRow => (
{ 'Failed to display tokens list' )} > - +
diff --git a/packages/uikit/src/hooks/appContext.ts b/packages/uikit/src/hooks/appContext.ts index 361727d21..e876d001b 100644 --- a/packages/uikit/src/hooks/appContext.ts +++ b/packages/uikit/src/hooks/appContext.ts @@ -7,7 +7,6 @@ import { Tonendpoint, TonendpointConfig } from '@tonkeeper/core/dist/tonkeeperApi/tonendpoint'; -import { Configuration as TronConfiguration } from '@tonkeeper/core/dist/tronApi'; import React, { useContext } from 'react'; export interface IAppContext { @@ -32,20 +31,19 @@ export interface IAppContext { hideBrowser?: boolean; browserLength?: number; env?: { - tgAuthBotId: string; - stonfiReferralAddress: string; + tgAuthBotId?: string; + stonfiReferralAddress?: string; + tronApiKey?: string; }; defaultWalletVersion: WalletVersion; } export const AppContext = React.createContext({ mainnetApi: { - tonApiV2: new ConfigurationV2(), - tronApi: new TronConfiguration() + tonApiV2: new ConfigurationV2() }, testnetApi: { - tonApiV2: new ConfigurationV2(), - tronApi: new TronConfiguration() + tonApiV2: new ConfigurationV2() }, fiat: FiatCurrencies.USD, mainnetConfig: defaultTonendpointConfig, diff --git a/packages/uikit/src/hooks/blockchain/useEstimateTransfer.ts b/packages/uikit/src/hooks/blockchain/useEstimateTransfer.ts index bb4cfeca7..dffc1f424 100644 --- a/packages/uikit/src/hooks/blockchain/useEstimateTransfer.ts +++ b/packages/uikit/src/hooks/blockchain/useEstimateTransfer.ts @@ -9,7 +9,8 @@ import { BATTERY_SENDER_CHOICE, EXTERNAL_SENDER_CHOICE, SenderTypeUserAvailable, - useGetEstimationSender + useGetEstimationSender, + useGetTronEstimationSender } from './useSender'; import { useTonAssetTransferService } from './useBlockchainService'; import { useNotifyErrorHandle } from '../useNotification'; @@ -17,6 +18,8 @@ import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; import { useToQueryKeyPart } from '../useToQueryKeyPart'; import { useMemo } from 'react'; import { assertUnreachable } from '@tonkeeper/core/dist/utils/types'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; export function useEstimateTransfer({ recipient, @@ -59,9 +62,20 @@ export function useEstimateTransfer({ const transferService = useTonAssetTransferService(); const notifyError = useNotifyErrorHandle(); const getSenderKey = useToQueryKeyPart(getSender); + const getTronSender = useGetTronEstimationSender(); + const getTronEstimationSenderKey = useToQueryKeyPart(getTronSender); return useQuery( - [QueryKey.estimate, recipient, amount, isMax, getSenderKey, transferService, notifyError], + [ + QueryKey.estimate, + recipient, + amount, + isMax, + getSenderKey, + getTronEstimationSenderKey, + transferService, + notifyError + ], async () => { const comment = (recipient as TonRecipientData).comment; try { @@ -77,8 +91,14 @@ export function useEstimateTransfer({ isMax, payload: comment ? { type: 'comment', value: comment } : undefined }); + } else if (amount.asset.id === TRON_USDT_ASSET.id) { + const tronSender = getTronSender(); + return await tronSender.estimate( + recipient.address.address, + amount as AssetAmount + ); } else { - throw new Error('Tron is not supported'); + throw new Error('Unexpected asset'); } } catch (e) { await notifyError(e); diff --git a/packages/uikit/src/hooks/blockchain/useSendTransfer.ts b/packages/uikit/src/hooks/blockchain/useSendTransfer.ts index 85e38c861..c80f89a09 100644 --- a/packages/uikit/src/hooks/blockchain/useSendTransfer.ts +++ b/packages/uikit/src/hooks/blockchain/useSendTransfer.ts @@ -10,11 +10,14 @@ import { BATTERY_SENDER_CHOICE, EXTERNAL_SENDER_CHOICE, SenderTypeUserAvailable, - useGetSender + useGetSender, + useGetTronSender } from './useSender'; import { useTonAssetTransferService } from './useBlockchainService'; import { useNotifyErrorHandle } from '../useNotification'; import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; export function useSendTransfer({ recipient, @@ -34,6 +37,7 @@ export function useSendTransfer({ const notifyError = useNotifyErrorHandle(); const getSender = useGetSender(); const transferService = useTonAssetTransferService(); + const getTronSender = useGetTronSender(); return useMutation(async () => { try { @@ -79,8 +83,11 @@ export function useSendTransfer({ from: 'send_confirm', token: isTon(amount.asset.address) ? 'ton' : amount.asset.symbol }); + } else if (amount.asset.id === TRON_USDT_ASSET.id) { + const tronSender = getTronSender(); + await tronSender.send(recipient.address.address, amount as AssetAmount); } else { - throw new Error('Disable trc 20 transactions'); + throw new Error('Unexpected asset'); } } catch (e) { await notifyError(e); diff --git a/packages/uikit/src/hooks/blockchain/useSender.ts b/packages/uikit/src/hooks/blockchain/useSender.ts index 6d44e8fa0..97945d6ef 100644 --- a/packages/uikit/src/hooks/blockchain/useSender.ts +++ b/packages/uikit/src/hooks/blockchain/useSender.ts @@ -1,10 +1,10 @@ import { BatteryMessageSender, + GaslessMessageSender, LedgerMessageSender, - WalletMessageSender, MultisigCreateOrderSender, - GaslessMessageSender, - Sender + Sender, + WalletMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender'; import { useAppContext } from '../appContext'; import { @@ -23,13 +23,17 @@ import { useBatteryServiceConfig, useRequestBatteryAuthToken } from '../../state/battery'; -import { useGetAccountSigner } from '../../state/mnemonic'; +import { getTronSigner, useGetAccountSigner } from '../../state/mnemonic'; import { useCallback, useMemo } from 'react'; import { TonAsset, tonAssetAddressToString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; -import { Account, AccountTonMultisig } from '@tonkeeper/core/dist/entries/account'; +import { + Account, + AccountTonMultisig, + isAccountTronCompatible +} from '@tonkeeper/core/dist/entries/account'; import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { getMultisigSignerInfo } from '../../state/multisig'; import { GaslessConfig, MultisigApi } from '@tonkeeper/core/dist/tonApiV2'; @@ -50,6 +54,12 @@ import { } from '../../state/two-fa'; import { TwoFAMessageSender } from '@tonkeeper/core/dist/service/ton-blockchain/sender/two-fa-message-sender'; import { useConfirmTwoFANotification } from '../../components/modals/ConfirmTwoFANotificationControlled'; +import { useTronApi } from '../../state/tron/tron'; +import { TronSender } from '@tonkeeper/core/dist/service/tron-blockchain/tron-sender'; +import { useAppSdk } from '../appSdk'; +import { useCheckTouchId } from '../../state/password'; +import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; +import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; export type SenderChoice = | { type: 'multisig'; ttlSeconds: number } @@ -66,7 +76,7 @@ export type SenderTypeUserAvailable = SenderChoiceUserAvailable['type']; export const useAvailableSendersChoices = ( operation: - | { type: 'transfer'; asset: TonAsset } + | { type: 'transfer'; asset: TonAsset | TronAsset } | { type: 'multisend-transfer'; asset: TonAsset } | { type: 'nfr_transfer' } ) => { @@ -96,6 +106,9 @@ export const useAvailableSendersChoices = ( twoFaConfig ], () => { + if (asset?.blockchain === BLOCKCHAIN_NAME.TRON) { + return [EXTERNAL_SENDER_CHOICE]; + } if (account.type !== 'mnemonic' && account.type !== 'mam') { return [EXTERNAL_SENDER_CHOICE]; } @@ -627,3 +640,37 @@ const isGaslessAvailable = ({ account.activeTonWallet.version === WalletVersion.V5R1 ); }; + +export const useGetTronSender = () => { + const sdk = useAppSdk(); + const tronApi = useTronApi(); + const activeAccount = useActiveAccount(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); + + return useCallback(() => { + const signer = getTronSigner(sdk, tronApi, activeAccount, checkTouchId); + + if (!isAccountTronCompatible(activeAccount) || !activeAccount.activeTronWallet) { + throw new Error('Tron is not enabled for the active wallet'); + } + + return new TronSender(tronApi, activeAccount.activeTronWallet, signer); + }, [tronApi, activeAccount, checkTouchId]); +}; + +export const useGetTronEstimationSender = () => { + const tronApi = useTronApi(); + const activeAccount = useActiveAccount(); + + return useCallback(() => { + const signer = (): Promise => { + throw new Error('Unexpected call'); + }; + + if (!isAccountTronCompatible(activeAccount) || !activeAccount.activeTronWallet) { + throw new Error('Tron is not enabled for the active wallet'); + } + + return new TronSender(tronApi, activeAccount.activeTronWallet, signer); + }, [tronApi]); +}; diff --git a/packages/uikit/src/libs/queryKey.ts b/packages/uikit/src/libs/queryKey.ts index b0e38c66c..56294482b 100644 --- a/packages/uikit/src/libs/queryKey.ts +++ b/packages/uikit/src/libs/queryKey.ts @@ -36,7 +36,7 @@ export enum QueryKey { featuredRecommendations = 'recommendations', experimental = 'experimental', - tron = 'tron', + tronAssets = 'tronAssets', rate = 'rate', total = 'total', distribution = 'distribution', diff --git a/packages/uikit/src/pages/activity/Activity.tsx b/packages/uikit/src/pages/activity/Activity.tsx index e95e06521..8272aa10f 100644 --- a/packages/uikit/src/pages/activity/Activity.tsx +++ b/packages/uikit/src/pages/activity/Activity.tsx @@ -1,81 +1,36 @@ -import { useInfiniteQuery, useQueryClient } from '@tanstack/react-query'; -import { AccountsApi } from '@tonkeeper/core/dist/tonApiV2'; -import React, { FC, Suspense, useCallback, useMemo, useRef } from 'react'; +import React, { FC, Suspense, useRef } from 'react'; import { InnerBody } from '../../components/Body'; import { ActivityHeader } from '../../components/Header'; import { ActivitySkeletonPage, SkeletonListWithImages } from '../../components/Skeleton'; -import { MixedActivityGroup } from '../../components/activity/ActivityGroup'; import { useAppContext } from '../../hooks/appContext'; import { useFetchNext } from '../../hooks/useFetchNext'; -import { QueryKey } from '../../libs/queryKey'; -import { getMixedActivityGroups } from '../../state/mixedActivity'; -import { useActiveApi, useActiveTonNetwork, useActiveWallet } from '../../state/wallet'; -import { useScrollMonitor } from '../../state/activity'; +import { useFetchFilteredActivity, useScrollMonitor } from '../../state/activity'; +import { MobileActivityList } from '../../components/activity/MobileActivityList'; const EmptyActivity = React.lazy(() => import('../../components/activity/EmptyActivity')); const Activity: FC = () => { - const wallet = useActiveWallet(); - const network = useActiveTonNetwork(); - const api = useActiveApi(); const { standalone } = useAppContext(); const ref = useRef(null); const { - isFetched: isTonFetched, - fetchNextPage: fetchTonNextPage, - hasNextPage: hasTonNextPage, - isFetchingNextPage: isTonFetchingNextPage, - data: tonEvents - } = useInfiniteQuery({ - queryKey: [wallet.rawAddress, QueryKey.activity, 'all', network], - queryFn: ({ pageParam = undefined }) => - new AccountsApi(api.tonApiV2).getAccountEvents({ - accountId: wallet.rawAddress, - limit: 20, - beforeLt: pageParam, - subjectOnly: true - }), - getNextPageParam: lastPage => (lastPage.nextFrom > 0 ? lastPage.nextFrom : undefined) - }); + refetch, + isFetched: isActivityFetched, + fetchNextPage: fetchActivityNextPage, + hasNextPage: hasActivityNextPage, + isFetchingNextPage: isActivityFetchingNextPage, + data: activity + } = useFetchFilteredActivity(); - const client = useQueryClient(); - const invalidate = useCallback(() => { - return client.invalidateQueries([wallet.rawAddress, QueryKey.activity, 'all']); - }, []); + useScrollMonitor(ref, 5000, refetch); - useScrollMonitor(ref, 5000, invalidate); + const isFetchingNextPage = isActivityFetchingNextPage; - // const { - // isFetched: isTronFetched, - // data: tronEvents, - // isFetchingNextPage: isTronFetchingNextPage, - // hasNextPage: hasTronNextPage, - // fetchNextPage: fetchTronNextPage - // } = useInfiniteQuery({ - // queryKey: [wallet.tron?.ownerWalletAddress, wallet.network, QueryKey.tron], - // queryFn: ({ pageParam = undefined }) => - // new TronApi(api.tronApi).getTransactions({ - // ownerAddress: wallet.tron!.ownerWalletAddress, - // fingerprint: pageParam, - // limit: 100 - // }), - // getNextPageParam: lastPage => lastPage.fingerprint, - // enabled: wallet.tron !== undefined - // }); + useFetchNext(hasActivityNextPage, isFetchingNextPage, fetchActivityNextPage, standalone, ref); - const isFetchingNextPage = isTonFetchingNextPage; - - useFetchNext(hasTonNextPage, isFetchingNextPage, fetchTonNextPage, standalone, ref); - // useFetchNext(hasTronNextPage, isFetchingNextPage, fetchTronNextPage, standalone, ref); - - const activity = useMemo(() => { - return getMixedActivityGroups(tonEvents, undefined); - }, [tonEvents]); - - if (!isTonFetched) { + if (!isActivityFetched || !activity) { return ; } @@ -91,7 +46,7 @@ const Activity: FC = () => { <> - + {isFetchingNextPage && } diff --git a/packages/uikit/src/pages/coin/Coin.tsx b/packages/uikit/src/pages/coin/Coin.tsx index 65355980a..9a7b5093f 100644 --- a/packages/uikit/src/pages/coin/Coin.tsx +++ b/packages/uikit/src/pages/coin/Coin.tsx @@ -3,7 +3,8 @@ import { useNavigate, useParams } from 'react-router-dom'; import { AppRoute } from '../../libs/routes'; import { JettonContent } from './Jetton'; import { TonPage } from './Ton'; -import { TronPage } from './Tron'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { TronUsdtContent } from './TronUsdt'; const CoinPage = () => { const navigate = useNavigate(); @@ -17,8 +18,8 @@ const CoinPage = () => { if (!name) return <>; - if (name === 'tron') { - return ; + if (name === TRON_USDT_ASSET.id) { + return ; } else if (name === 'ton') { return ; } else { diff --git a/packages/uikit/src/pages/coin/Jetton.tsx b/packages/uikit/src/pages/coin/Jetton.tsx index 8d85e9e0f..7202c8caf 100644 --- a/packages/uikit/src/pages/coin/Jetton.tsx +++ b/packages/uikit/src/pages/coin/Jetton.tsx @@ -1,13 +1,16 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; import { Address } from '@ton/core'; import { tonAssetAddressToString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; -import { AccountsApi, JettonBalance, JettonInfo } from '@tonkeeper/core/dist/tonApiV2'; +import { JettonBalance, JettonInfo } from '@tonkeeper/core/dist/tonApiV2'; import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; -import React, { FC, useMemo, useRef } from 'react'; +import React, { FC, Suspense, useMemo, useRef } from 'react'; import { InnerBody } from '../../components/Body'; -import { CoinSkeletonPage } from '../../components/Skeleton'; +import { + ActivitySkeletonPage, + CoinSkeletonPage, + SkeletonListWithImages +} from '../../components/Skeleton'; import { SubHeader } from '../../components/SubHeader'; -import { ActivityList } from '../../components/activity/ActivityGroup'; +import { MobileActivityList } from '../../components/activity/MobileActivityList'; import { ActionsRow } from '../../components/home/Actions'; import { ReceiveAction } from '../../components/home/ReceiveAction'; import { SwapAction } from '../../components/home/SwapAction'; @@ -16,40 +19,57 @@ import { SendAction } from '../../components/transfer/SendActionButton'; import { useAppContext } from '../../hooks/appContext'; import { useFormatBalance } from '../../hooks/balance'; import { useFetchNext } from '../../hooks/useFetchNext'; -import { JettonKey, QueryKey } from '../../libs/queryKey'; import { useJettonBalance, useJettonInfo } from '../../state/jetton'; import { useFormatFiat, useRate } from '../../state/rates'; import { useAllSwapAssets } from '../../state/swap/useSwapAssets'; -import { useActiveApi, useActiveWallet, useIsActiveWalletWatchOnly } from '../../state/wallet'; +import { useIsActiveWalletWatchOnly } from '../../state/wallet'; +import { useFetchFilteredActivity, useScrollMonitor } from '../../state/activity'; +import EmptyActivity from '../../components/activity/EmptyActivity'; -const JettonHistory: FC<{ balance: JettonBalance; innerRef: React.RefObject }> = ({ - balance, - innerRef -}) => { - const api = useActiveApi(); +export const MobileAssetHistory: FC<{ + assetAddress: string; + innerRef: React.RefObject; +}> = ({ assetAddress, innerRef }) => { const { standalone } = useAppContext(); - const wallet = useActiveWallet(); - const { isFetched, hasNextPage, data, isFetchingNextPage, fetchNextPage } = useInfiniteQuery({ - queryKey: [balance.walletAddress.address, QueryKey.activity, JettonKey.history], - queryFn: ({ pageParam = undefined }) => - new AccountsApi(api.tonApiV2).getAccountJettonHistoryByID({ - accountId: wallet.rawAddress, - jettonId: balance.jetton.address, - limit: 20, - beforeLt: pageParam - }), - getNextPageParam: lastPage => (lastPage.nextFrom > 0 ? lastPage.nextFrom : undefined) - }); + const { + refetch, + isFetched: isActivityFetched, + fetchNextPage: fetchActivityNextPage, + hasNextPage: hasActivityNextPage, + isFetchingNextPage: isActivityFetchingNextPage, + data: activity + } = useFetchFilteredActivity(assetAddress); - useFetchNext(hasNextPage, isFetchingNextPage, fetchNextPage, standalone, innerRef); + useScrollMonitor(innerRef, 5000, refetch); + + const isFetchingNextPage = isActivityFetchingNextPage; + + useFetchNext( + hasActivityNextPage, + isFetchingNextPage, + fetchActivityNextPage, + standalone, + innerRef + ); + + if (!isActivityFetched || !activity) { + return ; + } + + if (activity.length === 0) { + return ( + }> + + + ); + } return ( - + <> + + {isFetchingNextPage && } + ); }; @@ -104,7 +124,7 @@ export const JettonContent: FC<{ jettonAddress: string }> = ({ jettonAddress }) {swapAsset && } - + ); diff --git a/packages/uikit/src/pages/coin/Ton.tsx b/packages/uikit/src/pages/coin/Ton.tsx index 92ca27617..15ded543d 100644 --- a/packages/uikit/src/pages/coin/Ton.tsx +++ b/packages/uikit/src/pages/coin/Ton.tsx @@ -1,23 +1,18 @@ -import { useInfiniteQuery } from '@tanstack/react-query'; import { BLOCKCHAIN_NAME, CryptoCurrency } from '@tonkeeper/core/dist/entries/crypto'; -import { Account, AccountsApi } from '@tonkeeper/core/dist/tonApiV2'; +import { Account } from '@tonkeeper/core/dist/tonApiV2'; import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; import React, { FC, useMemo, useRef } from 'react'; import { InnerBody } from '../../components/Body'; import { CoinSkeletonPage } from '../../components/Skeleton'; import { SubHeader } from '../../components/SubHeader'; -import { ActivityList } from '../../components/activity/ActivityGroup'; import { HomeActions } from '../../components/home/TonActions'; import { CoinInfo } from '../../components/jettons/Info'; -import { useAppContext } from '../../hooks/appContext'; import { useFormatBalance } from '../../hooks/balance'; import { useTranslation } from '../../hooks/translation'; -import { useFetchNext } from '../../hooks/useFetchNext'; -import { QueryKey } from '../../libs/queryKey'; import { useFormatFiat, useRate } from '../../state/rates'; -import { groupAndFilterTonActivityItems } from '../../state/ton/tonActivity'; -import { useActiveApi, useActiveWallet, useWalletAccountInfo } from '../../state/wallet'; +import { useWalletAccountInfo } from '../../state/wallet'; +import { MobileAssetHistory } from './Jetton'; const TonHeader: FC<{ info: Account }> = ({ info: { balance } }) => { const { t } = useTranslation(); @@ -44,28 +39,6 @@ export const TonPage = () => { const ref = useRef(null); const { data: info } = useWalletAccountInfo(); - const api = useActiveApi(); - - const { standalone } = useAppContext(); - const wallet = useActiveWallet(); - - const { fetchNextPage, hasNextPage, isFetchingNextPage, data, isFetched } = useInfiniteQuery({ - queryKey: [wallet.rawAddress, QueryKey.activity, 'ton'], - queryFn: ({ pageParam = undefined }) => - new AccountsApi(api.tonApiV2).getAccountEvents({ - accountId: wallet.rawAddress, - limit: 20, - beforeLt: pageParam, - subjectOnly: true - }), - getNextPageParam: lastPage => (lastPage.nextFrom > 0 ? lastPage.nextFrom : undefined) - }); - - useFetchNext(hasNextPage, isFetchingNextPage, fetchNextPage, standalone, ref); - - const activity = useMemo(() => { - return data ? groupAndFilterTonActivityItems(data) : undefined; - }, [data]); if (!info) { return ; @@ -77,11 +50,7 @@ export const TonPage = () => { - + ); diff --git a/packages/uikit/src/pages/coin/Tron.tsx b/packages/uikit/src/pages/coin/Tron.tsx deleted file mode 100644 index 2eb7bfd5c..000000000 --- a/packages/uikit/src/pages/coin/Tron.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; -import { TronWalletState } from '@tonkeeper/core/dist/entries/wallet'; -import { TronBalance } from '@tonkeeper/core/dist/tronApi'; -import { formatDecimals } from '@tonkeeper/core/dist/utils/balance'; -import React, { FC, useEffect, useMemo, useRef } from 'react'; -import { Route, Routes, useNavigate, useParams } from 'react-router-dom'; -import styled from 'styled-components'; -import { InnerBody } from '../../components/Body'; -import { CoinSkeletonPage } from '../../components/Skeleton'; -import { SubHeader } from '../../components/SubHeader'; -import { Body2 } from '../../components/Text'; -import { ActionsRow } from '../../components/home/Actions'; -import { ReceiveAction } from '../../components/home/ReceiveAction'; -import { CoinInfo } from '../../components/jettons/Info'; -import { SendAction } from '../../components/transfer/SendActionButton'; -import { useFormatBalance } from '../../hooks/balance'; -import { AppRoute } from '../../libs/routes'; -import { useFormatFiat, useRate } from '../../state/rates'; -import { useTronBalance, useTronWalletState } from '../../state/tron/tron'; - -const TronHeader: FC<{ tronBalance: TronBalance }> = ({ tronBalance: { token, weiAmount } }) => { - const amount = useMemo(() => formatDecimals(weiAmount, token.decimals), [weiAmount, token]); - const total = useFormatBalance(amount, token.decimals); - - const { data } = useRate(token.symbol); - const { fiatAmount } = useFormatFiat(data, amount); - - return ; -}; - -const TronActivity: FC<{ - tron: TronWalletState; - innerRef: React.RefObject; -}> = ({ tron, innerRef }) => { - return null; - /*const { - standalone, - api: { tronApi } - } = useAppContext(); - const { data, isFetched, isFetchingNextPage, hasNextPage, fetchNextPage } = useInfiniteQuery({ - queryKey: [tron.ownerWalletAddress, wallet.network, QueryKey.tron], - queryFn: ({ pageParam = undefined }) => - new TronApi(tronApi).getTransactions({ - ownerAddress: tron.ownerWalletAddress, - fingerprint: pageParam, - limit: 100 - }), - getNextPageParam: lastPage => lastPage.fingerprint - }); - - useFetchNext(hasNextPage, isFetchingNextPage, fetchNextPage, standalone, innerRef); - - return ( - - );*/ -}; - -const Layout = styled.div` - display: flex; - flex-direction: column; - align-items: center; - line-height: 22px; -`; - -const Label = styled(Body2)` - color: ${props => props.theme.textSecondary}; -`; - -const TronAsset: FC<{ tron: TronWalletState }> = ({ tron }) => { - const { address } = useParams(); - const navigate = useNavigate(); - const { data: tronBalance, isLoading, isError } = useTronBalance(tron, address); - useEffect(() => { - if (isError) { - navigate(AppRoute.home); - } - }, [isError]); - - const ref = useRef(null); - - if (isLoading || !tronBalance) { - return ; - } - - return ( - <> - -
{tronBalance.token.name}
- - - } - /> - - - - - - - - - - ); -}; - -export const TronPage = () => { - const navigate = useNavigate(); - const { data: state, isLoading, isError } = useTronWalletState(); - - useEffect(() => { - if (isError) { - navigate(AppRoute.home); - } - }, [isError]); - - if (isLoading || !state) { - return ; - } - - return ( - - } /> - - ); -}; diff --git a/packages/uikit/src/pages/coin/TronUsdt.tsx b/packages/uikit/src/pages/coin/TronUsdt.tsx new file mode 100644 index 000000000..ed22b157a --- /dev/null +++ b/packages/uikit/src/pages/coin/TronUsdt.tsx @@ -0,0 +1,73 @@ +import React, { FC, useRef } from 'react'; +import { InnerBody } from '../../components/Body'; +import { CoinSkeletonPage } from '../../components/Skeleton'; +import { SubHeader } from '../../components/SubHeader'; +import { Action, ActionsRow } from '../../components/home/Actions'; +import { CoinInfo } from '../../components/jettons/Info'; +import { useFormatFiat, useUSDTRate } from '../../state/rates'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; +import { useTronBalances } from '../../state/tron/tron'; +import { ReceiveIcon, SendIcon } from '../../components/home/HomeIcons'; +import { useAppSdk } from '../../hooks/appSdk'; +import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; +import { useSendTransferNotification } from '../../components/modals/useSendTransferNotification'; +import { MobileAssetHistory } from './Jetton'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; + +const TronUsdtHeader: FC<{ assetAmount: AssetAmount }> = ({ assetAmount }) => { + const { data: rate } = useUSDTRate(); + const { fiatAmount } = useFormatFiat(rate, assetAmount.relativeAmount); + + return ( + + ); +}; + +export const TronUsdtContent = () => { + const balance = useTronBalances().data?.usdt; + const ref = useRef(null); + const sdk = useAppSdk(); + const { onOpen } = useSendTransferNotification(); + + if (balance === undefined) { + return ; + } + + return ( + <> + + + + + } + title={'wallet_send'} + action={() => onOpen({ chain: BLOCKCHAIN_NAME.TRON })} + /> + } + title={'wallet_receive'} + action={() => + sdk.uiEvents.emit('receive', { + method: 'receive', + params: { + chain: BLOCKCHAIN_NAME.TRON, + jetton: balance.asset.id + } + }) + } + /> + + + + + + ); +}; diff --git a/packages/uikit/src/pages/home/Home.tsx b/packages/uikit/src/pages/home/Home.tsx index 07986c863..19b4b15eb 100644 --- a/packages/uikit/src/pages/home/Home.tsx +++ b/packages/uikit/src/pages/home/Home.tsx @@ -4,22 +4,19 @@ import { FC } from 'react'; import { HomeSkeleton } from '../../components/Skeleton'; import { Balance } from '../../components/home/Balance'; import { CompactView } from '../../components/home/CompactView'; -import { AssetData } from '../../components/home/Jettons'; import { TabsView } from '../../components/home/TabsView'; import { HomeActions } from '../../components/home/TonActions'; -import { useAssets } from '../../state/home'; +import { useAllChainsAssets } from '../../state/home'; import { usePreFetchRates } from '../../state/rates'; import { useWalletFilteredNftList } from '../../state/nft'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; const HomeAssets: FC<{ - assets: AssetData; + assets: AssetAmount[]; nfts: NFT[]; }> = ({ assets, nfts }) => { - if ( - assets.ton.jettons.balances.length + nfts.length < 10 || - assets.ton.jettons.balances.length < 3 - ) { + if (assets.length + nfts.length < 11 || assets.length < 4) { return ; } else { return ; @@ -29,11 +26,11 @@ const HomeAssets: FC<{ const Home = () => { const { isFetched } = usePreFetchRates(); - const [assets, error, isAssetLoading, jettonError] = useAssets(); + const { assets, error } = useAllChainsAssets(); - const { data: nfts, error: nftError, isFetching: isNftLoading } = useWalletFilteredNftList(); + const { data: nfts, isFetching: isNftLoading } = useWalletFilteredNftList(); - const isLoading = isAssetLoading || isNftLoading; + const isLoading = !assets || isNftLoading; if (!nfts || !assets || !isFetched) { return ; @@ -41,8 +38,7 @@ const Home = () => { return ( <> - - {/* TODO: ENABLE TRON */} + diff --git a/packages/uikit/src/pages/home/MainColumn.tsx b/packages/uikit/src/pages/home/MainColumn.tsx deleted file mode 100644 index 29b9eefbd..000000000 --- a/packages/uikit/src/pages/home/MainColumn.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { BLOCKCHAIN_NAME } from '@tonkeeper/core/dist/entries/crypto'; -import { memo, useEffect, useMemo } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; -import { useTheme } from 'styled-components'; -import { ActivityIcon, BrowserIcon, NftIcon, WalletIcon } from '../../components/NavigationIcons'; -import { SkeletonAction } from '../../components/Skeleton'; -import { ActionsRow } from '../../components/home/Actions'; -import { Balance, BalanceSkeleton } from '../../components/home/Balance'; -import { HomeActions } from '../../components/home/TonActions'; -import { SettingsItem, SettingsList } from '../../components/settings/SettingsList'; -import { useAppSdk } from '../../hooks/appSdk'; -import { useTranslation } from '../../hooks/translation'; -import { scrollToTop } from '../../libs/common'; -import { AppRoute } from '../../libs/routes'; -import { useAssets } from '../../state/home'; - -import { useWalletFilteredNftList } from "../../state/nft"; - -const MainColumnSkeleton = memo(() => { - const sdk = useAppSdk(); - useEffect(() => { - return () => { - sdk.uiEvents.emit('loading'); - }; - }, []); - - return ( - <> - - - - - - {/* */} - - - ); -}); - -const Navigation = () => { - const { t } = useTranslation(); - const location = useLocation(); - const navigate = useNavigate(); - - const { data: nfts } = useWalletFilteredNftList(); - - const active = useMemo(() => { - if (location.pathname.includes(AppRoute.activity)) { - return AppRoute.activity; - } - if (location.pathname.includes(AppRoute.browser)) { - return AppRoute.browser; - } - if (location.pathname.includes(AppRoute.purchases)) { - return AppRoute.purchases; - } - return AppRoute.home; - }, [location.pathname]); - - const theme = useTheme(); - const items = useMemo(() => { - const handleClick = (route: AppRoute) => { - if (location.pathname !== route) { - return navigate(route); - } else { - scrollToTop(); - } - }; - - const items: SettingsItem[] = [ - { - name: t('wallet_title'), - icon: , - iconColor: - active === AppRoute.home ? theme.tabBarActiveIcon : theme.tabBarInactiveIcon, - action: () => handleClick(AppRoute.home) - }, - { - name: t('browser_title'), - icon: , - iconColor: - active === AppRoute.browser ? theme.tabBarActiveIcon : theme.tabBarInactiveIcon, - action: () => handleClick(AppRoute.browser) - }, - { - name: t('activity_screen_title'), - icon: , - iconColor: - active === AppRoute.activity - ? theme.tabBarActiveIcon - : theme.tabBarInactiveIcon, - action: () => handleClick(AppRoute.activity) - } - ]; - if (nfts && nfts.length > 0) { - items.push({ - name: t('purchases_screen_title'), - icon: , - iconColor: - active === AppRoute.purchases - ? theme.tabBarActiveIcon - : theme.tabBarInactiveIcon, - action: () => handleClick(AppRoute.purchases) - }); - } - return items; - }, [t, location, navigate, active, theme, nfts]); - - return ; -}; - -export const MainColumn = () => { - const [assets, error, isAssetLoading] = useAssets(); - - if (!assets) { - return ; - } - - return ( - <> - - - - - ); -}; diff --git a/packages/uikit/src/pages/import/ImportExistingWallet.tsx b/packages/uikit/src/pages/import/ImportExistingWallet.tsx index 87197a8b9..21862d05b 100644 --- a/packages/uikit/src/pages/import/ImportExistingWallet.tsx +++ b/packages/uikit/src/pages/import/ImportExistingWallet.tsx @@ -6,7 +6,6 @@ import { FinalView } from './Password'; import { Subscribe } from './Subscribe'; import { useAccountsState, - useActiveTonNetwork, useCreateAccountMAM, useCreateAccountMnemonic, useMutateRenameAccount, @@ -44,12 +43,14 @@ import { } from '@tonkeeper/core/dist/service/mnemonicService'; import { MnemonicType } from '@tonkeeper/core/dist/entries/password'; import { Network } from '@tonkeeper/core/dist/entries/network'; +import { useIsTronEnabledGlobally } from '../../state/tron/tron'; const useProcessMnemonic = () => { const context = useAppContext(); const fiat = useUserFiat(); const sdk = useAppSdk(); const accounts = useAccountsState(); + const isTronEnabled = useIsTronEnabledGlobally(); return useMutation< { @@ -123,7 +124,8 @@ const useProcessMnemonic = () => { WalletVersion.V4R2, WalletVersion.V3R2, WalletVersion.V3R1 - ] + ], + generateTronWallet: isTronEnabled } ); @@ -171,7 +173,8 @@ const useProcessMnemonic = () => { WalletVersion.V4R2, WalletVersion.V3R2, WalletVersion.V3R1 - ] + ], + generateTronWallet: isTronEnabled } ); diff --git a/packages/uikit/src/pages/settings/Dev.tsx b/packages/uikit/src/pages/settings/Dev.tsx index 89493207a..f3541d71f 100644 --- a/packages/uikit/src/pages/settings/Dev.tsx +++ b/packages/uikit/src/pages/settings/Dev.tsx @@ -65,21 +65,48 @@ const EnableTwoFASettings = () => { return null; } + return ( + + + + + Enable 2FA + Experimental + + mutateSettings({ twoFAEnabled: checked })} + /> + + + + ); +}; + +const EnableTronSettings = () => { + const { mutate: mutateSettings } = useMutateDevSettings(); + const { data: devSettings } = useDevSettings(); + + const config = useActiveConfig(); + if (config.flags?.disable_tron) { + return null; + } + return ( - Enable 2FA - Experimental + Enable TRON USDT + Experimental - Available only for W5 wallets mutateSettings({ twoFAEnabled: checked })} + checked={!!devSettings?.tronEnabled} + onChange={checked => mutateSettings({ tronEnabled: checked })} /> @@ -93,9 +120,8 @@ export const DevSettings = React.memo(() => { + - {/* TODO: ENABLE TRON */} - {/* */} ); diff --git a/packages/uikit/src/pages/settings/Jettons.tsx b/packages/uikit/src/pages/settings/Jettons.tsx index fd2378761..8d737c16c 100644 --- a/packages/uikit/src/pages/settings/Jettons.tsx +++ b/packages/uikit/src/pages/settings/Jettons.tsx @@ -1,5 +1,4 @@ import { TonWalletConfig } from '@tonkeeper/core/dist/entries/wallet'; -import { JettonBalance } from '@tonkeeper/core/dist/tonApiV2'; import { FC, useCallback, useMemo } from 'react'; import { DragDropContext, @@ -16,7 +15,6 @@ import { ListBlock, ListItemElement, ListItemPayload } from '../../components/Li import { SkeletonListWithImages } from '../../components/Skeleton'; import { SubHeader } from '../../components/SubHeader'; import { H3 } from '../../components/Text'; -import { useCoinFullBalance } from '../../hooks/balance'; import { useTranslation } from '../../hooks/translation'; import { useJettonRawList, @@ -25,6 +23,13 @@ import { useTogglePinJettonMutation } from '../../state/jetton'; import { useActiveTonWalletConfig } from '../../state/wallet'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { + assetAddressToString, + jettonToTonAssetAmount +} from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; +import { useTronBalances } from '../../state/tron/tron'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; const TurnOnIcon = styled.span` color: ${props => props.theme.accentBlue}; @@ -40,10 +45,10 @@ const Row = styled.div` display: flex; gap: 1rem; `; -const Logo = styled.img` +const Logo = styled.img<{ $noCorners?: boolean }>` width: 44px; height: 44px; - border-radius: ${props => props.theme.cornerFull}; + border-radius: ${props => (props.$noCorners ? 'none' : props.theme.cornerFull)}; `; const Icon = styled.span` @@ -57,37 +62,40 @@ const RadioWrapper = styled.span` cursor: pointer; `; -const SampleJettonRow: FC<{ jetton: JettonBalance; config: TonWalletConfig }> = ({ +const SampleJettonRow: FC<{ jetton: AssetAmount; config: TonWalletConfig }> = ({ jetton, config }) => { const { t } = useTranslation(); - const balance = useCoinFullBalance(jetton.balance, jetton.jetton.decimals); + const jettonAddress = assetAddressToString(jetton.asset.address); const { mutate: togglePin } = useTogglePinJettonMutation(); const { mutate: toggleHide } = useToggleHideJettonMutation(); const visible = useMemo(() => { - return !config.hiddenTokens.includes(jetton.jetton.address); + return !config.hiddenTokens.includes(jettonAddress); }, [config.hiddenTokens]); const pinned = useMemo(() => { - return config.pinnedTokens.includes(jetton.jetton.address); + return config.pinnedTokens.includes(jettonAddress); }, [config.pinnedTokens]); return ( - + {visible && ( - togglePin({ config, jetton })}> + togglePin({ config, jettonAddress })}> {pinned ? ( @@ -99,7 +107,7 @@ const SampleJettonRow: FC<{ jetton: JettonBalance; config: TonWalletConfig }> = )} )} - toggleHide({ config, jetton })}> + toggleHide({ config, jettonAddress })}> {visible ? ( @@ -117,19 +125,19 @@ const SampleJettonRow: FC<{ jetton: JettonBalance; config: TonWalletConfig }> = export const PinnedJettonList: FC<{ config: TonWalletConfig; - jettons: JettonBalance[]; + jettons: AssetAmount[]; }> = ({ config, jettons }) => { const { mutate } = useSavePinnedJettonOrderMutation(); const list = useMemo( () => config.pinnedTokens.reduce((acc, item) => { - const jetton = jettons.find(j => j.jetton.address === item); + const jetton = jettons.find(j => assetAddressToString(j.asset.address) === item); if (jetton) { acc.push(jetton); } return acc; - }, [] as JettonBalance[]), + }, [] as AssetAmount[]), [jettons, config] ); @@ -140,7 +148,7 @@ export const PinnedJettonList: FC<{ const [reorderedItem] = updatedList.splice(droppedItem.source.index, 1); updatedList.splice(droppedItem.destination.index, 0, reorderedItem); - const pinnedTokens = updatedList.map(item => item.jetton.address); + const pinnedTokens = updatedList.map(item => assetAddressToString(item.asset.address)); mutate({ config, pinnedTokens }); }, [config, list, mutate] @@ -153,8 +161,8 @@ export const PinnedJettonList: FC<{ {list.map((jetton, index) => ( {p => ( @@ -182,7 +190,7 @@ export const PinnedJettonList: FC<{ }; const JettonRow: FC<{ - jetton: JettonBalance; + jetton: AssetAmount; config: TonWalletConfig; dragHandleProps: DraggableProvidedDragHandleProps | null | undefined; }> = ({ jetton, config, dragHandleProps }) => { @@ -190,19 +198,27 @@ const JettonRow: FC<{ const { mutate: togglePin } = useTogglePinJettonMutation(); - const balance = useCoinFullBalance(jetton.balance, jetton.jetton.decimals); - return ( - + - togglePin({ config, jetton })}> + + togglePin({ + config, + jettonAddress: assetAddressToString(jetton.asset.address) + }) + } + > @@ -236,9 +252,23 @@ export const JettonsSettings = () => { const { t } = useTranslation(); const { data: jettons } = useJettonRawList(); + const { data: tronBalances } = useTronBalances(); const { data: config } = useActiveTonWalletConfig(); - if (!jettons || !config) { + const assets = useMemo(() => { + if (!jettons || tronBalances === undefined) { + return undefined; + } + + const tonAssets = jettons.balances.map(jettonToTonAssetAmount); + if (!tronBalances?.usdt) { + return tonAssets as AssetAmount[]; + } + + return [tronBalances.usdt as AssetAmount].concat(tonAssets as AssetAmount[]); + }, [jettons, tronBalances]); + + if (!assets || !config) { return ; } @@ -249,14 +279,14 @@ export const JettonsSettings = () => { {config.pinnedTokens.length > 0 ? ( <> {t('pinned_jettons')} - + ) : undefined} {t('all_assets_jettons')} - {jettons.balances.map(jetton => ( - + {assets.map(jetton => ( + ))} diff --git a/packages/uikit/src/pages/settings/Recovery.tsx b/packages/uikit/src/pages/settings/Recovery.tsx index e69f4411b..b9d017073 100644 --- a/packages/uikit/src/pages/settings/Recovery.tsx +++ b/packages/uikit/src/pages/settings/Recovery.tsx @@ -1,4 +1,9 @@ -import { AccountId, isMnemonicAndPassword } from '@tonkeeper/core/dist/entries/account'; +import { + AccountId, + isAccountBip39, + isAccountTronCompatible, + isMnemonicAndPassword +} from '@tonkeeper/core/dist/entries/account'; import { WalletId } from '@tonkeeper/core/dist/entries/wallet'; import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { Navigate, useNavigate, useParams, useSearchParams } from 'react-router-dom'; @@ -9,6 +14,11 @@ import { useAppSdk } from '../../hooks/appSdk'; import { getAccountMnemonic, getMAMWalletMnemonic } from '../../state/mnemonic'; import { useCheckTouchId } from '../../state/password'; import { useAccountState, useActiveAccount } from '../../state/wallet'; +import { Body2Class } from '../../components/Text'; +import { useTranslation } from '../../hooks/translation'; +import { tonMnemonicToTronMnemonic } from '@tonkeeper/core/dist/service/walletService'; +import { SpinnerRing } from '../../components/Icon'; +import { useSetNotificationOnBack } from '../../components/Notification'; export const ActiveRecovery = () => { const account = useActiveAccount(); @@ -75,32 +85,82 @@ const BackButtonBlockStyled = styled(BackButtonBlock)` `} `; +const TronButton = styled.button` + margin-top: 15px; + padding: 4px 8px; + background-color: transparent; + border: none; + outline: none; + + ${Body2Class}; + color: ${p => p.theme.textSecondary}; +`; + +const SpinnerRingStyled = styled(SpinnerRing)` + margin: 16px auto; +`; + export const RecoveryContent: FC<{ accountId: AccountId; walletId?: WalletId; isPage?: boolean; onClose?: () => void; }> = ({ accountId, walletId, isPage = true, onClose }) => { + const { t } = useTranslation(); const navigate = useNavigate(); - const onBack = useCallback(() => { - onClose ? onClose() : navigate(-1); - }, [onClose, navigate]); + const onBack = useCallback(() => (onClose ? onClose() : navigate(-1)), [onClose, navigate]); const mnemonic = useMnemonic(onBack, accountId, walletId); const account = useAccountState(accountId); + const [isExportingTRC20, setIsExportingTrc20] = useState(false); + + const [mnemonicToShow, setMnemonicToShow] = useState(mnemonic); + useEffect(() => { + setMnemonicToShow(mnemonic); + }, [mnemonic]); - if (!mnemonic) { - return ; + const onHideTron = () => { + setMnemonicToShow(mnemonic); + setIsExportingTrc20(false); + }; + + useSetNotificationOnBack(isExportingTRC20 ? onHideTron : undefined); + + if (!mnemonicToShow) { + return ( + + + + ); } + const hasTronWallet = + account && + isAccountTronCompatible(account) && + !!account.activeTronWallet && + !isAccountBip39(account); + + const onShowTron = async () => { + const tronMnemonic = await tonMnemonicToTronMnemonic(mnemonic!); + setMnemonicToShow(tronMnemonic); + setIsExportingTrc20(true); + }; + + const wordsType = + account?.type === 'mam' && walletId === undefined + ? 'mam' + : isExportingTRC20 + ? 'tron' + : 'standard'; + return ( {isPage && } - + + + {hasTronWallet && !isExportingTRC20 && ( + {t('export_trc_20_wallet')} + )} ); }; diff --git a/packages/uikit/src/state/activity.ts b/packages/uikit/src/state/activity.ts index 55bb6f04c..23ee17019 100644 --- a/packages/uikit/src/state/activity.ts +++ b/packages/uikit/src/state/activity.ts @@ -2,20 +2,24 @@ import { useInfiniteQuery } from '@tanstack/react-query'; import { CryptoCurrency } from '@tonkeeper/core/dist/entries/crypto'; import { isTon, - TonAsset, tonAssetAddressToString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { intlLocale } from '@tonkeeper/core/dist/entries/language'; -import { AccountEvents, AccountsApi } from '@tonkeeper/core/dist/tonApiV2'; +import { AccountEvent, AccountEvents, AccountsApi } from '@tonkeeper/core/dist/tonApiV2'; import { useCallback, useEffect, useLayoutEffect, useState } from 'react'; import { atom, useAtom } from '../libs/atom'; import { QueryKey } from '../libs/queryKey'; import { useGlobalPreferences, useMutateGlobalPreferences } from './global-preferences'; -import { MixedActivity } from './mixedActivity'; import { seeIfTonTransfer } from './ton/tonActivity'; import { useActiveApi, useActiveWallet } from './wallet'; import { debounce } from '@tonkeeper/core/dist/utils/common'; import { useTwoFAWalletConfig } from './two-fa'; +import { useActiveTronWallet, useTronApi } from './tron/tron'; +import { APIConfig } from '@tonkeeper/core/dist/entries/apis'; +import { TonContract } from '@tonkeeper/core/dist/entries/wallet'; +import { TronApi, TronHistoryItem } from '@tonkeeper/core/dist/tronApi'; +import { TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { Asset, isTonAsset } from '@tonkeeper/core/dist/entries/crypto/asset/asset'; export const formatActivityDate = (language: string, key: string, timestamp: number): string => { const date = new Date(timestamp); @@ -93,10 +97,14 @@ const getEventGroup = (timestamp: number, today: Date, yesterday: Date): string return `year-${date.getFullYear()}-${date.getMonth() + 1}`; }; -export type GenericActivity = { timestamp: number; key: string; event: T }; -export type GenericActivityGroup = [string, GenericActivity[]]; +type ActivityItemCommon = { timestamp: number; key: string }; +type ActivityItemTon = ActivityItemCommon & { type: 'ton'; event: AccountEvent }; +type ActivityItemTron = ActivityItemCommon & { type: 'tron'; event: TronHistoryItem }; -export const groupGenericActivity = (list: GenericActivity[]) => { +export type ActivityItem = ActivityItemTon | ActivityItemTron; +export type ActivityItemsDatedGroup = [string, ActivityItem[]]; + +export const groupActivityItems = (list: ActivityItem[]) => { list.sort((a, b) => b.timestamp - a.timestamp); const todayDate = new Date(); @@ -111,9 +119,9 @@ export const groupGenericActivity = (list: GenericActivity[]) => { acc[group] = [item]; } return acc; - }, {} as Record[]>); + }, {} as Record); - const result = [] as GenericActivityGroup[]; + const result = [] as ActivityItemsDatedGroup[]; if (today) { result.push(['today', today]); } @@ -132,119 +140,249 @@ export const groupGenericActivity = (list: GenericActivity[]) => { return result; }; -export const groupActivityGeneric = ( - list: T[], - toTimestamp: (item: T) => number, - toKey: (item: T) => string -): GenericActivityGroup[] => { - const activity = list.map(item => ({ - timestamp: toTimestamp(item), - key: toKey(item), - event: item - })); - return groupGenericActivity(activity); -}; - -export const useFetchFilteredActivity = (asset?: string) => { +export const useFetchFilteredActivity = (assetAddress?: string) => { const wallet = useActiveWallet(); const api = useActiveApi(); const { asset: selectedAsset, filterSpam, onlyInitiator } = useHistoryFilters(); const { data: twoFAConfig } = useTwoFAWalletConfig(); const twoFaPlugin = twoFAConfig?.status === 'active' ? twoFAConfig.pluginAddress : undefined; + const tronApi = useTronApi(); + const tronWallet = useActiveTronWallet(); - return useInfiniteQuery({ + const query = useInfiniteQuery({ queryKey: [ wallet.rawAddress, QueryKey.activity, - asset, + assetAddress, selectedAsset?.id, onlyInitiator, filterSpam, - twoFaPlugin + twoFaPlugin, + tronWallet ], queryFn: async ({ pageParam = undefined }) => { - let activity: AccountEvents; - if (!asset && !selectedAsset) { - activity = await new AccountsApi(api.tonApiV2).getAccountEvents({ - accountId: wallet.rawAddress, - limit: 20, - beforeLt: pageParam, - subjectOnly: true, - initiator: onlyInitiator ? onlyInitiator : undefined - }); - } else { - let assetTonApiId: string; - if (selectedAsset) { + let assetTonApiId: string | undefined; + let isTronUSDTAsset = false; + if (selectedAsset) { + if (selectedAsset.id === TRON_USDT_ASSET.id) { + isTronUSDTAsset = true; + } else if (isTonAsset(selectedAsset)) { assetTonApiId = tonAssetAddressToString(selectedAsset.address); } - if (asset) { - assetTonApiId = - asset.toLowerCase() === CryptoCurrency.TON.toLowerCase() ? 'TON' : asset; - } - - if (assetTonApiId! === 'TON') { - activity = await new AccountsApi(api.tonApiV2).getAccountEvents({ - accountId: wallet.rawAddress, - limit: 20, - beforeLt: pageParam, - subjectOnly: true, - initiator: onlyInitiator ? onlyInitiator : undefined - }); - - activity.events = activity.events.filter(event => { - event.actions = event.actions.filter(seeIfTonTransfer); - return event.actions.length > 0; - }); + } + if (assetAddress) { + if (assetAddress === TRON_USDT_ASSET.address) { + isTronUSDTAsset = true; } else { - activity = await new AccountsApi(api.tonApiV2).getAccountJettonHistoryByID({ - accountId: wallet.rawAddress, - jettonId: assetTonApiId!, - limit: 20, - beforeLt: pageParam - }); + assetTonApiId = + assetAddress.toLowerCase() === CryptoCurrency.TON.toLowerCase() + ? 'TON' + : assetAddress; } } - if (filterSpam) { - activity.events = activity.events.filter(event => !event.isScam); - } - - if (twoFaPlugin) { - activity.events = activity.events.map(event => { - if ( - event.actions[0].tonTransfer && - event.actions[0].tonTransfer.sender.address === twoFaPlugin - ) { - event.actions = event.actions.slice(1); - } - - return event; - }); - } - - return activity; + const emptyResult = Promise.resolve({ + nextFrom: 0, + events: [] + }); + + const [tonActivity, tronActivity] = await Promise.all([ + isTronUSDTAsset + ? emptyResult + : fetchTonActivity({ + pageParam: pageParam?.tonNextFrom, + assetTonApiId, + api, + wallet, + onlyInitiator, + filterSpam, + twoFaPluginAddress: twoFaPlugin + }), + assetTonApiId + ? emptyResult + : fetchTronActivity({ + tronApi, + tronWalletAddress: tronWallet?.address, + pageParam: pageParam?.tronNextFrom, + onlyInitiator, + filterSpam + }) + ]); + + return { + tonNextFrom: tonActivity.nextFrom, + tronNextFrom: tronActivity.nextFrom, + events: sortAndPackActivityItems(tonActivity.events, tronActivity.events) + }; }, - getNextPageParam: lastPage => (lastPage.nextFrom > 0 ? lastPage.nextFrom : undefined), + getNextPageParam: lastPage => + lastPage.tronNextFrom > 0 || lastPage.tonNextFrom > 0 + ? { tonNextFrom: lastPage.tonNextFrom, tronNextFrom: lastPage.tronNextFrom } + : undefined, keepPreviousData: true }); + + const data = query.data ? query.data.pages.flatMap(i => i.events) : undefined; + + return { ...query, data }; }; -export type GroupedActivityItemSingle = { +const sortAndPackActivityItems = ( + tonItems: AccountEvent[], + tronItems: TronHistoryItem[] +): ActivityItem[] => { + return [ + ...tonItems.map( + item => + ({ + type: 'ton' as const, + timestamp: item.timestamp * 1000, + key: item.eventId, + event: item + } satisfies ActivityItemTon) + ), + ...tronItems.map( + item => + ({ + type: 'tron' as const, + timestamp: item.timestamp, + key: item.transactionHash, + event: item + } satisfies ActivityItemTron) + ) + ].sort((a, b) => b.timestamp - a.timestamp); +}; + +async function fetchTronActivity({ + tronApi, + tronWalletAddress, + pageParam, + onlyInitiator, + filterSpam +}: { + tronApi: TronApi; + tronWalletAddress?: string; + pageParam?: number; + onlyInitiator: boolean; + filterSpam: boolean; +}) { + if (pageParam === 0 || !tronWalletAddress) { + return { + nextFrom: 0, + events: [] + }; + } + + const pageLimit = 20; + + const tronActivity = await tronApi.getTransfersHistory(tronWalletAddress, { + limit: pageLimit, + maxTimestamp: pageParam ? pageParam - 1 : undefined, + onlyInitiator, + filterSpam + }); + + const nextFrom = + tronActivity.length < pageLimit ? 0 : tronActivity[tronActivity.length - 1].timestamp; + + return { + nextFrom, + events: tronActivity + }; +} + +async function fetchTonActivity({ + pageParam = undefined, + assetTonApiId, + api, + wallet, + onlyInitiator, + filterSpam, + twoFaPluginAddress +}: { + pageParam?: number; + assetTonApiId?: string; + api: APIConfig; + wallet: TonContract; + onlyInitiator: boolean; + filterSpam: boolean; + twoFaPluginAddress?: string; +}) { + if (pageParam === 0) { + return { + nextFrom: 0, + events: [] + }; + } + + let tonActivity: AccountEvents; + if (!assetTonApiId) { + tonActivity = await new AccountsApi(api.tonApiV2).getAccountEvents({ + accountId: wallet.rawAddress, + limit: 20, + beforeLt: pageParam, + subjectOnly: true, + initiator: onlyInitiator ? onlyInitiator : undefined + }); + } else { + if (assetTonApiId! === 'TON') { + tonActivity = await new AccountsApi(api.tonApiV2).getAccountEvents({ + accountId: wallet.rawAddress, + limit: 20, + beforeLt: pageParam, + subjectOnly: true, + initiator: onlyInitiator ? onlyInitiator : undefined + }); + + tonActivity.events = tonActivity.events.filter(event => { + event.actions = event.actions.filter(seeIfTonTransfer); + return event.actions.length > 0; + }); + } else { + tonActivity = await new AccountsApi(api.tonApiV2).getAccountJettonHistoryByID({ + accountId: wallet.rawAddress, + jettonId: assetTonApiId!, + limit: 20, + beforeLt: pageParam + }); + } + } + + if (filterSpam) { + tonActivity.events = tonActivity.events.filter(event => !event.isScam); + } + + if (twoFaPluginAddress) { + tonActivity.events = tonActivity.events.map(event => { + if ( + event.actions[0].tonTransfer && + event.actions[0].tonTransfer.sender.address === twoFaPluginAddress + ) { + event.actions = event.actions.slice(1); + } + + return event; + }); + } + + return tonActivity; +} + +export type CategorizedActivityItemSingle = { type: 'single'; - item: GenericActivity; + item: ActivityItem; key: string; }; -export type GroupedActivityItemGroup = { +export type CategorizedActivityItemGroup = { type: 'group'; - items: GenericActivity[]; + items: ActivityItem[]; category: 'spam'; // probably will be other groupBy categories in the future key: string; }; -export type GroupedActivityItem = GroupedActivityItemSingle | GroupedActivityItemGroup; - -export type GroupedActivity = GroupedActivityItem[]; +export type CategorizedActivityItem = CategorizedActivityItemSingle | CategorizedActivityItemGroup; +export type CategorizedActivity = CategorizedActivityItem[]; export const defaultHistoryFilters = { asset: undefined, @@ -253,7 +391,7 @@ export const defaultHistoryFilters = { }; const historyFilters$ = atom<{ - asset: TonAsset | undefined; + asset: Asset | undefined; onlyInitiator: boolean; filterSpam: boolean; }>(defaultHistoryFilters); @@ -283,7 +421,7 @@ export const useHistoryFilters = () => { }, [setFilters, mutate, filters.filterSpam]); const setAsset = useCallback( - (asset: TonAsset | undefined) => { + (asset: Asset | undefined) => { setFilters(f => ({ ...f, asset })); }, [setFilters] @@ -297,11 +435,11 @@ export const useHistoryFilters = () => { }; }; -export const isInitiatorFiltrationForAssetAvailable = (asset: TonAsset | undefined): boolean => { +export const isInitiatorFiltrationForAssetAvailable = (asset: Asset | undefined): boolean => { if (!asset) { return true; } - return isTon(asset.address); + return !isTonAsset(asset) || isTon(asset.address); }; export const useScrollMonitor = ( diff --git a/packages/uikit/src/state/asset.ts b/packages/uikit/src/state/asset.ts index 35d9e3ddb..7cbbd8fb4 100644 --- a/packages/uikit/src/state/asset.ts +++ b/packages/uikit/src/state/asset.ts @@ -8,10 +8,10 @@ import { isBasicAsset, packAssetId } from '@tonkeeper/core/dist/entries/crypto/a import { KNOWN_TON_ASSETS, TON_ASSET, + TRON_TRX_ASSET, TRON_USDT_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { TonAsset, legacyTonAssetId } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; -import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; import { DashboardCellNumeric } from '@tonkeeper/core/dist/entries/dashboard'; import { getDashboardData } from '@tonkeeper/core/dist/service/proService'; import { JettonBalance } from '@tonkeeper/core/dist/tonApiV2'; @@ -28,7 +28,8 @@ import { tokenRate as getTokenRate, getTonFiatAmount, toTokenRate, - useRate + useRate, + useUSDTRate } from './rates'; import { useTronBalances } from './tron/tron'; import { useAccountsState, useActiveAccount, useWalletAccountInfo } from './wallet'; @@ -61,13 +62,10 @@ export function useUserAssetBalance< } } else { isLoading = tronBalances.isLoading; - const token = tronBalances.data?.balances.find(i => i.token.address === asset.address); - if (token && isBasicAsset(asset)) { - data = new AssetAmount({ - asset, - weiAmount: token.weiAmount, - image: token.token.image - }); + if (asset.address === TRON_USDT_ASSET.address) { + data = tronBalances.data?.usdt; + } else if (asset.address === TRON_TRX_ASSET.address) { + data = tronBalances.data?.trx; } else { data = '0'; } @@ -82,22 +80,21 @@ export function useUserAssetBalance< export function useAssetImage({ blockchain, address }: AssetIdentification): string | undefined { const id = packAssetId(blockchain, address); const { data: jettons } = useJettonList(); - const { data: balances } = useTronBalances(); if (id === TON_ASSET.id) { return 'https://wallet.tonkeeper.com/img/toncoin.svg'; } if (id === TRON_USDT_ASSET.id) { - return 'https://wallet-dev.tonkeeper.com/img/usdt.svg'; + return TRON_USDT_ASSET.image; } if (typeof address === 'string') { - return balances?.balances.find(i => i.token.address === address)?.token.image; - } else { - return jettons?.balances.find(i => address.equals(Address.parse(i.jetton.address)))?.jetton - .image; + throw new Error('Unexpected address'); } + + return jettons?.balances.find(i => address.equals(Address.parse(i.jetton.address)))?.jetton + .image; } export function useAssetAmountFiatEquivalent(assetAmount: AssetAmount): { @@ -153,6 +150,9 @@ export const useWalletTotalBalance = () => { const { data: tonRate } = useRate(CryptoCurrency.TON); const fiat = useUserFiat(); + const { data: tronBalances } = useTronBalances(); + const { data: usdtRate } = useUSDTRate(); + const client = useQueryClient(); return useQuery( [QueryKey.total, fiat, assets, tonRate], @@ -160,13 +160,19 @@ export const useWalletTotalBalance = () => { if (!assets) { return new BigNumber(0); } - return ( - getTonFiatAmount(client, fiat, assets) - // .plus(getTRC20FiatAmount(client, fiat, assets)) - .plus(getJettonsFiatAmount(fiat, assets)) + const tonAssetsAmount = getTonFiatAmount(client, fiat, assets).plus( + getJettonsFiatAmount(fiat, assets) + ); + + if (!tronBalances || !usdtRate?.prices) { + return tonAssetsAmount; + } + + return tonAssetsAmount.plus( + tronBalances.usdt.relativeAmount.multipliedBy(usdtRate.prices) ); }, - { enabled: !!assets && !!tonRate } + { enabled: !!assets && !!tonRate && !!usdtRate && tronBalances !== undefined } ); }; @@ -207,6 +213,8 @@ export const useAccountTotalBalance = () => { () => account.allTonWallets.map(w => w.rawAddress), [account] ); + const { data: tronBalances } = useTronBalances(); + const { data: rate } = useUSDTRate(); return useQuery( [QueryKey.allWalletsTotalBalance, fiat, allWalletsAddresses], @@ -219,10 +227,19 @@ export const useAccountTotalBalance = () => { currency: fiat }); - return result + const totalTonAssetsBalances = result .map(row => new BigNumber((row.cells[0] as DashboardCellNumeric).value)) .reduce((v, acc) => acc.plus(v), new BigNumber(0)); - } + + if (!tronBalances) { + return totalTonAssetsBalances; + } + + return totalTonAssetsBalances.plus( + tronBalances.usdt.relativeAmount.multipliedBy(rate?.prices ?? 0) + ); + }, + { enabled: tronBalances !== undefined && rate !== undefined } ); }; diff --git a/packages/uikit/src/state/home.ts b/packages/uikit/src/state/home.ts index e1c21387b..6e54d2255 100644 --- a/packages/uikit/src/state/home.ts +++ b/packages/uikit/src/state/home.ts @@ -4,29 +4,75 @@ import BigNumber from 'bignumber.js'; import { useMemo } from 'react'; import { AssetData } from '../components/home/Jettons'; import { useJettonList } from './jetton'; -import { useActiveWallet, useWalletAccountInfo } from './wallet'; +import { useActiveTonWalletConfig, useActiveWallet, useWalletAccountInfo } from './wallet'; import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; -import { TON_ASSET } from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { + TON_ASSET, + TRON_TRX_ASSET, + TRON_USDT_ASSET +} from '@tonkeeper/core/dist/entries/crypto/asset/constants'; import { jettonToTonAssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; import { packAssetId } from '@tonkeeper/core/dist/entries/crypto/asset/basic-asset'; +import { useTronBalances } from './tron/tron'; +import { assertUnreachable } from '@tonkeeper/core/dist/utils/types'; export const useAssets = () => { const wallet = useActiveWallet(); const { data: info, error, isFetching: isAccountLoading } = useWalletAccountInfo(); const { data: jettons, error: jettonError, isFetching: isJettonLoading } = useJettonList(); + const { data: tronBalances } = useTronBalances(); const assets = useMemo(() => { - if (!info || !jettons) return undefined; + if (!info || !jettons || tronBalances === undefined) return undefined; return { ton: { info, jettons: jettons ?? { balances: [] } }, - tron: { balances: [] } + tron: tronBalances }; - }, [info, jettons, wallet]); + }, [info, jettons, wallet, tronBalances]); return [assets, error, isJettonLoading || isAccountLoading, jettonError] as const; }; +export const useAllChainsAssets = () => { + const [assets, error, _, jettonError] = useAssets(); + const { data: config } = useActiveTonWalletConfig(); + + return useMemo<{ assets: AssetAmount[] | undefined; error: Error | undefined }>(() => { + if (!assets || !config) return { assets: undefined, error: undefined }; + + const result: AssetAmount[] = [ + new AssetAmount({ asset: TON_ASSET, weiAmount: assets.ton.info.balance }) + ]; + config.pinnedTokens.forEach(p => { + if (p === TRON_USDT_ASSET.address && assets.tron) { + result.push(assets.tron.usdt); + } else { + const jetton = assets.ton.jettons.balances.find(i => i.jetton.address === p); + if (jetton) { + result.push(jettonToTonAssetAmount(jetton)); + } + } + }); + + if ( + assets.tron && + !result.some(i => i.asset.id === TRON_USDT_ASSET.id) && + !config.hiddenTokens.includes(TRON_USDT_ASSET.address) + ) { + result.push(assets.tron.usdt); + } + + assets.ton.jettons.balances + .filter(b => !config.pinnedTokens.includes(b.jetton.address)) + .forEach(b => { + result.push(jettonToTonAssetAmount(b)); + }); + + return { assets: result, error: error ?? jettonError ?? undefined }; + }, [assets, error, jettonError, config]); +}; + export const useAssetWeiBalance = (asset: AssetIdentification) => { const [assets] = useAssets(); @@ -43,11 +89,17 @@ export const useAssetWeiBalance = (asset: AssetIdentification) => { return new BigNumber( assets.ton.jettons.balances.find(i => i.jetton.address === jAddress)?.balance || '0' ); + } else if (asset.blockchain === BLOCKCHAIN_NAME.TRON) { + if (asset.address === TRON_USDT_ASSET.address) { + return assets.tron?.usdt.weiAmount || new BigNumber(0); + } else if (asset.address === TRON_TRX_ASSET.address) { + return assets.tron?.trx.weiAmount || new BigNumber(0); + } else { + throw new Error('Unexpected asset'); + } + } else { + assertUnreachable(asset); } - - return new BigNumber( - assets.tron.balances.find(i => i.token.address === asset.address)?.weiAmount || '0' - ); }, [assets, asset]); }; diff --git a/packages/uikit/src/state/jetton.ts b/packages/uikit/src/state/jetton.ts index c8bbc8404..e7c3e7713 100644 --- a/packages/uikit/src/state/jetton.ts +++ b/packages/uikit/src/state/jetton.ts @@ -136,11 +136,11 @@ export const useTogglePinJettonMutation = () => { const wallet = useActiveWallet(); const network = useActiveTonNetwork(); - return useMutation( - async ({ config, jetton }) => { - const pinnedTokens = config.pinnedTokens.includes(jetton.jetton.address) - ? config.pinnedTokens.filter(item => item !== jetton.jetton.address) - : config.pinnedTokens.concat([jetton.jetton.address]); + return useMutation( + async ({ config, jettonAddress }) => { + const pinnedTokens = config.pinnedTokens.includes(jettonAddress) + ? config.pinnedTokens.filter(item => item !== jettonAddress) + : config.pinnedTokens.concat([jettonAddress]); const newConfig = { ...config, @@ -179,23 +179,19 @@ export const useToggleHideJettonMutation = () => { const wallet = useActiveWallet(); const network = useActiveTonNetwork(); - return useMutation( - async ({ config, jetton }) => { + return useMutation( + async ({ config, jettonAddress }) => { let newConfig: TonWalletConfig; - if (config.hiddenTokens.includes(jetton.jetton.address)) { - const hiddenTokens = config.hiddenTokens.filter( - item => item !== jetton.jetton.address - ); + if (config.hiddenTokens.includes(jettonAddress)) { + const hiddenTokens = config.hiddenTokens.filter(item => item !== jettonAddress); newConfig = { ...config, hiddenTokens }; } else { - const hiddenTokens = config.hiddenTokens.concat([jetton.jetton.address]); - const pinnedTokens = config.pinnedTokens.filter( - item => item !== jetton.jetton.address - ); + const hiddenTokens = config.hiddenTokens.concat([jettonAddress]); + const pinnedTokens = config.pinnedTokens.filter(item => item !== jettonAddress); newConfig = { ...config, hiddenTokens, diff --git a/packages/uikit/src/state/mixedActivity.ts b/packages/uikit/src/state/mixedActivity.ts deleted file mode 100644 index b49f823d5..000000000 --- a/packages/uikit/src/state/mixedActivity.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { InfiniteData } from '@tanstack/react-query'; -import { AccountEvent, AccountEvents } from '@tonkeeper/core/dist/tonApiV2'; -import { TronEvent, TronEvents } from '@tonkeeper/core/dist/tronApi'; -import { GenericActivity, groupGenericActivity } from './activity'; - -export interface TronActivity { - kind: 'tron'; - event: TronEvent; -} - -export interface TonActivity { - kind: 'ton'; - event: AccountEvent; -} - -const cutOffTronEvents = ( - tonEvents: InfiniteData | undefined, - tronEvents: InfiniteData -): InfiniteData => { - if (!tonEvents || !tonEvents.pages.length) { - return tronEvents; - } - - if (tonEvents.pageParams.length) { - const pastPageParam = tonEvents.pageParams[tonEvents.pageParams.length - 1]; - if (pastPageParam === undefined || pastPageParam === 0) { - return tronEvents; - } - } - - const { events } = tonEvents.pages[tonEvents.pages.length - 1]; - if (!events.length) return tronEvents; - - const lastEvents = events[events.length - 1]; - - const lastTime = lastEvents.timestamp * 1000; - - return { - pageParams: tronEvents.pageParams, - pages: tronEvents.pages.map(page => { - return { ...page, events: page.events.filter(event => event.timestamp > lastTime) }; - }) - }; -}; - -export type MixedActivity = TronActivity | TonActivity; - -export const getMixedActivity = ( - tonEvents: InfiniteData | undefined, - tronEvents: InfiniteData | undefined -) => { - const activity: GenericActivity[] = []; - - if (tonEvents) { - tonEvents.pages.forEach(page => { - const tonActivity: GenericActivity[] = page.events.map(event => ({ - timestamp: event.timestamp * 1000, - key: event.eventId, - event: { kind: 'ton', event } - })); - activity.push(...tonActivity); - }); - } - - if (tronEvents) { - const events = cutOffTronEvents(tonEvents, tronEvents); - events.pages.forEach(page => { - const tronActivity: GenericActivity[] = page.events.map(event => ({ - timestamp: event.timestamp, - key: `${event.txHash}-${event.actions.map(item => item.type).join('-')}`, - event: { kind: 'tron', event } - })); - activity.push(...tronActivity); - }); - } - - return activity; -}; - -export const getMixedActivityGroups = ( - tonEvents: InfiniteData | undefined, - tronEvents: InfiniteData | undefined -) => { - return groupGenericActivity(getMixedActivity(tonEvents, tronEvents)); -}; diff --git a/packages/uikit/src/state/mnemonic.ts b/packages/uikit/src/state/mnemonic.ts index e18578cc1..3fdba25e0 100644 --- a/packages/uikit/src/state/mnemonic.ts +++ b/packages/uikit/src/state/mnemonic.ts @@ -2,9 +2,14 @@ import { TonKeychainRoot } from '@ton-keychain/core'; import { Cell } from '@ton/core'; import { sha256_sync, sign } from '@ton/crypto'; import { IAppSdk } from '@tonkeeper/core/dist/AppSdk'; -import { AccountId, isMnemonicAndPassword } from '@tonkeeper/core/dist/entries/account'; +import { + AccountId, + isAccountTronCompatible, + isMnemonicAndPassword, + Account +} from '@tonkeeper/core/dist/entries/account'; import { AuthPassword, MnemonicType } from '@tonkeeper/core/dist/entries/password'; -import { CellSigner, Signer } from '@tonkeeper/core/dist/entries/signer'; +import { CellSigner, Signer, TronSigner } from '@tonkeeper/core/dist/entries/signer'; import { TonWalletStandard, WalletId } from '@tonkeeper/core/dist/entries/wallet'; import { accountsStorage } from '@tonkeeper/core/dist/service/accountsStorage'; import { KeystoneMessageType } from '@tonkeeper/core/dist/service/keystone/types'; @@ -30,6 +35,10 @@ import { useAppSdk } from '../hooks/appSdk'; import { useCallback } from 'react'; import { useActiveAccount } from './wallet'; import { useCheckTouchId } from './password'; +import { tonMnemonicToTronMnemonic } from '@tonkeeper/core/dist/service/walletService'; +import { TronWeb } from 'tronweb'; +import type { Transaction } from 'tronweb/src/types/Transaction'; +import { TronApi } from '@tonkeeper/core/dist/tronApi'; export const signTonConnectOver = ({ sdk, @@ -252,6 +261,68 @@ export const getSigner = async ( } }; +export const getTronSigner = ( + sdk: IAppSdk, + tronApi: TronApi, + account: Account, + checkTouchId: () => Promise +): TronSigner => { + try { + if (!isAccountTronCompatible(account)) { + throw new Error("Account doesn't support tron"); + } + + const wallet = account.activeTronWallet; + + if (!wallet) { + throw new Error('Wallet not found'); + } + + switch (account.type) { + case 'mam': { + return async (tx: Transaction) => { + const tonMnemonic = await getMAMWalletMnemonic( + sdk, + account.id, + wallet!.id, + checkTouchId + ); + const tronMnemonic = await tonMnemonicToTronMnemonic(tonMnemonic, 'ton'); + + const tronWeb = new TronWeb({ + fullHost: tronApi.baseURL, + privateKey: TronWeb.fromMnemonic(tronMnemonic.join(' ')).privateKey.slice(2) + }); + + return tronWeb.trx.sign(tx); + }; + } + case 'mnemonic': { + return async (tx: Transaction) => { + const tonMnemonic = await getAccountMnemonic(sdk, account.id, checkTouchId); + const tronMnemonic = await tonMnemonicToTronMnemonic( + tonMnemonic, + account.mnemonicType + ); + + const tronWeb = new TronWeb({ + fullHost: tronApi.baseURL, + privateKey: TronWeb.fromMnemonic(tronMnemonic.join(' ')).privateKey.slice(2) + }); + + return tronWeb.trx.sign(tx); + }; + } + default: { + assertUnreachable(account); + } + } + } catch (e) { + console.error(e); + throw e; + } +}; + export const getAccountMnemonic = async ( sdk: IAppSdk, accountId: AccountId, @@ -504,3 +575,14 @@ const pairLedgerByNotification = async ( } ); }; + +export const useGetActiveAccountMnemonic = () => { + const sdk = useAppSdk(); + const { mutateAsync: checkTouchId } = useCheckTouchId(); + const activeAccount = useActiveAccount(); + const accountId = activeAccount.id; + + return useCallback(async () => { + return getAccountMnemonic(sdk, accountId, checkTouchId); + }, [sdk, checkTouchId, accountId]); +}; diff --git a/packages/uikit/src/state/rates.ts b/packages/uikit/src/state/rates.ts index bcc143e8a..dac8d0743 100644 --- a/packages/uikit/src/state/rates.ts +++ b/packages/uikit/src/state/rates.ts @@ -10,6 +10,11 @@ import { useAppContext } from '../hooks/appContext'; import { formatFiatCurrency } from '../hooks/balance'; import { QueryKey } from '../libs/queryKey'; import { useActiveApi } from './wallet'; +import { + TON_USDT_ASSET, + TRON_USDT_ASSET +} from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { tonAssetAddressToString } from '@tonkeeper/core/dist/entries/crypto/asset/ton-asset'; export interface TokenRate { diff7d: string; @@ -90,6 +95,34 @@ export const useRate = (token: string) => { ); }; +export const useUSDTRate = () => { + const { tonApiV2 } = useActiveApi(); + const { fiat } = useAppContext(); + + return useQuery( + getRateKey(fiat, TRON_USDT_ASSET.address), + async () => { + const token = tonAssetAddressToString(TON_USDT_ASSET.address); + const value = await new RatesApi(tonApiV2).getRates({ + tokens: [token], + currencies: [fiat] + }); + + try { + const tokenRate = toTokenRate(value.rates[token], fiat); + if (!tokenRate || !tokenRate.prices) { + throw new Error(`Missing price for token: ${token}`); + } + + return tokenRate; + } catch (e) { + throw e; + } + }, + { retry: 0 } + ); +}; + export const useFormatFiat = (rate: TokenRate | undefined, tokenAmount: BigNumber.Value) => { const { fiat } = useAppContext(); diff --git a/packages/uikit/src/state/tron/tron.ts b/packages/uikit/src/state/tron/tron.ts index e59f84d9f..5cd4368df 100644 --- a/packages/uikit/src/state/tron/tron.ts +++ b/packages/uikit/src/state/tron/tron.ts @@ -1,76 +1,86 @@ import { useQuery } from '@tanstack/react-query'; -import { TronWalletState } from '@tonkeeper/core/dist/entries/wallet'; -import { TronApi, TronBalance, TronBalances, TronToken } from '@tonkeeper/core/dist/tronApi'; import { useAppContext } from '../../hooks/appContext'; -import { useAppSdk } from '../../hooks/appSdk'; import { QueryKey } from '../../libs/queryKey'; import { DefaultRefetchInterval } from '../tonendpoint'; -import { useActiveApi } from '../wallet'; +import { useActiveAccount, useActiveConfig } from '../wallet'; +import { useMemo } from 'react'; +import { isAccountTronCompatible } from '@tonkeeper/core/dist/entries/account'; +import { TronWallet } from '@tonkeeper/core/dist/entries/tron/tron-wallet'; +import { TronApi } from '@tonkeeper/core/dist/tronApi'; +import { AssetAmount } from '@tonkeeper/core/dist/entries/crypto/asset/asset-amount'; +import { + TRON_TRX_ASSET, + TRON_USDT_ASSET +} from '@tonkeeper/core/dist/entries/crypto/asset/constants'; +import { TronAsset } from '@tonkeeper/core/dist/entries/crypto/asset/tron-asset'; +import { useDevSettings } from '../dev'; + +export const useTronApi = () => { + const appContext = useAppContext(); + const apiKey = appContext.env?.tronApiKey; + + const apiUrl = appContext.mainnetConfig.tron_api_url || 'https://api.trongrid.io'; + + return useMemo(() => new TronApi(apiUrl, apiKey), [apiKey, apiUrl]); +}; -enum TronKeys { - state, - balance -} +export const useIsTronEnabledGlobally = () => { + const { data: devSettings } = useDevSettings(); + const config = useActiveConfig(); -export const useTronWalletState = (enabled = true) => { - const { tronApi } = useActiveApi(); - /* const client = useQueryClient(); - const { mutateAsync: checkTouchId } = useCheckTouchId();*/ + if (config.flags?.disable_tron) { + return false; + } - return useQuery( - [TronKeys.state], - async () => { - return undefined; - /* if (wallet.tron) { - return getTronWalletState(wallet.tron, wallet.network); - } + return devSettings?.tronEnabled; +}; - const mnemonic = await getMnemonic(sdk, wallet.publicKey, checkTouchId); - const tron = await importTronWallet(sdk.storage, tronApi, wallet, mnemonic); +export const useCanUseTronForActiveWallet = () => { + const isTronEnabled = useIsTronEnabledGlobally(); + const account = useActiveAccount(); - const result = getTronWalletState(tron, wallet.network); + if (!isTronEnabled) { + return false; + } - client.invalidateQueries([QueryKey.account, QueryKey.wallet]); - return result;*/ - }, - { enabled } - ); + return isAccountTronCompatible(account); }; -export const useTronTokens = () => { - const { tronApi } = useActiveApi(); - // const wallet = useWalletContext(); - return useQuery( - [QueryKey.tron, /*wallet.network,*/ TronKeys.balance], - async () => { - const sdk = new TronApi(tronApi); - const { tokens } = await sdk.getSettings(); - return tokens; - } - ); +export const useActiveTronWallet = (): TronWallet | undefined => { + const account = useActiveAccount(); + + if (isAccountTronCompatible(account)) { + return account.activeTronWallet; + } + + return undefined; }; +export type TronBalances = { trx: AssetAmount; usdt: AssetAmount } | null; + export const useTronBalances = () => { - const { tronApi } = useActiveApi(); - /*const wallet = useWalletContext();*/ + const tronApi = useTronApi(); + const activeWallet = useActiveTronWallet(); return useQuery( - [QueryKey.tron, /*wallet.network,*/ TronKeys.balance], + [QueryKey.tronAssets, activeWallet?.address], async () => { - return { balances: [] }; - /*const sdk = new TronApi(tronApi); - - if (wallet.tron) { - const { walletAddress } = getTronWalletState(wallet.tron, wallet.network); - return sdk.getWalletBalances({ - walletAddress - }); - } else { - // const { tokens } = await sdk.getSettings(); - return { - balances: [] // tokens.map(token => ({ token, weiAmount: '0' })) - }; - }*/ + if (!activeWallet) { + return null; + } + const balances = await tronApi.getBalances(activeWallet?.address); + + const trx = new AssetAmount({ + asset: TRON_TRX_ASSET, + weiAmount: balances.trx + }); + + const usdt = new AssetAmount({ + asset: TRON_USDT_ASSET, + weiAmount: balances.usdt + }); + + return { trx, usdt }; }, { refetchInterval: DefaultRefetchInterval, @@ -80,43 +90,3 @@ export const useTronBalances = () => { } ); }; - -export const useTronBalance = (tron: TronWalletState, address: string | undefined) => { - const { tronApi } = useActiveApi(); - // const wallet = useWalletContext(); - - return useQuery([QueryKey.tron, address], async () => { - if (!address) { - throw new Error('missing token address'); - } - const sdk = new TronApi(tronApi); - - const { balances } = await sdk.getWalletBalances({ - walletAddress: tron.walletAddress - }); - - const balance = balances.find(item => item.token.address === address); - if (!balance) { - throw new Error('missing token balance'); - } - - return balance; - }); -}; - -/* -export const useCleanUpTronStore = () => { - const wallet = useWalletContext(); - const sdk = useAppSdk(); - const client = useQueryClient(); - - return useMutation(async () => { - // eslint-disable-next-line unused-imports/no-unused-vars - const { tron, ...rest } = wallet; - if (wallet.tron) { - await setWalletState(sdk.storage, rest); - await client.invalidateQueries(); - } - }); -}; -*/ diff --git a/packages/uikit/src/state/wallet.ts b/packages/uikit/src/state/wallet.ts index fca3c98f5..48bfe636c 100644 --- a/packages/uikit/src/state/wallet.ts +++ b/packages/uikit/src/state/wallet.ts @@ -13,6 +13,7 @@ import { getNetworkByAccount, getWalletById, isAccountTonWalletStandard, + isAccountTronCompatible, isAccountVersionEditable, isMnemonicAndPassword } from '@tonkeeper/core/dist/entries/account'; @@ -40,7 +41,10 @@ import { getContextApiByNetwork, getStandardTonWalletVersions, getTonWalletStandard, - getWalletAddress + getWalletAddress, + mamAccountToMamAccountWithTron, + standardTonAccountToAccountWithTron, + tronWalletByTonMnemonic } from '@tonkeeper/core/dist/service/walletService'; import { Account as TonapiAccount, AccountsApi } from '@tonkeeper/core/dist/tonApiV2'; import { seeIfValidTonAddress } from '@tonkeeper/core/dist/utils/common'; @@ -49,13 +53,19 @@ import { useAppContext } from '../hooks/appContext'; import { useAppSdk } from '../hooks/appSdk'; import { useAccountsStorage } from '../hooks/useStorage'; import { anyOfKeysParts, QueryKey } from '../libs/queryKey'; -import { getAccountMnemonic, getPasswordByNotification } from './mnemonic'; +import { + getAccountMnemonic, + getPasswordByNotification, + useGetActiveAccountMnemonic +} from './mnemonic'; import { useCheckTouchId } from './password'; import { seeIfMnemonicValid } from '@tonkeeper/core/dist/service/mnemonicService'; import { useAccountsStateQuery, useAccountsState } from './accounts'; import { useGlobalPreferences } from './global-preferences'; import { useDeleteFolder } from './folders'; import { useRemoveAccountTwoFAData } from './two-fa'; +import { assertUnreachable } from '@tonkeeper/core/dist/utils/types'; +import { useIsTronEnabledGlobally } from './tron/tron'; export { useAccountsStateQuery, useAccountsState }; @@ -207,6 +217,7 @@ export const useCreateMAMAccountDerivation = () => { const appContext = useAppContext(); const network = useActiveTonNetwork(); const { mutateAsync: checkTouchId } = useCheckTouchId(); + const isTronEnabled = useIsTronEnabledGlobally(); return useMutation(async ({ accountId }) => { const account = await storage.getAccount(accountId); @@ -239,7 +250,8 @@ export const useCreateMAMAccountDerivation = () => { emoji: account.emoji, index: newDerivationIndex, tonWallets, - activeTonWalletId: tonWallets[0].id + activeTonWalletId: tonWallets[0].id, + tronWallet: isTronEnabled ? await tronWalletByTonMnemonic(mnemonic) : undefined }); await storage.updateAccountInState(account); @@ -465,11 +477,43 @@ export const useCreateAccountTestnet = () => { }); }; +export const useAddTronToAccount = () => { + const getMnemonic = useGetActiveAccountMnemonic(); + const activeAccount = useActiveAccount(); + const accountsStorage = useAccountsStorage(); + const client = useQueryClient(); + + return useMutation(async () => { + if (!isAccountTronCompatible(activeAccount)) { + throw new Error('Account is not tron compatible'); + } + + let updatedAccount: Account; + switch (activeAccount.type) { + case 'mnemonic': + updatedAccount = await standardTonAccountToAccountWithTron( + activeAccount, + getMnemonic + ); + break; + case 'mam': + updatedAccount = await mamAccountToMamAccountWithTron(activeAccount, getMnemonic); + break; + default: + assertUnreachable(activeAccount); + } + + await accountsStorage.updateAccountInState(updatedAccount); + await client.invalidateQueries(anyOfKeysParts(QueryKey.account, updatedAccount.id)); + }); +}; + export const useCreateAccountMnemonic = () => { const sdk = useAppSdk(); const context = useAppContext(); const { mutateAsync: addAccountToState } = useAddAccountToStateMutation(); const { mutateAsync: selectAccountMutation } = useMutateActiveAccount(); + const isTronEnabled = useIsTronEnabledGlobally(); return useMutation< AccountTonMnemonic, @@ -497,7 +541,8 @@ export const useCreateAccountMnemonic = () => { auth: { kind: 'keychain' }, - versions + versions, + generateTronWallet: isTronEnabled } ); @@ -528,7 +573,8 @@ export const useCreateAccountMnemonic = () => { kind: 'password', encryptedMnemonic }, - versions + versions, + generateTronWallet: isTronEnabled } ); @@ -545,6 +591,7 @@ export const useCreateAccountMAM = () => { const context = useAppContext(); const { mutateAsync: addAccountToState } = useAddAccountToStateMutation(); const { mutateAsync: selectAccountMutation } = useMutateActiveAccount(); + const isTronEnabled = useIsTronEnabledGlobally(); return useMutation< AccountMAM, @@ -561,7 +608,8 @@ export const useCreateAccountMAM = () => { selectedDerivations, auth: { kind: 'keychain' - } + }, + generateTronWallet: isTronEnabled }); await sdk.keychain.setPassword( @@ -586,7 +634,8 @@ export const useCreateAccountMAM = () => { auth: { kind: 'password', encryptedMnemonic - } + }, + generateTronWallet: isTronEnabled }); await addAccountToState(account); diff --git a/yarn.lock b/yarn.lock index 9820b601b..998b7a50c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26,10 +26,10 @@ __metadata: languageName: node linkType: hard -"@adraffy/ens-normalize@npm:1.10.0": - version: 1.10.0 - resolution: "@adraffy/ens-normalize@npm:1.10.0" - checksum: 10/5cdb5d2a9c9f8c0a71a7bb830967da0069cae1f1235cd41ae11147e4000f368f6958386e622cd4d52bf45c1ed3f8275056b387cba28902b83354e40ff323ecde +"@adraffy/ens-normalize@npm:1.10.1": + version: 1.10.1 + resolution: "@adraffy/ens-normalize@npm:1.10.1" + checksum: 10/4cb938c4abb88a346d50cb0ea44243ab3574330c81d4f5aaaf9dfee584b96189d0faa404de0fcbef5a1b73909ea4ebc3e63d84bd23f9949e5c8d4085207a5091 languageName: node linkType: hard @@ -5115,6 +5115,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:^1.3.3": + version: 1.6.1 + resolution: "@noble/hashes@npm:1.6.1" + checksum: 10/74d9ad7b1437a22ba3b877584add3367587fbf818113152f293025d20d425aa74c191d18d434797312f2270458bc9ab3241c34d14ec6115fb16438b3248f631f + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -7313,6 +7320,18 @@ __metadata: languageName: node linkType: hard +"@ton-keychain/trx@npm:^0.0.7": + version: 0.0.7 + resolution: "@ton-keychain/trx@npm:0.0.7" + dependencies: + "@noble/hashes": "npm:^1.3.3" + "@scure/bip39": "npm:^1.2.2" + "@ton-keychain/core": "npm:^0.0.4" + ethers: "npm:^6.9.2" + checksum: 10/2a0e93f610218104fdfae7e7bf0efbf7093d380c0de1555d98c603e1844013c3048981227774a8254db1b94720d2e8c4cc9860c5f1d37088b4c494256ac7d29f + languageName: node + linkType: hard + "@ton/core@npm:0.56.0": version: 0.56.0 resolution: "@ton/core@npm:0.56.0" @@ -7380,14 +7399,16 @@ __metadata: "@ledgerhq/hw-transport-webusb": "npm:^6.28.6" "@ton-community/ton-ledger": "npm:^7.2.0-pre.3" "@ton-keychain/core": "npm:^0.0.4" + "@ton-keychain/trx": "npm:^0.0.7" "@ton/core": "npm:0.56.0" "@ton/crypto": "npm:3.2.0" "@ton/ton": "npm:^15.1.0" bignumber.js: "npm:^9.1.1" bip39: "npm:^3.1.0" - ethers: "npm:^6.6.5" + ethers: "npm:^6.13.4" npm-run-all: "npm:^4.1.5" query-string: "npm:^8.1.0" + tronweb: "npm:^6.0.0" tweetnacl: "npm:^1.0.3" typescript: "npm:^4.9.4" yarn-run-all: "npm:^3.1.1" @@ -7639,6 +7660,7 @@ __metadata: "@tanstack/react-query": "npm:4.3.4" "@tanstack/react-virtual": "npm:^3.8.3" "@ton-keychain/core": "npm:^0.0.4" + "@ton-keychain/trx": "npm:^0.0.7" "@ton/core": "npm:0.56.0" "@ton/crypto": "npm:3.2.0" "@tonkeeper/core": "npm:0.1.0" @@ -7661,6 +7683,7 @@ __metadata: country-flag-icons: "npm:^1.5.7" country-list-js: "npm:^3.1.7" dayjs: "npm:^1.11.10" + ethers: "npm:^6.13.4" framer-motion: "npm:^11.3.28" react: "npm:^18.2.0" react-beautiful-dnd: "npm:^13.1.1" @@ -7679,6 +7702,7 @@ __metadata: slick-carousel: "npm:^1.8.1" storybook-addon-turbo-build: "npm:^1.1.0" styled-components: "npm:^6.1.1" + tronweb: "npm:^6.0.0" tweetnacl: "npm:^1.0.3" typescript: "npm:^4.9.4" uuid: "npm:^9.0.0" @@ -7782,6 +7806,13 @@ __metadata: languageName: node linkType: hard +"@tronweb3/google-protobuf@npm:^3.21.2": + version: 3.21.2 + resolution: "@tronweb3/google-protobuf@npm:3.21.2" + checksum: 10/00d9344e0d32af7af3e55087ffb60b8663ce2c180e8450e5e76b72e5d7309dc2b6ce9999dece7aa48050b626e6f50622dc2614d80e5bb4bcd259512363aaa048 + languageName: node + linkType: hard + "@trysound/sax@npm:0.2.0": version: 0.2.0 resolution: "@trysound/sax@npm:0.2.0" @@ -8366,10 +8397,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.15.13": - version: 18.15.13 - resolution: "@types/node@npm:18.15.13" - checksum: 10/b9bbe923573797ef7c5fd2641a6793489e25d9369c32aeadcaa5c7c175c85b42eb12d6fe173f6781ab6f42eaa1ebd9576a419eeaa2a1ec810094adb8adaa9a54 +"@types/node@npm:22.7.5": + version: 22.7.5 + resolution: "@types/node@npm:22.7.5" + dependencies: + undici-types: "npm:~6.19.2" + checksum: 10/e8ba102f8c1aa7623787d625389be68d64e54fcbb76d41f6c2c64e8cf4c9f4a2370e7ef5e5f1732f3c57529d3d26afdcb2edc0101c5e413a79081449825c57ac languageName: node linkType: hard @@ -10460,6 +10493,17 @@ __metadata: languageName: node linkType: hard +"axios@npm:^1.7.4": + version: 1.7.9 + resolution: "axios@npm:1.7.9" + dependencies: + follow-redirects: "npm:^1.15.6" + form-data: "npm:^4.0.0" + proxy-from-env: "npm:^1.1.0" + checksum: 10/b7a5f660ea53ba9c2a745bf5ad77ad8bf4f1338e13ccc3f9f09f810267d6c638c03dac88b55dae8dc98b79c57d2d6835be651d58d2af97c174f43d289a9fd007 + languageName: node + linkType: hard + "axobject-query@npm:^3.2.1": version: 3.2.1 resolution: "axobject-query@npm:3.2.1" @@ -15346,18 +15390,18 @@ __metadata: languageName: node linkType: hard -"ethers@npm:^6.6.5": - version: 6.9.0 - resolution: "ethers@npm:6.9.0" +"ethers@npm:^6.13.1, ethers@npm:^6.13.4, ethers@npm:^6.9.2": + version: 6.13.4 + resolution: "ethers@npm:6.13.4" dependencies: - "@adraffy/ens-normalize": "npm:1.10.0" + "@adraffy/ens-normalize": "npm:1.10.1" "@noble/curves": "npm:1.2.0" "@noble/hashes": "npm:1.3.2" - "@types/node": "npm:18.15.13" + "@types/node": "npm:22.7.5" aes-js: "npm:4.0.0-beta.5" - tslib: "npm:2.4.0" - ws: "npm:8.5.0" - checksum: 10/a48f268aa934900d90e34fd6b607db3d774ce58c73d249697f68398afa673b483fda19c6fa91eb91784fdfbaa07eb4c753b245c2123c5d2072f8dc1897cd556d + tslib: "npm:2.7.0" + ws: "npm:8.17.1" + checksum: 10/221192fed93f6b0553f3e5e72bfd667d676220577d34ff854f677e955d6f608e60636a9c08b5d54039c532a9b9b7056384f0d7019eb6e111d53175806f896ac6 languageName: node linkType: hard @@ -15376,6 +15420,13 @@ __metadata: languageName: node linkType: hard +"eventemitter3@npm:^3.1.0": + version: 3.1.2 + resolution: "eventemitter3@npm:3.1.2" + checksum: 10/e2886001beb52cd2fe47d2470fd6266b7c70bd3ac356c0041a7e64336ed57bb1fc9b07bc9043d34b39913488a8d81bfcde62d3af597974980aa01b50844d869b + languageName: node + linkType: hard + "eventemitter3@npm:^4.0.0, eventemitter3@npm:^4.0.1": version: 4.0.7 resolution: "eventemitter3@npm:4.0.7" @@ -27898,6 +27949,23 @@ __metadata: languageName: node linkType: hard +"tronweb@npm:^6.0.0": + version: 6.0.0 + resolution: "tronweb@npm:6.0.0" + dependencies: + "@babel/runtime": "npm:^7.0.0" + "@tronweb3/google-protobuf": "npm:^3.21.2" + axios: "npm:^1.7.4" + bignumber.js: "npm:^9.0.1" + ethereum-cryptography: "npm:^2.1.3" + ethers: "npm:^6.13.1" + eventemitter3: "npm:^3.1.0" + semver: "npm:^5.6.0" + validator: "npm:^13.7.0" + checksum: 10/93ae9d761273018150e90fb5f20f78df109a58cff031a3bc05f31370c05ca1b6c9b4a3bc1a39c24a59b3c12915243c577935d6d8ce2b4accf36c38bd81c29047 + languageName: node + linkType: hard + "trough@npm:^1.0.0": version: 1.0.5 resolution: "trough@npm:1.0.5" @@ -28018,10 +28086,10 @@ __metadata: languageName: node linkType: hard -"tslib@npm:2.4.0": - version: 2.4.0 - resolution: "tslib@npm:2.4.0" - checksum: 10/d8379e68b36caf082c1905ec25d17df8261e1d68ddc1abfd6c91158a064f6e4402039ae7c02cf4c81d12e3a2a2c7cd8ea2f57b233eb80136a2e3e7279daf2911 +"tslib@npm:2.7.0": + version: 2.7.0 + resolution: "tslib@npm:2.7.0" + checksum: 10/9a5b47ddac65874fa011c20ff76db69f97cf90c78cff5934799ab8894a5342db2d17b4e7613a087046bc1d133d21547ddff87ac558abeec31ffa929c88b7fce6 languageName: node linkType: hard @@ -28434,7 +28502,7 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~6.19.8": +"undici-types@npm:~6.19.2, undici-types@npm:~6.19.8": version: 6.19.8 resolution: "undici-types@npm:6.19.8" checksum: 10/cf0b48ed4fc99baf56584afa91aaffa5010c268b8842f62e02f752df209e3dea138b372a60a963b3b2576ed932f32329ce7ddb9cb5f27a6c83040d8cd74b7a70 @@ -29021,6 +29089,13 @@ __metadata: languageName: node linkType: hard +"validator@npm:^13.7.0": + version: 13.12.0 + resolution: "validator@npm:13.12.0" + checksum: 10/db6eb0725e2b67d60d30073ae8573982713b5903195d031dc3c7db7e82df8b74e8c13baef8e2106d146d979599fd61a06cde1fec5c148e4abd53d52817ff0fd9 + languageName: node + linkType: hard + "vary@npm:~1.1.2": version: 1.1.2 resolution: "vary@npm:1.1.2" @@ -30115,18 +30190,18 @@ __metadata: languageName: node linkType: hard -"ws@npm:8.5.0": - version: 8.5.0 - resolution: "ws@npm:8.5.0" +"ws@npm:8.17.1": + version: 8.17.1 + resolution: "ws@npm:8.17.1" peerDependencies: bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 + utf-8-validate: ">=5.0.2" peerDependenciesMeta: bufferutil: optional: true utf-8-validate: optional: true - checksum: 10/f0ee700970a0bf925b1ec213ca3691e84fb8b435a91461fe3caf52f58c6cec57c99ed5890fbf6978824c932641932019aafc55d864cad38ac32577496efd5d3a + checksum: 10/4264ae92c0b3e59c7e309001e93079b26937aab181835fb7af79f906b22cd33b6196d96556dafb4e985742dd401e99139572242e9847661fdbc96556b9e6902d languageName: node linkType: hard