diff --git a/README.md b/README.md index abecca92..6238ecf1 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ export default function App({ Component, pageProps }) { | Near | :x: | :x: | :x: | :x: | :x: | | Bitcoin | :x: | :x: | :x: | :x: | :x: | | Casper | :x: | :x: | :x: | :x: | :x: | -| Ton | :x: | :x: | :x: | :x: | :x: | +| Ton | :white_check_mark: | :x: | :x: | :x: | :x: | | Algorand | :x: | :x: | :x: | :x: | :x: | #### Hooks diff --git a/example-next/public/tonconnect-manifest.json b/example-next/public/tonconnect-manifest.json new file mode 100644 index 00000000..16c9a5e1 --- /dev/null +++ b/example-next/public/tonconnect-manifest.json @@ -0,0 +1,5 @@ +{ + "url": "http://localhost:3000", + "name": "Tangled3 Next Example", + "iconUrl": "https://ton.vote/logo.png" +} diff --git a/example-next/src/components/Providers.tsx b/example-next/src/components/Providers.tsx index 7fc6721a..81a4ead0 100644 --- a/example-next/src/components/Providers.tsx +++ b/example-next/src/components/Providers.tsx @@ -24,6 +24,8 @@ const Providers = ({ children }: { children: ReactNode }) => { }, projectId: '41980758771052df3f01be0a46f172a5', + + tonconnectManifestUrl: `${window.location.origin}/tonconnect-manifest.json`, }} > {children} diff --git a/example-next/src/components/Tokens.tsx b/example-next/src/components/Tokens.tsx index f7a980af..ffd6b523 100644 --- a/example-next/src/components/Tokens.tsx +++ b/example-next/src/components/Tokens.tsx @@ -135,11 +135,18 @@ const tokens: TokenMetadata[] = [ chainId: 'alephZero', }, { - address: '5CMdxZDuprVZKnw6tEWjhEtK17Z52PUJo2dj1JLdyKeuUcfH', + address: ETH_ADDRESS, + decimals: 9, + name: 'Toncoin', + symbol: 'TON', + chainId: '-239', + }, + { + address: 'EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs', decimals: 6, - name: 'USDT', + name: 'Tether USD', symbol: 'USDT', - chainId: 'alephZero', + chainId: '-239', }, ]; diff --git a/packages/react/README.md b/packages/react/README.md index 43130b6a..99774ace 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -72,7 +72,7 @@ export default function App({ Component, pageProps }) { | Near | :x: | :x: | :x: | :x: | :x: | | Bitcoin | :x: | :x: | :x: | :x: | :x: | | Casper | :x: | :x: | :x: | :x: | :x: | -| Ton | :x: | :x: | :x: | :x: | :x: | +| Ton | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | Algorand | :x: | :x: | :x: | :x: | :x: | #### Hooks diff --git a/packages/react/package.json b/packages/react/package.json index 0d2efe9e..7259b5b8 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -41,6 +41,8 @@ "@solana/web3.js": "^1.94.0", "@tangled3/solana-react": "workspace:*", "@tanstack/react-query": "^5.44.0", + "@ton/ton": "^15.0.0", + "@tonconnect/ui-react": "^2.0.9", "@tronweb3/tronwallet-abstract-adapter": "^1.1.6", "@tronweb3/tronwallet-adapters": "^1.2.1", "@wagmi/core": "^2.11.0", diff --git a/packages/react/src/actions/getToken.ts b/packages/react/src/actions/getToken.ts index 23e65807..fa91f6dc 100644 --- a/packages/react/src/actions/getToken.ts +++ b/packages/react/src/actions/getToken.ts @@ -9,6 +9,7 @@ import { areTokensEqual } from '../utils/index.js'; import { getAlephZeroTokenBalanceAndAllowance, getAlephZeroTokenMetadata } from './alephZero/getAlephZeroToken.js'; import { getEVMTokenBalanceAndAllowance, getEVMTokenMetadata } from './evm/getEVMToken.js'; import { getSolanaTokenBalanceAndAllowance } from './solana/getSolanaToken.js'; +import { getTonTokenBalanceAndAllowance, getTonTokenMetadata } from './ton/getTonToken.js'; /** * Get token metadata @@ -83,6 +84,17 @@ export const getTokenMetadata = async ({ token, chain, config }: GetTokenMetadat }; } + if (chain.type === 'ton') { + if (areTokensEqual(token, ETH_ADDRESS)) { + return { ...chain.nativeCurrency, address: ETH_ADDRESS, chainId: chain.id }; + } + const res = await getTonTokenMetadata({ token, chainId: chain.id }); + return { + ...res, + chainId: chain.id, + }; + } + throw new Error('Chain type not supported'); }; @@ -177,5 +189,14 @@ export const getTokenBalanceAndAllowance = (async (params) => { }); } + if (chain.type === 'ton') { + return getTonTokenBalanceAndAllowance({ + account, + token, + spender, + config, + }); + } + throw new Error('Chain type not supported'); }) as GetTokenBalanceAndAllowanceFunction; diff --git a/packages/react/src/actions/sendTransaction.ts b/packages/react/src/actions/sendTransaction.ts index 8f96ab7a..1db89267 100644 --- a/packages/react/src/actions/sendTransaction.ts +++ b/packages/react/src/actions/sendTransaction.ts @@ -1,5 +1,7 @@ import { Signer, SubmittableExtrinsic } from '@polkadot/api/types'; import { VersionedTransaction as SolanaVersionedTransaction } from '@solana/web3.js'; +import { Cell } from '@ton/ton'; +import { CHAIN } from '@tonconnect/ui-react'; import { sendTransaction as sendEVMTransaction } from '@wagmi/core'; import { Address as EVMAddress } from 'viem'; import { ChainData, ChainType, ConnectionOrConfig } from '../types/index.js'; @@ -29,7 +31,16 @@ type TransactionArgs = CType extends 'evm' | 'tron' ? { submittableExtrinsic: SubmittableExtrinsic<'promise' | 'rxjs'>; } - : never; + : CType extends 'ton' + ? { + tonArgs: { + validUntil: number; // transaction deadline in unix epoch seconds. + network?: CHAIN; // (MAINNET: "-239" & TESTNET: "-3") + payload?: string; + stateInit?: string; + }; + } + : never; type SendTransactionReturnType = C extends 'alephZero' ? { @@ -136,5 +147,38 @@ export const sendTransactionToChain = (async ({ chain, to, from, value, args, co }; } + if (chain.type === 'ton') { + const { tonArgs } = args as TransactionArgs<'ton'>; + const messages: Array<{ + address: string; + amount: string; + payload?: string; + stateInit?: string; + }> = [ + { + address: to, + amount: value.toString(), + payload: tonArgs.payload, + stateInit: tonArgs.stateInit, + }, + ]; + const transaction = { + from, + messages, + network: tonArgs.network, + validUntil: tonArgs.validUntil, + }; + + const walletConnector = config.connector as WalletInstance<'ton'>; + // send transaction to TON chain + const tx = await walletConnector.sendTransaction(transaction); + + const cell = Cell.fromBase64(tx.boc); + const buffer = cell.hash(); + const hashHex = buffer.toString('hex'); + + return { txHash: hashHex }; + } + throw new Error('Chain not supported'); }) as SendTransactionToChainFunction; diff --git a/packages/react/src/actions/ton/getTonClient.ts b/packages/react/src/actions/ton/getTonClient.ts new file mode 100644 index 00000000..c98c2eb5 --- /dev/null +++ b/packages/react/src/actions/ton/getTonClient.ts @@ -0,0 +1,9 @@ +import { TonClient } from '@ton/ton'; +import { OtherChainData } from '../../types/index.js'; + +export const getTonClient = (chain: OtherChainData<'ton'>) => { + const tonClient = new TonClient({ + endpoint: chain.rpcUrls.default.http[0] ?? '', + }); + return tonClient; +}; diff --git a/packages/react/src/actions/ton/getTonToken.ts b/packages/react/src/actions/ton/getTonToken.ts new file mode 100644 index 00000000..84ce7899 --- /dev/null +++ b/packages/react/src/actions/ton/getTonToken.ts @@ -0,0 +1,54 @@ +import { Address, JettonMaster } from '@ton/ton'; +import { CHAIN } from '@tonconnect/ui-react'; +import { ChainId, ConnectionOrConfig } from '../../types/index.js'; + +export const getTonTokenMetadata = async ({ token, chainId }: { token: string; chainId: ChainId }) => { + const data = await ( + await fetch( + chainId === CHAIN.MAINNET + ? `https://toncenter.com/api/v3/jetton/masters?address=${token}&limit=128&offset=0` + : `https://testnet.toncenter.com/api/v3/jetton/masters?address=${token}&limit=128&offset=0`, + ) + ).json(); + + const jettonContent = data.jetton_masters[0].jetton_content; + + return { + symbol: jettonContent.symbol ?? 'USDT', + name: jettonContent.name ?? 'Tether USD', + decimals: Number(jettonContent.decimals), + address: jettonContent.address ?? token, + }; +}; + +export const getTonTokenBalanceAndAllowance = async ({ + account, + token, + config, +}: { + account: string; + token: string; + spender: string | undefined; + config: ConnectionOrConfig; +}) => { + let balance = 0n; + const allowance = 0n; + + try { + const jettonMaster = new JettonMaster(Address.parse(token)); + const contractProvider = config.tonClient.provider(Address.parse(token)); + + const walletAddress = await jettonMaster.getWalletAddress(contractProvider, Address.parse(account)); + + const walletAddressContractCallResult = await config.tonClient.runMethod(walletAddress, 'get_wallet_data'); + const accountBalance = walletAddressContractCallResult.stack.readNumber(); + balance = BigInt(accountBalance); + } catch (error) { + console.error('error - ', error); + } + + return { + balance, + allowance, + }; +}; diff --git a/packages/react/src/actions/waitForTransaction.ts b/packages/react/src/actions/waitForTransaction.ts index 90d3bc8a..ae319b2b 100644 --- a/packages/react/src/actions/waitForTransaction.ts +++ b/packages/react/src/actions/waitForTransaction.ts @@ -1,3 +1,4 @@ +import { Address } from '@ton/ton'; import { waitForTransactionReceipt } from '@wagmi/core'; import { ReplacementReturnType } from 'viem'; import { ChainData, ChainType, ConnectionOrConfig, TransactionReceipt } from '../types/index.js'; @@ -19,7 +20,12 @@ export type WatchTransactionOverrides = DefaultOverrides & ? { maxSupportedTransactionVersion: number; } - : any); + : C extends 'ton' + ? { + accountAddress: string; + lt: string; + } + : any); export type DefaultTransactionParams = { txHash: string; @@ -157,5 +163,25 @@ export const waitForTransaction = (async ({ chain, config, overrides, transactio return receipt; } + if (chain.type === 'ton') { + const _overrides = (overrides || {}) as WatchTransactionOverrides<'ton'>; + const { txHash } = transactionParams as TransactionParams<'ton'>; + + const receipt = await pollCallback( + async () => { + return await config.tonClient.getTransaction(Address.parse(_overrides.accountAddress), _overrides.lt, txHash); + }, + { + interval: overrides?.interval || DEFAULT_POLLING_INTERVAL, + timeout: overrides?.timeout, + }, + ); + + if (!receipt) { + throw new Error('Transaction not found'); + } + return receipt; + } + throw new Error('Chain not supported'); }) as WatchTransactionFunction; diff --git a/packages/react/src/chains/index.ts b/packages/react/src/chains/index.ts index c7fcd1b2..5d6329ee 100644 --- a/packages/react/src/chains/index.ts +++ b/packages/react/src/chains/index.ts @@ -4,5 +4,6 @@ export * from './evm.testnet.js'; export * from './solana.devnet.js'; export * from './solana.js'; export * from './solana.testnet.js'; +export * from './ton.js'; export * from './tron.js'; export * from './tron.shasta.js'; diff --git a/packages/react/src/chains/ton.testnet.ts b/packages/react/src/chains/ton.testnet.ts new file mode 100644 index 00000000..67d5937f --- /dev/null +++ b/packages/react/src/chains/ton.testnet.ts @@ -0,0 +1,25 @@ +import { CHAIN } from '@tonconnect/ui-react'; +import { ChainData } from '../types/index.js'; + +export const tonTestnet: ChainData = { + id: CHAIN.TESTNET, // '-3' + name: 'Ton Testnet', + type: 'ton', + nativeCurrency: { + name: 'TON', + symbol: 'TON', + decimals: 9, + }, + rpcUrls: { + default: { + http: ['https://testnet.toncenter.com/api/v2/jsonRPC'], + webSocket: [''], + }, + }, + blockExplorers: { + default: { + name: 'Tonscan', + url: 'https://testnet.tonscan.org/', + }, + }, +} as const; diff --git a/packages/react/src/chains/ton.ts b/packages/react/src/chains/ton.ts new file mode 100644 index 00000000..e80b40f4 --- /dev/null +++ b/packages/react/src/chains/ton.ts @@ -0,0 +1,25 @@ +import { CHAIN } from '@tonconnect/ui-react'; +import { OtherChainData } from '../types/index.js'; + +export const tonMainnet: OtherChainData<'ton'> = { + id: CHAIN.MAINNET, // '-239' + name: 'Ton', + type: 'ton', + nativeCurrency: { + name: 'TON', + symbol: 'TON', + decimals: 9, + }, + rpcUrls: { + default: { + http: ['https://toncenter.com/api/v2/jsonRPC'], + webSocket: [''], + }, + }, + blockExplorers: { + default: { + name: 'Tonscan', + url: 'https://tonscan.org/', + }, + }, +} as const; diff --git a/packages/react/src/connectors/ton/connector.ts b/packages/react/src/connectors/ton/connector.ts new file mode 100644 index 00000000..9e36d268 --- /dev/null +++ b/packages/react/src/connectors/ton/connector.ts @@ -0,0 +1,98 @@ +import { ConnectedWallet, TonConnectError, TonConnectUI, TonConnectUIError } from '@tonconnect/ui-react'; +import { ChainId } from '../../types/index.js'; +import { Wallet } from '../../types/wallet.js'; + +export const connectExternalWallet = async (connector: TonConnectUI): Promise => { + const abortController = new AbortController(); + + connector.openModal(); + + const unsubscribe = connector.onModalStateChange((state) => { + const { status, closeReason } = state; + if (status === 'opened') { + return; + } + + unsubscribe(); + if (closeReason === 'action-cancelled') { + abortController.abort(); + } + }); + + return await waitForWalletConnection({ + connector, + ignoreErrors: true, + signal: abortController.signal, + }); +}; + +export const waitForWalletConnection = async ({ + connector, + ignoreErrors, + signal, +}: { + connector: TonConnectUI; + ignoreErrors?: boolean; + signal?: AbortSignal; +}): Promise => { + return new Promise((resolve, reject) => { + if (signal && signal.aborted) { + console.info('[TON CONNECTION TRACKER] - connection was cancelled'); + return reject(new TonConnectUIError('Wallet was not connected')); + } + + const statusChangeHandler = async (wallet: ConnectedWallet | null): Promise => { + if (!wallet) { + console.info('[TON CONNECTION TRACKER] - connection was cancelled'); + + if (ignoreErrors) return; + + unsubscribe(); + reject(new TonConnectUIError('Wallet was not connected')); + } else { + console.info('[TON CONNECTION TRACKER] - wallet connected'); + unsubscribe(); + resolve(wallet); + } + }; + + const OnErrorsHandler = (reason: TonConnectError): void => { + console.info('[TON CONNECTION TRACKER] - connection error', reason.message); + + if (ignoreErrors) return; + + unsubscribe(); + reject(reason); + }; + + const unsubscribe = connector.onStatusChange( + (wallet: ConnectedWallet | null) => statusChangeHandler(wallet), + (reason: TonConnectError) => OnErrorsHandler(reason), + ); + + if (signal) { + signal.addEventListener( + 'abort', + (): void => { + unsubscribe(); + reject(new TonConnectUIError('Wallet was not connected')); + }, + { once: true }, + ); + } + }); +}; + +export const createTonWalletInstance = ( + connectedTonWallet: { + account: string | null; + chainId: ChainId | undefined; + }, + walletInstance: Wallet, +) => ({ + ...walletInstance, + // @ts-expect-error - adapter does not exist on WalletInstance of type Wallet + id: connectedTonWallet.adapter.wallet.appName, + // @ts-expect-error - adapter does not exist on WalletInstance of type Wallet + name: connectedTonWallet.adapter.wallet.name, +}); diff --git a/packages/react/src/constants/index.ts b/packages/react/src/constants/index.ts index 4562aee4..a84acc1b 100644 --- a/packages/react/src/constants/index.ts +++ b/packages/react/src/constants/index.ts @@ -17,11 +17,13 @@ import { polygonZkEvm, scroll, solana, + tonMainnet, tronMainnet, zkSync, } from '../chains/index.js'; import { solanaDevnet } from '../chains/solana.devnet.js'; import { solanaTestnet } from '../chains/solana.testnet.js'; +import { tonTestnet } from '../chains/ton.testnet.js'; import { tronShasta } from '../chains/tron.shasta.js'; import { Chain, ChainData, ChainId, ChainType } from '../types/index.js'; @@ -67,6 +69,10 @@ export const CHAIN_ID = { alephZero: 'alephZero', + // ton + tonMainnet: '-239', + tonTestnet: '-3', + // testnets // goerli: '5', // fuji: '43113', @@ -115,6 +121,10 @@ export const CHAIN_DATA: Record = { [CHAIN_ID.solanaTestnet]: solanaTestnet, [CHAIN_ID.solanaDevnet]: solanaDevnet, + // ton + [CHAIN_ID.tonMainnet]: tonMainnet, + [CHAIN_ID.tonTestnet]: tonTestnet, + // testnets // 5: goerli, // 80001: polygonMumbai, @@ -133,6 +143,7 @@ export const CHAIN_TYPE_LABEL: Record = { casper: 'Casper', alephZero: 'Aleph Zero', bitcoin: 'Bitcoin', + ton: 'Ton', } as const; /** diff --git a/packages/react/src/hooks/useConnect.ts b/packages/react/src/hooks/useConnect.ts index b95f430d..9503b3fa 100644 --- a/packages/react/src/hooks/useConnect.ts +++ b/packages/react/src/hooks/useConnect.ts @@ -3,10 +3,12 @@ import { useWallet as useSolanaWallet } from '@tangled3/solana-react'; import { useMutation } from '@tanstack/react-query'; import { useCallback } from 'react'; import { useConnect as useWagmiConnect } from 'wagmi'; +import { createTonWalletInstance } from '../connectors/ton/connector.js'; import { useWalletsStore } from '../store/Wallet.js'; import { ChainType } from '../types/index.js'; import { Wallet, WalletInstance } from '../types/wallet.js'; import { useAlephContext } from './useAlephContext.js'; +import { useTonContext } from './useTonContext.js'; import { useTronContext } from './useTronContext.js'; import { useWallets } from './useWallets.js'; @@ -18,6 +20,7 @@ export const useConnect = () => { const { connect: connectSolanaWallet } = useSolanaWallet(); const { connect: connectTronWallet } = useTronContext(); const { connect: connectAlephWallet } = useAlephContext(); + const { connect: connectTonWallet } = useTonContext(); const connectedWallets = useWalletsStore((state) => state.connectedWalletsByChain); const setCurrentWallet = useWalletsStore((state) => state.setCurrentWallet); @@ -49,13 +52,28 @@ export const useConnect = () => { await connectEVM({ connector: walletInstance.connector as WalletInstance<'evm'> }); } else if (params.chainType === 'alephZero') { await connectAlephWallet(walletInstance.name); + } else if (params.chainType === 'ton') { + const connectedTonWallet = await connectTonWallet(walletInstance.id); + if (walletInstance.id === 'ton-connect') { + const tonWalletInstance = createTonWalletInstance(connectedTonWallet, walletInstance); + return { walletInstance: tonWalletInstance, name: tonWalletInstance.name, id: tonWalletInstance.id }; + } } else { + // @ts-expect-error - connect does not exist on TonConnectUI await walletInstance.connector.connect(); } return { walletInstance, name: walletInstance.name, id: params.walletId }; }, - [connectAlephWallet, connectEVM, connectSolanaWallet, connectTronWallet, connectedWallets, wallets], + [ + connectAlephWallet, + connectEVM, + connectSolanaWallet, + connectTronWallet, + connectedWallets, + wallets, + connectTonWallet, + ], ); const mutation = useMutation({ diff --git a/packages/react/src/hooks/useConnectionOrConfig.ts b/packages/react/src/hooks/useConnectionOrConfig.ts index a0019a5c..fa166ef2 100644 --- a/packages/react/src/hooks/useConnectionOrConfig.ts +++ b/packages/react/src/hooks/useConnectionOrConfig.ts @@ -3,6 +3,7 @@ import { useMemo } from 'react'; import { useConfig as useWagmiConfig } from 'wagmi'; import { ConnectionOrConfig } from '../types/index.js'; import { useAlephStore } from './useAlephStore.js'; +import { useTonStore } from './useTonStore.js'; import { useTronStore } from './useTronStore.js'; /** @@ -13,6 +14,7 @@ export const useConnectionOrConfig = (): ConnectionOrConfig | undefined => { const { connection: solanaConnection } = useSolanaConnection(); const tronWeb = useTronStore((state) => state.tronweb); const alephZeroApi = useAlephStore((state) => state.api); + const tonClient = useTonStore((state) => state.tonClient); return useMemo(() => { if (!alephZeroApi) return undefined; @@ -22,6 +24,7 @@ export const useConnectionOrConfig = (): ConnectionOrConfig | undefined => { solanaConnection, tronWeb, alephZeroApi: alephZeroApi, + tonClient, }; - }, [wagmiConfig, solanaConnection, tronWeb, alephZeroApi]); + }, [wagmiConfig, solanaConnection, tronWeb, alephZeroApi, tonClient]); }; diff --git a/packages/react/src/hooks/useDisconnect.ts b/packages/react/src/hooks/useDisconnect.ts index bd0c9db2..004547c7 100644 --- a/packages/react/src/hooks/useDisconnect.ts +++ b/packages/react/src/hooks/useDisconnect.ts @@ -5,6 +5,7 @@ import { useDisconnect as useEVMDisconnect } from 'wagmi'; import { ChainType } from '../types/index.js'; import { Wallet, WalletInstance } from '../types/wallet.js'; import { useAlephContext } from './useAlephContext.js'; +import { useTonContext } from './useTonContext.js'; import { useTronContext } from './useTronContext.js'; import { useWallets } from './useWallets.js'; @@ -18,6 +19,7 @@ export const useDisconnect = () => { const { disconnect: disconnectSolanaWallet } = useSolanaWallet(); const { disconnect: disconnectTronWallet } = useTronContext(); const { disconnect: disconnectAlephWallet } = useAlephContext(); + const { disconnect: disconnectTonWallet } = useTonContext(); const disconnectWallet = useCallback( async (params: DisconnectParams) => { @@ -41,11 +43,13 @@ export const useDisconnect = () => { await disconnectEVM({ connector: walletInstance.connector as WalletInstance<'evm'> }); } else if (params.chainType === 'alephZero') { await disconnectAlephWallet(); + } else if (params.chainType === 'ton') { + await disconnectTonWallet(); } else { await walletInstance.connector.disconnect(); } }, - [disconnectAlephWallet, disconnectEVM, disconnectSolanaWallet, disconnectTronWallet, wallets], + [disconnectAlephWallet, disconnectEVM, disconnectSolanaWallet, disconnectTronWallet, disconnectTonWallet, wallets], ); const mutation = useMutation({ diff --git a/packages/react/src/hooks/useSendTransaction.ts b/packages/react/src/hooks/useSendTransaction.ts index 39cc6760..db8f5779 100644 --- a/packages/react/src/hooks/useSendTransaction.ts +++ b/packages/react/src/hooks/useSendTransaction.ts @@ -49,6 +49,10 @@ export const useSendTransaction = () => { console.error(e); throw e; }); + if (chain.type === 'ton') { + console.error(`Please switch to ${chain.name} manually`); + throw new Error(`Please switch to ${chain.name} manually`); + } if (!switchedChain || switchedChain.id.toString() !== chain.id.toString()) { throw new Error('Failed to switch network'); } diff --git a/packages/react/src/hooks/useTonContext.ts b/packages/react/src/hooks/useTonContext.ts new file mode 100644 index 00000000..aa3bcccb --- /dev/null +++ b/packages/react/src/hooks/useTonContext.ts @@ -0,0 +1,6 @@ +import { useContext } from 'react'; +import { TonContext } from '../providers/TonProvider.js'; + +export const useTonContext = () => { + return useContext(TonContext); +}; diff --git a/packages/react/src/hooks/useTonStore.ts b/packages/react/src/hooks/useTonStore.ts new file mode 100644 index 00000000..7b46a46a --- /dev/null +++ b/packages/react/src/hooks/useTonStore.ts @@ -0,0 +1,10 @@ +import { useContext } from 'react'; +import { useStore } from 'zustand'; +import { TonContext } from '../providers/TonProvider.js'; +import { TonState } from '../store/Ton.js'; + +export function useTonStore(selector: (state: TonState) => T): T { + const { store } = useContext(TonContext); + if (!store) throw new Error('Missing Ton Provider in the tree'); + return useStore(store, selector); +} diff --git a/packages/react/src/hooks/useWallets.ts b/packages/react/src/hooks/useWallets.ts index 47668b37..24dc865b 100644 --- a/packages/react/src/hooks/useWallets.ts +++ b/packages/react/src/hooks/useWallets.ts @@ -1,8 +1,9 @@ // import { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react'; import { useWallet as useSolanaWallet } from '@tangled3/solana-react'; -import { useMemo } from 'react'; +import { useContext, useMemo } from 'react'; import { Connector, useConnectors as useEVMConnectors } from 'wagmi'; import { walletConfigs } from '../connectors/evm/walletConfigs.js'; +import { TonContext } from '../providers/TonProvider.js'; import { ChainType } from '../types/index.js'; import { Wallet } from '../types/wallet.js'; import { useAlephStore } from './useAlephStore.js'; @@ -30,6 +31,8 @@ export const useWallets = (options?: UseWalletsOptions): { [key in ChainType]: W const tronConnectors = useTronStore((state) => state.connectors); + const { wallets: tonWallets, tonAdapter } = useContext(TonContext); + const extendedEvmWallets = useMemo[]>(() => { const prepareWallets = (connector: Connector): Wallet<'evm'> | undefined => { const walletId = Object.keys(walletConfigs).find( @@ -149,18 +152,50 @@ export const useWallets = (options?: UseWalletsOptions): { [key in ChainType]: W return registryWallets; }, [alephAdapter, options?.onlyInstalled]); + const extendedTonWallets = useMemo[]>(() => { + const detected: Wallet<'ton'>[] = tonWallets.map((wallet) => ({ + id: wallet.appName, + name: wallet.name, + connector: tonAdapter, + icon: wallet.imageUrl, + type: 'ton', + // @ts-expect-error - `injected` doesn't exist on WalletInfo type + installed: wallet.injected, + url: wallet.aboutUrl, + })); + + // for ton connect modal option + const tonConnectOption: Wallet<'ton'> = { + id: 'ton-connect', + name: 'Ton Connect', + connector: tonAdapter, + icon: 'https://cryptologos.cc/logos/toncoin-ton-logo.png?v=035', + type: 'ton', + installed: true, + }; + + const walletList = [tonConnectOption, ...detected]; + + if (options?.onlyInstalled) { + return walletList.filter((wallet) => wallet.installed); + } + + return walletList; + }, [tonWallets, tonAdapter, options]); + return useMemo( () => ({ evm: extendedEvmWallets, solana: extendedSolanaWallets, tron: extendedTronWallets, alephZero: extendedAlephWallets, + ton: extendedTonWallets, bitcoin: [], casper: [], cosmos: [], near: [], sui: [], }), - [extendedEvmWallets, extendedSolanaWallets, extendedTronWallets, extendedAlephWallets], + [extendedEvmWallets, extendedSolanaWallets, extendedTronWallets, extendedAlephWallets, extendedTonWallets], ); }; diff --git a/packages/react/src/providers/TangledContext.tsx b/packages/react/src/providers/TangledContext.tsx index a57da81f..20d9f63a 100644 --- a/packages/react/src/providers/TangledContext.tsx +++ b/packages/react/src/providers/TangledContext.tsx @@ -1,3 +1,4 @@ +import { TonConnectUIProvider } from '@tonconnect/ui-react'; import { ReactNode, createContext, useState } from 'react'; import { ChainData, ChainId, SupportedChainsByType, TangledConfig } from '../types/index.js'; import { ChainConnectors } from '../types/wallet.js'; @@ -6,6 +7,7 @@ import { createChainConnectors } from '../utils/createChainConnectors.js'; import { AlephProvider } from './AlephProvider.js'; import EVMProvider from './EVMProvider.js'; import { SolanaProvider } from './SolanaProvider.js'; +import { TonProvider } from './TonProvider.js'; import { TronProvider } from './TronProvider.js'; import WalletsProvider from './WalletsProvider.js'; @@ -35,6 +37,12 @@ export const TangledContextProvider = ({ children, config }: { children: ReactNo const [connectors] = useState(() => { return createChainConnectors(config, chains); }); + const [tonconnectManifestUrl] = useState(() => { + return config.tonconnectManifestUrl; + }); + const [twaReturnUrl] = useState(() => { + return config.twaReturnUrl; + }); return ( @@ -48,7 +56,15 @@ export const TangledContextProvider = ({ children, config }: { children: ReactNo > - {children} + {/* getting error if combined with TonProvider */} + + + {children} + + diff --git a/packages/react/src/providers/TonProvider.tsx b/packages/react/src/providers/TonProvider.tsx new file mode 100644 index 00000000..4cbd5253 --- /dev/null +++ b/packages/react/src/providers/TonProvider.tsx @@ -0,0 +1,120 @@ +import { useMutation } from '@tanstack/react-query'; +import { TonConnectUI, toUserFriendlyAddress, useTonConnectUI, WalletInfo } from '@tonconnect/ui-react'; +import { createContext, useEffect, useRef, useState } from 'react'; +import { useStore } from 'zustand'; +import { connectExternalWallet } from '../connectors/ton/connector.js'; +import { createTonStore, TonStore } from '../store/Ton.js'; +import { ChainId, OtherChainData } from '../types/index.js'; + +export interface TonContextValues { + connect: (adapterId: string) => Promise<{ account: string | null; chainId: ChainId | undefined }>; + disconnect: () => Promise; + store: TonStore | null; + tonAdapter: TonConnectUI | undefined; + wallets: WalletInfo[]; +} + +export const TonContext = createContext({ + connect: async () => ({ account: '', chainId: undefined }), + disconnect: async () => {}, + store: null, + tonAdapter: undefined, + wallets: [], +}); + +/** + * @notice This provider is used to connect to the Ton network. + * @param adapters - Supported adapters for the Ton network. + * @returns The Ton provider context with the connect and disconnect functions. + */ +export const TonProvider = ({ children, chain }: { children: React.ReactNode; chain: OtherChainData }) => { + const tonStore = useRef(createTonStore({ chain: chain as OtherChainData<'ton'> })).current; + const connectedAdapter = useStore(tonStore, (state) => state.connectedAdapter); + const setConnectedAdapter = useStore(tonStore, (state) => state.setConnectedAdapter); + const setConnectors = useStore(tonStore, (state) => state.setConnectors); + const setAddress = useStore(tonStore, (state) => state.setAddress); + + const [tonConnectUI] = useTonConnectUI(); + const [tonWallets, setTonWallets] = useState([]); + + ///////////////// + /// Mutations /// + ///////////////// + const { mutateAsync: connect } = useMutation({ + mutationKey: ['ton connect'], + mutationFn: async (adapterId: string) => { + if (!tonConnectUI) { + throw new Error('No ton adapter found'); + } + + if (tonConnectUI.connected) { + const connectedWalletId = tonConnectUI.wallet?.device.appName.toLowerCase(); + if (adapterId !== connectedWalletId) { + await disconnect(); + } else { + return { + account: toUserFriendlyAddress(tonConnectUI.connector.account!.address), + chainId: undefined, + adapter: tonConnectUI, + }; + } + } + + const tonWallet = tonWallets.find((wallet) => wallet.appName === adapterId); + + if (tonWallet) { + tonConnectUI.connector.connect(tonWallet); + } else { + await connectExternalWallet(tonConnectUI); + } + + return { account: '', chainId: undefined, adapter: tonConnectUI }; + }, + }); + + const { mutateAsync: disconnect } = useMutation({ + mutationKey: ['ton disconnect'], + mutationFn: async () => { + if (!connectedAdapter) { + throw new Error('No ton adapter found'); + } + + await connectedAdapter.connector.disconnect(); + + setAddress(''); + setConnectors(connectedAdapter); + setConnectedAdapter(connectedAdapter); + }, + }); + + // subscribing to the wallet connection status change + useEffect(() => { + tonConnectUI.connector.onStatusChange((status) => { + if (status) { + setAddress(toUserFriendlyAddress(tonConnectUI.connector.account!.address)); + setConnectedAdapter(tonConnectUI); + setConnectors(tonConnectUI); + } + }); + }, [tonConnectUI, setAddress, setConnectedAdapter, setConnectors, connect]); + + // fetching supported ton wallets + useEffect(() => { + tonConnectUI + .getWallets() + .then((wallets) => { + setTonWallets(wallets); + }) + .catch((error) => { + console.error('Failed to fetch ton wallets', error); + }); + }, [tonConnectUI]); + + return ( + + {children} + + ); +}; diff --git a/packages/react/src/providers/WalletsProvider.tsx b/packages/react/src/providers/WalletsProvider.tsx index 40f336d8..6af7246e 100644 --- a/packages/react/src/providers/WalletsProvider.tsx +++ b/packages/react/src/providers/WalletsProvider.tsx @@ -3,6 +3,7 @@ import { ReactNode, useEffect } from 'react'; import { useConnections as useEVMConnections } from 'wagmi'; import { useAlephStore } from '../hooks/useAlephStore.js'; import { useTangledConfig } from '../hooks/useTangledConfig.js'; +import { useTonStore } from '../hooks/useTonStore.js'; import { useTronStore } from '../hooks/useTronStore.js'; import { useWalletsStore } from '../store/Wallet.js'; import { ChainId } from '../types/index.js'; @@ -16,6 +17,8 @@ const WalletsProvider = ({ children }: { children: ReactNode }) => { const alephConnectors = useAlephStore((state) => state.connectors); const alephAccounts = useAlephStore((state) => state.connectedAdapter); const alephAddress = useAlephStore((state) => state.address); + const tonConnectors = useTonStore((state) => state.connectors); + const tonAddress = useTonStore((state) => state.address); // Wallet store states const currentWallet = useWalletsStore((state) => state.currentWallet); @@ -146,6 +149,39 @@ const WalletsProvider = ({ children }: { children: ReactNode }) => { }); }, [setChainConnectedAccounts, setConnectedWallets, alephAccounts, chains.alephZero, alephConnectors, alephAddress]); + // ton + useEffect(() => { + const _tonAccounts: { [x: string]: ConnectedAccount } = {}; + const _tonWallets: { [x: string]: ConnectedWallet<'ton'> } = {}; + + for (const [name, adapter] of Object.entries(tonConnectors)) { + const address = tonAddress ?? ''; + + if (address === '') { + continue; + } + + _tonAccounts[name] = { + address: address, + chainId: adapter.wallet?.account.chain, + chainType: 'ton', + wallet: name, + }; + + _tonWallets[name] = { + address: address, + chainId: adapter.wallet?.account.chain, + chainType: 'ton', + connector: adapter, + }; + } + + setChainConnectedAccounts({ ton: _tonAccounts }); + setConnectedWallets({ + ton: _tonWallets, + }); + }, [setChainConnectedAccounts, setConnectedWallets, alephAccounts, chains.ton, tonConnectors, tonAddress]); + // when currentWallet changes, update currentAccount useEffect(() => { if (!currentWallet) { diff --git a/packages/react/src/store/Ton.ts b/packages/react/src/store/Ton.ts new file mode 100644 index 00000000..6e687dba --- /dev/null +++ b/packages/react/src/store/Ton.ts @@ -0,0 +1,63 @@ +import { TonClient } from '@ton/ton'; +import { TonConnectUI } from '@tonconnect/ui-react'; +import { createStore } from 'zustand'; +import { devtools } from 'zustand/middleware'; +import { getTonClient } from '../actions/ton/getTonClient.js'; +import { OtherChainData } from '../types/index.js'; + +interface TonProps { + chain: OtherChainData<'ton'>; +} +export interface TonState { + connectors: { + [key in string]: TonConnectUI; + }; + connectedAdapter: TonConnectUI | undefined; + address: string | null; + tonClient: TonClient; + + setAddress: (address: string) => void; + setConnectors: (connector: TonConnectUI) => void; + setConnectedAdapter: (adapter: TonConnectUI | undefined) => void; + setTonClient: (tonClient: TonClient) => void; +} + +export type TonStore = ReturnType; + +export const createTonStore = (props: TonProps) => { + const DEFAULT_TON_STATE: TonState = { + connectors: {}, + connectedAdapter: undefined, + address: null, + tonClient: getTonClient(props.chain), + + setAddress: () => {}, + setConnectors: () => {}, + setConnectedAdapter: () => {}, + setTonClient: () => {}, + }; + + const connectors: Record = {}; + + return createStore()( + devtools((set) => ({ + ...DEFAULT_TON_STATE, + connectors, + setConnectedAdapter: (connectedAdapter) => set(() => ({ connectedAdapter })), + setAddress: (address) => set(() => ({ address })), + setConnectors: (connector) => { + if (!connector.wallet) { + console.error('no ton connector.wallet found'); + } + + const slug = connector.connector.wallet?.device.appName.toLowerCase(); // converting to lowercase to match the ton wallet id + if (!slug) { + console.error('no ton wallet slug found'); + return; + } + set(() => ({ connectors: { [slug as string]: connector } })); + }, + setTonClient: (tonClient) => set(() => ({ tonClient })), + })), + ); +}; diff --git a/packages/react/src/types/index.ts b/packages/react/src/types/index.ts index 4287889b..ef805164 100644 --- a/packages/react/src/types/index.ts +++ b/packages/react/src/types/index.ts @@ -1,11 +1,13 @@ import { type ApiPromise } from '@polkadot/api'; import { Connection as SolanaConnection } from '@solana/web3.js'; +import { TonClient } from '@ton/ton'; import { GetTransactionReceiptReturnType as EVMTxReceipt } from '@wagmi/core'; import { Types as TronWebTypes, type TronWeb } from 'tronweb'; import { Chain as ViemChain } from 'viem'; import { Config as WagmiConfig } from 'wagmi'; import { CHAIN_ID } from '../constants/index.js'; import { AlephTransactionData } from './aleph.js'; +import { TonTransactionInfo } from './ton.js'; import { ChainConnectors } from './wallet.js'; export const CHAIN_TYPES = [ @@ -18,6 +20,7 @@ export const CHAIN_TYPES = [ 'casper', 'alephZero', 'bitcoin', + 'ton', ] as const; export type ChainType = (typeof CHAIN_TYPES)[number]; @@ -87,6 +90,11 @@ export interface TangledConfig { projectId: string; chainConnectors?: Partial; + + /** Manifest url for ton connect */ + tonconnectManifestUrl?: string; + /** Telegram mini app url */ + twaReturnUrl?: `${string}://${string}`; } type ChainRpcUrls = { @@ -115,6 +123,7 @@ export type ConnectionOrConfig = { solanaConnection: SolanaConnection; tronWeb: TronWeb; alephZeroApi: ApiPromise; + tonClient: TonClient; }; export type GetTokenMetadataParams = { @@ -129,4 +138,6 @@ export type TransactionReceipt = C extends 'evm' ? TronWebTypes.TransactionInfo : C extends 'alephZero' ? AlephTransactionData - : unknown; + : C extends 'ton' + ? TonTransactionInfo + : unknown; diff --git a/packages/react/src/types/ton.ts b/packages/react/src/types/ton.ts new file mode 100644 index 00000000..d82e5bb8 --- /dev/null +++ b/packages/react/src/types/ton.ts @@ -0,0 +1,17 @@ +import { AccountStatus, CurrencyCollection, Dictionary, HashUpdate, Message, TransactionDescription } from '@ton/ton'; + +export type TonTransactionInfo = { + address: bigint; + description: TransactionDescription; + endStatus: AccountStatus; + inMessage: undefined | Message; + lt: bigint; + now: number; + oldStatus: AccountStatus; + outMessages: Dictionary; + outMessagesCount: number; + prevTransactionHash: bigint; + prevTransactionLt: bigint; + stateUpdate: HashUpdate; + totalFees: CurrencyCollection; +}; diff --git a/packages/react/src/types/wallet.ts b/packages/react/src/types/wallet.ts index 12d254f5..b8d8c2d5 100644 --- a/packages/react/src/types/wallet.ts +++ b/packages/react/src/types/wallet.ts @@ -1,5 +1,6 @@ import { NightlyConnectAdapter } from '@nightlylabs/wallet-selector-polkadot'; import { Adapter as SolanaAdapter } from '@solana/wallet-adapter-base'; +import { TonConnectUI } from '@tonconnect/ui-react'; import { Adapter as TronAdapter, AdapterState as TronAdapterReadyState } from '@tronweb3/tronwallet-abstract-adapter'; import { Mutable } from '@wagmi/core/internal'; import { CreateConnectorFn, Connector as EVMConnector } from 'wagmi'; @@ -21,6 +22,8 @@ export type ChainConnectors = { alephZero: any[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any bitcoin: any[]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ton: any[]; }; export type WalletBase = { @@ -59,7 +62,9 @@ export type WalletInstance = T extends 'evm' ? TronAdapter : T extends 'alephZero' ? NightlyConnectAdapter - : DefaultConnector; + : T extends 'ton' + ? TonConnectUI + : DefaultConnector; export type ConnectedWallet = { address: string; diff --git a/packages/react/src/utils/createChainConfigs.ts b/packages/react/src/utils/createChainConfigs.ts index 667d355c..97031417 100644 --- a/packages/react/src/utils/createChainConfigs.ts +++ b/packages/react/src/utils/createChainConfigs.ts @@ -17,6 +17,7 @@ const createChainConfigs = ( solana: [], sui: [], tron: [], + ton: [], }; const defaultChains = getDefaultSupportedChains(testnet); @@ -45,6 +46,7 @@ const overrideChainConfig = ( solana: [], sui: [], tron: [], + ton: [], }; for (const chains of Object.values(chainsByType)) { diff --git a/packages/react/src/utils/getDefaultSupportedChains.ts b/packages/react/src/utils/getDefaultSupportedChains.ts index 55bf0ed0..d5156772 100644 --- a/packages/react/src/utils/getDefaultSupportedChains.ts +++ b/packages/react/src/utils/getDefaultSupportedChains.ts @@ -1,6 +1,7 @@ import { alephZero } from '../chains/alephZero.js'; import * as evm from '../chains/evm.js'; import { solana } from '../chains/solana.js'; +import { tonMainnet } from '../chains/ton.js'; import { tronMainnet } from '../chains/tron.js'; import { EVMChain, OtherChainData, SupportedChainsByType, TronChain } from '../types/index.js'; @@ -15,6 +16,7 @@ const getDefaultSupportedChains = (testnet?: boolean): SupportedChainsByType => solana: [], sui: [], tron: [], + ton: [], }; if (testnet) { @@ -43,6 +45,7 @@ const getDefaultSupportedChains = (testnet?: boolean): SupportedChainsByType => supportedChains.solana = [solana] as OtherChainData<'solana'>[]; supportedChains.tron = [tronMainnet] as TronChain[]; supportedChains.alephZero = [alephZero] as OtherChainData<'alephZero'>[]; + supportedChains.ton = [tonMainnet] as OtherChainData<'ton'>[]; } return supportedChains; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6723b2da..6b4ba420 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,12 @@ importers: '@tanstack/react-query': specifier: ^5.44.0 version: 5.44.0(react@18.3.1) + '@ton/ton': + specifier: ^15.0.0 + version: 15.0.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0) + '@tonconnect/ui-react': + specifier: ^2.0.9 + version: 2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tronweb3/tronwallet-abstract-adapter': specifier: ^1.1.6 version: 1.1.6 @@ -2716,6 +2722,44 @@ packages: peerDependencies: react: ^18.0.0 + '@ton/core@0.57.0': + resolution: {integrity: sha512-UOehEXEV5yqi+17qmmWdD01YfVgQlYtitSm5OfN/WMg6PAMkt+Uf91JRC4mdPNtkKDhyKuujJuhYs6QiOsHPfw==} + peerDependencies: + '@ton/crypto': '>=3.2.0' + + '@ton/crypto-primitives@2.1.0': + resolution: {integrity: sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==} + + '@ton/crypto@3.3.0': + resolution: {integrity: sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==} + + '@ton/ton@15.0.0': + resolution: {integrity: sha512-0xBivkN0u0+vczt3mKJrx5yqkCxRR00wM8gXCmN/eOzzHKXRFWDEjJp8EEs5J/mq1Op/OhCOFExmOZkr58wgOA==} + peerDependencies: + '@ton/core': '>=0.56.0' + '@ton/crypto': '>=3.2.0' + + '@tonconnect/isomorphic-eventsource@0.0.2': + resolution: {integrity: sha512-B4UoIjPi0QkvIzZH5fV3BQLWrqSYABdrzZQSI9sJA9aA+iC0ohOzFwVVGXanlxeDAy1bcvPbb29f6sVUk0UnnQ==} + + '@tonconnect/isomorphic-fetch@0.0.3': + resolution: {integrity: sha512-jIg5nTrDwnite4fXao3dD83eCpTvInTjZon/rZZrIftIegh4XxyVb5G2mpMqXrVGk1e8SVXm3Kj5OtfMplQs0w==} + + '@tonconnect/protocol@2.2.6': + resolution: {integrity: sha512-kyoDz5EqgsycYP+A+JbVsAUYHNT059BCrK+m0pqxykMODwpziuSAXfwAZmHcg8v7NB9VKYbdFY55xKeXOuEd0w==} + + '@tonconnect/sdk@3.0.5': + resolution: {integrity: sha512-ow0qnN4s3iQ/r2uXobZ7YzdQBtan/36CgCT9IP35G07g38UxsUXwzw8ANmJTDj/JPiQcIKuYBMfIwIX9zLM0wg==} + + '@tonconnect/ui-react@2.0.9': + resolution: {integrity: sha512-wN7tEZpQiRYSUcdNAxFsDkk5TYo8krIu00ZLE1R5kXyr+XpO120jOmTEweBSXvIzTgEVkD/PxDZbBQQxRTXsUw==} + peerDependencies: + react: '>=17.0.0' + react-dom: '>=17.0.0' + + '@tonconnect/ui@2.0.9': + resolution: {integrity: sha512-ZxofTBf81NqrxyD0ybI8AuFHN11uKVg/00xTDFhP5FoPB8rYC7En9qE2VJ6IvwvtTpmh8jspi2ancOHUMBoCQA==} + '@tronweb3/google-protobuf@3.21.2': resolution: {integrity: sha512-IVcT2GfWX3K6tHUVhs14NP5uzKhQt4KeDya1g9ACxuZsUzsaoGUIGzceK2Ltu7xp1YV94AaHOf4yxLAivlvEkQ==} @@ -3207,6 +3251,7 @@ packages: '@walletconnect/sign-client@2.11.0': resolution: {integrity: sha512-H2ukscibBS+6WrzQWh+WyVBqO5z4F5et12JcwobdwgHnJSlqIoZxqnUYYWNCI5rUR5UKsKWaUyto4AE9N5dw4Q==} + deprecated: Reliability and performance greatly improved - please see https://github.com/WalletConnect/walletconnect-monorepo/releases '@walletconnect/sign-client@2.13.0': resolution: {integrity: sha512-En7KSvNUlQFx20IsYGsFgkNJ2lpvDvRsSFOT5PTdGskwCkUfOpB33SQJ6nCrN19gyoKPNvWg80Cy6MJI0TjNYA==} @@ -3724,6 +3769,9 @@ packages: citty@0.1.6: resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + classnames@2.5.1: + resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} + clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} engines: {node: '>=12'} @@ -4022,6 +4070,9 @@ packages: resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} engines: {node: '>= 0.4'} + dataloader@2.2.2: + resolution: {integrity: sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==} + date-fns@2.30.0: resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} engines: {node: '>=0.11'} @@ -4535,6 +4586,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource@2.0.2: + resolution: {integrity: sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==} + engines: {node: '>=12.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -5401,6 +5456,9 @@ packages: resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} engines: {'0': node >= 0.2.0} + jssha@3.2.0: + resolution: {integrity: sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==} + jsx-ast-utils@3.3.5: resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} engines: {node: '>=4.0'} @@ -7071,6 +7129,9 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + symbol.inspect@1.0.1: + resolution: {integrity: sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==} + synckit@0.8.8: resolution: {integrity: sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -7105,6 +7166,9 @@ packages: engines: {node: '>=10'} hasBin: true + teslabot@1.5.0: + resolution: {integrity: sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg==} + text-encoding-utf-8@1.0.2: resolution: {integrity: sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==} @@ -7203,6 +7267,12 @@ packages: tty-browserify@0.0.1: resolution: {integrity: sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==} + tweetnacl-util@0.15.1: + resolution: {integrity: sha512-RKJBIj8lySrShN4w6i/BonWp2Z/uxwC3h4y7xsRrpP59ZboCd0GpEVsOnMDYLMmKBpYhb5TgHzZXy7wTfYFBRw==} + + tweetnacl@1.0.3: + resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -7263,6 +7333,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ua-parser-js@1.0.38: + resolution: {integrity: sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==} + ufo@1.5.3: resolution: {integrity: sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==} @@ -7865,7 +7938,7 @@ snapshots: dependencies: '@babel/compat-data': 7.24.7 '@babel/helper-validator-option': 7.24.7 - browserslist: 4.23.1 + browserslist: 4.23.3 lru-cache: 5.1.1 semver: 6.3.1 @@ -11401,6 +11474,73 @@ snapshots: '@tanstack/query-core': 5.44.0 react: 18.3.1 + '@ton/core@0.57.0(@ton/crypto@3.3.0)': + dependencies: + '@ton/crypto': 3.3.0 + symbol.inspect: 1.0.1 + + '@ton/crypto-primitives@2.1.0': + dependencies: + jssha: 3.2.0 + + '@ton/crypto@3.3.0': + dependencies: + '@ton/crypto-primitives': 2.1.0 + jssha: 3.2.0 + tweetnacl: 1.0.3 + + '@ton/ton@15.0.0(@ton/core@0.57.0(@ton/crypto@3.3.0))(@ton/crypto@3.3.0)': + dependencies: + '@ton/core': 0.57.0(@ton/crypto@3.3.0) + '@ton/crypto': 3.3.0 + axios: 1.7.3 + dataloader: 2.2.2 + symbol.inspect: 1.0.1 + teslabot: 1.5.0 + zod: 3.23.8 + transitivePeerDependencies: + - debug + + '@tonconnect/isomorphic-eventsource@0.0.2': + dependencies: + eventsource: 2.0.2 + + '@tonconnect/isomorphic-fetch@0.0.3': + dependencies: + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@tonconnect/protocol@2.2.6': + dependencies: + tweetnacl: 1.0.3 + tweetnacl-util: 0.15.1 + + '@tonconnect/sdk@3.0.5': + dependencies: + '@tonconnect/isomorphic-eventsource': 0.0.2 + '@tonconnect/isomorphic-fetch': 0.0.3 + '@tonconnect/protocol': 2.2.6 + transitivePeerDependencies: + - encoding + + '@tonconnect/ui-react@2.0.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@tonconnect/ui': 2.0.9 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - encoding + + '@tonconnect/ui@2.0.9': + dependencies: + '@tonconnect/sdk': 3.0.5 + classnames: 2.5.1 + deepmerge: 4.3.1 + ua-parser-js: 1.0.38 + transitivePeerDependencies: + - encoding + '@tronweb3/google-protobuf@3.21.2': {} '@tronweb3/tronwallet-abstract-adapter@1.1.6': @@ -13291,6 +13431,8 @@ snapshots: dependencies: consola: 3.2.3 + classnames@2.5.1: {} + clean-stack@4.2.0: dependencies: escape-string-regexp: 5.0.0 @@ -13538,7 +13680,7 @@ snapshots: core-js-compat@3.37.1: dependencies: - browserslist: 4.23.1 + browserslist: 4.23.3 core-util-is@1.0.3: {} @@ -13673,6 +13815,8 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.1 + dataloader@2.2.2: {} + date-fns@2.30.0: dependencies: '@babel/runtime': 7.24.7 @@ -14393,6 +14537,8 @@ snapshots: events@3.3.0: {} + eventsource@2.0.2: {} + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -15337,6 +15483,8 @@ snapshots: jsonparse@1.3.1: {} + jssha@3.2.0: {} + jsx-ast-utils@3.3.5: dependencies: array-includes: 3.1.8 @@ -16692,7 +16840,7 @@ snapshots: regenerator-transform@0.15.2: dependencies: - '@babel/runtime': 7.24.7 + '@babel/runtime': 7.25.0 regexp.prototype.flags@1.5.2: dependencies: @@ -17236,6 +17384,8 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} + symbol.inspect@1.0.1: {} + synckit@0.8.8: dependencies: '@pkgr/core': 0.1.1 @@ -17287,6 +17437,8 @@ snapshots: commander: 2.20.3 source-map-support: 0.5.21 + teslabot@1.5.0: {} + text-encoding-utf-8@1.0.2: {} text-extensions@1.9.0: {} @@ -17388,6 +17540,10 @@ snapshots: tty-browserify@0.0.1: {} + tweetnacl-util@0.15.1: {} + + tweetnacl@1.0.3: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -17446,6 +17602,8 @@ snapshots: typescript@5.5.4: {} + ua-parser-js@1.0.38: {} + ufo@1.5.3: {} uglify-js@3.18.0: