├── public ├── _redirects ├── favicon.png ├── robots.txt ├── scaleton.png ├── manifest.json └── index.html ├── src ├── modules │ ├── dapps │ │ └── dex.swap │ │ │ ├── static │ │ │ ├── pairs.mainnet.yaml │ │ │ ├── pairs.sandbox.yaml │ │ │ └── pairs.testnet.yaml │ │ │ ├── types │ │ │ └── Pair.ts │ │ │ ├── enums │ │ │ └── SwapStatus.ts │ │ │ ├── utils │ │ │ ├── timeout.ts │ │ │ ├── generateQueryId.ts │ │ │ └── wait.ts │ │ │ ├── selectors │ │ │ ├── selectDestinationAmountOut.ts │ │ │ ├── selectAvailableSources.ts │ │ │ ├── selectSourceSymbol.ts │ │ │ ├── selectDestinationSymbol.ts │ │ │ ├── selectAvailableDestinations.ts │ │ │ └── selectImpactPrice.ts │ │ │ ├── pages │ │ │ ├── SwapView.scss │ │ │ └── SwapView.tsx │ │ │ ├── components │ │ │ ├── RefreshLink.tsx │ │ │ ├── ImpactPrice.tsx │ │ │ ├── ProgressStep.tsx │ │ │ ├── ConfirmSwapModal.scss │ │ │ └── ConfirmSwapModal.tsx │ │ │ ├── services │ │ │ ├── PairCatalog.ts │ │ │ ├── index.ts │ │ │ └── TradeService.ts │ │ │ └── store.ts │ ├── jettons │ │ ├── utils │ │ │ ├── ipfs.ts │ │ │ ├── presentBalance.ts │ │ │ ├── truncateAddress.ts │ │ │ └── timeAgo.ts │ │ ├── types │ │ │ ├── Jetton.ts │ │ │ ├── JettonBurnTransaction.ts │ │ │ ├── JettonIncomeTransaction.ts │ │ │ ├── JettonOutcomeTransaction.ts │ │ │ └── JettonTransaction.ts │ │ ├── enums │ │ │ └── JettonOperation.ts │ │ ├── pages │ │ │ ├── JettonWalletView.scss │ │ │ └── JettonWalletView.tsx │ │ └── contracts │ │ │ └── JettonV1.ts │ ├── assets │ │ ├── types │ │ │ ├── TransactionType.ts │ │ │ ├── AssetType.ts │ │ │ ├── AssetBalances.ts │ │ │ ├── AssetRef.ts │ │ │ ├── NativeCoinRef.ts │ │ │ ├── JettonRef.ts │ │ │ ├── index.ts │ │ │ └── Transaction.ts │ │ ├── components │ │ │ ├── AssetOperationTag │ │ │ │ ├── AssetOperationTag.scss │ │ │ │ └── AssetOperationTag.tsx │ │ │ ├── AssetsTabPaneContent │ │ │ │ ├── AssetsTabPaneContent.scss │ │ │ │ └── AssetsTabPaneContent.tsx │ │ │ ├── AssetTransferForm.tsx │ │ │ ├── AssetHistoryModal.tsx │ │ │ ├── AssetTransferFormModal.tsx │ │ │ └── ImportJettonModal.tsx │ │ ├── services │ │ │ ├── AssetStore.ts │ │ │ ├── AssetAdapter.ts │ │ │ ├── stores │ │ │ │ └── LocalStorageAssetStore.ts │ │ │ ├── index.ts │ │ │ ├── AssetCatalog.ts │ │ │ └── adapters │ │ │ │ ├── JettonAssetAdapter.ts │ │ │ │ └── NativeAssetAdapter.ts │ │ ├── selectors │ │ │ ├── createAssetSelector.ts │ │ │ ├── createHistorySelector.ts │ │ │ ├── createAssetSymbolSelector.ts │ │ │ └── createHistoryLoadingSelector.ts │ │ ├── actions │ │ │ ├── index.ts │ │ │ ├── hideAsset.ts │ │ │ ├── importJetton.ts │ │ │ ├── refreshBalances.ts │ │ │ ├── fetchHistory.ts │ │ │ └── requestAssetTransfer.ts │ │ ├── static │ │ │ ├── assets.sandbox.yaml │ │ │ ├── assets.testnet.yaml │ │ │ └── assets.mainnet.yaml │ │ └── store.ts │ ├── contracts │ │ ├── enums │ │ │ ├── TradeDirection.ts │ │ │ └── PoolStatus.ts │ │ ├── parsers │ │ │ ├── parseBurnTransaction.ts │ │ │ ├── parseInternalTransferTransaction.ts │ │ │ └── parseTransferTransaction.ts │ │ ├── JettonMasterContract.ts │ │ ├── PoolContract.ts │ │ └── JettonWalletContract.ts │ ├── wallets │ │ ├── common │ │ │ ├── utils │ │ │ │ ├── index.ts │ │ │ │ ├── timeout.ts │ │ │ │ └── stringToCell.ts │ │ │ ├── constants.ts │ │ │ ├── Wallet.ts │ │ │ ├── WalletFeature.ts │ │ │ ├── components │ │ │ │ ├── WalletIcon │ │ │ │ │ ├── WalletIcon.scss │ │ │ │ │ ├── icons │ │ │ │ │ │ ├── sandbox.png │ │ │ │ │ │ ├── tonhub.png │ │ │ │ │ │ ├── ton-wallet.png │ │ │ │ │ │ └── tonkeeper.svg │ │ │ │ │ └── WalletIcon.tsx │ │ │ │ ├── ConnectWalletDropdownMenu.tsx │ │ │ │ └── ConnectWalletButton.tsx │ │ │ ├── selectors │ │ │ │ ├── selectWalletFeatures.ts │ │ │ │ ├── selectWalletAddress.ts │ │ │ │ └── selectWalletName.ts │ │ │ ├── pages │ │ │ │ ├── ConnectWalletView.scss │ │ │ │ └── ConnectWalletView.tsx │ │ │ ├── TransactionRequest.ts │ │ │ ├── WalletAdapter.ts │ │ │ ├── WalletService.ts │ │ │ └── store.ts │ │ ├── store.ts │ │ ├── ton-wallet │ │ │ ├── types │ │ │ │ └── TonWalletProvider.ts │ │ │ ├── TonWalletClient.ts │ │ │ └── TonWalletWalletAdapter.ts │ │ ├── tonkeeper │ │ │ ├── actions │ │ │ │ └── requestTransfer.ts │ │ │ ├── components │ │ │ │ ├── TonkeeperConnectModal.scss │ │ │ │ └── TonkeeperConnectModal.tsx │ │ │ ├── store.ts │ │ │ └── TonkeeperWalletAdapter.ts │ │ └── tonhub │ │ │ ├── components │ │ │ └── TonhubConnectModal │ │ │ │ ├── TonhubConnectModal.scss │ │ │ │ └── TonhubConnectModal.tsx │ │ │ └── TonhubWalletAdapter.ts │ ├── common │ │ ├── components │ │ │ ├── SquareImage │ │ │ │ ├── SquareImage.scss │ │ │ │ └── SquareImage.tsx │ │ │ └── AddressText │ │ │ │ ├── AddressText.scss │ │ │ │ └── AddressText.tsx │ │ ├── tonWebClient.ts │ │ ├── utils │ │ │ └── preloadImage.ts │ │ ├── index.ts │ │ └── network.ts │ ├── nfts │ │ ├── api │ │ │ ├── getNftItemsByOwner.ts │ │ │ └── getNftItem.ts │ │ ├── components │ │ │ ├── NftsTabPaneContent │ │ │ │ ├── NftsTabPaneContent.scss │ │ │ │ └── NftsTabPaneContent.tsx │ │ │ └── NftItemPreviewModal │ │ │ │ └── NftItemPreviewModal.tsx │ │ └── types │ │ │ └── NftItem.ts │ ├── layout │ │ ├── components │ │ │ ├── Spoiler │ │ │ │ ├── Spoiler.scss │ │ │ │ └── Spoiler.tsx │ │ │ ├── ScaletonIcon.svg │ │ │ ├── NavBarWalletMenu.tsx │ │ │ ├── NavBar.scss │ │ │ └── NavBar.tsx │ │ └── icons │ │ │ └── telegram.svg │ └── search │ │ └── views │ │ └── SearchView │ │ ├── SearchView.scss │ │ └── SearchView.tsx ├── hooks │ ├── index.ts │ ├── useAppDispatch.ts │ └── useAppSelector.ts ├── react-app-env.d.ts ├── mixins │ └── mixins.scss ├── index.scss ├── store.ts ├── pages │ ├── Jettons.tsx │ ├── Trade.tsx │ ├── Connect.tsx │ └── Search.tsx ├── index.tsx ├── App.tsx └── App.scss ├── .env.example ├── .gitignore ├── tsconfig.json ├── config-overrides.js ├── README.md ├── SECURITY.md ├── package.json └── LICENSE /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/static/pairs.mainnet.yaml: -------------------------------------------------------------------------------- 1 | [] 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleton-co/scaleton/HEAD/public/favicon.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/scaleton.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleton-co/scaleton/HEAD/public/scaleton.png -------------------------------------------------------------------------------- /src/modules/jettons/utils/ipfs.ts: -------------------------------------------------------------------------------- 1 | export const IPFS_GATEWAY_PREFIX = 'https://ipfs.io/ipfs/'; 2 | -------------------------------------------------------------------------------- /src/modules/assets/types/TransactionType.ts: -------------------------------------------------------------------------------- 1 | export type TransactionType = 'in' | 'out' | 'mint' | 'burn'; 2 | -------------------------------------------------------------------------------- /src/modules/assets/types/AssetType.ts: -------------------------------------------------------------------------------- 1 | export enum AssetType { 2 | NATIVE = 'native', 3 | JETTON = 'jetton', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/contracts/enums/TradeDirection.ts: -------------------------------------------------------------------------------- 1 | export enum TradeDirection { 2 | A_TO_B = 1, 3 | B_TO_A = 0, 4 | } 5 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export { useAppDispatch } from './useAppDispatch'; 2 | export { useAppSelector } from './useAppSelector'; 3 | -------------------------------------------------------------------------------- /src/modules/wallets/common/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { stringToCell } from './stringToCell'; 2 | export { timeout } from './timeout'; 3 | -------------------------------------------------------------------------------- /src/modules/assets/types/AssetBalances.ts: -------------------------------------------------------------------------------- 1 | export type AssetBalances = { 2 | [assetId: string]: { 3 | balance: string; 4 | }; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/wallets/common/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_JETTON_GAS_FEE = 0.045; 2 | export const DEFAULT_JETTON_FORWARD_AMOUNT = 0.01; 3 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetOperationTag/AssetOperationTag.scss: -------------------------------------------------------------------------------- 1 | .asset-operation-tag { 2 | width: 100%; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/contracts/enums/PoolStatus.ts: -------------------------------------------------------------------------------- 1 | export enum PoolStatus { 2 | NEW = 0x00, 3 | INITIALIZING = 0x01, 4 | INITIALIZED = 0x02, 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/wallets/common/Wallet.ts: -------------------------------------------------------------------------------- 1 | export interface Wallet { 2 | address: string; 3 | publicKey: string; 4 | walletVersion: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/wallets/common/WalletFeature.ts: -------------------------------------------------------------------------------- 1 | export enum WalletFeature { 2 | TRANSFER = 'transfer', 3 | JETTON_TRANSFER = 'jetton-transfer', 4 | } 5 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/WalletIcon.scss: -------------------------------------------------------------------------------- 1 | .wallet-icon { 2 | width: 16px; 3 | height: 16px; 4 | 5 | margin-right: 8px; 6 | } 7 | -------------------------------------------------------------------------------- /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.yaml' { 4 | const data:any; 5 | export default data; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/types/Pair.ts: -------------------------------------------------------------------------------- 1 | export interface PairRef { 2 | contractAddress: string; 3 | leftAssetId: string; 4 | rightAssetId: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/jettons/utils/presentBalance.ts: -------------------------------------------------------------------------------- 1 | export function presentBalance(amount: string) { 2 | return Number(amount).toFixed(9).replace(/\.?0+$/, ''); 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/jettons/utils/truncateAddress.ts: -------------------------------------------------------------------------------- 1 | export function truncateAddress(address: string) { 2 | return address.replace(/^(.{4}).*(.{4})$/, '$1...$2'); 3 | } 4 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetsTabPaneContent/AssetsTabPaneContent.scss: -------------------------------------------------------------------------------- 1 | .assets-tab-pane-content { 2 | &> .ant-page-header { 3 | padding: 0 0 16px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/icons/sandbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleton-co/scaleton/HEAD/src/modules/wallets/common/components/WalletIcon/icons/sandbox.png -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/icons/tonhub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleton-co/scaleton/HEAD/src/modules/wallets/common/components/WalletIcon/icons/tonhub.png -------------------------------------------------------------------------------- /src/hooks/useAppDispatch.ts: -------------------------------------------------------------------------------- 1 | import { useDispatch } from 'react-redux'; 2 | import { AppDispatch } from '../store'; 3 | 4 | export const useAppDispatch = () => useDispatch(); 5 | -------------------------------------------------------------------------------- /src/modules/assets/types/AssetRef.ts: -------------------------------------------------------------------------------- 1 | import { JettonRef } from './JettonRef'; 2 | import { NativeCoinRef } from './NativeCoinRef'; 3 | 4 | export type AssetRef = JettonRef | NativeCoinRef; 5 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/icons/ton-wallet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scaleton-co/scaleton/HEAD/src/modules/wallets/common/components/WalletIcon/icons/ton-wallet.png -------------------------------------------------------------------------------- /src/modules/assets/services/AssetStore.ts: -------------------------------------------------------------------------------- 1 | import { AssetRef } from '../types/AssetRef'; 2 | 3 | export interface AssetStore { 4 | load(): AssetRef[]; 5 | store(assets: AssetRef[]): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/jettons/types/Jetton.ts: -------------------------------------------------------------------------------- 1 | export interface Jetton { 2 | address: string; 3 | name: string; 4 | symbol?: string; 5 | url?: string; 6 | imageUrl?: string; 7 | custom?: boolean; 8 | } 9 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | REACT_APP_SENTRY_DSN= 2 | REACT_APP_API_MAINNET_URL=https://api.scaleton.io 3 | REACT_APP_API_TESTNET_URL=https://testnet-api.scaleton.io 4 | REACT_APP_API_SANDBOX_URL=https://sandbox-api.scaleton.io 5 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/enums/SwapStatus.ts: -------------------------------------------------------------------------------- 1 | export enum SwapStatus { 2 | IDLE = 0, 3 | CONFIRMING, 4 | CONFIRMED, 5 | SENT, 6 | RECEIVED, 7 | 8 | // Errors 9 | CONFIRM_FAILED, 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/wallets/common/selectors/selectWalletFeatures.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../../../../store'; 2 | 3 | export const selectWalletFeatures = (state: RootState) => state.wallets.common.features; 4 | -------------------------------------------------------------------------------- /src/hooks/useAppSelector.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useSelector } from 'react-redux'; 2 | import { RootState } from '../store'; 3 | 4 | export const useAppSelector: TypedUseSelectorHook = useSelector; 5 | -------------------------------------------------------------------------------- /src/mixins/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin compressed-layout { 2 | max-width: 1200px; 3 | margin: 0 auto; 4 | 5 | @media screen and (min-width: 1200px) { 6 | & { 7 | padding: 0 50px; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/wallets/common/selectors/selectWalletAddress.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../../../../store'; 2 | 3 | export const selectWalletAddress = (state: RootState) => state.wallets.common.wallet?.address ?? null; 4 | -------------------------------------------------------------------------------- /src/modules/wallets/common/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | export function timeout(ms: number, message: string) { 2 | return new Promise((_, reject) => setTimeout( 3 | () => reject(new Error(message)), 4 | ms, 5 | )); 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/jettons/utils/timeAgo.ts: -------------------------------------------------------------------------------- 1 | import TimeAgo from 'javascript-time-ago'; 2 | import en from 'javascript-time-ago/locale/en.json'; 3 | 4 | TimeAgo.addDefaultLocale(en); 5 | 6 | export const timeAgo = new TimeAgo('en-US'); 7 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/utils/timeout.ts: -------------------------------------------------------------------------------- 1 | export function timeout(ms: number) { 2 | return new Promise((_, reject) => setTimeout( 3 | () => reject(new Error(`Timeout of ${ms} milliseconds exceeded.`)), 4 | ms, 5 | )); 6 | } 7 | -------------------------------------------------------------------------------- /src/modules/common/components/SquareImage/SquareImage.scss: -------------------------------------------------------------------------------- 1 | .square-image-container { 2 | width: 100%; 3 | height: 0; 4 | padding-bottom: 100%; 5 | background-color: #f0f0f0; 6 | 7 | img { 8 | width: 100%; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/assets/selectors/createAssetSelector.ts: -------------------------------------------------------------------------------- 1 | import { assetCatalog } from '../services'; 2 | 3 | export function createAssetSelector(assetId: string) { 4 | return () => assetId 5 | ? assetCatalog.getAsset(assetId) 6 | : null; 7 | } 8 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/static/pairs.sandbox.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - contractAddress: EQD4vUD2PYRLQd0mSwjmnnWSpeulTjZoFypJVUJAyJoUbrRu 3 | leftAssetId: ton 4 | rightAssetId: jetton:0:1b310ceeb868829ded11ed0123f67ad7b2333b80d8de566ae5059a2ec82f9208 5 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/static/pairs.testnet.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - contractAddress: EQD4vUD2PYRLQd0mSwjmnnWSpeulTjZoFypJVUJAyJoUbrRu 3 | leftAssetId: ton 4 | rightAssetId: jetton:0:1b310ceeb868829ded11ed0123f67ad7b2333b80d8de566ae5059a2ec82f9208 5 | -------------------------------------------------------------------------------- /src/modules/wallets/store.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from '@reduxjs/toolkit'; 2 | import common from './common/store'; 3 | import tonkeeper from './common/store'; 4 | 5 | export default combineReducers({ 6 | common, 7 | tonkeeper, 8 | }); 9 | -------------------------------------------------------------------------------- /src/modules/wallets/common/utils/stringToCell.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from 'ton'; 2 | 3 | export function stringToCell(value: string) { 4 | const cell = new Cell(); 5 | 6 | cell.bits.writeBuffer(Buffer.from(value)); 7 | 8 | return cell; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/wallets/ton-wallet/types/TonWalletProvider.ts: -------------------------------------------------------------------------------- 1 | export interface TonWalletProvider { 2 | isTonWallet: boolean; 3 | send(method: string, params?: any[]): Promise; 4 | on(eventName: string, handler: (...data: any[]) => any): void; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/common/tonWebClient.ts: -------------------------------------------------------------------------------- 1 | import TonWeb from 'tonweb'; 2 | import { API_URLS } from './index'; 3 | import { CURRENT_NETWORK } from './network'; 4 | 5 | export const tonWebClient = new TonWeb.HttpProvider( 6 | `${API_URLS[CURRENT_NETWORK]}/v1/jsonRPC`, 7 | ); 8 | -------------------------------------------------------------------------------- /src/modules/jettons/types/JettonBurnTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonOperation } from '../enums/JettonOperation'; 2 | 3 | export interface JettonBurnTransaction { 4 | operation: JettonOperation.BURN; 5 | time: number; 6 | queryId: string; 7 | amount: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/assets/selectors/createHistorySelector.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../../../store'; 2 | 3 | export function createHistorySelector(account: string, assetId: string) { 4 | return (state: RootState) => state.assets.history[assetId]?.transactions || [] 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/assets/types/NativeCoinRef.ts: -------------------------------------------------------------------------------- 1 | import { AssetType } from './AssetType'; 2 | 3 | export interface NativeCoinRef { 4 | id: string; 5 | type: AssetType.NATIVE; 6 | isCustom: boolean; 7 | name: string; 8 | symbol: string; 9 | url: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/jettons/enums/JettonOperation.ts: -------------------------------------------------------------------------------- 1 | export enum JettonOperation { 2 | TRANSFER = 0xf8a7ea5, 3 | TRANSFER_NOTIFICATION = 0x7362d09c, 4 | INTERNAL_TRANSFER = 0x178d4519, 5 | EXCESSES = 0xd53276db, 6 | BURN = 0x595f07bc, 7 | BURN_NOTIFICATION = 0x7bdd97de, 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/assets/selectors/createAssetSymbolSelector.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../store'; 2 | 3 | export function createAssetSymbolSelector(assetId: string) { 4 | return (state: RootState) => state.assets.assets.find((asset) => asset.id === assetId)?.symbol ?? null; 5 | } 6 | -------------------------------------------------------------------------------- /src/modules/assets/selectors/createHistoryLoadingSelector.ts: -------------------------------------------------------------------------------- 1 | import { RootState } from '../../../store'; 2 | 3 | export function createHistoryLoadingSelector(account: string, assetId: string) { 4 | return (state: RootState) => !!state.assets.history[assetId]?.isLoading ?? true 5 | } 6 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | min-width: 360px; 3 | overflow-x: hidden; 4 | 5 | display: flex; 6 | flex-direction:column; 7 | min-height: 100vh; 8 | min-height: -webkit-fill-available; 9 | 10 | overscroll-behavior-y: none; 11 | } 12 | 13 | #root { 14 | flex: 1; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/assets/actions/index.ts: -------------------------------------------------------------------------------- 1 | export { fetchHistory } from './fetchHistory'; 2 | export { hideAsset } from './hideAsset'; 3 | export { importJetton } from './importJetton'; 4 | export { refreshBalances } from './refreshBalances'; 5 | export { requestAssetTransfer } from './requestAssetTransfer'; 6 | -------------------------------------------------------------------------------- /src/modules/assets/types/JettonRef.ts: -------------------------------------------------------------------------------- 1 | import { AssetType } from './AssetType'; 2 | 3 | export interface JettonRef { 4 | id: string; 5 | type: AssetType.JETTON; 6 | isCustom?: boolean; 7 | name: string; 8 | symbol: string; 9 | contractAddress: string; 10 | url?: string; 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/common/utils/preloadImage.ts: -------------------------------------------------------------------------------- 1 | export function preloadImage(src: string): Promise { 2 | return new Promise((resolve, reject) => { 3 | const image = new Image(); 4 | 5 | image.src = src; 6 | image.onload = () => resolve(); 7 | image.onerror = () => reject(); 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/utils/generateQueryId.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | 3 | export function generateQueryId(): BN { 4 | const randomBytes = window.crypto.getRandomValues(new Uint8Array(8)); 5 | 6 | return new BN( 7 | Buffer.from(randomBytes).toString('hex'), 8 | 'hex', 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/nfts/api/getNftItemsByOwner.ts: -------------------------------------------------------------------------------- 1 | import { NftItem } from '../types/NftItem'; 2 | import { scaletonClient } from '../../common'; 3 | 4 | export async function getNftItemsByOwner(address: string): Promise { 5 | const { data } = await scaletonClient.get(`/v1/accounts/${address}/nfts`); 6 | 7 | return data.items; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/jettons/types/JettonIncomeTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonOperation } from '../enums/JettonOperation'; 2 | 3 | export interface JettonIncomeTransaction { 4 | operation: JettonOperation.INTERNAL_TRANSFER; 5 | time: number; 6 | queryId: string; 7 | amount: string; 8 | from: string | null; 9 | comment: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/jettons/types/JettonOutcomeTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonOperation } from '../enums/JettonOperation'; 2 | 3 | export interface JettonOutcomeTransaction { 4 | operation: JettonOperation.TRANSFER; 5 | time: number; 6 | queryId: string; 7 | amount: string; 8 | destination: string | null; 9 | comment: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectDestinationAmountOut.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../../../../store'; 2 | 3 | export function selectDestinationAmountOut(state: RootState) { 4 | if (!state.dex.swap.sourceAmountIn) return ''; 5 | if (!state.dex.swap.currentPrice) return ''; 6 | 7 | return state.dex.swap.destinationAmountOut; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectAvailableSources.ts: -------------------------------------------------------------------------------- 1 | import { assetCatalog } from '../../../assets/services'; 2 | import { pairCatalog } from '../services'; 3 | 4 | export function selectAvailableSources() { 5 | return assetCatalog 6 | .getAssets() 7 | .filter(asset => pairCatalog.getPairsByAsset(asset.id).length > 0); 8 | } 9 | 10 | -------------------------------------------------------------------------------- /src/modules/assets/services/AssetAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import { Address } from 'ton'; 3 | import { AssetRef, Transaction } from '../types'; 4 | 5 | export interface AssetAdapter { 6 | getBalance(ownerAddress: Address, asset: AssetRef): Promise; 7 | getTransactions(ownerAddress: Address, asset: AssetRef): Promise; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectSourceSymbol.ts: -------------------------------------------------------------------------------- 1 | import { assetCatalog } from '../../../assets/services'; 2 | import type { RootState } from '../../../../store'; 3 | 4 | export function selectSourceSymbol(state: RootState) { 5 | return state.dex.swap.sourceId 6 | ? assetCatalog.getAsset(state.dex.swap.sourceId)?.symbol ?? null 7 | : null; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/layout/components/Spoiler/Spoiler.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../mixins/mixins.scss'; 2 | 3 | 4 | .spoiler { 5 | &.ant-layout-header { 6 | background-color: #fadb14; 7 | line-height: 48px; 8 | height: 48px; 9 | font-weight: 600; 10 | } 11 | 12 | .compressed { 13 | @include compressed-layout; 14 | } 15 | } 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/icons/tonkeeper.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/modules/jettons/types/JettonTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonIncomeTransaction } from './JettonIncomeTransaction'; 2 | import { JettonOutcomeTransaction } from './JettonOutcomeTransaction'; 3 | import { JettonBurnTransaction } from './JettonBurnTransaction'; 4 | 5 | export type JettonTransaction = JettonIncomeTransaction | JettonOutcomeTransaction | JettonBurnTransaction; 6 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectDestinationSymbol.ts: -------------------------------------------------------------------------------- 1 | import { assetCatalog } from '../../../assets/services'; 2 | import type { RootState } from '../../../../store'; 3 | 4 | export function selectDestinationSymbol(state: RootState) { 5 | return state.dex.swap.destinationId 6 | ? assetCatalog.getAsset(state.dex.swap.destinationId)?.symbol ?? null 7 | : null; 8 | } 9 | -------------------------------------------------------------------------------- /src/modules/wallets/common/pages/ConnectWalletView.scss: -------------------------------------------------------------------------------- 1 | $connect-wallet-button-width: 165px; 2 | 3 | .connect-wallet-view { 4 | display: flex; 5 | justify-content: center; 6 | align-items: center; 7 | 8 | height: 100%; 9 | 10 | .connect-wallet-button { 11 | max-width: $connect-wallet-button-width; 12 | width: $connect-wallet-button-width; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/modules/nfts/api/getNftItem.ts: -------------------------------------------------------------------------------- 1 | import { NftItem } from '../types/NftItem'; 2 | import { scaletonClient } from '../../common'; 3 | 4 | export async function getNftItem(address: string): Promise { 5 | try { 6 | const { data } = await scaletonClient.get(`/v1/nfts/${address}`); 7 | return data.item; 8 | } catch { 9 | return null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Scaleton", 3 | "name": "Scaleton - DeFi platform for TON", 4 | "icons": [ 5 | { 6 | "src": "favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#002457" 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/assets/types/index.ts: -------------------------------------------------------------------------------- 1 | export type { AssetBalances } from './AssetBalances'; 2 | export type { AssetRef } from './AssetRef'; 3 | export { AssetType } from './AssetType'; 4 | export type { JettonRef } from './JettonRef'; 5 | export type { NativeCoinRef } from './NativeCoinRef'; 6 | export type { Transaction } from './Transaction'; 7 | export type { TransactionType } from './TransactionType'; 8 | -------------------------------------------------------------------------------- /src/modules/layout/components/Spoiler/Spoiler.tsx: -------------------------------------------------------------------------------- 1 | import { Layout } from 'antd'; 2 | import React from 'react'; 3 | import './Spoiler.scss'; 4 | 5 | const { Header } = Layout; 6 | 7 | export function Spoiler({ children }: { children: React.ReactNode }) { 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/nfts/components/NftsTabPaneContent/NftsTabPaneContent.scss: -------------------------------------------------------------------------------- 1 | .nft-tab-pane-content { 2 | .ant-page-header { 3 | padding: 0 0 16px; 4 | } 5 | } 6 | 7 | 8 | .nft-item-section { 9 | &>.title { 10 | margin-bottom: 8px; 11 | color: rgba(0, 0, 0, 0.45); 12 | font-size: 14px; 13 | } 14 | } 15 | 16 | .nft-item-section + .nft-item-section { 17 | margin-top: 16px; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/wallets/common/selectors/selectWalletName.ts: -------------------------------------------------------------------------------- 1 | import type { RootState } from '../../../../store'; 2 | 3 | export const selectWalletName = (state: RootState) => { 4 | switch (state.wallets.common.adapterId) { 5 | case 'ton-wallet': return 'TON Wallet'; 6 | case 'tonhub': return 'Tonhub'; 7 | case 'tonkeeper': return 'Tonkeeper'; 8 | default: return null; 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/wallets/common/TransactionRequest.ts: -------------------------------------------------------------------------------- 1 | import type { Cell } from 'ton'; 2 | 3 | export interface TransactionRequest { 4 | /** Destination */ 5 | to: string; 6 | 7 | /** Amount in nano-tons */ 8 | value: string; 9 | 10 | /** Timeout */ 11 | timeout: number; 12 | 13 | stateInit?: Buffer | null; 14 | 15 | text?: string | null; 16 | 17 | payload?: Cell | Buffer | null; 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/pages/SwapView.scss: -------------------------------------------------------------------------------- 1 | .swap-button-section { 2 | margin-top: 16px; 3 | 4 | .ant-tooltip-disabled-compatible-wrapper { 5 | width: 100%; 6 | } 7 | 8 | .ant-btn[type="button"] { 9 | width: 100%; 10 | } 11 | } 12 | 13 | .trade-direction { 14 | text-align: center; 15 | 16 | //.reverse-trade-direction { 17 | // display: none; 18 | //} 19 | 20 | &:hover { 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/wallets/tonkeeper/actions/requestTransfer.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | 3 | interface RequestTransferPayload { 4 | destination: string; 5 | amount: string; 6 | comment: string; 7 | deeplink: string; 8 | } 9 | 10 | export const requestTransfer = createAsyncThunk( 11 | 'tonkeeper/requestTransfer', 12 | async (payload: RequestTransferPayload) => { 13 | // 14 | }, 15 | ); 16 | -------------------------------------------------------------------------------- /src/modules/assets/actions/hideAsset.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { assetCatalog } from '../services'; 3 | import type { AssetRef } from '../types/AssetRef'; 4 | 5 | export const hideAsset = createAsyncThunk( 6 | 'assets/hideAsset', 7 | async (assetId: string) => { 8 | assetCatalog.removeAsset(assetId); 9 | 10 | return assetCatalog.getAssets(); 11 | }, 12 | ); 13 | -------------------------------------------------------------------------------- /src/modules/assets/static/assets.sandbox.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: ton 3 | type: native 4 | name: The Open Network 5 | symbol: TON 6 | url: https://ton.org 7 | 8 | - id: jetton:0:1b310ceeb868829ded11ed0123f67ad7b2333b80d8de566ae5059a2ec82f9208 9 | type: jetton 10 | name: Scaleton 11 | symbol: SCALE 12 | contractAddress: kQAbMQzuuGiCne0R7QEj9nrXsjM7gNjeVmrlBZouyC-SCALE 13 | image: ipfs://QmSMiXsZYMefwrTQ3P6HnDQaCpecS4EWLpgKK5EX1G8iA8 14 | -------------------------------------------------------------------------------- /src/modules/assets/static/assets.testnet.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: ton 3 | type: native 4 | name: The Open Network 5 | symbol: TON 6 | url: https://ton.org 7 | 8 | - id: jetton:0:1b310ceeb868829ded11ed0123f67ad7b2333b80d8de566ae5059a2ec82f9208 9 | type: jetton 10 | name: Scaleton 11 | symbol: SCALE 12 | contractAddress: kQAbMQzuuGiCne0R7QEj9nrXsjM7gNjeVmrlBZouyC-SCALE 13 | image: ipfs://QmSMiXsZYMefwrTQ3P6HnDQaCpecS4EWLpgKK5EX1G8iA8 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /.idea 5 | /node_modules 6 | /.pnp 7 | .pnp.js 8 | .env 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .netlify 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /src/modules/assets/types/Transaction.ts: -------------------------------------------------------------------------------- 1 | import type { Big } from 'big.js'; 2 | import type BN from 'bn.js'; 3 | import type { Address } from 'ton'; 4 | import { TransactionType } from './TransactionType'; 5 | 6 | export interface Transaction { 7 | queryId: BN; 8 | time: Date; 9 | operation: TransactionType; 10 | from: Address | null; 11 | to: Address | null; 12 | amount: Big; 13 | comment: string | null; 14 | body: Buffer | null; 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/common/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { CURRENT_NETWORK, Network } from './network'; 3 | 4 | export const API_URLS = { 5 | [Network.MAINNET]: process.env.REACT_APP_API_MAINNET_URL!, 6 | [Network.TESTNET]: process.env.REACT_APP_API_TESTNET_URL!, 7 | [Network.SANDBOX]: process.env.REACT_APP_API_SANDBOX_URL!, 8 | }; 9 | 10 | export const scaletonClient = axios.create({ 11 | baseURL: API_URLS[CURRENT_NETWORK], 12 | }); 13 | -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | import dex from './modules/dapps/dex.swap/store'; 3 | import assets from './modules/assets/store'; 4 | import wallets from './modules/wallets/store'; 5 | 6 | export const store = configureStore({ 7 | reducer: { 8 | assets, 9 | dex, 10 | wallets, 11 | }, 12 | }); 13 | 14 | export type RootState = ReturnType; 15 | export type AppDispatch = typeof store.dispatch; 16 | -------------------------------------------------------------------------------- /src/pages/Jettons.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation, useParams } from 'react-router-dom'; 2 | import { JettonWalletView } from '../modules/jettons/pages/JettonWalletView'; 3 | 4 | export function Jettons() { 5 | const location = useLocation(); 6 | const { address } = useParams(); 7 | 8 | if (location.pathname.startsWith('/address/')) { 9 | return ; 10 | } 11 | 12 | return ( 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/common/components/AddressText/AddressText.scss: -------------------------------------------------------------------------------- 1 | $gray-7: #8c8c8c; 2 | 3 | span.ton-address { 4 | display: inline-flex; 5 | max-width: 100%; 6 | 7 | &:before { 8 | content: attr(data-head); 9 | overflow: hidden; 10 | text-overflow: ellipsis; 11 | } 12 | 13 | &:after { 14 | content: attr(data-tail); 15 | } 16 | } 17 | 18 | span.ton-empty-address { 19 | text-transform: uppercase; 20 | color: $gray-7; 21 | font-weight: bold; 22 | } 23 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/utils/wait.ts: -------------------------------------------------------------------------------- 1 | export async function wait( 2 | callback: () => Promise, 3 | interval: number, 4 | ): Promise { 5 | return new Promise(resolve => { 6 | const intervalTimer = setInterval( 7 | async () => { 8 | const result = await callback(); 9 | if (result !== null) { 10 | clearInterval(intervalTimer); 11 | resolve(result); 12 | } 13 | }, 14 | interval, 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/search/views/SearchView/SearchView.scss: -------------------------------------------------------------------------------- 1 | .search-view { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: center; 5 | align-items: center; 6 | height: 100%; 7 | 8 | .search-logo { 9 | border: 3px #002457 solid; 10 | margin-bottom: 64px; 11 | border-radius: 50%; 12 | padding: 4px; 13 | 14 | img { 15 | max-width: 128px; 16 | } 17 | } 18 | 19 | .ant-input-search { 20 | width: 80%; 21 | max-width: 620px; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/common/components/AddressText/AddressText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './AddressText.scss'; 3 | 4 | export function AddressText({ value }: { value: string | null }) { 5 | if (!value) { 6 | return ( 7 | n/a 8 | ); 9 | } 10 | 11 | return ( 12 | 17 | 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/components/RefreshLink.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOutlined, ReloadOutlined } from '@ant-design/icons'; 2 | import React from 'react'; 3 | 4 | interface RefreshLinkProps { 5 | isLoading: boolean; 6 | onClick: () => void; 7 | } 8 | 9 | export function RefreshLink({ isLoading, onClick, ...rest }: RefreshLinkProps & React.AnchorHTMLAttributes) { 10 | const icon = isLoading 11 | ? 12 | : ; 13 | 14 | return {icon} 15 | } 16 | -------------------------------------------------------------------------------- /src/modules/jettons/pages/JettonWalletView.scss: -------------------------------------------------------------------------------- 1 | span.ant-page-header-heading-title { 2 | font-size: 14pt; 3 | } 4 | 5 | .clickable-address { 6 | &:hover { 7 | cursor: pointer; 8 | } 9 | } 10 | 11 | .jetton-wallet-view { 12 | padding: 0 50px; 13 | width: 100%; 14 | 15 | &> .ant-page-header { 16 | padding: 16px 0; 17 | 18 | .my-account-badge { 19 | margin-left: 8px; 20 | font-size: 12pt; 21 | color: #888; 22 | } 23 | } 24 | 25 | .asset-name .symbol { 26 | display: none; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/components/ImpactPrice.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd'; 2 | import React from 'react'; 3 | 4 | const { Text } = Typography; 5 | 6 | export function ImpactPrice({ value }: { value: number }) { 7 | if (value >= 5) { 8 | return {value} %; 9 | } 10 | 11 | if (value >= 3) { 12 | return {value} %; 13 | } 14 | 15 | if (value < 1) { 16 | return {value} %; 17 | } 18 | 19 | return {value} %; 20 | } 21 | -------------------------------------------------------------------------------- /src/modules/nfts/types/NftItem.ts: -------------------------------------------------------------------------------- 1 | export interface NftItem { 2 | address: string; 3 | index: number; 4 | name: string | null; 5 | description: string | null; 6 | imageUrl: string | null; 7 | ownerAddress: string | null; 8 | collectionAddress: string; 9 | attributes: { 10 | traitType: string; 11 | value: string | number; 12 | }[]; 13 | sale: { 14 | marketplace: 'get-gems' | 'disintar'; 15 | marketplaceFee: number | null; 16 | fullPrice: number | null; 17 | royaltyAmount: number | null; 18 | } | null; 19 | } 20 | -------------------------------------------------------------------------------- /src/modules/layout/components/ScaletonIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/modules/assets/services/stores/LocalStorageAssetStore.ts: -------------------------------------------------------------------------------- 1 | import { AssetRef } from '../../types/AssetRef'; 2 | import { AssetStore } from '../AssetStore'; 3 | 4 | export class LocalStorageAssetStore implements AssetStore { 5 | constructor( 6 | private readonly path: string, 7 | ) { 8 | } 9 | 10 | load(): AssetRef[] { 11 | try { 12 | return JSON.parse(localStorage.getItem(this.path) ?? '[]'); 13 | } catch { 14 | return []; 15 | } 16 | } 17 | 18 | store(assets: AssetRef[]): void { 19 | localStorage.setItem(this.path, JSON.stringify(assets)); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/modules/assets/actions/importJetton.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { assetCatalog } from '../services'; 3 | import type { Jetton } from '../../jettons/types/Jetton'; 4 | import type { AssetRef } from '../types'; 5 | 6 | export const importJetton = createAsyncThunk( 7 | 'assets/importJetton', 8 | (jetton: Jetton) => { 9 | assetCatalog.importJetton({ 10 | name: jetton.name, 11 | symbol: jetton.symbol ?? jetton.name, 12 | contractAddress: jetton.address, 13 | }); 14 | 15 | return assetCatalog.getAssets(); 16 | }, 17 | ); 18 | -------------------------------------------------------------------------------- /src/modules/layout/components/NavBarWalletMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from 'antd'; 2 | import React, { useCallback } from 'react'; 3 | import { useAppDispatch } from '../../../hooks'; 4 | import { terminateSession } from '../../wallets/common/store'; 5 | 6 | export function NavBarWalletMenu() { 7 | const dispatch = useAppDispatch(); 8 | 9 | const handleDisconnect = useCallback( 10 | () => dispatch(terminateSession()), 11 | [dispatch], 12 | ); 13 | 14 | return ( 15 | 16 | Disconnect 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/Trade.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Navigate } from 'react-router-dom'; 3 | import { useAppSelector } from '../hooks'; 4 | import { SwapView } from '../modules/dapps/dex.swap/pages/SwapView'; 5 | import { isMainnet } from '../modules/common/network'; 6 | import { selectWalletAddress } from '../modules/wallets/common/selectors/selectWalletAddress'; 7 | 8 | export function Trade() { 9 | const walletAddress = useAppSelector(selectWalletAddress); 10 | 11 | if (isMainnet() || !walletAddress) { 12 | return 13 | } 14 | 15 | return ( 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectAvailableDestinations.ts: -------------------------------------------------------------------------------- 1 | import { assetCatalog } from '../../../assets/services'; 2 | import { pairCatalog } from '../services'; 3 | import type { RootState } from '../../../../store'; 4 | import type { AssetRef } from '../../../assets/types/AssetRef'; 5 | 6 | export function selectAvailableDestinations(state: RootState) { 7 | const { sourceId } = state.dex.swap; 8 | 9 | return pairCatalog 10 | .getPairsByAsset(sourceId) 11 | .map(pair => assetCatalog.getAsset(pair.leftAssetId !== sourceId ? pair.leftAssetId : pair.rightAssetId)) 12 | .filter(asset => !!asset) as AssetRef[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/selectors/selectImpactPrice.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import type { RootState } from '../../../../store'; 3 | 4 | export function selectImpactPrice(state: RootState) { 5 | const { currentPrice, sourceAmountIn, destinationAmountOut } = state.dex.swap; 6 | 7 | if (!currentPrice || !sourceAmountIn || !destinationAmountOut) { 8 | return null; 9 | } 10 | 11 | const exactQuote = new Big(currentPrice).mul(sourceAmountIn); 12 | 13 | if (exactQuote.eq(0)) { 14 | return 0; 15 | } 16 | 17 | return exactQuote.sub(destinationAmountOut).div(exactQuote).mul(100).round(2).toNumber(); 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | module.exports = function override(config) { 4 | const fallback = config.resolve.fallback || {}; 5 | 6 | Object.assign(fallback, { 7 | 'buffer': require.resolve('buffer'), 8 | }); 9 | 10 | config.resolve.fallback = fallback; 11 | config.plugins = (config.plugins || []).concat([ 12 | new webpack.ProvidePlugin({ 13 | Buffer: ['buffer', 'Buffer'] 14 | }), 15 | ]); 16 | 17 | config.module.rules.find(rule => !!rule.oneOf).oneOf.unshift({ 18 | test: /\.(yml|yaml)$/, 19 | use: ['yaml-loader'], 20 | }); 21 | 22 | return config; 23 | } 24 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Scaleton - DeFi platform for TON 12 | 13 | 14 | 15 |
16 | 17 | 18 | -------------------------------------------------------------------------------- /src/modules/wallets/tonkeeper/components/TonkeeperConnectModal.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | 3 | .tonkeeper-connect-modal { 4 | width: 288px !important; 5 | 6 | .ant-modal-body { 7 | padding: 16px; 8 | 9 | ol { 10 | padding-left: 16px; 11 | margin-bottom: 16px; 12 | } 13 | 14 | .confirmation-status { 15 | width: 256px; 16 | height: 256px; 17 | font-size: 96px; 18 | line-height: 256px; 19 | text-align: center; 20 | 21 | .anticon { 22 | color: $primary-color !important; 23 | font-size: 96px; 24 | 25 | &.anticon-sync { 26 | animation-duration: 2s; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/modules/wallets/tonhub/components/TonhubConnectModal/TonhubConnectModal.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | 3 | .tonhub-connect-modal { 4 | width: 288px !important; 5 | 6 | .ant-modal-body { 7 | padding: 16px; 8 | 9 | ol { 10 | padding-left: 16px; 11 | margin-bottom: 16px; 12 | } 13 | 14 | .confirmation-status { 15 | width: 256px; 16 | height: 256px; 17 | font-size: 96px; 18 | line-height: 256px; 19 | text-align: center; 20 | 21 | .anticon { 22 | color: $primary-color !important; 23 | font-size: 96px; 24 | 25 | &.anticon-sync { 26 | animation-duration: 2s; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/pages/Connect.tsx: -------------------------------------------------------------------------------- 1 | import { Navigate, useLocation } from 'react-router-dom'; 2 | import { useAppSelector } from '../hooks'; 3 | import { ConnectWalletView } from '../modules/wallets/common/pages/ConnectWalletView'; 4 | import { selectWalletAddress } from '../modules/wallets/common/selectors/selectWalletAddress'; 5 | 6 | export function Connect() { 7 | const walletAddress = useAppSelector(selectWalletAddress); 8 | const location = useLocation(); 9 | 10 | if (walletAddress) { 11 | return ; 12 | } 13 | 14 | if (!location.pathname.startsWith('/connect')) { 15 | return ; 16 | } 17 | 18 | return ; 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/Search.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useNavigate } from 'react-router-dom'; 3 | import { SearchView } from '../modules/search/views/SearchView/SearchView'; 4 | import { selectWalletAddress } from '../modules/wallets/common/selectors/selectWalletAddress'; 5 | import { useAppSelector } from '../hooks'; 6 | 7 | export function Search() { 8 | const navigate = useNavigate(); 9 | const walletAddress = useAppSelector(selectWalletAddress); 10 | const [initialWalletAddress] = useState(walletAddress); 11 | 12 | useEffect( 13 | () => { 14 | if (!walletAddress) return; 15 | if (initialWalletAddress) return; 16 | 17 | navigate(`/${walletAddress}/assets`); 18 | }, 19 | [navigate, initialWalletAddress, walletAddress], 20 | ); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/services/PairCatalog.ts: -------------------------------------------------------------------------------- 1 | import { PairRef } from '../types/Pair'; 2 | 3 | export class PairCatalog { 4 | constructor( 5 | private readonly pairs: PairRef[], 6 | ) { 7 | } 8 | 9 | getAll(): PairRef[] { 10 | return this.pairs; 11 | } 12 | 13 | getPairsByAsset(assetId: string): PairRef[] { 14 | return this 15 | .getAll() 16 | .filter(pair => pair.leftAssetId === assetId || pair.rightAssetId === assetId); 17 | } 18 | 19 | getPairByAssets(firstAssetId: string, secondAssetId: string): PairRef | null { 20 | return this 21 | .getAll() 22 | .find( 23 | pair => (pair.leftAssetId === firstAssetId && pair.rightAssetId === secondAssetId) 24 | || (pair.leftAssetId === secondAssetId && pair.rightAssetId === firstAssetId) 25 | ) ?? null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/wallets/common/WalletAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from './Wallet'; 2 | import { WalletFeature } from './WalletFeature'; 3 | 4 | export interface WalletAdapter { 5 | readonly features: WalletFeature[]; 6 | 7 | isAvailable(): boolean; 8 | createSession(): Promise; 9 | awaitReadiness(session: S): Promise; 10 | getWallet(session: S): Promise; 11 | 12 | requestTransfer( 13 | session: S, 14 | destination: string, 15 | amount: string, 16 | comment: string, 17 | timeout: number, 18 | ): Promise; 19 | 20 | requestJettonTransfer( 21 | session: S, 22 | contractAddress: string, 23 | destination: string, 24 | amount: string, 25 | forwardPayload: string, 26 | requestTimeout: number, 27 | forwardAmount?: number, 28 | gasFee?: number, 29 | ): Promise; 30 | } 31 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/services/index.ts: -------------------------------------------------------------------------------- 1 | import MAINNET_PAIRS from '../static/pairs.mainnet.yaml'; 2 | import TESTNET_PAIRS from '../static/pairs.testnet.yaml'; 3 | import SANDBOX_PAIRS from '../static/pairs.sandbox.yaml'; 4 | import { CURRENT_NETWORK, Network } from '../../../common/network'; 5 | import { PairCatalog } from './PairCatalog'; 6 | import { TradeService } from './TradeService'; 7 | import { assetCatalog, tonClient } from '../../../assets/services'; 8 | import { walletService } from '../../../wallets/common/WalletService'; 9 | 10 | const ALL_PAIRS = { 11 | [Network.MAINNET]: MAINNET_PAIRS, 12 | [Network.TESTNET]: TESTNET_PAIRS, 13 | [Network.SANDBOX]: SANDBOX_PAIRS, 14 | }; 15 | 16 | export const pairCatalog = new PairCatalog(ALL_PAIRS[CURRENT_NETWORK]); 17 | 18 | export const tradeService = new TradeService(tonClient, assetCatalog, walletService); 19 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetOperationTag/AssetOperationTag.tsx: -------------------------------------------------------------------------------- 1 | import { Tag } from 'antd'; 2 | import React from 'react'; 3 | import './AssetOperationTag.scss'; 4 | import { TransactionType } from '../../types'; 5 | 6 | interface AssetOperationTagProps { 7 | type: TransactionType; 8 | } 9 | 10 | export function AssetOperationTag({ type }: AssetOperationTagProps) { 11 | switch (type) { 12 | case 'in': 13 | return IN 14 | 15 | case 'out': 16 | return OUT; 17 | 18 | case 'mint': 19 | return MINT; 20 | 21 | case 'burn': 22 | return BURN; 23 | 24 | default: 25 | return UNKNOWN; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/assets/actions/refreshBalances.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { Address } from 'ton'; 3 | import { assetCatalog } from '../services'; 4 | import type { AssetBalances } from '../types/AssetBalances'; 5 | 6 | export const refreshBalances = createAsyncThunk( 7 | 'assets/refreshBalances', 8 | async (owner: string) => { 9 | const result: AssetBalances = {}; 10 | 11 | const ownerAddress = Address.parse(owner); 12 | const assets = assetCatalog.getAssets(); 13 | 14 | await Promise.all( 15 | assets.map(async (asset) => { 16 | try { 17 | const balance = await assetCatalog.getBalance(ownerAddress, asset.id); 18 | 19 | result[asset.id] = { 20 | balance: balance.toString(), 21 | }; 22 | } catch (e) { 23 | console.error(e); 24 | } 25 | }), 26 | ); 27 | 28 | return result; 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /src/modules/contracts/parsers/parseBurnTransaction.ts: -------------------------------------------------------------------------------- 1 | import type { Slice, TonTransaction } from 'ton'; 2 | import { JettonOperation } from '../../jettons/enums/JettonOperation'; 3 | import { JettonBurnTransaction } from '../../jettons/types/JettonBurnTransaction'; 4 | 5 | /** 6 | burn#595f07bc query_id:uint64 amount:(VarUInteger 16) 7 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 8 | = InternalMsgBody; 9 | */ 10 | 11 | export function parseBurnTransaction(bodySlice: Slice, transaction: TonTransaction): JettonBurnTransaction { 12 | const queryId = bodySlice.readUint(64); 13 | const amount = bodySlice.readCoins(); 14 | 15 | // bodySlice.readAddress(); // response_destination 16 | // bodySlice.skip(1); // custom_payload 17 | 18 | return { 19 | operation: JettonOperation.BURN, 20 | time: transaction.time, 21 | queryId: queryId.toString(10), 22 | amount: amount.toString(10), 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/modules/common/network.ts: -------------------------------------------------------------------------------- 1 | export enum Network { 2 | MAINNET = 'mainnet', 3 | TESTNET = 'testnet', 4 | SANDBOX = 'sandbox', 5 | } 6 | 7 | export function getNetwork(host: string): Network { 8 | switch(host) { 9 | case 'testnet.scaleton.co': 10 | case 'testnet.scaleton.io': 11 | case 'localhost:3001': 12 | return Network.TESTNET; 13 | 14 | case 'sandbox.scaleton.co': 15 | case 'sandbox.scaleton.io': 16 | case 'localhost:3002': 17 | return Network.SANDBOX; 18 | 19 | case 'scaleton.co': 20 | case 'scaleton.io': 21 | case 'localhost:3003': 22 | default: 23 | return Network.MAINNET; 24 | } 25 | } 26 | 27 | export const CURRENT_NETWORK = getNetwork(document.location.host); 28 | 29 | export const isMainnet = () => CURRENT_NETWORK === Network.MAINNET; 30 | export const isTestnet = () => CURRENT_NETWORK === Network.TESTNET; 31 | export const isSandbox = () => CURRENT_NETWORK === Network.SANDBOX; 32 | -------------------------------------------------------------------------------- /src/modules/layout/icons/telegram.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/modules/wallets/tonkeeper/store.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { requestTransfer } from './actions/requestTransfer'; 3 | 4 | interface TonkeeperState { 5 | transfer: { 6 | status: 'inactive' | 'pending' | 'timeout' | 'confirmed'; 7 | deeplink: string | null; 8 | }; 9 | } 10 | 11 | const initialState: TonkeeperState = { 12 | transfer: { 13 | status: 'inactive', 14 | deeplink: null, 15 | }, 16 | }; 17 | 18 | const tonkeeperSlice = createSlice({ 19 | name: 'tonkeeper', 20 | initialState, 21 | reducers: { 22 | cancelTransfer(state) { 23 | state.transfer.status = 'inactive'; 24 | state.transfer.deeplink = null; 25 | }, 26 | }, 27 | extraReducers(builder) { 28 | builder.addCase(requestTransfer.fulfilled, (state, action) => { 29 | state.transfer.status = 'pending'; 30 | state.transfer.deeplink = action.meta.arg.deeplink; 31 | }); 32 | }, 33 | }); 34 | 35 | export const { cancelTransfer } = tonkeeperSlice.actions; 36 | 37 | export default tonkeeperSlice.reducer; 38 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/components/ProgressStep.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingOutlined } from '@ant-design/icons'; 2 | import { Timeline } from 'antd'; 3 | import React from 'react'; 4 | 5 | interface ProgressStepProps { 6 | status: 'idle' | 'pending' | 'done' | 'failed'; 7 | idleText: string; 8 | pendingText: string; 9 | doneText: string; 10 | failedText?: string; 11 | last?: boolean 12 | } 13 | 14 | export function ProgressStep({ 15 | status, 16 | idleText, 17 | pendingText, 18 | doneText, 19 | failedText, 20 | last, 21 | }: ProgressStepProps) { 22 | if (status === 'idle') { 23 | return {idleText}; 24 | } 25 | 26 | if (status === 'pending') { 27 | return }>{pendingText}; 28 | } 29 | 30 | if (status === 'done') { 31 | return {doneText}; 32 | } 33 | 34 | if (status === 'failed') { 35 | return {failedText}; 36 | } 37 | 38 | return null; 39 | } 40 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/WalletIcon/WalletIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import tonWalletIconPath from './icons/ton-wallet.png'; 3 | import tonkeeperIconPath from './icons/tonkeeper.svg'; 4 | import tonhubIconPath from './icons/tonhub.png'; 5 | import sandboxIconPath from './icons/sandbox.png'; 6 | import './WalletIcon.scss'; 7 | 8 | export function WalletIcon({ wallet }: { wallet: 'ton-wallet' | 'tonkeeper' | 'tonhub' | 'sandbox' }) { 9 | switch (wallet) { 10 | case 'ton-wallet': 11 | return ( 12 | TON Wallet 13 | ); 14 | 15 | case 'tonkeeper': 16 | return ( 17 | Tonkeeper 18 | ); 19 | 20 | case 'tonhub': 21 | return ( 22 | Tonhub 23 | ); 24 | 25 | case 'sandbox': 26 | return ( 27 | Sandbox 28 | ); 29 | 30 | default: 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/wallets/common/pages/ConnectWalletView.tsx: -------------------------------------------------------------------------------- 1 | import { WalletOutlined } from '@ant-design/icons'; 2 | import { Result } from 'antd'; 3 | import React from 'react'; 4 | import { TON_WALLET_EXTENSION_URL } from '../../ton-wallet/TonWalletClient'; 5 | import { ConnectWalletButton } from '../components/ConnectWalletButton'; 6 | import './ConnectWalletView.scss'; 7 | 8 | export function ConnectWalletView() { 9 | return ( 10 |
11 | } 13 | title="Connect your wallet to see your assets!" 14 | subTitle={( 15 | <> 16 | It requires TON Wallet extension, 17 | {' '}Tonkeeper{' '} 18 | or Tonhub. 19 | 20 | )} 21 | extra={ 22 | 23 | } 24 | /> 25 |
26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/modules/assets/actions/fetchHistory.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { Address } from 'ton'; 3 | import { assetCatalog } from '../services'; 4 | import { TransactionType } from '../types/TransactionType'; 5 | 6 | interface FetchHistoryInput { 7 | account: string; 8 | assetId: string; 9 | } 10 | 11 | interface Transaction { 12 | time: string; 13 | operation: TransactionType; 14 | from: string | null; 15 | to: string | null; 16 | amount: string; 17 | comment: string | null; 18 | } 19 | 20 | export const fetchHistory = createAsyncThunk( 21 | 'assets/fetchHistory', 22 | async (input, thunkAPI) => { 23 | const account = Address.parse(input.account); 24 | const transactions = await assetCatalog.getTransactions(account, input.assetId); 25 | 26 | return transactions.map(transaction => ({ 27 | time: transaction.time.toISOString(), 28 | operation: transaction.operation, 29 | from: transaction.from?.toFriendly() ?? null, 30 | to: transaction.to?.toFriendly() ?? null, 31 | amount: transaction.amount.toString(), 32 | comment: transaction.comment, 33 | })); 34 | }, 35 | ); 36 | -------------------------------------------------------------------------------- /src/modules/layout/components/NavBar.scss: -------------------------------------------------------------------------------- 1 | $gray-1: #ffffff; 2 | $green-5: #73d13d; 3 | $orange-6: #fa8c16; 4 | $wallet-button-width: 150px; 5 | 6 | .navbar-logo { 7 | padding-right: 20px; 8 | 9 | img { 10 | height: 32px; 11 | width: 32px; 12 | margin-right: 8px; 13 | } 14 | 15 | a { 16 | color: $gray-1; 17 | font-size: 24px; 18 | 19 | &:hover { 20 | color: $gray-1; 21 | } 22 | } 23 | } 24 | 25 | .ant-col.ton-price { 26 | @media (max-width: 655px) { 27 | display: none; 28 | } 29 | } 30 | 31 | span.network-badge { 32 | text-transform: uppercase; 33 | font-size: 13pt; 34 | font-weight: bold; 35 | 36 | &.testnet { 37 | color: $green-5; 38 | } 39 | 40 | &.sandbox { 41 | color: $orange-6; 42 | } 43 | } 44 | 45 | .wallet-button-section { 46 | &> button { 47 | max-width: $wallet-button-width; 48 | width: $wallet-button-width; 49 | } 50 | } 51 | 52 | .header { 53 | &.ant-layout-header { 54 | background: #002457; 55 | } 56 | 57 | .links { 58 | text-align: right; 59 | margin-right: 16px; 60 | } 61 | 62 | img.navbar-icon { 63 | height: 24px; 64 | vertical-align: middle; 65 | filter: invert(1); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/components/ConfirmSwapModal.scss: -------------------------------------------------------------------------------- 1 | $primary-color: #1890ff; 2 | $success-color: #52c41a; 3 | $failure-color: #ff7875; 4 | 5 | .confirm-swap-modal { 6 | div.swap-status-icon { 7 | .anticon { 8 | color: $primary-color !important; 9 | font-size: 96px; 10 | 11 | &.anticon-sync { 12 | animation-duration: 2s; 13 | } 14 | 15 | &.anticon-check-circle { 16 | color: $success-color !important; 17 | } 18 | 19 | &.anticon-stop { 20 | color: $failure-color !important; 21 | } 22 | } 23 | } 24 | 25 | .pulsing-wallet-icon { 26 | box-shadow: 0 0 0 0 $primary-color; 27 | border-radius: 6px; 28 | transform: scale(1); 29 | animation: wallet-pulse 2s infinite; 30 | } 31 | 32 | @keyframes wallet-pulse { 33 | 0% { 34 | transform: scale(0.95); 35 | box-shadow: 0 0 0 0 rgba($primary-color, 0.7); 36 | } 37 | 38 | 70% { 39 | transform: scale(1); 40 | box-shadow: 0 0 0 10px rgba($primary-color, 0); 41 | } 42 | 43 | 100% { 44 | transform: scale(0.95); 45 | box-shadow: 0 0 0 0 rgba($primary-color, 0); 46 | } 47 | } 48 | 49 | .ant-timeline-item:last-of-type { 50 | padding-bottom: 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/assets/static/assets.mainnet.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | - id: ton 3 | type: native 4 | name: The Open Network 5 | symbol: TON 6 | url: https://ton.org 7 | 8 | - id: jetton:0:65aac9b5e380eae928db3c8e238d9bc0d61a9320fdc2bc7a2f6c87d6fedf9208 9 | type: jetton 10 | name: Scaleton 11 | symbol: SCALE 12 | contractAddress: EQBlqsm144Dq6SjbPI4jjZvA1hqTIP3CvHovbIfW_t-SCALE 13 | image: ipfs://QmSMiXsZYMefwrTQ3P6HnDQaCpecS4EWLpgKK5EX1G8iA8 14 | url: https://scaleton.co 15 | 16 | - id: jetton:0:5d4797ee7ca47227f6424fc827ae95ba69a97a733d0a5a3af8ae58a6c7ea2699 17 | type: jetton 18 | name: SCAM Jetton 19 | symbol: SCAM 20 | contractAddress: EQBdR5fufKRyJ_ZCT8gnrpW6aal6cz0KWjr4rlimx-ommXvf 21 | image: ipfs://bafkreihbvg33rcwsqc74hydu72oy3vk76jmqpaefywgdufdp4pkgacog7i 22 | url: https://t.me/scamoton 23 | 24 | - id: jetton:0:2f0df5851b4a185f5f63c0d0cd0412f5aca353f577da18ff47c936f99dbd849a 25 | type: jetton 26 | name: Tegro 27 | symbol: TGR 28 | contractAddress: EQAvDfWFG0oYX19jwNDNBBL1rKNT9XfaGP9HyTb5nb2Eml6y 29 | image: https://tegro.io/tgr.png 30 | 31 | - id: jetton:0:f4bdd480fcd79d47dbaf6e037d1229115feb2e7ac0f119e160ebd5d031abdf2e 32 | type: jetton 33 | name: Bolt 34 | symbol: BOLT 35 | contractAddress: EQD0vdSA_NedR9uvbgN9EikRX-suesDxGeFg69XQMavfLqIw 36 | -------------------------------------------------------------------------------- /src/modules/assets/services/index.ts: -------------------------------------------------------------------------------- 1 | import { TonClient } from 'ton'; 2 | import MAINNET_ASSETS from '../static/assets.mainnet.yaml'; 3 | import TESTNET_ASSETS from '../static/assets.testnet.yaml'; 4 | import SANDBOX_ASSETS from '../static/assets.sandbox.yaml'; 5 | import { API_URLS } from '../../common'; 6 | import { CURRENT_NETWORK, Network } from '../../common/network'; 7 | import { AssetCatalog } from './AssetCatalog'; 8 | import { JettonAssetAdapter } from './adapters/JettonAssetAdapter'; 9 | import { NativeAssetAdapter } from './adapters/NativeAssetAdapter'; 10 | import { LocalStorageAssetStore } from './stores/LocalStorageAssetStore'; 11 | import { AssetType } from '../types'; 12 | 13 | const ALL_ASSETS = { 14 | [Network.MAINNET]: MAINNET_ASSETS, 15 | [Network.TESTNET]: TESTNET_ASSETS, 16 | [Network.SANDBOX]: SANDBOX_ASSETS, 17 | }; 18 | 19 | export const tonClient = new TonClient({ 20 | endpoint: `${API_URLS[CURRENT_NETWORK]}/v1/jsonRPC`, 21 | }); 22 | 23 | export const assetStore = new LocalStorageAssetStore(`assets:${CURRENT_NETWORK}`); 24 | export const assetCatalog = new AssetCatalog(assetStore, ALL_ASSETS[CURRENT_NETWORK]); 25 | 26 | assetCatalog.registerAdapter(AssetType.NATIVE, new NativeAssetAdapter(tonClient)); 27 | assetCatalog.registerAdapter(AssetType.JETTON, new JettonAssetAdapter(tonClient)); 28 | -------------------------------------------------------------------------------- /src/modules/jettons/contracts/JettonV1.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Cell } from 'ton'; 3 | import { tonWebClient } from '../../common/tonWebClient'; 4 | import type { HttpProvider } from 'tonweb/dist/types/providers/http-provider'; 5 | 6 | export class JettonV1 { 7 | constructor( 8 | private readonly provider: HttpProvider, 9 | ) { 10 | } 11 | 12 | async getData(jettonMasterAddress: string) { 13 | const { stack, exit_code } = await this.provider.call(jettonMasterAddress, 'get_jetton_data'); 14 | 15 | if (exit_code !== 0) { 16 | throw new Error('Cannot retrieve jetton data.'); 17 | } 18 | 19 | const [adminAddressCell] = Cell.fromBoc( 20 | Buffer.from(stack[2][1].bytes, 'base64') 21 | ); 22 | 23 | const [contentUriCell] = Cell.fromBoc( 24 | Buffer.from(stack[3][1].bytes, 'base64') 25 | ); 26 | 27 | const totalSupply = new BN(stack[0][1].substring(2), 'hex'); 28 | const isMutable = stack[1][1] !== '0x0'; 29 | const adminAddress = adminAddressCell.beginParse().readAddress(); 30 | const contentUri = contentUriCell.bits.buffer.slice(1).toString(); 31 | 32 | return { 33 | totalSupply, 34 | isMutable, 35 | adminAddress, 36 | contentUri, 37 | }; 38 | } 39 | } 40 | 41 | export const jettonV1 = new JettonV1(tonWebClient); 42 | -------------------------------------------------------------------------------- /src/modules/wallets/common/WalletService.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from './Wallet'; 2 | import { WalletAdapter } from './WalletAdapter'; 3 | 4 | export class WalletService { 5 | private readonly adapters: Map> = new Map(); 6 | 7 | registerAdapter(adapterId: string, adapter: WalletAdapter) { 8 | this.adapters.set(adapterId, adapter); 9 | } 10 | 11 | createSession(adapterId: string): Promise { 12 | const adapter = this.adapters.get(adapterId) as WalletAdapter; 13 | return adapter.createSession(); 14 | } 15 | 16 | async awaitReadiness(adapterId: string, session: S): Promise { 17 | const adapter = this.adapters.get(adapterId) as WalletAdapter; 18 | return adapter.awaitReadiness(session); 19 | } 20 | 21 | async getWallet(adapterId: string, session: S): Promise { 22 | const adapter = this.adapters.get(adapterId) as WalletAdapter; 23 | return adapter.getWallet(session); 24 | } 25 | 26 | getWalletAdapter(adapterId: string): WalletAdapter { 27 | const adapter = this.adapters.get(adapterId) as WalletAdapter; 28 | 29 | if (!adapter) { 30 | throw new Error('Wallet adapter is not registered.'); 31 | } 32 | 33 | return adapter; 34 | } 35 | } 36 | 37 | export const walletService = new WalletService(); 38 | -------------------------------------------------------------------------------- /src/modules/contracts/parsers/parseInternalTransferTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonOperation } from '../../jettons/enums/JettonOperation'; 2 | import type { JettonIncomeTransaction } from '../../jettons/types/JettonIncomeTransaction'; 3 | import type { Slice, TonTransaction } from 'ton'; 4 | 5 | /** 6 | internal_transfer query_id:uint64 amount:(VarUInteger 16) from:MsgAddress 7 | response_address:MsgAddress 8 | forward_ton_amount:(VarUInteger 16) 9 | forward_payload:(Either Cell ^Cell) 10 | = InternalMsgBody; 11 | */ 12 | 13 | export function parseInternalTransferTransaction(bodySlice: Slice, transaction: TonTransaction): JettonIncomeTransaction { 14 | const queryId = bodySlice.readUint(64); 15 | const amount = bodySlice.readCoins(); 16 | const from = bodySlice.readAddress(); 17 | 18 | bodySlice.readAddress(); // response_address 19 | bodySlice.readCoins(); // forward_ton_amount 20 | 21 | const comment = (bodySlice.remaining && !bodySlice.readBit() && bodySlice.remaining && (bodySlice.remaining % 8) === 0) 22 | ? bodySlice.readRemainingBytes().toString() 23 | : ''; 24 | 25 | return { 26 | operation: JettonOperation.INTERNAL_TRANSFER, 27 | time: transaction.time, 28 | queryId: queryId.toString(10), 29 | amount: amount.toString(10), 30 | from: from?.toFriendly() ?? null, 31 | comment, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/modules/contracts/parsers/parseTransferTransaction.ts: -------------------------------------------------------------------------------- 1 | import { JettonOperation } from '../../jettons/enums/JettonOperation'; 2 | import type { JettonOutcomeTransaction } from '../../jettons/types/JettonOutcomeTransaction'; 3 | import type { Slice, TonTransaction } from 'ton'; 4 | 5 | /** 6 | transfer query_id:uint64 amount:(VarUInteger 16) destination:MsgAddress 7 | response_destination:MsgAddress custom_payload:(Maybe ^Cell) 8 | forward_ton_amount:(VarUInteger 16) forward_payload:(Either Cell ^Cell) 9 | = InternalMsgBody; 10 | */ 11 | 12 | export function parseTransferTransaction(bodySlice: Slice, transaction: TonTransaction): JettonOutcomeTransaction { 13 | const queryId = bodySlice.readUint(64); 14 | const amount = bodySlice.readCoins(); 15 | const destination = bodySlice.readAddress(); 16 | 17 | bodySlice.readAddress(); // response_destination 18 | bodySlice.skip(1); // custom_payload 19 | bodySlice.readCoins(); // forward_ton_amount 20 | 21 | const commentCell = bodySlice.readBit() 22 | ? bodySlice.readRef() 23 | : bodySlice; 24 | 25 | if (commentCell.remaining >= 32) { 26 | commentCell.skip(32); 27 | } 28 | 29 | const comment = commentCell.remaining % 8 === 0 30 | ? commentCell.readRemainingBytes().toString() 31 | : ''; 32 | 33 | return { 34 | operation: JettonOperation.TRANSFER, 35 | time: transaction.time, 36 | queryId: queryId.toString(10), 37 | amount: amount.toString(10), 38 | destination: destination?.toFriendly() ?? null, 39 | comment, 40 | }; 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scaleton [![Scaleton Channel](https://badgen.net/badge/follow/@Scaleton/blue?icon=telegram)](https://t.me/Scaleton) 2 | 3 | The first DeFi wallet for TON with Jettons ([TIP-74](https://github.com/ton-blockchain/TIPs/issues/74)) support. 4 | 5 | [![License](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) 6 | ![GitHub last commit](https://img.shields.io/github/last-commit/scaleton-co/scaleton) 7 | [![Netlify Status](https://api.netlify.com/api/v1/badges/b75479d1-492e-4d7e-add3-4449638200a4/deploy-status)](https://scaleton.co) 8 | 9 | ## Features 10 | 11 | * Transfers 12 | * History of transactions 13 | * Import ([TIP-74](https://github.com/ton-blockchain/TIPs/issues/74)) contracts 14 | * Explore accounts NFTs 15 | * Authentication via TON Wallet / Tonkeeper / Tonhub 16 | 17 | ### Security Issues 18 | 19 | If you discover a security vulnerability, please see [Security Policies and Procedures](SECURITY.md). 20 | 21 | ### Links 22 | 23 | * [scaleton.co](https://scaleton.io) - mainnet 24 | * [testnet.scaleton.co](https://testnet.scaleton.io) - testnet 25 | * [sandbox.scaleton.co](https://sandbox.scaleton.io) - sandbox 26 | 27 | ### People 28 | 29 | * [Nick Nekilov](https://t.me/NickNekilov) 30 | 31 | ### Support 32 | 33 | If the project helped you somehow, you may thank the project: 34 | 35 | * `EQDIhshKpDtDt-uTawnlGIq-cNijBgB7jyYprczoAOXTWLwf` - TON 36 | * `0x7633945B2c284113BDf703A970088174b3841138` - ETH / BNB 37 | 38 | ## License 39 | 40 | Scaleton is [Apache 2.0](LICENSE) licensed. 41 | -------------------------------------------------------------------------------- /src/modules/assets/actions/requestAssetTransfer.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk } from '@reduxjs/toolkit'; 2 | import { toNano } from 'ton'; 3 | import { walletService } from '../../wallets/common/WalletService'; 4 | import { assetCatalog } from '../services'; 5 | import { AssetType } from '../types'; 6 | 7 | interface RequestAssetTransferInput { 8 | adapterId: string; 9 | session: S; 10 | assetId: string; 11 | recipient: string; 12 | amount: string; 13 | comment: string; 14 | } 15 | 16 | const ASSET_TRANSFER_TIMEOUT = 2 * 60 * 1000; // 2 minutes 17 | 18 | export const requestAssetTransfer = createAsyncThunk( 19 | 'assets/requestAssetTransfer', 20 | async (input) => { 21 | const asset = assetCatalog.getAsset(input.assetId); 22 | const walletAdapter = walletService.getWalletAdapter(input.adapterId); 23 | 24 | switch (asset.type) { 25 | case AssetType.JETTON: 26 | return walletAdapter.requestJettonTransfer( 27 | input.session, 28 | asset.contractAddress, 29 | input.recipient, 30 | toNano(input.amount).toString(), 31 | input.comment, 32 | ASSET_TRANSFER_TIMEOUT, 33 | ); 34 | 35 | case AssetType.NATIVE: 36 | return walletAdapter.requestTransfer( 37 | input.session, 38 | input.recipient, 39 | toNano(input.amount).toString(), 40 | input.comment, 41 | ASSET_TRANSFER_TIMEOUT, 42 | ); 43 | 44 | default: 45 | throw new Error('Asset type is not supported.'); 46 | } 47 | }, 48 | ); 49 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/ConnectWalletDropdownMenu.tsx: -------------------------------------------------------------------------------- 1 | import { Menu } from 'antd'; 2 | import { ItemType } from 'antd/lib/menu/hooks/useItems'; 3 | import { MenuTheme } from 'antd/lib/menu/MenuContext'; 4 | import { isMobile } from 'react-device-detect'; 5 | import { WalletIcon } from './WalletIcon/WalletIcon'; 6 | import { isMainnet, isSandbox, isTestnet } from '../../../common/network'; 7 | 8 | export function ConnectWalletDropdownMenu({ 9 | handleConnectTonkeeper, 10 | handleConnectTonhub, 11 | handleConnectTonWallet, 12 | theme, 13 | }: { 14 | handleConnectTonkeeper?: () => void; 15 | handleConnectTonhub?: () => void; 16 | handleConnectTonWallet?: () => void; 17 | theme?: MenuTheme; 18 | }) { 19 | const wallets: ItemType[] = []; 20 | 21 | if (isMainnet() || isTestnet()) { 22 | wallets.push({ 23 | key: 'tonkeeper', 24 | label: 'Tonkeeper', 25 | icon: , 26 | onClick: handleConnectTonkeeper, 27 | }); 28 | 29 | if (!isMobile) { 30 | wallets.push({ 31 | key: 'ton-wallet', 32 | label: 'TON Wallet', 33 | icon: , 34 | onClick: handleConnectTonWallet, 35 | }); 36 | } 37 | } 38 | 39 | if (isMainnet() || isSandbox()) { 40 | wallets.push({ 41 | key: 'tonhub', 42 | label: isSandbox() ? 'Sandbox' : 'Tonhub', 43 | icon: , 44 | onClick: handleConnectTonhub, 45 | }); 46 | } 47 | 48 | return ( 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/modules/search/views/SearchView/SearchView.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from 'antd'; 2 | import { useCallback, useMemo, useState } from 'react'; 3 | import { useNavigate } from 'react-router-dom'; 4 | import { Address } from 'ton'; 5 | import './SearchView.scss'; 6 | 7 | function isValidAddress(address: string) { 8 | try { 9 | Address.parse(address); 10 | return true; 11 | } catch { 12 | return false; 13 | } 14 | } 15 | 16 | export function SearchView() { 17 | const [address, setAddress] = useState(''); 18 | const navigate = useNavigate(); 19 | 20 | const navigateAddress = useCallback( 21 | (target: string) => { 22 | navigate(`/${target}/assets`); 23 | }, 24 | [navigate], 25 | ); 26 | 27 | const isValid = useMemo( 28 | () => isValidAddress(address), 29 | [address], 30 | ); 31 | 32 | const handleChange = useCallback( 33 | (event) => setAddress(event.target.value), 34 | [setAddress], 35 | ); 36 | 37 | const handleSearch = useCallback( 38 | () => { 39 | if (!address || !isValid) return; 40 | 41 | navigateAddress(address); 42 | }, 43 | [navigateAddress, address, isValid], 44 | ); 45 | 46 | return ( 47 | <> 48 |
49 |
50 | Scaleton 51 |
52 | 53 | 62 |
63 | 64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policies and Procedures 2 | 3 | This document outlines security procedures and general policies for the Express 4 | project. 5 | 6 | * [Reporting a Bug](#reporting-a-bug) 7 | * [Disclosure Policy](#disclosure-policy) 8 | * [Comments on this Policy](#comments-on-this-policy) 9 | 10 | ## Reporting a Bug 11 | 12 | We take all security bugs seriously. 13 | Thank you for improving the security of Scaleton. We appreciate your efforts and 14 | responsible disclosure and will make every effort to acknowledge your 15 | contributions. 16 | 17 | Report security bugs by contacting [Nick Nekilov](https://t.me/NickNekilov) with Telegram. 18 | 19 | The lead maintainer will acknowledge your report within 48 hours, and will send a 20 | more detailed response within 48 hours indicating the next steps in handling 21 | your report. After the initial reply to your report, the security team will 22 | endeavor to keep you informed of the progress towards a fix and full 23 | announcement, and may ask for additional information or guidance. 24 | 25 | Report security bugs in third-party modules to the person or team maintaining 26 | the module. You can also report a vulnerability through the 27 | [Node Security Project](https://nodesecurity.io/report). 28 | 29 | ## Disclosure Policy 30 | 31 | When the security team receives a security bug report, they will assign it to a 32 | primary handler. This person will coordinate the fix and release process, 33 | involving the following steps: 34 | 35 | * Confirm the problem and determine the affected versions. 36 | * Audit code to find any potential similar problems. 37 | * Prepare fixes for all releases still under maintenance. These fixes will be 38 | released as fast as possible to npm. 39 | 40 | ## Comments on this Policy 41 | 42 | If you have suggestions on how this process could be improved please submit a 43 | pull request. 44 | -------------------------------------------------------------------------------- /src/modules/wallets/ton-wallet/TonWalletClient.ts: -------------------------------------------------------------------------------- 1 | import { Wallet } from '../common/Wallet'; 2 | import { TonWalletProvider } from './types/TonWalletProvider'; 3 | 4 | export const TON_WALLET_EXTENSION_URL = 'https://chrome.google.com/webstore/detail/ton-wallet/nphplpgoakhhjchkkhmiggakijnkhfnd'; 5 | 6 | declare global { 7 | interface Window { 8 | ton?: TonWalletProvider; 9 | } 10 | } 11 | 12 | export class TonWalletClient { 13 | constructor( 14 | private readonly window: Window, 15 | ) { 16 | } 17 | 18 | private get ton(): TonWalletProvider | undefined { 19 | return this.window.ton; 20 | } 21 | 22 | get isAvailable(): boolean { 23 | return !!this.ton?.isTonWallet; 24 | } 25 | 26 | ready(timeout: number = 5000): Promise { 27 | return new Promise((resolve, reject) => { 28 | const timerId = setInterval( 29 | () => { 30 | if (this.isAvailable) { 31 | clearInterval(timerId); 32 | resolve(); 33 | } 34 | }, 35 | 50, 36 | ); 37 | 38 | setTimeout( 39 | () => reject(new Error('TON Wallet cannot be initialized')), 40 | timeout, 41 | ); 42 | }); 43 | } 44 | 45 | requestWallets(): Promise { 46 | return this.ton!.send('ton_requestWallets'); 47 | } 48 | 49 | watchAccounts(callback: (accounts: string[]) => void): void { 50 | this.ton!.on('ton_requestAccounts', callback); 51 | } 52 | 53 | sign(hexData: string): Promise { 54 | return this.ton!.send('ton_rawSign', [ 55 | { data: hexData }, 56 | ]); 57 | } 58 | 59 | sendTransaction(options: { 60 | to: string, 61 | value: string, 62 | data?: string, 63 | dataType?: 'boc' | 'hex' | 'base64' | 'text', 64 | stateInit?: string, 65 | }): Promise { 66 | return this.ton!.send('ton_sendTransaction', [options]); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/react'; 2 | import React from 'react'; 3 | import { isMobile } from 'react-device-detect'; 4 | import { createRoot } from 'react-dom/client'; 5 | import { Provider } from 'react-redux'; 6 | import { BrowserRouter } from 'react-router-dom'; 7 | import { TonhubConnector } from 'ton-x'; 8 | import App from './App'; 9 | import { tonClient } from './modules/assets/services'; 10 | import { CURRENT_NETWORK, isMainnet, isSandbox, isTestnet } from './modules/common/network'; 11 | import { TonWalletWalletAdapter } from './modules/wallets/ton-wallet/TonWalletWalletAdapter'; 12 | import { TonkeeperWalletAdapter } from './modules/wallets/tonkeeper/TonkeeperWalletAdapter'; 13 | import { TonhubWalletAdapter } from './modules/wallets/tonhub/TonhubWalletAdapter'; 14 | import { TonWalletClient } from './modules/wallets/ton-wallet/TonWalletClient'; 15 | 16 | import { walletService } from './modules/wallets/common/WalletService'; 17 | import { store } from './store'; 18 | import 'antd/dist/antd.css'; 19 | import './index.scss'; 20 | 21 | Sentry.init({ 22 | dsn: process.env.REACT_APP_SENTRY_DSN, 23 | environment: CURRENT_NETWORK, 24 | autoSessionTracking: true, 25 | }); 26 | 27 | if (isMainnet() || isTestnet()) { 28 | walletService.registerAdapter('tonkeeper', new TonkeeperWalletAdapter(store)); 29 | } 30 | 31 | if (!isMobile) { 32 | walletService.registerAdapter('ton-wallet', new TonWalletWalletAdapter(tonClient, new TonWalletClient(window))); 33 | } 34 | 35 | if (isMainnet() || isSandbox()) { 36 | walletService.registerAdapter('tonhub', new TonhubWalletAdapter( 37 | tonClient, 38 | new TonhubConnector({ testnet: isSandbox() }), 39 | )); 40 | } 41 | 42 | const container = document.getElementById('root'); 43 | const root = createRoot(container!); 44 | 45 | root.render( 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | -------------------------------------------------------------------------------- /src/modules/wallets/common/components/ConnectWalletButton.tsx: -------------------------------------------------------------------------------- 1 | import { WalletOutlined } from '@ant-design/icons'; 2 | import { Button, ButtonProps, Dropdown } from 'antd'; 3 | import React, { useCallback, useState } from 'react'; 4 | import { useAppDispatch, useAppSelector } from '../../../../hooks'; 5 | import { createWalletSession } from '../store'; 6 | import { ConnectWalletDropdownMenu } from './ConnectWalletDropdownMenu'; 7 | import type { MenuTheme } from 'antd/lib/menu/MenuContext'; 8 | 9 | export function ConnectWalletButton(props: ButtonProps & { theme?: MenuTheme }) { 10 | const [isVisible, setVisible] = useState(false); 11 | const dispatch = useAppDispatch(); 12 | const isLoading = useAppSelector(state => !!state.wallets.common.session && !state.wallets.common.wallet); 13 | 14 | const handleConnectTonWallet = useCallback( 15 | () => { 16 | setVisible(false); 17 | dispatch(createWalletSession('ton-wallet')); 18 | }, 19 | [dispatch], 20 | ); 21 | 22 | const handleConnectTonkeeper = useCallback( 23 | () => { 24 | setVisible(false); 25 | dispatch(createWalletSession('tonkeeper')); 26 | }, 27 | [dispatch] 28 | ); 29 | 30 | const handleConnectTonhub = useCallback( 31 | () => { 32 | setVisible(false); 33 | dispatch(createWalletSession('tonhub')); 34 | }, 35 | [dispatch] 36 | ); 37 | 38 | return ( 39 | 49 | )} 50 | placement="bottomRight" 51 | trigger={['click']} 52 | > 53 | 61 | 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { Col, Layout, Row } from 'antd'; 2 | import React, { useEffect } from 'react'; 3 | import { Route, Routes } from 'react-router-dom'; 4 | import { useAppDispatch } from './hooks'; 5 | import { NavBar } from './modules/layout/components/NavBar'; 6 | import { TonhubConnectModal } from './modules/wallets/tonhub/components/TonhubConnectModal/TonhubConnectModal'; 7 | import { TonkeeperConnectModal } from './modules/wallets/tonkeeper/components/TonkeeperConnectModal'; 8 | import { restoreSession } from './modules/wallets/common/store'; 9 | import { Connect } from './pages/Connect'; 10 | import { Jettons } from './pages/Jettons'; 11 | import { Search } from './pages/Search'; 12 | import { Trade } from './pages/Trade'; 13 | import './App.scss'; 14 | 15 | const { Footer } = Layout; 16 | 17 | function App() { 18 | const dispatch = useAppDispatch(); 19 | 20 | useEffect( 21 | () => { 22 | dispatch(restoreSession()); 23 | }, 24 | [dispatch], 25 | ); 26 | 27 | return ( 28 | <> 29 | 30 | 31 | 32 | 33 | }/> 34 | }/> 35 | }/> 36 | }/> 37 | }/> 38 | }/> 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | Scaleton © 2022 47 | 48 | 49 | 50 | Telegram 51 | GitHub 52 | 53 | 54 |
55 |
56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default App; 65 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/services/TradeService.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Address, TonClient } from 'ton'; 3 | import { TradeDirection } from '../../../contracts/enums/TradeDirection'; 4 | import { PoolContract } from '../../../contracts/PoolContract'; 5 | import { WalletService } from '../../../wallets/common/WalletService'; 6 | import { AssetCatalog } from '../../../assets/services/AssetCatalog'; 7 | import type { AssetRef } from '../../../assets/types'; 8 | import { AssetType } from '../../../assets/types'; 9 | 10 | const TRADE_GAS_FEES = 0.15; 11 | const FORWARD_FEES = 0.11; 12 | 13 | export class TradeService { 14 | constructor( 15 | private readonly tonClient: TonClient, 16 | private readonly assetCatalog: AssetCatalog, 17 | private readonly walletService: WalletService, 18 | ) { 19 | } 20 | 21 | async requestSwap( 22 | adapterId: string, 23 | session: S, 24 | asset: AssetRef, 25 | poolContractAddress: Address, 26 | tradeDirection: TradeDirection, 27 | sourceAmountIn: BN | number, 28 | minimumAmountOut: BN | number, 29 | queryId: BN | number, 30 | ): Promise { 31 | const poolContract = new PoolContract(this.tonClient, null as any, poolContractAddress); 32 | const swapRequestText = poolContract.createSwapRequestText(tradeDirection, minimumAmountOut, queryId); 33 | const wallet = this.walletService.getWalletAdapter(adapterId); 34 | 35 | switch (asset.type) { 36 | case AssetType.NATIVE: 37 | return wallet.requestTransfer( 38 | session, 39 | poolContract.address.toFriendly(), 40 | sourceAmountIn.toString(10), 41 | swapRequestText, 42 | 2 * 60 * 1000, 43 | ); 44 | 45 | case AssetType.JETTON: 46 | return wallet.requestJettonTransfer( 47 | session, 48 | asset.contractAddress, 49 | poolContract.address.toFriendly(), 50 | sourceAmountIn.toString(10), 51 | swapRequestText, 52 | 2 * 60 * 1000, 53 | FORWARD_FEES, 54 | TRADE_GAS_FEES, 55 | ); 56 | 57 | default: 58 | throw new Error('Swap operation with this asset type is not supported.'); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaleton-wallet", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@ant-design/icons": "^4.7.0", 7 | "@reduxjs/toolkit": "^1.8.1", 8 | "@sentry/react": "^6.19.6", 9 | "antd": "^4.19.5", 10 | "big.js": "^6.1.1", 11 | "buffer": "^6.0.3", 12 | "javascript-time-ago": "^2.3.13", 13 | "react": "^18.0.0", 14 | "react-device-detect": "^2.2.2", 15 | "react-dom": "^18.0.0", 16 | "react-ga": "^3.3.0", 17 | "react-qrcode-logo": "^2.7.0", 18 | "react-redux": "^8.0.0", 19 | "react-router-dom": "6", 20 | "ton": "^9.6.3", 21 | "ton-x": "^0.4.1", 22 | "tonweb": "^0.0.38" 23 | }, 24 | "scripts": { 25 | "start:testnet": "PORT=3001 react-app-rewired start", 26 | "start:sandbox": "PORT=3002 react-app-rewired start", 27 | "start:mainnet": "PORT=3003 react-app-rewired start", 28 | "build": "GENERATE_SOURCEMAP=false react-app-rewired build", 29 | "test": "react-app-rewired test", 30 | "lint": "eslint ./src" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "react-app", 35 | "react-app/jest" 36 | ], 37 | "rules": { 38 | "jsx-a11y/anchor-is-valid": "off", 39 | "no-multiple-empty-lines": "error", 40 | "quotes": [ 41 | "error", 42 | "single" 43 | ] 44 | } 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | }, 58 | "devDependencies": { 59 | "@testing-library/jest-dom": "^5.16.4", 60 | "@testing-library/react": "^12.1.4", 61 | "@testing-library/user-event": "^13.5.0", 62 | "@types/big.js": "^6.1.3", 63 | "@types/bn.js": "^5.1.0", 64 | "@types/javascript-time-ago": "^2.0.3", 65 | "@types/jest": "^27.4.1", 66 | "@types/node": "^16.11.26", 67 | "@types/react": "^17.0.44", 68 | "@types/react-dom": "^18.0.1", 69 | "react-app-rewired": "^2.2.1", 70 | "react-scripts": "5.0.0", 71 | "sass": "^1.50.0", 72 | "typescript": "^4.6.3", 73 | "yaml-loader": "^0.7.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/App.scss: -------------------------------------------------------------------------------- 1 | @import './mixins/mixins.scss'; 2 | 3 | $background-color: #ffffff; 4 | 5 | /* Layout */ 6 | 7 | section.ant-layout { 8 | background-color: $background-color; 9 | } 10 | 11 | main.compressed, 12 | footer > div.compressed, 13 | section.ant-layout > .ant-layout-header > div.ant-row { 14 | @include compressed-layout; 15 | } 16 | 17 | @media screen and (max-width: 410px) { 18 | .ant-layout-header .ant-menu-horizontal .ant-menu-item { 19 | .ant-tag { 20 | display: none; 21 | } 22 | } 23 | } 24 | 25 | @media screen and (max-width: 780px) { 26 | .ant-layout-header { 27 | padding: 0 16px !important; 28 | 29 | .navbar-logo { 30 | padding-right: 10px; 31 | 32 | .logo-text { 33 | display: none; 34 | } 35 | } 36 | 37 | .ant-menu-horizontal .ant-menu-item { 38 | padding: 0 10px !important; 39 | } 40 | 41 | .ant-col.links { 42 | display: none; 43 | } 44 | 45 | .wallet-button-section { 46 | &> button { 47 | width: 50px; 48 | 49 | & > span:last-of-type, 50 | .connect-wallet-text { 51 | display: none !important; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .ant-layout-footer { 58 | padding: 24px !important; 59 | } 60 | 61 | .jetton-wallet-view { 62 | padding: 0 24px !important; 63 | 64 | .asset-name { 65 | .name { 66 | display: none; 67 | } 68 | .symbol { 69 | display: inline !important; 70 | } 71 | } 72 | 73 | .asset-balance .symbol { 74 | display: none; 75 | } 76 | } 77 | } 78 | 79 | section.main-layout { 80 | background-color: $background-color; 81 | min-height: 100%; 82 | height: 100%; 83 | padding-bottom: 120px; 84 | margin-bottom: -120px; 85 | } 86 | 87 | footer.ant-layout-footer { 88 | margin-top: 50px; 89 | background: #002457; 90 | color: #ffffff; 91 | 92 | .links-section { 93 | text-align: right; 94 | 95 | a { 96 | color: rgba(#ffffff, 0.75); 97 | &:hover { 98 | color: #ffffff; 99 | } 100 | 101 | & + a { 102 | margin-left: 12px; 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/modules/jettons/pages/JettonWalletView.tsx: -------------------------------------------------------------------------------- 1 | import { CopyOutlined } from '@ant-design/icons'; 2 | import { Layout, PageHeader, message, Tabs } from 'antd'; 3 | import React, { useCallback } from 'react'; 4 | import { useNavigate, useParams } from 'react-router-dom'; 5 | import { useAppSelector } from '../../../hooks'; 6 | import { ConnectWalletView } from '../../wallets/common/pages/ConnectWalletView'; 7 | import { AddressText } from '../../common/components/AddressText/AddressText'; 8 | import { AssetsTabPaneContent } from '../../assets/components/AssetsTabPaneContent/AssetsTabPaneContent'; 9 | import { NftsTabPaneContent } from '../../nfts/components/NftsTabPaneContent/NftsTabPaneContent'; 10 | import { selectWalletAddress } from '../../wallets/common/selectors/selectWalletAddress'; 11 | import './JettonWalletView.scss'; 12 | 13 | const { Content } = Layout; 14 | 15 | export function JettonWalletView() { 16 | const walletAddress = useAppSelector(selectWalletAddress); 17 | const { address: account, module } = useParams(); 18 | const navigate = useNavigate(); 19 | const activeTab = module === 'nfts' ? 'nfts' : 'assets'; 20 | 21 | const isMyAccount = walletAddress === account; 22 | 23 | const handleAddressClick = useCallback( 24 | (e) => { 25 | e.preventDefault(); 26 | if (!account) return; 27 | navigator.clipboard.writeText(account); 28 | message.info('Address is saved to clipboard.'); 29 | }, 30 | [account], 31 | ); 32 | 33 | const handleChangeTab = useCallback( 34 | (tab: string) => navigate(`/${account}/${tab}`), 35 | [navigate, account], 36 | ); 37 | 38 | if (!account) { 39 | return ( 40 | 41 | ); 42 | } 43 | 44 | return ( 45 | 46 | 50 | 51 | 53 | {isMyAccount && ( 54 | (it's you) 55 | )} 56 | 57 | )} 58 | /> 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/assets/store.ts: -------------------------------------------------------------------------------- 1 | import { createSlice } from '@reduxjs/toolkit'; 2 | import { fetchHistory, hideAsset, importJetton, refreshBalances } from './actions'; 3 | import { assetCatalog } from './services'; 4 | import { TransactionType } from './types/TransactionType'; 5 | import type { AssetBalances, AssetRef } from './types'; 6 | 7 | interface AssetsState { 8 | assets: AssetRef[]; 9 | balancesLoading: boolean; 10 | balances: { 11 | [owner: string]: AssetBalances; 12 | }; 13 | 14 | history: { 15 | [assetId: string]: { 16 | transactions: { 17 | time: string; 18 | operation: TransactionType; 19 | from: string | null; 20 | to: string | null; 21 | amount: string; 22 | comment: string | null; 23 | }[]; 24 | isLoading: boolean; 25 | }; 26 | }; 27 | } 28 | 29 | const initialState: AssetsState = { 30 | assets: assetCatalog.getAssets(), 31 | balancesLoading: false, 32 | balances: {}, 33 | history: {}, 34 | }; 35 | 36 | const store = createSlice({ 37 | name: 'assets', 38 | initialState, 39 | reducers: {}, 40 | extraReducers(builder) { 41 | builder.addCase(refreshBalances.pending, (state) => { 42 | state.balancesLoading = true; 43 | }); 44 | 45 | builder.addCase(refreshBalances.fulfilled, (state, action) => { 46 | state.balancesLoading = false; 47 | state.balances[action.meta.arg] = action.payload; 48 | }); 49 | 50 | builder.addCase(refreshBalances.rejected, (state) => { 51 | state.balancesLoading = false; 52 | }); 53 | 54 | builder.addCase(fetchHistory.pending, (state, action) => { 55 | const accountHistory = state.history[action.meta.arg.assetId]; 56 | 57 | state.history[action.meta.arg.assetId] = { 58 | transactions: accountHistory?.transactions, 59 | isLoading: true, 60 | }; 61 | }); 62 | 63 | builder.addCase(fetchHistory.fulfilled, (state, action) => { 64 | state.history[action.meta.arg.assetId] = { 65 | transactions: action.payload, 66 | isLoading: false, 67 | }; 68 | }); 69 | 70 | builder.addCase(fetchHistory.rejected, (state, action) => { 71 | const accountHistory = state.history[action.meta.arg.assetId]; 72 | 73 | state.history[action.meta.arg.assetId] = { 74 | transactions: accountHistory?.transactions, 75 | isLoading: false, 76 | }; 77 | }); 78 | 79 | builder.addCase(importJetton.fulfilled, (state, action) => { 80 | state.assets = action.payload; 81 | }); 82 | 83 | builder.addCase(hideAsset.fulfilled, (state, action) => { 84 | state.assets = action.payload; 85 | }); 86 | } 87 | }); 88 | 89 | export default store.reducer; 90 | -------------------------------------------------------------------------------- /src/modules/assets/services/AssetCatalog.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import { Address } from 'ton'; 3 | import { AssetStore } from './AssetStore'; 4 | import { AssetType } from '../types'; 5 | import type { AssetRef, JettonRef, Transaction } from '../types'; 6 | import type { AssetAdapter } from './AssetAdapter'; 7 | 8 | export class AssetCatalog { 9 | private assets: AssetRef[] = []; 10 | private adapters: Map = new Map(); 11 | 12 | constructor( 13 | private readonly assetStore: AssetStore, 14 | private readonly standardAssets: AssetRef[], 15 | ) { 16 | this.reload(); 17 | } 18 | 19 | registerAdapter(type: AssetRef['type'], adapter: AssetAdapter) { 20 | this.adapters.set(type, adapter); 21 | } 22 | 23 | reload() { 24 | this.assets = [ 25 | ...this.standardAssets, 26 | ...this.assetStore.load().map(asset => ({ 27 | ...asset, 28 | isCustom: true, 29 | })), 30 | ]; 31 | } 32 | 33 | getAssets(): AssetRef[] { 34 | return this.assets; 35 | } 36 | 37 | getAsset(assetId: string): AssetRef { 38 | const asset = this.assets.find(asset => asset.id === assetId); 39 | 40 | if (!asset) { 41 | throw new Error('Asset is not registered.'); 42 | } 43 | 44 | return asset; 45 | } 46 | 47 | importJetton(jetton: Omit) { 48 | const jettonAddress = Address.parse(jetton.contractAddress); 49 | 50 | this.assetStore.store([ 51 | ...this.assetStore.load(), 52 | { 53 | id: `jetton:${jettonAddress.toString()}`, 54 | type: AssetType.JETTON, 55 | ...jetton, 56 | }, 57 | ]); 58 | 59 | this.reload(); 60 | } 61 | 62 | removeAsset(assetId: string): void { 63 | this.assetStore.store( 64 | this.assetStore 65 | .load() 66 | .filter(asset => asset.id !== assetId), 67 | ); 68 | 69 | this.reload(); 70 | } 71 | 72 | async getBalance(ownerAddress: Address, assetId: string): Promise { 73 | const asset = this.getAsset(assetId); 74 | const adapter = this.getAdapter(asset.type); 75 | 76 | return adapter.getBalance(ownerAddress, asset); 77 | } 78 | 79 | async getTransactions(ownerAddress: Address, assetId: string): Promise { 80 | const asset = this.getAsset(assetId); 81 | const adapter = this.getAdapter(asset.type); 82 | 83 | return adapter.getTransactions(ownerAddress, asset); 84 | } 85 | 86 | private getAdapter(assetType: AssetType): AssetAdapter { 87 | const adapter = this.adapters.get(assetType); 88 | 89 | if (!adapter) { 90 | throw new Error('Adapter is not registered.'); 91 | } 92 | 93 | return adapter; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/modules/assets/services/adapters/JettonAssetAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import BN from 'bn.js'; 3 | import { Address, TonClient } from 'ton'; 4 | import { JettonMasterContract } from '../../../contracts/JettonMasterContract'; 5 | import { JettonOperation } from '../../../jettons/enums/JettonOperation'; 6 | import type { JettonRef, Transaction } from '../../types'; 7 | import type { AssetAdapter } from '../AssetAdapter'; 8 | 9 | export class JettonAssetAdapter implements AssetAdapter { 10 | constructor( 11 | private readonly tonClient: TonClient, 12 | ) { 13 | } 14 | 15 | async getBalance(ownerAddress: Address, asset: JettonRef): Promise { 16 | try { 17 | const contract = new JettonMasterContract( 18 | this.tonClient, 19 | null as any, 20 | Address.parse(asset.contractAddress), 21 | ); 22 | 23 | const jettonWallet = await contract.getJettonWallet(ownerAddress); 24 | const { balance } = await jettonWallet.getData(); 25 | 26 | return new Big(balance.toString()).div(1_000_000_000); 27 | } catch { 28 | return new Big(0); 29 | } 30 | } 31 | 32 | async getTransactions(ownerAddress: Address, asset: JettonRef): Promise { 33 | try { 34 | const contract = new JettonMasterContract(this.tonClient, null as any, Address.parse(asset.contractAddress)); 35 | const jettonWallet = await contract.getJettonWallet(ownerAddress); 36 | const jettonTransactions = await jettonWallet.getTransactions(); 37 | 38 | return jettonTransactions.map((jettonTransaction): Transaction => ({ 39 | queryId: new BN(jettonTransaction.queryId), 40 | time: new Date(jettonTransaction.time * 1000), 41 | operation: jettonTransaction.operation === JettonOperation.TRANSFER 42 | ? 'out' 43 | : (jettonTransaction.operation === JettonOperation.BURN 44 | ? 'burn' 45 | : (jettonTransaction.from ? 'in' : 'mint')), 46 | from: jettonTransaction.operation === JettonOperation.INTERNAL_TRANSFER 47 | ? (jettonTransaction.from ? Address.parse(jettonTransaction.from) : null) 48 | : ownerAddress, 49 | to: jettonTransaction.operation === JettonOperation.TRANSFER 50 | ? (jettonTransaction.destination ? Address.parse(jettonTransaction.destination) : null) 51 | : ownerAddress, 52 | amount: new Big(jettonTransaction.amount).div(1_000_000_000), 53 | comment: jettonTransaction.operation === JettonOperation.TRANSFER || jettonTransaction.operation === JettonOperation.INTERNAL_TRANSFER 54 | ? jettonTransaction.comment 55 | : null, 56 | body: null, 57 | })); 58 | } catch { 59 | return []; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/modules/contracts/JettonMasterContract.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Cell } from 'ton'; 3 | import { JettonWalletContract } from './JettonWalletContract'; 4 | import type { ContractSource, TonClient } from 'ton'; 5 | import { Address } from 'ton'; 6 | 7 | export class JettonMasterContract { 8 | private static knownWallets: Map = new Map(); 9 | 10 | constructor( 11 | private readonly client: TonClient, 12 | public readonly source: ContractSource, 13 | public readonly address: Address, 14 | ) { 15 | this.client = client; 16 | this.source = source; 17 | this.address = address; 18 | } 19 | 20 | createChangeAdminRequest(newAdminAddress: Address): Cell { 21 | const cell = new Cell(); 22 | 23 | cell.bits.writeUint(3, 32); // op 24 | cell.bits.writeUint(0, 64); // queryId 25 | cell.bits.writeAddress(newAdminAddress); 26 | 27 | return cell; 28 | } 29 | 30 | async getJettonData() { 31 | const { stack } = await this.client.callGetMethod(this.address, 'get_jetton_data', []); 32 | 33 | const totalSupply = new BN(stack[0][1].replace(/^0x/, ''), 'hex'); 34 | 35 | const adminAddress = Cell 36 | .fromBoc(Buffer.from(stack[2][1].bytes, 'base64'))[0] 37 | .beginParse() 38 | .readAddress(); 39 | 40 | const content = Cell.fromBoc(Buffer.from(stack[3][1].bytes, 'base64'))[0].bits.buffer.slice(1); 41 | const jettonWalletCode = Cell.fromBoc(Buffer.from(stack[4][1].bytes, 'base64'))[0]; 42 | 43 | return { 44 | totalSupply, 45 | adminAddress, 46 | content, 47 | jettonWalletCode, 48 | }; 49 | } 50 | 51 | private async resolveWalletAddress(owner: Address): Promise
{ 52 | let knownWalletAddress = JettonMasterContract.knownWallets.get(`${this.address.toString()}|${owner.toString()}`); 53 | 54 | if (knownWalletAddress) { 55 | return Address.parseRaw(knownWalletAddress); 56 | } 57 | 58 | const ownerAddressCell = new Cell(); 59 | ownerAddressCell.bits.writeAddress(owner); 60 | 61 | const { stack } = await this.client.callGetMethod(this.address, 'get_wallet_address', [ 62 | [ 63 | 'tvm.Slice', 64 | ownerAddressCell.toBoc({ idx: false }).toString('base64'), 65 | ], 66 | ]); 67 | 68 | const walletAddress = Cell 69 | .fromBoc(Buffer.from(stack[0][1].bytes, 'base64'))[0] 70 | .beginParse() 71 | .readAddress()!; 72 | 73 | JettonMasterContract.knownWallets.set(`${this.address.toString()}|${owner.toString()}`, walletAddress.toString()); 74 | 75 | return walletAddress; 76 | } 77 | 78 | async getJettonWallet(ownerAddress: Address): Promise { 79 | return new JettonWalletContract( 80 | this.client, 81 | null as any, 82 | await this.resolveWalletAddress(ownerAddress), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetTransferForm.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input } from 'antd'; 2 | import Big from 'big.js'; 3 | import React, { useCallback } from 'react'; 4 | import TonWeb from 'tonweb'; 5 | 6 | export interface AssetTransferFormValues { 7 | recipient: string; 8 | amount: string; 9 | comment: string; 10 | } 11 | 12 | interface AssetTransferFormProps { 13 | isLoading: boolean; 14 | assetSymbol: string; 15 | onChange: (hasErrors: boolean, values: AssetTransferFormValues) => void; 16 | } 17 | 18 | export function AssetTransferForm({ isLoading, assetSymbol, onChange }: AssetTransferFormProps) { 19 | const [form] = Form.useForm(); 20 | 21 | const handleChange = useCallback( 22 | () => { 23 | form 24 | .validateFields() 25 | .then(values => onChange(false, values)) 26 | .catch(error => onChange(true, error.values)); 27 | }, 28 | [form, onChange], 29 | ); 30 | 31 | return ( 32 |
41 | 60 | 61 | 62 | 63 | 80 | 85 | 86 | 87 | 95 | 96 | 97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /src/modules/wallets/tonkeeper/components/TonkeeperConnectModal.tsx: -------------------------------------------------------------------------------- 1 | import { QrcodeOutlined, SyncOutlined } from '@ant-design/icons'; 2 | import { Modal } from 'antd'; 3 | import React, { useCallback, useEffect } from 'react'; 4 | import { isAndroid, isIOS, isMobile } from 'react-device-detect'; 5 | import { QRCode } from 'react-qrcode-logo'; 6 | import { useAppDispatch, useAppSelector } from '../../../../hooks'; 7 | import { TonkeeperSession } from '../TonkeeperWalletAdapter'; 8 | import { resetSession } from '../../common/store'; 9 | import tonkeeperIconPath from '../../common/components/WalletIcon/icons/tonkeeper.svg'; 10 | import './TonkeeperConnectModal.scss'; 11 | 12 | export function TonkeeperConnectModal() { 13 | const dispatch = useAppDispatch(); 14 | 15 | const isTonkeeper = useAppSelector(state => state.wallets.common.adapterId === 'tonkeeper'); 16 | const session = useAppSelector(state => state.wallets.common.session as TonkeeperSession); 17 | const isWalletReady = useAppSelector(state => !!state.wallets.common.wallet); 18 | const isConnecting = useAppSelector(state => state.wallets.common.isConnecting); 19 | 20 | const isMobileAppSupported = isIOS || isAndroid; 21 | 22 | const handleCancel = useCallback( 23 | () => { 24 | dispatch(resetSession()); 25 | }, 26 | [dispatch], 27 | ); 28 | 29 | useEffect( 30 | () => { 31 | if (!isTonkeeper || !session || isWalletReady || !isConnecting || !isMobileAppSupported) { 32 | return; 33 | } 34 | 35 | window.location.assign(session.link); 36 | }, 37 | [isTonkeeper, session, isWalletReady, isConnecting, isMobileAppSupported], 38 | ); 39 | 40 | if (!isTonkeeper || !session || isWalletReady || !isConnecting) { 41 | return null; 42 | } 43 | 44 | return ( 45 | 53 | {isMobileAppSupported ? ( 54 | <> 55 |
    56 |
  1. Open Tonkeeper application
  2. 57 |
  3. Confirm the authentication request
  4. 58 |
59 | 60 |
61 | 62 |
63 | 64 | ) : ( 65 | <> 66 |
    67 |
  1. Open Tonkeeper application
  2. 68 |
  3. Touch icon in the top right corner
  4. 69 |
  5. Scan the next QR code:
  6. 70 |
71 | 72 | 85 | 86 | )} 87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/modules/nfts/components/NftsTabPaneContent/NftsTabPaneContent.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Col, Empty, PageHeader, Row, Skeleton } from 'antd'; 2 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 3 | import { SquareImage } from '../../../common/components/SquareImage/SquareImage'; 4 | import { NftItemPreviewModal } from '../NftItemPreviewModal/NftItemPreviewModal'; 5 | import { getNftItemsByOwner } from '../../api/getNftItemsByOwner'; 6 | import type { NftItem } from '../../types/NftItem'; 7 | import './NftsTabPaneContent.scss'; 8 | 9 | interface NftsTabPaneContentProps { 10 | account: string; 11 | } 12 | 13 | export function NftsTabPaneContent({ account }: NftsTabPaneContentProps) { 14 | const [assets, setAssets] = useState([]); 15 | const [isLoading, setLoading] = useState(true); 16 | 17 | const [previewAssetAddress, setPreviewAssetAddress] = useState(null); 18 | const previewAsset = useMemo( 19 | () => assets.find(asset => asset.address === previewAssetAddress) ?? null, 20 | [assets, previewAssetAddress], 21 | ); 22 | 23 | const fetchAssets = useCallback( 24 | () => { 25 | setLoading(true); 26 | getNftItemsByOwner(account) 27 | .then(items => setAssets(items)) 28 | .catch((error) => { 29 | console.log(error); 30 | // Show error 31 | }) 32 | .then(() => setLoading(false)); 33 | }, 34 | [setAssets, setLoading, account], 35 | ); 36 | 37 | useEffect( 38 | () => fetchAssets(), 39 | [setAssets, setLoading, account, fetchAssets], 40 | ); 41 | 42 | return ( 43 |
44 | 49 | Refresh 50 | , 51 | ]} 52 | /> 53 | 54 | {isLoading && !assets.length && ( 55 | 56 | )} 57 | 58 | {!!assets.length && ( 59 | 60 | {assets.map((asset, assetIndex) => ( 61 | 62 | setPreviewAssetAddress(asset.address)} 65 | cover={ 66 | 70 | } 71 | > 72 | 73 | 74 | 75 | ))} 76 | 77 | )} 78 | 79 | {!isLoading && !assets.length && ( 80 |
81 | 85 |
86 | )} 87 | 88 | {previewAsset && ( 89 | setPreviewAssetAddress(null)} 92 | /> 93 | )} 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/modules/assets/services/adapters/NativeAssetAdapter.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import BN from 'bn.js'; 3 | import { Address, Cell, parseTransaction, TonClient } from 'ton'; 4 | import { AssetRef, Transaction } from '../../types'; 5 | import { AssetAdapter } from '../AssetAdapter'; 6 | 7 | export class NativeAssetAdapter implements AssetAdapter { 8 | constructor( 9 | private readonly tonClient: TonClient, 10 | ) { 11 | } 12 | 13 | async getBalance(ownerAddress: Address, asset: AssetRef): Promise { 14 | try { 15 | const balance = await this.tonClient.getBalance(ownerAddress); 16 | return new Big(balance.toString()).div(1_000_000_000); 17 | } catch { 18 | return new Big(0); 19 | } 20 | } 21 | 22 | async getTransactions(ownerAddress: Address, asset: AssetRef): Promise { 23 | const transactions = await this.tonClient.getTransactions(ownerAddress, { 24 | limit: 20, 25 | }); 26 | 27 | const extractQueryId = (data?: Buffer): BN => { 28 | try { 29 | if (!data || !data.length) return new BN(0); 30 | 31 | const body = Cell.fromBoc(data)[0]; 32 | const bodySlice = body.beginParse(); 33 | 34 | bodySlice.skip(32); 35 | 36 | return bodySlice.readUint(64); 37 | } catch { 38 | return new BN(0); 39 | } 40 | } 41 | 42 | return transactions 43 | .map((transaction): Transaction[] | null => { 44 | if (!transaction.inMessage) return null; 45 | 46 | const time = new Date(transaction.time * 1000); 47 | 48 | const result: Transaction[] = []; 49 | 50 | // Note: Skip external messages of the wallet to hide "noise". 51 | if (transaction.inMessage.source) { 52 | const inMessageBody = parseTransaction(0, Cell.fromBoc(Buffer.from(transactions[0].data, 'base64'))[0].beginParse()) 53 | .inMessage?.body.beginParse(); 54 | 55 | result.push({ 56 | queryId: new BN(0), 57 | time, 58 | operation: 'in', 59 | from: transaction.inMessage.source ?? null, 60 | to: transaction.inMessage.destination ?? null, 61 | amount: new Big(transaction.inMessage.value.toString()).div(1_000_000_000), 62 | comment: transaction.inMessage.body?.type === 'text' 63 | ? transaction.inMessage.body.text 64 | : null, 65 | body: inMessageBody?.readRemainingBytes() ?? null, 66 | }); 67 | } 68 | 69 | result.push( 70 | ...transaction.outMessages.map((message): Transaction => ({ 71 | queryId: message.body?.type === 'data' ? extractQueryId(message.body.data) : new BN(0), 72 | time, 73 | operation: 'out', 74 | from: message.source ?? null, 75 | to: message.destination ?? null, 76 | amount: new Big(message.value.toString()).div(1_000_000_000), 77 | comment: message.body?.type === 'text' 78 | ? message.body.text 79 | : null, 80 | body: null, 81 | })), 82 | ); 83 | 84 | result.reverse(); 85 | 86 | return result; 87 | }) 88 | .flat() 89 | .filter(transaction => !!transaction) as Transaction[]; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/modules/wallets/tonhub/components/TonhubConnectModal/TonhubConnectModal.tsx: -------------------------------------------------------------------------------- 1 | import { QrcodeOutlined, SyncOutlined } from '@ant-design/icons'; 2 | import { Modal } from 'antd'; 3 | import React, { useCallback, useEffect } from 'react'; 4 | import { isAndroid, isIOS, isMobile } from 'react-device-detect'; 5 | import { QRCode } from 'react-qrcode-logo'; 6 | import { TonhubCreatedSession } from 'ton-x'; 7 | import { useAppDispatch, useAppSelector } from '../../../../../hooks'; 8 | import { resetSession } from '../../../common/store'; 9 | import tonhubIconPath from '../../../common/components/WalletIcon/icons/tonhub.png'; 10 | import sandboxIconPath from '../../../common/components/WalletIcon/icons/sandbox.png'; 11 | import { isSandbox } from '../../../../common/network'; 12 | import './TonhubConnectModal.scss'; 13 | 14 | export function TonhubConnectModal() { 15 | const dispatch = useAppDispatch(); 16 | 17 | const isTonhub = useAppSelector(state => state.wallets.common.adapterId === 'tonhub'); 18 | const session = useAppSelector(state => state.wallets.common.session as TonhubCreatedSession); 19 | const isWalletReady = useAppSelector(state => !!state.wallets.common.wallet); 20 | const isConnecting = useAppSelector(state => state.wallets.common.isConnecting); 21 | 22 | const applicationName = isSandbox() ? 'Sandbox' : 'Tonhub'; 23 | const applicationIcon = isSandbox() ? sandboxIconPath : tonhubIconPath; 24 | 25 | const isMobileAppSupported = isIOS || isAndroid; 26 | 27 | const handleCancel = useCallback( 28 | () => { 29 | dispatch(resetSession()); 30 | }, 31 | [dispatch], 32 | ); 33 | 34 | useEffect( 35 | () => { 36 | if (!isTonhub || !session || isWalletReady || !isConnecting || !isMobileAppSupported) { 37 | return; 38 | } 39 | 40 | window.location.assign(session.link); 41 | }, 42 | [isTonhub, session, isWalletReady, isConnecting, isMobileAppSupported], 43 | ); 44 | 45 | if (!isTonhub || !session || isWalletReady || !isConnecting) { 46 | return null; 47 | } 48 | 49 | return ( 50 | 58 | {isMobileAppSupported ? ( 59 | <> 60 |
    61 |
  1. Open {applicationName} application
  2. 62 |
  3. Confirm the authentication request
  4. 63 |
64 | 65 |
66 | 67 |
68 | 69 | ) : ( 70 | <> 71 |
    72 |
  1. Open {applicationName} application
  2. 73 |
  3. Touch icon in the top right corner
  4. 74 |
  5. Scan the next QR code:
  6. 75 |
76 | 77 | 90 | 91 | )} 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /src/modules/contracts/PoolContract.ts: -------------------------------------------------------------------------------- 1 | import { Big } from 'big.js'; 2 | import BN from 'bn.js'; 3 | import { contractAddress, TonClient, ContractSource, Address } from 'ton'; 4 | import { PoolStatus } from './enums/PoolStatus'; 5 | import { TradeDirection } from './enums/TradeDirection'; 6 | 7 | export class PoolContract { 8 | constructor( 9 | private readonly client: TonClient, 10 | public readonly source: ContractSource, 11 | public readonly address: Address, 12 | ) { 13 | } 14 | 15 | static create( 16 | client: TonClient, 17 | source: ContractSource, 18 | ): PoolContract { 19 | return new PoolContract( 20 | client, 21 | source, 22 | contractAddress(source), 23 | ); 24 | } 25 | 26 | isDeployed(): Promise { 27 | return this.client.isContractDeployed(this.address); 28 | } 29 | 30 | createDepositRequestText( 31 | tradeDirection: TradeDirection, 32 | minimumPoolTokenAmount: number | BN = 0, 33 | queryId: number | BN = 0, 34 | ) { 35 | const queryIdText = new BN(queryId).toString('hex', 16); 36 | const tradeDirectionText = new BN(tradeDirection).toString('hex', 1); 37 | const minimumPoolTokenAmountText = new BN(minimumPoolTokenAmount).toString('hex', 64); 38 | 39 | return `dep#${queryIdText}${tradeDirectionText}${minimumPoolTokenAmountText}`; 40 | } 41 | 42 | createSwapRequestText( 43 | tradeDirection: TradeDirection, 44 | minimumAmountOut: number | BN = 0, 45 | queryId: number | BN = 0, 46 | ) { 47 | const queryIdText = new BN(queryId).toString('hex', 16); 48 | const tradeDirectionText = new BN(tradeDirection).toString('hex', 1); 49 | const minimumAmountOutText = new BN(minimumAmountOut).toString('hex', 64); 50 | 51 | return `swp#${queryIdText}${tradeDirectionText}${minimumAmountOutText}`; 52 | } 53 | 54 | createWithdrawSingleRequestText( 55 | tradeDirection: TradeDirection, 56 | minimumAmountOut: BN | number, 57 | queryId: number | BN = 0, 58 | ) { 59 | const queryIdText = new BN(queryId).toString('hex', 16); 60 | const tradeDirectionText = new BN(tradeDirection).toString('hex', 1); 61 | const minimumAmountOutText = new BN(minimumAmountOut).toString('hex', 64); 62 | 63 | return `wds#${queryIdText}${tradeDirectionText}${minimumAmountOutText}`; 64 | } 65 | 66 | async getStatus(): Promise { 67 | const { stack } = await this.client.callGetMethod(this.address, 'get_status', []); 68 | 69 | return parseInt(stack[0][1].replace(/^0x/, ''), 16); 70 | } 71 | 72 | async getTokenPrices(): Promise { 73 | const { stack } = await this.client.callGetMethod(this.address, 'get_token_price', []); 74 | 75 | const leftTokenNumerator = new BN(stack[0][1].replace(/^0x/, ''), 'hex'); 76 | const leftTokenDenominator = new BN(stack[1][1].replace(/^0x/, ''), 'hex'); 77 | 78 | const leftTokenPrice = new Big(leftTokenNumerator.toString()).div(leftTokenDenominator.toString()); 79 | const rightTokenPrice = new Big(1).div(leftTokenPrice); 80 | 81 | console.log(leftTokenPrice.toNumber()); 82 | 83 | return { 84 | leftTokenPrice, 85 | rightTokenPrice, 86 | }; 87 | } 88 | 89 | async estimateSwap(amountIn: number | BN, tradeDirection: TradeDirection): Promise { 90 | const { stack } = await this.client.callGetMethod(this.address, 'estimate_swap', [ 91 | ['int', amountIn.toString()], 92 | ['int', tradeDirection.toString()], 93 | ]); 94 | 95 | return new Big(new BN(stack[0][1].replace(/^0x/, ''), 'hex').toString()).div(1_000_000_000); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/modules/contracts/JettonWalletContract.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Address, Cell } from 'ton'; 3 | import { JettonOperation } from '../jettons/enums/JettonOperation'; 4 | import { parseInternalTransferTransaction } from './parsers/parseInternalTransferTransaction'; 5 | import { parseTransferTransaction } from './parsers/parseTransferTransaction'; 6 | import type { JettonTransaction } from '../jettons/types/JettonTransaction'; 7 | import type { TonClient, Contract, ContractSource } from 'ton'; 8 | import { parseBurnTransaction } from './parsers/parseBurnTransaction'; 9 | 10 | export class JettonWalletContract implements Contract { 11 | constructor( 12 | private readonly client: TonClient, 13 | public readonly source: ContractSource, 14 | public readonly address: Address, 15 | ) { 16 | } 17 | 18 | isDeployed() { 19 | return this.client.isContractDeployed(this.address); 20 | } 21 | 22 | async getData() { 23 | const { stack } = await this.client.callGetMethod(this.address, 'get_wallet_data', []); 24 | 25 | const balance = new BN(stack[0][1].replace(/^0x/, ''), 'hex'); 26 | // balance 27 | // owner_address 28 | // jetton_master_address 29 | // jetton_wallet_code 30 | 31 | return { 32 | balance, 33 | }; 34 | } 35 | 36 | async getTransactions() { 37 | const transactions = await this.client.getTransactions(this.address, { 38 | limit: 20, 39 | }); 40 | 41 | return transactions 42 | .map((transaction): JettonTransaction | null => { 43 | if (transaction.inMessage?.body?.type !== 'data') { 44 | return null; // Not a jetton transaction 45 | } 46 | 47 | const bodySlice = Cell.fromBoc(transaction.inMessage.body.data)[0].beginParse(); 48 | const operation = bodySlice.readUint(32).toNumber(); 49 | 50 | switch (operation) { 51 | case JettonOperation.TRANSFER: 52 | return parseTransferTransaction(bodySlice, transaction); 53 | 54 | case JettonOperation.INTERNAL_TRANSFER: 55 | return parseInternalTransferTransaction(bodySlice, transaction); 56 | 57 | case JettonOperation.BURN: 58 | return parseBurnTransaction(bodySlice, transaction); 59 | 60 | default: 61 | return null; // Unknown operation 62 | } 63 | }) 64 | .filter(transaction => !!transaction) as JettonTransaction[]; 65 | } 66 | 67 | createTransferRequest({ 68 | queryId = 0, 69 | amount, 70 | destination, 71 | responseDestination = null, 72 | forwardAmount = 0, 73 | forwardPayload = null, 74 | }: { 75 | queryId: number | BN, 76 | amount: number | BN; 77 | destination: Address; 78 | responseDestination?: Address | null; 79 | forwardAmount: number | BN; 80 | forwardPayload: Cell | null; 81 | }): Cell { 82 | const cell = new Cell(); 83 | 84 | cell.bits.writeUint(0xf8a7ea5, 32); 85 | cell.bits.writeUint(queryId, 64); 86 | cell.bits.writeCoins(amount); 87 | cell.bits.writeAddress(destination); 88 | cell.bits.writeAddress(responseDestination); 89 | cell.bits.writeBit(false); 90 | cell.bits.writeCoins(forwardAmount); 91 | 92 | if (!forwardPayload || forwardPayload.bits.length <= cell.bits.available) { 93 | cell.bits.writeBit(false); 94 | if (forwardPayload) { 95 | cell.writeCell(forwardPayload); 96 | } 97 | } else { 98 | cell.bits.writeBit(true); 99 | cell.withReference(forwardPayload); 100 | } 101 | 102 | return cell; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetHistoryModal.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Modal, Table } from 'antd'; 2 | import React from 'react'; 3 | import { useAppSelector } from '../../../hooks'; 4 | import { presentBalance } from '../../jettons/utils/presentBalance'; 5 | import { timeAgo } from '../../jettons/utils/timeAgo'; 6 | import { AddressText } from '../../common/components/AddressText/AddressText'; 7 | import { AssetOperationTag } from './AssetOperationTag/AssetOperationTag'; 8 | import { createHistorySelector } from '../selectors/createHistorySelector'; 9 | import { createHistoryLoadingSelector } from '../selectors/createHistoryLoadingSelector'; 10 | import { createAssetSymbolSelector } from '../selectors/createAssetSymbolSelector'; 11 | import { TransactionType } from '../types'; 12 | import { Link } from 'react-router-dom'; 13 | 14 | interface AssetHistoryModalProps { 15 | account: string; 16 | assetId: string; 17 | onClose: () => void; 18 | } 19 | 20 | export function AssetHistoryModal({ account, assetId, onClose }: AssetHistoryModalProps) { 21 | const selectHistoryLoading = createHistoryLoadingSelector(account, assetId); 22 | const selectHistory = createHistorySelector(account, assetId); 23 | const selectAssetSymbol = createAssetSymbolSelector(assetId); 24 | 25 | const isHistoryLoading = useAppSelector(selectHistoryLoading); 26 | const history = useAppSelector(selectHistory); 27 | const symbol = useAppSelector(selectAssetSymbol); 28 | 29 | return ( 30 | 37 | Close 38 | , 39 | ]} 40 | onCancel={onClose} 41 | destroyOnClose 42 | > 43 | ({ 47 | key: transactionIndex, 48 | time: timeAgo.format(new Date(transaction.time)), 49 | operation: transaction.operation, 50 | from: transaction.operation === 'out' ? account : transaction.from, 51 | to: transaction.operation === 'out' ? transaction.to : account, 52 | amount: transaction.amount, 53 | comment: transaction.comment, 54 | }))} 55 | > 56 | 63 | 64 | } 70 | /> 71 | 72 | } 78 | /> 79 | 80 | } 86 | /> 87 | 88 | <>{presentBalance(amount)} {symbol}} 96 | /> 97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /src/modules/common/components/SquareImage/SquareImage.tsx: -------------------------------------------------------------------------------- 1 | import { Image } from 'antd'; 2 | import './SquareImage.scss'; 3 | 4 | interface SquareImageProps { 5 | alt: string; 6 | src: string; 7 | preview?: boolean; 8 | } 9 | 10 | const FALLBACK_IMAGE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMIAAADDCAYAAADQvc6UAAABRWlDQ1BJQ0MgUHJvZmlsZQAAKJFjYGASSSwoyGFhYGDIzSspCnJ3UoiIjFJgf8LAwSDCIMogwMCcmFxc4BgQ4ANUwgCjUcG3awyMIPqyLsis7PPOq3QdDFcvjV3jOD1boQVTPQrgSkktTgbSf4A4LbmgqISBgTEFyFYuLykAsTuAbJEioKOA7DkgdjqEvQHEToKwj4DVhAQ5A9k3gGyB5IxEoBmML4BsnSQk8XQkNtReEOBxcfXxUQg1Mjc0dyHgXNJBSWpFCYh2zi+oLMpMzyhRcASGUqqCZ16yno6CkYGRAQMDKMwhqj/fAIcloxgHQqxAjIHBEugw5sUIsSQpBobtQPdLciLEVJYzMPBHMDBsayhILEqEO4DxG0txmrERhM29nYGBddr//5/DGRjYNRkY/l7////39v///y4Dmn+LgeHANwDrkl1AuO+pmgAAADhlWElmTU0AKgAAAAgAAYdpAAQAAAABAAAAGgAAAAAAAqACAAQAAAABAAAAwqADAAQAAAABAAAAwwAAAAD9b/HnAAAHlklEQVR4Ae3dP3PTWBSGcbGzM6GCKqlIBRV0dHRJFarQ0eUT8LH4BnRU0NHR0UEFVdIlFRV7TzRksomPY8uykTk/zewQfKw/9znv4yvJynLv4uLiV2dBoDiBf4qP3/ARuCRABEFAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghggQAQZQKAnYEaQBAQaASKIAQJEkAEEegJmBElAoBEgghgg0Aj8i0JO4OzsrPv69Wv+hi2qPHr0qNvf39+iI97soRIh4f3z58/u7du3SXX7Xt7Z2enevHmzfQe+oSN2apSAPj09TSrb+XKI/f379+08+A0cNRE2ANkupk+ACNPvkSPcAAEibACyXUyfABGm3yNHuAECRNgAZLuYPgEirKlHu7u7XdyytGwHAd8jjNyng4OD7vnz51dbPT8/7z58+NB9+/bt6jU/TI+AGWHEnrx48eJ/EsSmHzx40L18+fLyzxF3ZVMjEyDCiEDjMYZZS5wiPXnyZFbJaxMhQIQRGzHvWR7XCyOCXsOmiDAi1HmPMMQjDpbpEiDCiL358eNHurW/5SnWdIBbXiDCiA38/Pnzrce2YyZ4//59F3ePLNMl4PbpiL2J0L979+7yDtHDhw8vtzzvdGnEXdvUigSIsCLAWavHp/+qM0BcXMd/q25n1vF57TYBp0a3mUzilePj4+7k5KSLb6gt6ydAhPUzXnoPR0dHl79WGTNCfBnn1uvSCJdegQhLI1vvCk+fPu2ePXt2tZOYEV6/fn31dz+shwAR1sP1cqvLntbEN9MxA9xcYjsxS1jWR4AIa2Ibzx0tc44fYX/16lV6NDFLXH+YL32jwiACRBiEbf5KcXoTIsQSpzXx4N28Ja4BQoK7rgXiydbHjx/P25TaQAJEGAguWy0+2Q8PD6/Ki4R8EVl+bzBOnZY95fq9rj9zAkTI2SxdidBHqG9+skdw43borCXO/ZcJdraPWdv22uIEiLA4q7nvvCug8WTqzQveOH26fodo7g6uFe/a17W3+nFBAkRYENRdb1vkkz1CH9cPsVy/jrhr27PqMYvENYNlHAIesRiBYwRy0V+8iXP8+/fvX11Mr7L7ECueb/r48eMqm7FuI2BGWDEG8cm+7G3NEOfmdcTQw4h9/55lhm7DekRYKQPZF2ArbXTAyu4kDYB2YxUzwg0gi/41ztHnfQG26HbGel/crVrm7tNY+/1btkOEAZ2M05r4FB7r9GbAIdxaZYrHdOsgJ/wCEQY0J74TmOKnbxxT9n3FgGGWWsVdowHtjt9Nnvf7yQM2aZU/TIAIAxrw6dOnAWtZZcoEnBpNuTuObWMEiLAx1HY0ZQJEmHJ3HNvGCBBhY6jtaMoEiJB0Z29vL6ls58vxPcO8/zfrdo5qvKO+d3Fx8Wu8zf1dW4p/cPzLly/dtv9Ts/EbcvGAHhHyfBIhZ6NSiIBTo0LNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiECRCjUbEPNCRAhZ6NSiAARCjXbUHMCRMjZqBQiQIRCzTbUnAARcjYqhQgQoVCzDTUnQIScjUohAkQo1GxDzQkQIWejUogAEQo121BzAkTI2agUIkCEQs021JwAEXI2KoUIEKFQsw01J0CEnI1KIQJEKNRsQ80JECFno1KIABEKNdtQcwJEyNmoFCJAhELNNtScABFyNiqFCBChULMNNSdAhJyNSiEC/wGgKKC4YMA4TAAAAABJRU5ErkJggg=='; 11 | 12 | export function SquareImage({ alt, src, preview }: SquareImageProps) { 13 | return ( 14 |
15 | {alt} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/modules/layout/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { WalletOutlined } from '@ant-design/icons'; 2 | import { Button, Col, Dropdown, Layout, Menu, Row, Tag, Typography } from 'antd'; 3 | import React from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | import { useAppSelector } from '../../../hooks'; 6 | import { truncateAddress } from '../../jettons/utils/truncateAddress'; 7 | import { isMainnet, isSandbox, isTestnet } from '../../common/network'; 8 | import { ConnectWalletButton } from '../../wallets/common/components/ConnectWalletButton'; 9 | import telegramIcon from '../icons/telegram.svg'; 10 | import { NavBarWalletMenu } from './NavBarWalletMenu'; 11 | import ScaletonIcon from './ScaletonIcon.svg'; 12 | import './NavBar.scss'; 13 | 14 | const { Header } = Layout; 15 | 16 | export function NavBar() { 17 | const wallet = useAppSelector(state => state.wallets.common.wallet); 18 | 19 | return ( 20 |
21 | 22 | 23 |
24 | 25 | Scaleton Scaleton 26 | 27 | {' '} 28 | 29 | {isTestnet() && ( 30 | testnet 31 | )} 32 | 33 | {isSandbox() && ( 34 | sandbox 35 | )} 36 | 37 |
38 | 39 | 40 | 41 | My Wallet 53 | ), 54 | }, 55 | isMainnet() ? ({ 56 | key: 'dapps.dex.swap', 57 | label: ( 58 | 59 | DEX NEW 60 | 61 | ), 62 | style: { 63 | cursor: 'not-allowed', 64 | }, 65 | }) : ({ 66 | key: 'dapps.dex.swap', 67 | label: ( 68 | 69 | DEX NEW 70 | 71 | ), 72 | }), 73 | ]} 74 | /> 75 | 76 | 77 | 78 | 83 | @Scaleton 84 | 85 | 86 | 87 | 88 | {wallet ? ( 89 | } placement="bottomRight" trigger={['click']}> 90 | 93 | 94 | ) : ( 95 | 96 | )} 97 | 98 | 99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /src/modules/wallets/tonkeeper/TonkeeperWalletAdapter.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { Store } from 'redux'; 3 | import { backoff } from 'ton-x/dist/utils/backoff'; 4 | import { Wallet } from '../common/Wallet'; 5 | import { WalletAdapter } from '../common/WalletAdapter'; 6 | import { delay } from '../ton-wallet/TonWalletWalletAdapter'; 7 | import { requestTransfer } from './actions/requestTransfer'; 8 | import { WalletFeature } from '../common/WalletFeature'; 9 | import { API_URLS, scaletonClient } from '../../common'; 10 | import { CURRENT_NETWORK } from '../../common/network'; 11 | import { DEFAULT_JETTON_FORWARD_AMOUNT, DEFAULT_JETTON_GAS_FEE } from '../common/constants'; 12 | import { preloadImage } from '../../common/utils/preloadImage'; 13 | import tonkeeperIcon from '../common/components/WalletIcon/icons/tonkeeper.svg'; 14 | 15 | export type TonkeeperSession = { 16 | sessionId: string; 17 | link: string; 18 | }; 19 | 20 | const TONKEEPER_TIMEOUT = 5 * 60 * 1000; 21 | 22 | export class TonkeeperWalletAdapter implements WalletAdapter { 23 | readonly features: WalletFeature[] = []; 24 | 25 | constructor( 26 | private readonly store: Store, 27 | ) { 28 | preloadImage(tonkeeperIcon); 29 | } 30 | 31 | async createSession(): Promise { 32 | const apiUrl = API_URLS[CURRENT_NETWORK]; 33 | 34 | const { data: session } = await axios.post(`${apiUrl}/v1/sessions`); 35 | 36 | const connectLink = `${apiUrl}/v1/sessions/${session.id}/init` 37 | .replace(/^https?:\/\//, 'https://app.tonkeeper.com/ton-login/'); 38 | 39 | return { 40 | sessionId: session.id, 41 | link: connectLink, 42 | }; 43 | } 44 | 45 | async awaitReadiness(session: TonkeeperSession): Promise { 46 | const expires = Date.now() + TONKEEPER_TIMEOUT; 47 | 48 | const state = await backoff(async () => { 49 | while (Date.now() < expires) { 50 | const { data: existing } = await scaletonClient.get(`/v1/sessions/${session.sessionId}`); 51 | 52 | if (existing.state === 'ready') { 53 | return existing; 54 | } 55 | 56 | await delay(1000); 57 | } 58 | 59 | return { state: 'expired' }; 60 | }); 61 | 62 | if (state.state === 'expired') { 63 | throw new Error('Connection was not confirmed.'); 64 | } 65 | 66 | return { 67 | address: state.address, 68 | publicKey: state.publicKey, 69 | walletVersion: state.walletVersion, 70 | }; 71 | } 72 | 73 | getWallet(session: TonkeeperSession): Promise { 74 | return this.awaitReadiness(session); 75 | } 76 | 77 | isAvailable(): boolean { 78 | return false; 79 | } 80 | 81 | async requestTransfer( 82 | session: TonkeeperSession, 83 | destination: string, 84 | amount: string, 85 | comment: string, 86 | timeout: number, 87 | ): Promise { 88 | const deeplink = `https://app.tonkeeper.com/transfer/${destination}?amount=${amount}&text=${encodeURIComponent(comment)}`; 89 | 90 | this.store.dispatch( 91 | requestTransfer({ 92 | destination, 93 | amount, 94 | comment, 95 | deeplink, 96 | }) as any, 97 | ); 98 | } 99 | 100 | async requestJettonTransfer( 101 | session: TonkeeperSession, 102 | contractAddress: string, 103 | destination: string, 104 | amount: string, 105 | forwardPayload: string, 106 | requestTimeout: number, 107 | forwardAmount: number = DEFAULT_JETTON_FORWARD_AMOUNT, 108 | gasFee: number = DEFAULT_JETTON_GAS_FEE, 109 | ): Promise { 110 | throw new Error('Not supported.'); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/modules/nfts/components/NftItemPreviewModal/NftItemPreviewModal.tsx: -------------------------------------------------------------------------------- 1 | import { LinkOutlined } from '@ant-design/icons'; 2 | import { Col, Modal, Row, Space, Statistic, Tag, Typography } from 'antd'; 3 | import { useEffect, useState } from 'react'; 4 | import { isMobile } from 'react-device-detect'; 5 | import { SquareImage } from '../../../common/components/SquareImage/SquareImage'; 6 | import { getNftItem } from '../../api/getNftItem'; 7 | import type { NftItem } from '../../types/NftItem'; 8 | 9 | const { Link, Text } = Typography; 10 | 11 | interface NftItemPreviewModalProps { 12 | item: NftItem; 13 | onClose: () => void; 14 | } 15 | 16 | export function NftItemPreviewModal({ item, onClose }: NftItemPreviewModalProps) { 17 | const [fullItem, setFullItem] = useState(null); 18 | const [fullItemLoading, setFullItemLoading] = useState(false); 19 | 20 | useEffect( 21 | () => { 22 | setFullItemLoading(true); 23 | getNftItem(item.address) 24 | .then(setFullItem) 25 | .catch(() => { 26 | // Handle errors 27 | }) 28 | .then(() => setFullItemLoading(false)); 29 | }, 30 | [item], 31 | ); 32 | 33 | return ( 34 | {item.name!} #{item.index} 37 | )} 38 | onCancel={onClose} 39 | footer={null} 40 | centered={isMobile} 41 | bodyStyle={{ padding: 16 }} 42 | width={750} 43 | visible 44 | > 45 | 46 | 47 | 52 | 53 | 54 | 55 | {item.description && ( 56 |
57 |
Description
58 |
{item.description}
59 |
60 | )} 61 | 62 | {item.attributes.length > 0 && ( 63 |
64 |
Attributes
65 | 66 | {item.attributes.map(attribute => ( 67 | {attribute.traitType}: {attribute.value} 68 | ))} 69 | 70 |
71 | )} 72 | 73 |
74 | 84 | 85 | {fullItem?.sale?.marketplace === 'get-gems' && ( 86 | 90 | Getgems 91 | 92 | )} 93 | 94 | {fullItem?.sale?.marketplace === 'disintar' && ( 95 | 99 | Disintar 100 | 101 | )} 102 |
103 | 104 |
105 |
106 | ); 107 | } 108 | -------------------------------------------------------------------------------- /src/modules/wallets/ton-wallet/TonWalletWalletAdapter.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Address, Cell, toNano, TonClient } from 'ton'; 3 | import { TON_WALLET_EXTENSION_URL, TonWalletClient } from './TonWalletClient'; 4 | import { Wallet } from '../common/Wallet'; 5 | import { WalletAdapter } from '../common/WalletAdapter'; 6 | import { WalletFeature } from '../common/WalletFeature'; 7 | import { JettonMasterContract } from '../../contracts/JettonMasterContract'; 8 | import { timeout } from '../common/utils'; 9 | import { DEFAULT_JETTON_FORWARD_AMOUNT, DEFAULT_JETTON_GAS_FEE } from '../common/constants'; 10 | import { preloadImage } from '../../common/utils/preloadImage'; 11 | import tonWalletIcon from '../common/components/WalletIcon/icons/ton-wallet.png'; 12 | 13 | export function delay(ms: number) { 14 | return new Promise(resolve => setTimeout(resolve, ms)); 15 | } 16 | 17 | export class TonWalletWalletAdapter implements WalletAdapter { 18 | readonly features: WalletFeature[] = [ 19 | WalletFeature.TRANSFER, 20 | WalletFeature.JETTON_TRANSFER, 21 | ]; 22 | 23 | constructor( 24 | private readonly tonClient: TonClient, 25 | private readonly tonWalletClient: TonWalletClient, 26 | ) { 27 | preloadImage(tonWalletIcon); 28 | } 29 | 30 | async createSession(): Promise { 31 | try { 32 | await this.tonWalletClient.ready(150); 33 | return true; 34 | } catch (error) { 35 | window.open(TON_WALLET_EXTENSION_URL, '_blank'); 36 | throw error; 37 | } 38 | } 39 | 40 | async awaitReadiness(session: boolean): Promise { 41 | await this.tonWalletClient.ready(); 42 | 43 | const [[wallet]] = await Promise.all([ 44 | this.tonWalletClient.requestWallets(), 45 | delay(150), 46 | ]); 47 | 48 | if (!wallet) { 49 | throw new Error('TON Wallet is not configured.'); 50 | } 51 | 52 | return wallet; 53 | } 54 | 55 | getWallet(session: boolean): Promise { 56 | return this.awaitReadiness(session); 57 | } 58 | 59 | isAvailable(): boolean { 60 | return !!window.ton?.isTonWallet; 61 | } 62 | 63 | async requestTransfer( 64 | session: boolean, 65 | destination: string, 66 | amount: string, 67 | comment: string, 68 | requestTimeout: number, 69 | ): Promise { 70 | await Promise.race([ 71 | this.tonWalletClient.sendTransaction({ 72 | to: destination, 73 | value: amount, 74 | dataType: 'text', 75 | data: comment, 76 | }), 77 | timeout(requestTimeout, 'Transaction request exceeded timeout.'), 78 | ]); 79 | } 80 | 81 | async requestJettonTransfer( 82 | session: boolean, 83 | contractAddress: string, 84 | destination: string, 85 | amount: string, 86 | forwardPayload: string, 87 | requestTimeout: number, 88 | forwardAmount: number = DEFAULT_JETTON_FORWARD_AMOUNT, 89 | gasFee: number = DEFAULT_JETTON_GAS_FEE, 90 | ): Promise { 91 | const contract = new JettonMasterContract(this.tonClient, null as any, Address.parse(contractAddress)); 92 | const wallet = await this.getWallet(session); 93 | const ownerAddress = Address.parse(wallet.address); 94 | const jettonWallet = await contract.getJettonWallet(ownerAddress); 95 | 96 | const forwardComment = new Cell(); 97 | 98 | forwardComment.bits.writeUint(0, 32); 99 | forwardComment.bits.writeBuffer(Buffer.from(forwardPayload)); 100 | 101 | const transferRequest = jettonWallet.createTransferRequest({ 102 | queryId: 0, 103 | amount: new BN(amount), 104 | destination: Address.parse(destination), 105 | forwardAmount: toNano(forwardAmount), 106 | forwardPayload: forwardComment, 107 | }); 108 | 109 | await Promise.race([ 110 | this.tonWalletClient.sendTransaction({ 111 | to: jettonWallet.address.toFriendly(), 112 | value: toNano(gasFee).toString(10), 113 | dataType: 'boc', 114 | data: transferRequest.toBoc().toString('base64'), 115 | }), 116 | timeout(requestTimeout, 'Transaction request exceeded timeout.'), 117 | ]); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetTransferFormModal.tsx: -------------------------------------------------------------------------------- 1 | import { Modal, Button, Result } from 'antd'; 2 | import { useCallback, useEffect, useState } from 'react'; 3 | import { isMobile } from 'react-device-detect'; 4 | import { useAppDispatch, useAppSelector } from '../../../hooks'; 5 | import { requestAssetTransfer } from '../actions'; 6 | import { createAssetSelector } from '../selectors/createAssetSelector'; 7 | import { AssetTransferForm, AssetTransferFormValues } from './AssetTransferForm'; 8 | 9 | interface AssetTransferFormModalProps { 10 | account: string; 11 | assetId: string; 12 | onCancel: () => void; 13 | } 14 | 15 | export function AssetTransferFormModal({ account, assetId, onCancel }: AssetTransferFormModalProps) { 16 | const selectAsset = createAssetSelector(assetId); 17 | 18 | const walletAdapterId = useAppSelector(state => state.wallets.common.adapterId); 19 | const walletSession = useAppSelector(state => state.wallets.common.session); 20 | 21 | const asset = useAppSelector(selectAsset); 22 | 23 | const [inputHasErrors, setInputHasErrors] = useState(false); 24 | const [input, setInput] = useState(null); 25 | 26 | const [isLoading, setLoading] = useState(false); 27 | const [isSucceeded, setSucceeded] = useState(false); 28 | const [isFailed, setFailed] = useState(false); 29 | 30 | const dispatch = useAppDispatch(); 31 | 32 | useEffect( 33 | () => { 34 | setLoading(false); 35 | setSucceeded(false); 36 | setFailed(false); 37 | }, 38 | [setLoading, setSucceeded, setFailed], 39 | ); 40 | 41 | const handleFormChange = useCallback( 42 | (hasErrors: boolean, values: AssetTransferFormValues) => { 43 | setInputHasErrors(hasErrors); 44 | setInput(values); 45 | }, 46 | [setInputHasErrors, setInput], 47 | ); 48 | 49 | const handleRequestTransfer = useCallback( 50 | () => { 51 | if (!walletAdapterId) return; 52 | if (!assetId) return; 53 | if (!input) return; 54 | 55 | (async () => { 56 | try { 57 | setLoading(true); 58 | 59 | await dispatch( 60 | requestAssetTransfer({ 61 | adapterId: walletAdapterId, 62 | session: walletSession, 63 | assetId, 64 | recipient: input.recipient, 65 | amount: input.amount, 66 | comment: input.comment ?? '', 67 | }), 68 | ); 69 | 70 | setLoading(false); 71 | setSucceeded(true); 72 | } catch { 73 | setLoading(false); 74 | setFailed(true); 75 | } 76 | })(); 77 | }, 78 | [dispatch, input, walletAdapterId, walletSession, assetId], 79 | ); 80 | 81 | if (!asset) { 82 | return <>; 83 | } 84 | 85 | return ( 86 | 99 | {isSucceeded && ( 100 | Close, 105 | ]} 106 | /> 107 | )} 108 | 109 | {isFailed && ( 110 | Close, 116 | ]} 117 | /> 118 | )} 119 | 120 | {(!isSucceeded && !isFailed) && ( 121 | 126 | )} 127 | 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/components/ConfirmSwapModal.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleOutlined, StopOutlined, SyncOutlined, WalletOutlined } from '@ant-design/icons'; 2 | import { Divider, Modal, Timeline } from 'antd'; 3 | import React, { useCallback } from 'react'; 4 | import { isMobile } from 'react-device-detect'; 5 | import { useAppDispatch, useAppSelector } from '../../../../hooks'; 6 | import { selectWalletName } from '../../../wallets/common/selectors/selectWalletName'; 7 | import { SwapStatus } from '../enums/SwapStatus'; 8 | import { selectDestinationSymbol } from '../selectors/selectDestinationSymbol'; 9 | import { selectSourceSymbol } from '../selectors/selectSourceSymbol'; 10 | import { cancelSwap } from '../store'; 11 | import { ProgressStep } from './ProgressStep'; 12 | import './ConfirmSwapModal.scss'; 13 | 14 | export function ConfirmSwapModal() { 15 | const walletName = useAppSelector(selectWalletName); 16 | const sourceSymbol = useAppSelector(selectSourceSymbol); 17 | const destinationSymbol = useAppSelector(selectDestinationSymbol); 18 | const status: SwapStatus = useAppSelector(state => state.dex.swap.status); 19 | const receivedAmount = useAppSelector(state => state.dex.swap.receivedAmount); 20 | 21 | const isFailed = status >= SwapStatus.CONFIRM_FAILED; 22 | const isConfirmed = status >= SwapStatus.CONFIRMED && status <= SwapStatus.RECEIVED; 23 | const isConfirmFailed = status === SwapStatus.CONFIRM_FAILED; 24 | const isSent = status >= SwapStatus.SENT && status <= SwapStatus.RECEIVED; 25 | const isReceived = status >= SwapStatus.RECEIVED && status <= SwapStatus.RECEIVED; 26 | 27 | const dispatch = useAppDispatch(); 28 | 29 | const handleClose = useCallback( 30 | () => { 31 | dispatch(cancelSwap()); 32 | }, 33 | [dispatch], 34 | ); 35 | 36 | return ( 37 | = SwapStatus.CONFIRMING} 40 | width={340} 41 | footer={false} 42 | className="confirm-swap-modal" 43 | centered={isMobile} 44 | onCancel={handleClose} 45 | > 46 |
47 |
48 | {isReceived ? ( 49 | 50 | ) : isConfirmed ? ( 51 | 52 | ) : isFailed ? ( 53 | 54 | ) : ( 55 | 56 | )} 57 |
58 | 59 |

60 | {isReceived ? ( 61 | <>You received {receivedAmount} {destinationSymbol}. 62 | ) : isConfirmed ? ( 63 | <> 64 | ) : isFailed ? ( 65 | <>Unfortunately transaction was failed. 66 | ) : ( 67 | <>Confirm the transaction in {walletName} 68 | )} 69 |

70 |
71 | 72 | 73 | 74 | 75 | 82 | 83 | 90 | 91 | 99 | 100 |
101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/modules/wallets/common/store.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import { notification } from 'antd'; 3 | import { Wallet } from './Wallet'; 4 | import { walletService } from './WalletService'; 5 | import { WalletFeature } from './WalletFeature'; 6 | 7 | interface WalletState { 8 | adapterId: string | null; 9 | features: WalletFeature[]; 10 | session: unknown; 11 | wallet: Wallet | null; 12 | isRestoring: boolean; 13 | isConnecting: boolean; 14 | } 15 | 16 | export const restoreSession = createAsyncThunk( 17 | 'wallet/restoreSession', 18 | async (_, thunkAPI): Promise<[string, unknown, WalletFeature[]]> => { 19 | const adapterId = localStorage.getItem('wallet:adapter-id'); 20 | const session = localStorage.getItem('wallet:session'); 21 | 22 | if (!adapterId || !session) { 23 | throw new Error('Nothing to restore.'); 24 | } 25 | 26 | (async () => { 27 | try { 28 | const wallet = await walletService.awaitReadiness( 29 | adapterId, 30 | JSON.parse(session), 31 | ); 32 | 33 | thunkAPI.dispatch(activateWallet(wallet)); 34 | } catch { 35 | thunkAPI.dispatch(resetSession()); 36 | } 37 | })(); 38 | 39 | const { features } = walletService.getWalletAdapter(adapterId); 40 | 41 | return [ 42 | adapterId, 43 | JSON.parse(session), 44 | features, 45 | ]; 46 | }, 47 | ); 48 | 49 | export const createWalletSession = createAsyncThunk<[unknown, WalletFeature[]], string>( 50 | 'wallet/createSession', 51 | async (adapterId: string, thunkAPI) => { 52 | const session = await walletService.createSession(adapterId); 53 | 54 | (async () => { 55 | try { 56 | const wallet = await walletService.awaitReadiness(adapterId, session); 57 | 58 | localStorage.setItem('wallet:adapter-id', adapterId); 59 | localStorage.setItem('wallet:session', JSON.stringify(session)); 60 | 61 | thunkAPI.dispatch(activateWallet(wallet)); 62 | } catch (e: any) { 63 | notification.error({ 64 | message: e.message, 65 | }); 66 | 67 | thunkAPI.dispatch(resetSession()); 68 | } 69 | })(); 70 | 71 | const { features } = walletService.getWalletAdapter(adapterId); 72 | 73 | return [session, features]; 74 | }, 75 | ); 76 | 77 | export const terminateSession = createAsyncThunk( 78 | 'wallet/terminateSession', 79 | async () => { 80 | localStorage.removeItem('wallet:adapter-id'); 81 | localStorage.removeItem('wallet:session'); 82 | }, 83 | ); 84 | 85 | const initialState: WalletState = { 86 | adapterId: null, 87 | features: [], 88 | session: null, 89 | wallet: null, 90 | isRestoring: false, 91 | isConnecting: false, 92 | }; 93 | 94 | const walletSlice = createSlice({ 95 | name: 'wallet', 96 | initialState, 97 | reducers: { 98 | activateWallet(state, action: PayloadAction) { 99 | state.wallet = action.payload; 100 | state.isConnecting = false; 101 | state.isRestoring = false; 102 | }, 103 | 104 | resetSession(state) { 105 | state.adapterId = null; 106 | state.features = []; 107 | state.session = null; 108 | state.wallet = null; 109 | state.isConnecting = false; 110 | state.isRestoring = false; 111 | }, 112 | }, 113 | extraReducers(builder) { 114 | builder.addCase(createWalletSession.pending, (state, action) => { 115 | state.isConnecting = true; 116 | }); 117 | 118 | builder.addCase(createWalletSession.fulfilled, (state, action) => { 119 | const [session, features] = action.payload; 120 | 121 | state.adapterId = action.meta.arg; 122 | state.session = session; 123 | state.features = features; 124 | }); 125 | 126 | builder.addCase(restoreSession.pending, (state) => { 127 | state.isRestoring = true; 128 | }); 129 | 130 | builder.addCase(restoreSession.fulfilled, (state, action) => { 131 | const [adapterId, session, features] = action.payload; 132 | 133 | state.adapterId = adapterId; 134 | state.session = session; 135 | state.features = features; 136 | }); 137 | 138 | builder.addCase(terminateSession.fulfilled, (state) => { 139 | state.adapterId = null; 140 | state.features = []; 141 | state.session = null; 142 | state.wallet = null; 143 | }); 144 | }, 145 | }); 146 | 147 | export const { activateWallet, resetSession } = walletSlice.actions; 148 | 149 | export default walletSlice.reducer; 150 | 151 | -------------------------------------------------------------------------------- /src/modules/assets/components/ImportJettonModal.tsx: -------------------------------------------------------------------------------- 1 | import { Form, Input, Modal } from 'antd'; 2 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 3 | import TonWeb from 'tonweb'; 4 | import { useAppDispatch } from '../../../hooks'; 5 | import { jettonV1 } from '../../jettons/contracts/JettonV1'; 6 | import { importJetton } from '../actions'; 7 | import { IPFS_GATEWAY_PREFIX } from '../../jettons/utils/ipfs'; 8 | 9 | export function ImportJettonModal({ onCancel, onImport }: { 10 | onImport: () => void; 11 | onCancel: () => void; 12 | }) { 13 | const dispatch = useAppDispatch(); 14 | 15 | const [address, setAddress] = useState(''); 16 | const [isLoading, setLoading] = useState(false); 17 | 18 | const [name, setName] = useState(''); 19 | const [symbol, setSymbol] = useState(''); 20 | 21 | const [content, setContent] = useState(null); 22 | const [isContractValid, setContractValid] = useState(false); 23 | 24 | const isValidAddress = useMemo(() => TonWeb.Address.isValid(address), [address]); 25 | const nameFromContent = useMemo(() => content?.name ?? null, [content]); 26 | const symbolFromContent = useMemo(() => content?.symbol ?? content?.name ?? null, [content]); 27 | 28 | const handleAddressChange = useCallback( 29 | (event: React.ChangeEvent) => { 30 | setAddress(event.target.value); 31 | }, 32 | [setAddress], 33 | ); 34 | 35 | const handleNameChange = useCallback( 36 | (event: React.ChangeEvent) => { 37 | setName(event.target.value); 38 | }, 39 | [setName], 40 | ); 41 | 42 | const handleSymbolChange = useCallback( 43 | (event: React.ChangeEvent) => { 44 | setSymbol(event.target.value); 45 | }, 46 | [setSymbol], 47 | ); 48 | 49 | const handleOk = useCallback( 50 | () => { 51 | dispatch( 52 | importJetton({ 53 | address, 54 | name: nameFromContent || name, 55 | symbol: symbolFromContent || symbol, 56 | }), 57 | ); 58 | 59 | onImport(); 60 | }, 61 | [onImport, dispatch, address, nameFromContent, name, symbolFromContent, symbol], 62 | ); 63 | 64 | const resetForm = useCallback( 65 | () => { 66 | setAddress(''); 67 | setName(''); 68 | setSymbol(''); 69 | }, 70 | [setAddress, setName, setSymbol], 71 | ); 72 | 73 | useEffect( 74 | () => { 75 | resetForm(); 76 | }, 77 | [resetForm], 78 | ); 79 | 80 | useEffect( 81 | () => { 82 | if (!TonWeb.Address.isValid(address)) { 83 | setContent(null); 84 | return; 85 | } 86 | 87 | setLoading(true); 88 | setContractValid(false); 89 | jettonV1.getData(address) 90 | .then(jettonData => { 91 | setContractValid(true); 92 | 93 | return fetch(jettonData.contentUri.replace(/^ipfs:\/\//, IPFS_GATEWAY_PREFIX)); 94 | }) 95 | .then(response => response.json()) 96 | .then((response: any) => { 97 | setContent(response); 98 | setName(''); 99 | setSymbol(''); 100 | setLoading(false); 101 | }) 102 | .catch(() => { 103 | setContent(null); 104 | setLoading(false); 105 | }); 106 | }, 107 | [address], 108 | ); 109 | 110 | return ( 111 | 123 |
124 | 125 | 134 | 135 | 136 | 137 | 143 | 144 | 145 | 146 | 152 | 153 |
154 |
155 | ); 156 | } 157 | -------------------------------------------------------------------------------- /src/modules/wallets/tonhub/TonhubWalletAdapter.ts: -------------------------------------------------------------------------------- 1 | import BN from 'bn.js'; 2 | import { Address, Cell, ConfigStore, toNano, TonClient } from 'ton'; 3 | import { TonhubConnector } from 'ton-x'; 4 | import { TonhubCreatedSession, TonhubTransactionRequest } from 'ton-x/dist/connector/TonhubConnector'; 5 | import { Wallet } from '../common/Wallet'; 6 | import { WalletAdapter } from '../common/WalletAdapter'; 7 | import { WalletFeature } from '../common/WalletFeature'; 8 | import { JettonMasterContract } from '../../contracts/JettonMasterContract'; 9 | import { DEFAULT_JETTON_FORWARD_AMOUNT, DEFAULT_JETTON_GAS_FEE } from '../common/constants'; 10 | import { preloadImage } from '../../common/utils/preloadImage'; 11 | import { isMainnet } from '../../common/network'; 12 | import sandboxIcon from '../common/components/WalletIcon/icons/sandbox.png'; 13 | import tonhubIcon from '../common/components/WalletIcon/icons/tonhub.png'; 14 | 15 | const TONHUB_TIMEOUT = 5 * 60 * 1000; 16 | 17 | type TonhubSession = TonhubCreatedSession; 18 | 19 | export class TonhubWalletAdapter implements WalletAdapter { 20 | readonly features: WalletFeature[] = [ 21 | WalletFeature.TRANSFER, 22 | WalletFeature.JETTON_TRANSFER, 23 | ]; 24 | 25 | constructor( 26 | private readonly tonClient: TonClient, 27 | private readonly tonhubConnector: TonhubConnector, 28 | ) { 29 | preloadImage(isMainnet() ? tonhubIcon : sandboxIcon); 30 | } 31 | 32 | async createSession(): Promise { 33 | const { location } = document; 34 | 35 | const session = await this.tonhubConnector.createNewSession({ 36 | name: 'Scaleton', 37 | url: `${location.protocol}//${location.host}`, 38 | }); 39 | 40 | // NOTE: Every wallet tries to handle ton:// links. This is needed to launch exactly Tonhub / Sandbox application. 41 | const sessionLink = session.link 42 | .replace('ton-test://', 'https://test.tonhub.com/') 43 | .replace('ton://', 'https://tonhub.com/'); 44 | 45 | return { 46 | id: session.id, 47 | seed: session.seed, 48 | link: sessionLink, 49 | }; 50 | } 51 | 52 | async awaitReadiness(session: TonhubSession): Promise { 53 | const state = await this.tonhubConnector.awaitSessionReady(session.id, TONHUB_TIMEOUT); 54 | 55 | if (state.state === 'revoked') { 56 | throw new Error('Connection was cancelled.'); 57 | } 58 | 59 | if (state.state === 'expired') { 60 | throw new Error('Connection was not confirmed.'); 61 | } 62 | 63 | const walletConfig = new ConfigStore(state.wallet.walletConfig); 64 | 65 | return { 66 | address: state.wallet.address, 67 | publicKey: walletConfig.getString('pk'), 68 | walletVersion: state.wallet.walletType, 69 | }; 70 | } 71 | 72 | getWallet(session: TonhubSession): Promise { 73 | return this.awaitReadiness(session); 74 | } 75 | 76 | private async requestTransaction(session: TonhubSession, request: Omit): Promise { 77 | const state = await this.tonhubConnector.getSessionState(session.id); 78 | 79 | if (state.state !== 'ready') return; 80 | 81 | const response = await this.tonhubConnector.requestTransaction({ 82 | ...request, 83 | seed: session.seed, 84 | appPublicKey: state.wallet.appPublicKey, 85 | }); 86 | 87 | if (response.type === 'rejected') { 88 | throw new Error('Transaction was rejected.'); 89 | } 90 | 91 | if (response.type === 'expired') { 92 | throw new Error('Transaction was expired.'); 93 | } 94 | 95 | if (response.type === 'invalid_session') { 96 | throw new Error('Something went wrong. Refresh the page and try again.'); 97 | } 98 | 99 | if (response.type === 'success') { 100 | // Handle successful transaction 101 | // const externalMessage = response.response; // Signed external message that was sent to the network 102 | } 103 | } 104 | 105 | isAvailable(): boolean { 106 | return true; 107 | } 108 | 109 | async requestTransfer( 110 | session: TonhubSession, 111 | destination: string, 112 | amount: string, 113 | comment: string, 114 | timeout: number, 115 | ): Promise { 116 | await this.requestTransaction( 117 | session, 118 | { 119 | to: destination, 120 | value: amount, 121 | text: comment, 122 | timeout, 123 | }, 124 | ); 125 | } 126 | 127 | async requestJettonTransfer( 128 | session: TonhubSession, 129 | contractAddress: string, 130 | destination: string, 131 | amount: string, 132 | forwardPayload: string, 133 | requestTimeout: number, 134 | forwardAmount: number = DEFAULT_JETTON_FORWARD_AMOUNT, 135 | gasFee: number = DEFAULT_JETTON_GAS_FEE, 136 | ): Promise { 137 | const contract = new JettonMasterContract(this.tonClient, null as any, Address.parse(contractAddress)); 138 | const wallet = await this.getWallet(session); 139 | const ownerAddress = Address.parse(wallet.address); 140 | const jettonWallet = await contract.getJettonWallet(ownerAddress); 141 | 142 | const forwardComment = new Cell(); 143 | 144 | forwardComment.bits.writeUint(0, 32); 145 | forwardComment.bits.writeBuffer(Buffer.from(forwardPayload)); 146 | 147 | const transferRequest = jettonWallet.createTransferRequest({ 148 | queryId: 0, 149 | amount: new BN(amount), 150 | destination: Address.parse(destination), 151 | forwardAmount: toNano(forwardAmount), 152 | forwardPayload: forwardComment, 153 | }); 154 | 155 | await this.requestTransaction( 156 | session, 157 | { 158 | to: jettonWallet.address.toFriendly(), 159 | value: toNano(gasFee).toString(10), 160 | payload: transferRequest.toBoc().toString('base64'), 161 | timeout: requestTimeout, 162 | }, 163 | ); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/modules/assets/components/AssetsTabPaneContent/AssetsTabPaneContent.tsx: -------------------------------------------------------------------------------- 1 | import { HistoryOutlined, SendOutlined } from '@ant-design/icons'; 2 | import { Button, PageHeader, Popconfirm, Skeleton, Space, Table, Tooltip } from 'antd'; 3 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 4 | import { useAppDispatch, useAppSelector } from '../../../../hooks'; 5 | import { ImportJettonModal } from '../ImportJettonModal'; 6 | import { AssetTransferFormModal } from '../AssetTransferFormModal'; 7 | import { AssetHistoryModal } from '../AssetHistoryModal'; 8 | import { fetchHistory, hideAsset, refreshBalances } from '../../actions'; 9 | import { presentBalance } from '../../../jettons/utils/presentBalance'; 10 | import { selectWalletAddress } from '../../../wallets/common/selectors/selectWalletAddress'; 11 | import type { AssetRef } from '../../types'; 12 | import { AssetType } from '../../types'; 13 | import { selectWalletFeatures } from '../../../wallets/common/selectors/selectWalletFeatures'; 14 | import { WalletFeature } from '../../../wallets/common/WalletFeature'; 15 | import { selectWalletName } from '../../../wallets/common/selectors/selectWalletName'; 16 | import './AssetsTabPaneContent.scss'; 17 | 18 | const { Column } = Table; 19 | 20 | interface AssetsTabPaneProps { 21 | account: string; 22 | } 23 | 24 | export function AssetsTabPaneContent({ account }: AssetsTabPaneProps) { 25 | const walletName = useAppSelector(selectWalletName); 26 | const walletAddress = useAppSelector(selectWalletAddress); 27 | const walletFeatures = useAppSelector(selectWalletFeatures); 28 | 29 | const isMyAccount = walletAddress === account; 30 | 31 | const [transferAssetId, setTransferAssetId] = useState(null); 32 | const [isImportJettonActive, setImportJettonActive] = useState(false); 33 | 34 | const assets = useAppSelector(state => state.assets.assets); 35 | const allBalances = useAppSelector(state => state.assets.balances); 36 | const balancesLoading = useAppSelector(state => state.assets.balancesLoading); 37 | const balances = useMemo( 38 | () => account ? allBalances[account] : {}, 39 | [account, allBalances], 40 | ); 41 | 42 | const dispatch = useAppDispatch(); 43 | 44 | const handleRefresh = useCallback( 45 | () => { 46 | if (!account) return; 47 | dispatch(refreshBalances(account)); 48 | }, 49 | [dispatch, account], 50 | ); 51 | 52 | const showSendAsset = useCallback( 53 | (assetId: string) => setTransferAssetId(assetId), 54 | [setTransferAssetId], 55 | ); 56 | 57 | const [transactionsAssetId, setTransactionsAssetId] = useState(null); 58 | 59 | const showAssetTransactions = useCallback( 60 | (assetId: string) => { 61 | dispatch(fetchHistory({ account, assetId })); 62 | setTransactionsAssetId(assetId); 63 | }, 64 | [dispatch, setTransactionsAssetId, account], 65 | ); 66 | 67 | const displayImportJettonModal = useCallback( 68 | () => setImportJettonActive(true), 69 | [setImportJettonActive], 70 | ); 71 | 72 | const closeImportJettonModal = useCallback( 73 | () => setImportJettonActive(false), 74 | [setImportJettonActive], 75 | ); 76 | 77 | const handleImportJetton = useCallback( 78 | () => { 79 | if (!account) return; 80 | 81 | setImportJettonActive(false); 82 | dispatch(refreshBalances(account)); 83 | }, 84 | [setImportJettonActive, dispatch, account], 85 | ); 86 | 87 | const handleHideAsset = useCallback( 88 | (assetId: string) => { 89 | dispatch(hideAsset(assetId)); 90 | }, 91 | [dispatch], 92 | ); 93 | 94 | useEffect( 95 | () => { 96 | if (!account) return; 97 | 98 | closeImportJettonModal(); 99 | setTransferAssetId(null); 100 | setTransactionsAssetId(null); 101 | 102 | dispatch(refreshBalances(account)); 103 | }, 104 | [dispatch, account, closeImportJettonModal, setTransferAssetId, setTransactionsAssetId], 105 | ); 106 | 107 | return ( 108 |
109 | 114 | Import Jetton 115 | , 116 | , 119 | ]} 120 | /> 121 | 122 | {!balances && ( 123 | 124 | )} 125 | 126 | {balances && ( 127 | asset.id} 131 | pagination={false} 132 | > 133 | ( 139 | <> 140 | {name} 141 | {asset.symbol} 142 | 143 | )} 144 | /> 145 | 146 | ( 151 | <>{presentBalance(balances[asset.id]?.balance ?? '0')} {asset.symbol} 152 | )} 153 | /> 154 | 155 | ( 160 | 161 | 164 | 165 | {isMyAccount && walletName && ( 166 | <> 167 | {asset.type === AssetType.NATIVE && ( 168 | walletFeatures.includes(WalletFeature.TRANSFER) 169 | ? ( 170 | 177 | ) : ( 178 | 182 | 185 | 186 | ) 187 | )} 188 | 189 | {asset.type === AssetType.JETTON && ( 190 | walletFeatures.includes(WalletFeature.JETTON_TRANSFER) 191 | ? ( 192 | 195 | ) : ( 196 | 200 | 203 | 204 | ) 205 | )} 206 | 207 | )} 208 | 209 | {asset.isCustom && ( 210 | handleHideAsset(asset.id)} 215 | > 216 | Hide 217 | 218 | )} 219 | 220 | )} 221 | /> 222 |
223 | )} 224 | 225 | {isImportJettonActive && ( 226 | 230 | )} 231 | 232 | {transferAssetId && ( 233 | setTransferAssetId(null)} 237 | /> 238 | )} 239 | 240 | {transactionsAssetId && ( 241 | setTransactionsAssetId(null)} 245 | /> 246 | )} 247 |
248 | ); 249 | } 250 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/pages/SwapView.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | RetweetOutlined, 3 | SettingOutlined 4 | } from '@ant-design/icons'; 5 | import { Button, Card, Col, Divider, Form, InputNumber, Row, Select, Skeleton, Tooltip } from 'antd'; 6 | import Big from 'big.js'; 7 | import React, { useCallback, useEffect } from 'react'; 8 | import { useAppDispatch, useAppSelector } from '../../../../hooks'; 9 | import { ConfirmSwapModal } from '../components/ConfirmSwapModal'; 10 | import { ImpactPrice } from '../components/ImpactPrice'; 11 | import { RefreshLink } from '../components/RefreshLink'; 12 | import { selectAvailableDestinations } from '../selectors/selectAvailableDestinations'; 13 | import { selectAvailableSources } from '../selectors/selectAvailableSources'; 14 | import { selectDestinationAmountOut } from '../selectors/selectDestinationAmountOut'; 15 | import { selectDestinationSymbol } from '../selectors/selectDestinationSymbol'; 16 | import { selectImpactPrice } from '../selectors/selectImpactPrice'; 17 | import { selectSourceSymbol } from '../selectors/selectSourceSymbol'; 18 | import { 19 | estimateSwap, 20 | refreshBalances, 21 | refreshPrices, 22 | requestSwap, 23 | reset, reverseTradeDirection, 24 | setDestination, 25 | setSource, 26 | setSourceAmount, 27 | } from '../store'; 28 | import './SwapView.scss'; 29 | 30 | const { Option } = Select; 31 | 32 | export function SwapForm() { 33 | const [form] = Form.useForm(); 34 | const dispatch = useAppDispatch(); 35 | 36 | const availableSources = useAppSelector(selectAvailableSources); 37 | const availableDestinations = useAppSelector(selectAvailableDestinations); 38 | 39 | const slippage = useAppSelector(state => state.dex.swap.slippage); 40 | const sourceId = useAppSelector(state => state.dex.swap.sourceId); 41 | const sourceSymbol = useAppSelector(selectSourceSymbol); 42 | const sourceAmountIn = useAppSelector(state => state.dex.swap.sourceAmountIn); 43 | const sourceBalance = useAppSelector(state => state.dex.swap.sourceBalance); 44 | const sourceBalanceLoading = useAppSelector(state => state.dex.swap.sourceBalanceLoading); 45 | const destinationId = useAppSelector(state => state.dex.swap.destinationId); 46 | const destinationSymbol = useAppSelector(selectDestinationSymbol); 47 | const destinationAmountOut = useAppSelector(selectDestinationAmountOut); 48 | const destinationBalance = useAppSelector(state => state.dex.swap.destinationBalance); 49 | const destinationBalanceLoading = useAppSelector(state => state.dex.swap.destinationBalanceLoading); 50 | 51 | const estimatePrice = useAppSelector(state => state.dex.swap.currentPrice); 52 | const estimatePriceLoading = useAppSelector(state => state.dex.swap.currentPriceLoading); 53 | 54 | const impactPrice = useAppSelector(selectImpactPrice); 55 | 56 | const swapAvailable = sourceId 57 | && destinationSymbol 58 | && sourceAmountIn && new Big(sourceAmountIn).gt(0) 59 | && impactPrice && impactPrice < 15; 60 | 61 | const handleChangeSource = useCallback( 62 | async (assetId) => { 63 | await dispatch(setSource(assetId)); 64 | await dispatch(refreshBalances()); 65 | await dispatch(refreshPrices()); 66 | }, 67 | [dispatch], 68 | ); 69 | 70 | const handleChangeDestination = useCallback( 71 | async (assetId) => { 72 | await dispatch(setDestination(assetId)); 73 | await dispatch(refreshBalances()); 74 | await dispatch(refreshPrices()); 75 | }, 76 | [dispatch], 77 | ); 78 | 79 | const handleSourceAmount = useCallback( 80 | (amount) => dispatch(setSourceAmount(amount)), 81 | [dispatch], 82 | ); 83 | 84 | const handleRefreshPrices = useCallback( 85 | () => dispatch(refreshPrices()), 86 | [dispatch], 87 | ); 88 | 89 | const handleSwap = useCallback( 90 | () => { 91 | dispatch(requestSwap()); 92 | }, 93 | [dispatch], 94 | ); 95 | 96 | const handleReverseTradeDirection = useCallback( 97 | () => { 98 | dispatch(reverseTradeDirection()); 99 | dispatch(refreshPrices()); 100 | }, 101 | [dispatch], 102 | ); 103 | 104 | useEffect( 105 | () => { 106 | (async () => { 107 | await dispatch(reset()); 108 | await dispatch(refreshBalances()); 109 | await dispatch(refreshPrices()); 110 | })(); 111 | }, 112 | [dispatch], 113 | ); 114 | 115 | useEffect( 116 | () => { 117 | dispatch(estimateSwap()); 118 | }, 119 | [dispatch, sourceId, sourceAmountIn, destinationId], 120 | ); 121 | 122 | return ( 123 | <> 124 | 131 |
132 |

Swap

133 | Swap your tokens with ease 134 |
135 | 136 | 137 | 138 |
142 | 143 | 144 | 145 | 146 | style={{ flex: 1 }} 147 | size="large" 148 | min="0" 149 | max={sourceBalance} 150 | step="0.000000001" 151 | value={sourceAmountIn} 152 | onChange={handleSourceAmount} 153 | stringMode 154 | controls={false} 155 | /> 156 | 157 | 158 | 169 | 170 | 171 | 172 | {sourceBalance && ( 173 | 174 | 175 | {sourceBalanceLoading ? ( 176 | 177 | ) : ( 178 | <>Balance: {sourceBalance} 179 | )} 180 | 181 | 182 | )} 183 | 184 | 185 |
186 | 189 |
190 | 191 | 192 | 193 | 194 | 195 | style={{ flex: 1 }} 196 | size="large" 197 | step="0.000000001" 198 | stringMode 199 | controls={false} 200 | readOnly={true} 201 | value={destinationAmountOut} 202 | /> 203 | 204 | 205 | 217 | 218 | 219 | 220 | {destinationBalance && ( 221 | 222 | 223 | {destinationBalanceLoading ? ( 224 | 225 | ) : ( 226 | <>Balance: {destinationBalance} 227 | )} 228 | 229 | 230 | )} 231 | 232 | 233 | 234 | 235 | 236 | Price 237 | 238 | {(sourceId && destinationSymbol && estimatePrice) 239 | ? `${estimatePrice} ${destinationSymbol} per ${sourceSymbol}` 240 | : '-'} 241 | 242 | 247 | 248 | 249 | 250 | 251 | Slippage Tolerance 252 | 253 | {slippage} % 254 | 255 | 256 | 257 | 258 | 259 | 260 |
261 | 269 |
270 | 271 | 272 | 273 | 274 | Price Impact 275 | 276 | {impactPrice ? ( 277 | 278 | ) : '- %'} 279 | 280 | 281 | 282 |
283 | 284 | 285 | 286 | ); 287 | } 288 | 289 | export function SwapView() { 290 | return ( 291 |
292 |
293 | 294 |
295 |
296 | ); 297 | } 298 | -------------------------------------------------------------------------------- /src/modules/dapps/dex.swap/store.ts: -------------------------------------------------------------------------------- 1 | import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; 2 | import * as Sentry from '@sentry/react'; 3 | import BN from 'bn.js'; 4 | import { Address, toNano } from 'ton'; 5 | import { assetCatalog, tonClient } from '../../assets/services'; 6 | import { PoolContract } from '../../contracts/PoolContract'; 7 | import { TradeDirection } from '../../contracts/enums/TradeDirection'; 8 | import { SwapStatus } from './enums/SwapStatus'; 9 | import { pairCatalog, tradeService } from './services'; 10 | import { generateQueryId } from './utils/generateQueryId'; 11 | import { wait } from './utils/wait'; 12 | import type { RootState } from '../../../store'; 13 | import { selectWalletAddress } from '../../wallets/common/selectors/selectWalletAddress'; 14 | 15 | const DEFAULT_SLIPPAGE = 3; 16 | 17 | interface SwapState { 18 | swap: { 19 | sourceId: string; 20 | sourceAmountIn: string; 21 | sourceBalance: string; 22 | sourceBalanceLoading: boolean; 23 | 24 | destinationId: string; 25 | destinationBalance: string; 26 | destinationBalanceLoading: boolean; 27 | destinationAmountOut: string; 28 | 29 | slippage: number; 30 | 31 | currentPrice: string; 32 | currentPriceLoading: boolean; 33 | 34 | status: SwapStatus; 35 | receivedAmount: string; 36 | }; 37 | } 38 | 39 | const initialState: SwapState = { 40 | swap: { 41 | sourceId: '', 42 | sourceBalance: '-', 43 | sourceBalanceLoading: false, 44 | sourceAmountIn: '0', 45 | 46 | destinationId: '', 47 | destinationBalance: '-', 48 | destinationBalanceLoading: false, 49 | destinationAmountOut: '0', 50 | 51 | slippage: DEFAULT_SLIPPAGE, 52 | 53 | currentPrice: '', 54 | currentPriceLoading: false, 55 | 56 | status: SwapStatus.IDLE, 57 | receivedAmount: '', 58 | }, 59 | }; 60 | 61 | export const refreshBalances = createAsyncThunk<[string, string], void>( 62 | 'swap/refreshBalances', 63 | async (_, thunkAPI) => { 64 | const state = thunkAPI.getState() as RootState; 65 | const owner = Address.parse(selectWalletAddress(state)!); 66 | const { sourceId, destinationId } = state.dex.swap; 67 | 68 | const [sourceBalance, destinationBalance] = await Promise.all([ 69 | sourceId ? assetCatalog.getBalance(owner, sourceId) : Promise.resolve(null), 70 | destinationId ? assetCatalog.getBalance(owner, destinationId) : Promise.resolve(null), 71 | ]); 72 | 73 | return [ 74 | sourceBalance?.toString() ?? '-', 75 | destinationBalance?.toString() ?? '-', 76 | ]; 77 | }, 78 | ); 79 | 80 | export const refreshPrices = createAsyncThunk( 81 | 'swap/refreshPrices', 82 | async (_, thunkAPI) => { 83 | const state = thunkAPI.getState() as RootState; 84 | const { sourceId, destinationId } = state.dex.swap; 85 | 86 | const pair = pairCatalog.getPairByAssets(sourceId, destinationId)!; 87 | const contractAddress = Address.parse(pair.contractAddress); 88 | const poolContract = new PoolContract(tonClient, null as any, contractAddress); 89 | 90 | const prices = await poolContract.getTokenPrices(); 91 | 92 | return pair.leftAssetId === destinationId 93 | ? prices.leftTokenPrice.toFixed(6).replace(/0+$/, '').replace(/\.$/, '') 94 | : prices.rightTokenPrice.toFixed(6).replace(/0+$/, '').replace(/\.$/, ''); 95 | }, 96 | ); 97 | 98 | export const estimateSwap = createAsyncThunk( 99 | 'swap/estimateSwap', 100 | async (_, thunkAPI) => { 101 | const state = thunkAPI.getState() as RootState; 102 | const { sourceId, sourceAmountIn, destinationId } = state.dex.swap; 103 | 104 | if (!sourceId || !sourceAmountIn || !destinationId) { 105 | throw new Error('Options are not chosen yet.'); 106 | } 107 | 108 | const pair = pairCatalog.getPairByAssets(sourceId, destinationId)!; 109 | const contractAddress = Address.parse(pair.contractAddress); 110 | const poolContract = new PoolContract(tonClient, null as any, contractAddress); 111 | 112 | const amountOut = await poolContract.estimateSwap( 113 | toNano(sourceAmountIn), 114 | pair.leftAssetId === sourceId ? TradeDirection.A_TO_B : TradeDirection.B_TO_A, 115 | ); 116 | 117 | return amountOut.toString(); 118 | }, 119 | ); 120 | 121 | export const requestSwap = createAsyncThunk( 122 | 'swap/swap', 123 | async (_, thunkAPI) => { 124 | const state = thunkAPI.getState() as RootState; 125 | 126 | const { adapterId, session, wallet } = state.wallets.common; 127 | const { sourceId, destinationId, sourceAmountIn } = state.dex.swap; 128 | 129 | if (!adapterId || !wallet) { 130 | throw new Error('Wallet is not connected.'); 131 | } 132 | 133 | const walletAddress = Address.parse(wallet.address); 134 | 135 | const pair = pairCatalog.getPairByAssets(sourceId, destinationId)!; 136 | const tradeDirection = pair.leftAssetId === sourceId ? TradeDirection.A_TO_B : TradeDirection.B_TO_A; 137 | 138 | const minimumAmountOut = 0; 139 | const queryId = generateQueryId(); 140 | 141 | await thunkAPI.dispatch(markConfirming()); 142 | 143 | try { 144 | const sourceAsset = assetCatalog.getAsset(sourceId); 145 | 146 | await tradeService.requestSwap( 147 | adapterId, 148 | session, 149 | sourceAsset, 150 | Address.parse(pair.contractAddress), 151 | tradeDirection, 152 | toNano(sourceAmountIn), 153 | toNano(minimumAmountOut), 154 | queryId, 155 | ); 156 | } catch (error) { 157 | console.error(error); 158 | Sentry.captureException(error); 159 | throw error; 160 | } 161 | 162 | await thunkAPI.dispatch(markConfirmed()); 163 | 164 | await wait( 165 | async () => { 166 | const transactions = await assetCatalog.getTransactions(walletAddress, sourceId); 167 | 168 | return transactions.find(transaction => { 169 | try { 170 | if (transaction.operation !== 'out') return false; 171 | if (!transaction.comment?.startsWith('swp#')) return false; 172 | 173 | return queryId.eq(new BN(transaction.comment.substring(4, 20), 'hex')); 174 | } catch { 175 | return false; 176 | } 177 | }) ?? null; 178 | }, 179 | 3_000, 180 | ); 181 | 182 | await thunkAPI.dispatch(markSent()); 183 | 184 | // wait from income transaction 185 | const incomeTransaction = await wait( 186 | async () => { 187 | const transactions = await assetCatalog.getTransactions(walletAddress, destinationId); 188 | 189 | return transactions.find(transaction => { 190 | if (transaction.operation !== 'in') return false; 191 | if (transaction.queryId.eq(queryId)) return true; 192 | 193 | const { body } = transaction; 194 | 195 | return body && body.length > 4 && new BN(body.slice(4).toString('hex'), 'hex').eq(queryId); 196 | }) ?? null; 197 | }, 198 | 3_000, 199 | ); 200 | 201 | await thunkAPI.dispatch( 202 | markReceived(incomeTransaction.amount.toString()), 203 | ); 204 | }, 205 | ); 206 | 207 | const DEFAULT_SOURCE_ID = 'ton'; 208 | const DEFAULT_PAIR = pairCatalog.getPairsByAsset(DEFAULT_SOURCE_ID)[0]; 209 | const DEFAULT_DESTINATION_ID = DEFAULT_PAIR?.leftAssetId !== DEFAULT_SOURCE_ID 210 | ? DEFAULT_PAIR?.leftAssetId 211 | : DEFAULT_PAIR?.rightAssetId; 212 | 213 | const swapSlice = createSlice({ 214 | name: 'swap', 215 | initialState, 216 | reducers: { 217 | reset(state) { 218 | state.swap.sourceId = DEFAULT_SOURCE_ID; 219 | state.swap.sourceAmountIn = '0'; 220 | state.swap.destinationId = DEFAULT_DESTINATION_ID; 221 | state.swap.destinationAmountOut = '' 222 | state.swap.currentPrice = ''; 223 | state.swap.currentPriceLoading = false; 224 | state.swap.status = SwapStatus.IDLE; 225 | state.swap.receivedAmount = ''; 226 | }, 227 | 228 | setSourceAmount(state, action: PayloadAction) { 229 | state.swap.sourceAmountIn = action.payload; 230 | }, 231 | 232 | setSource(state, action: PayloadAction) { 233 | state.swap.sourceId = action.payload; 234 | }, 235 | 236 | setDestination(state, action: PayloadAction) { 237 | state.swap.destinationId = action.payload; 238 | }, 239 | 240 | markConfirming(state) { 241 | state.swap.status = SwapStatus.CONFIRMING; 242 | }, 243 | 244 | markConfirmed(state) { 245 | state.swap.status = SwapStatus.CONFIRMED; 246 | }, 247 | 248 | markConfirmFailed(state) { 249 | state.swap.status = SwapStatus.CONFIRM_FAILED; 250 | }, 251 | 252 | markSent(state) { 253 | state.swap.status = SwapStatus.SENT; 254 | }, 255 | 256 | markReceived(state, action: PayloadAction) { 257 | state.swap.status = SwapStatus.RECEIVED; 258 | state.swap.receivedAmount = action.payload; 259 | }, 260 | 261 | cancelSwap(state) { 262 | state.swap.status = SwapStatus.IDLE; 263 | state.swap.receivedAmount = ''; 264 | }, 265 | 266 | reverseTradeDirection(state) { 267 | const { 268 | sourceId, 269 | sourceBalance, 270 | destinationId, 271 | destinationBalance, 272 | } = state.swap; 273 | 274 | state.swap.sourceId = destinationId; 275 | state.swap.sourceBalance = destinationBalance; 276 | state.swap.destinationId = sourceId; 277 | state.swap.destinationBalance = sourceBalance; 278 | state.swap.currentPrice = ''; 279 | } 280 | }, 281 | extraReducers(builder) { 282 | builder.addCase(requestSwap.pending, (state) => { 283 | // 284 | }); 285 | 286 | builder.addCase(requestSwap.fulfilled, (state) => { 287 | // 288 | }); 289 | 290 | builder.addCase(requestSwap.rejected, (state) => { 291 | // 292 | }); 293 | 294 | builder.addCase(refreshBalances.fulfilled, (state, action) => { 295 | const [sourceBalance, destinationBalance] = action.payload; 296 | 297 | state.swap.sourceBalance = sourceBalance; 298 | state.swap.destinationBalance = destinationBalance; 299 | }); 300 | 301 | builder.addCase(refreshPrices.pending, (state, action) => { 302 | state.swap.currentPriceLoading = true; 303 | }); 304 | 305 | builder.addCase(refreshPrices.fulfilled, (state, action) => { 306 | state.swap.currentPriceLoading = false; 307 | state.swap.currentPrice = action.payload; 308 | }); 309 | 310 | builder.addCase(refreshPrices.rejected, (state, action) => { 311 | state.swap.currentPriceLoading = false; 312 | }); 313 | 314 | builder.addCase(estimateSwap.pending, (state, action) => { 315 | state.swap.destinationAmountOut = ''; 316 | }); 317 | 318 | builder.addCase(estimateSwap.fulfilled, (state, action) => { 319 | state.swap.destinationAmountOut = action.payload; 320 | }); 321 | 322 | builder.addCase(estimateSwap.rejected, (state, action) => { 323 | state.swap.destinationAmountOut = ''; 324 | }); 325 | }, 326 | }); 327 | 328 | export const { 329 | reset, 330 | setSourceAmount, 331 | setSource, 332 | setDestination, 333 | markConfirming, 334 | markConfirmed, 335 | markSent, 336 | markReceived, 337 | cancelSwap, 338 | reverseTradeDirection, 339 | } = swapSlice.actions; 340 | 341 | export default swapSlice.reducer; 342 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------