├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── apps ├── app │ ├── .gitignore │ ├── README.md │ ├── assets │ │ ├── asset.svg │ │ ├── b1.png │ │ ├── khala_claimer_abi.ts │ │ ├── nfts.svg │ │ ├── nova_logo.png │ │ ├── pha_vault_abi.ts │ │ ├── phala_logo.svg │ │ ├── powered_by_slpx.png │ │ ├── share_equation.png │ │ ├── stake_pool.svg │ │ ├── stake_pool_detailed.svg │ │ ├── subwallet_logo.png │ │ ├── vault.svg │ │ ├── vault_detailed.svg │ │ ├── whitelists.svg │ │ ├── wikiData.ts │ │ ├── withdraw_queue.svg │ │ └── worker.svg │ ├── components │ │ ├── AssetTransfer.tsx │ │ ├── BasePool │ │ │ ├── Chart.tsx │ │ │ ├── ClaimDelegation.tsx │ │ │ ├── ClaimReward.tsx │ │ │ ├── CreateButton.tsx │ │ │ ├── DelegateCard.tsx │ │ │ ├── DelegateInput.tsx │ │ │ ├── DetailPage.tsx │ │ │ ├── ExtraProperties.tsx │ │ │ ├── FarmCard.tsx │ │ │ ├── Identity.tsx │ │ │ ├── Intro.tsx │ │ │ ├── List.tsx │ │ │ ├── OwnerSettings.tsx │ │ │ ├── Whitelist │ │ │ │ ├── AddWhitelist.tsx │ │ │ │ └── List.tsx │ │ │ ├── WithdrawQueue.tsx │ │ │ └── Worker │ │ │ │ ├── AddWorker.tsx │ │ │ │ ├── Card.tsx │ │ │ │ ├── ChangeStake.tsx │ │ │ │ └── List.tsx │ │ ├── ClaimKhalaAssets.tsx │ │ ├── ClientOnly.tsx │ │ ├── DashboardAccount.tsx │ │ ├── DashboardAssetList.tsx │ │ ├── DashboardCarousel.tsx │ │ ├── DashboardNftList.tsx │ │ ├── Delegation │ │ │ ├── Chart.tsx │ │ │ ├── HorizonCard.tsx │ │ │ ├── List.tsx │ │ │ ├── Menu.tsx │ │ │ ├── NftCard.tsx │ │ │ └── Withdraw.tsx │ │ ├── DelegationDetailCard.tsx │ │ ├── DelegationNftCover.tsx │ │ ├── DelegationScatterChart.tsx │ │ ├── DelegationValueChart.tsx │ │ ├── DelegatorSelect.tsx │ │ ├── DownloadHistory.tsx │ │ ├── Empty.tsx │ │ ├── FarmChart.tsx │ │ ├── HydrateAtoms.tsx │ │ ├── Layout.tsx │ │ ├── ListSkeleton.tsx │ │ ├── PageHeader.tsx │ │ ├── PromiseButton.tsx │ │ ├── Property.tsx │ │ ├── RechartsTooltip.tsx │ │ ├── ScrollTop.tsx │ │ ├── SectionHeader.tsx │ │ ├── SnackbarProvider.tsx │ │ ├── Staking │ │ │ ├── Stake.tsx │ │ │ ├── Unstake.tsx │ │ │ └── index.tsx │ │ ├── SwitchChainButton.tsx │ │ ├── TextSkeleton.tsx │ │ ├── Title.tsx │ │ ├── TopBar │ │ │ ├── Account.tsx │ │ │ ├── ChainStatus.tsx │ │ │ ├── NetworkStats.tsx │ │ │ └── index.tsx │ │ ├── Vest.tsx │ │ ├── WalletDialog.tsx │ │ ├── Web3Provider.tsx │ │ ├── Wiki │ │ │ ├── Button.tsx │ │ │ ├── Dialog.tsx │ │ │ └── index.tsx │ │ ├── WrapDecimal.tsx │ │ └── ZendeskWidget.tsx │ ├── config.ts │ ├── hooks │ │ ├── khalaAssets.ts │ │ ├── staking.ts │ │ ├── useAccountQuery.ts │ │ ├── useAssetBalance.ts │ │ ├── useAssetsMetadata.ts │ │ ├── useAutoSwitchChain.ts │ │ ├── useDebounced.ts │ │ ├── useDelegationOneDayProfit.ts │ │ ├── useGetApr.ts │ │ ├── useGetAprMultiplier.ts │ │ ├── useListenBlockHeight.ts │ │ ├── useLockedWrappedBalance.ts │ │ ├── useNotice.tsx │ │ ├── usePolkadotApi.ts │ │ ├── usePoolFavorite.ts │ │ ├── usePoolIntro.ts │ │ ├── useResetVault.ts │ │ ├── useSelectedVaultState.ts │ │ ├── useShowWalletDialog.ts │ │ ├── useSignAndSend.tsx │ │ ├── useToday.ts │ │ └── useWrapAsset.ts │ ├── lib │ │ ├── apr.ts │ │ ├── getBasePoolServerSideProps.ts │ │ ├── getDelegationProfit.ts │ │ ├── getPoolPath.ts │ │ ├── getVaultOwnerCut.ts │ │ ├── styles.ts │ │ ├── subsquidQuery.ts │ │ ├── subsquidSdk.ts │ │ └── theme.ts │ ├── next.config.ts │ ├── package.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ ├── [chain] │ │ │ │ └── snapshots │ │ │ │ │ ├── account │ │ │ │ │ └── [account].ts │ │ │ │ │ ├── delegation │ │ │ │ │ └── [id].ts │ │ │ │ │ └── pool │ │ │ │ │ └── [pid].ts │ │ │ └── circulation.ts │ │ ├── delegate │ │ │ ├── [kind].tsx │ │ │ └── my-delegation.tsx │ │ ├── farm │ │ │ ├── stake-pool.tsx │ │ │ └── vault.tsx │ │ ├── index.tsx │ │ ├── khala-assets.tsx │ │ ├── stake-pool │ │ │ └── [pid].tsx │ │ ├── staking.tsx │ │ ├── vault │ │ │ └── [pid].tsx │ │ └── wiki.tsx │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── site.webmanifest │ │ └── vpha.svg │ ├── store │ │ ├── common.ts │ │ └── ui.ts │ ├── tsconfig.json │ └── types │ │ ├── global.d.ts │ │ ├── image.d.ts │ │ ├── mui.d.ts │ │ └── reset.d.ts └── subbridge │ ├── README.md │ ├── assets │ ├── evm_chains_data.json │ ├── moonriver_xtokens_abi.json │ ├── phala_brand_logo.svg │ ├── subbridge_logo_icon.svg │ ├── subbridge_logo_text.svg │ ├── sygma_logo.svg │ └── token_standard_abi.json │ ├── components │ ├── BridgeBody │ │ ├── Action │ │ │ ├── EvmAction.tsx │ │ │ ├── PolkadotAction.tsx │ │ │ └── index.tsx │ │ ├── AmountInput.tsx │ │ ├── AssetSelect.tsx │ │ ├── ChainSelect.tsx │ │ ├── DestinationAccountInput.tsx │ │ ├── DestinationAccountWarning.tsx │ │ ├── Extra.tsx │ │ ├── PoweredBySygma.tsx │ │ └── index.tsx │ ├── BridgeConfirmationDialog.tsx │ ├── BridgeHistory.tsx │ ├── ClientOnly.tsx │ ├── ConnectWalletButton.tsx │ ├── ExplorerLink.tsx │ ├── Footer.tsx │ ├── GlobalStyles.tsx │ ├── KhalaSunsetAlert.tsx │ ├── Layout.tsx │ ├── MuiThemeProvider.tsx │ ├── PolkadotWalletDialog.tsx │ └── TopBar │ │ ├── Account │ │ ├── AccountTemplate.tsx │ │ ├── EvmAccount.tsx │ │ ├── PolkadotAccount.tsx │ │ └── index.tsx │ │ ├── MoreButton.tsx │ │ ├── SubbridgeLogo.tsx │ │ └── index.tsx │ ├── config │ ├── asset.ts │ ├── bridge.ts │ ├── chain.ts │ └── error.ts │ ├── hooks │ ├── useBalance.ts │ ├── useBridgeFee.ts │ ├── useEstimatedGasFee.ts │ ├── useEthereumProviderInitialization.ts │ ├── useEthersContract.ts │ ├── useEthersProvider.ts │ ├── usePolkadotApi.ts │ ├── useSwitchNetwork.ts │ ├── useTransfer.ts │ └── useValidation.ts │ ├── lib │ ├── createEmotionCache.ts │ ├── createPhalaMultilocation.ts │ ├── ethersFetcher.ts │ ├── evmSygma.ts │ ├── getFullUrl.ts │ ├── getGeneralKey.ts │ ├── polkadotFetcher.ts │ ├── transferByEvmSygma.ts │ ├── transferByEvmXTokens.ts │ ├── transferByPhalaXTransfer.ts │ └── transferByPolkadotXTokens.ts │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── _document.tsx │ └── index.tsx │ ├── public │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── background_waves.svg │ ├── favicon.ico │ ├── favicon.svg │ └── site.webmanifest │ ├── store │ ├── bridge.ts │ ├── common.ts │ ├── ethers.ts │ └── polkadotWalletModal.ts │ ├── tsconfig.json │ └── types │ ├── global.d.ts │ └── svgr.d.ts ├── biome.json ├── bun.lock ├── bunfig.toml ├── lefthook.yml ├── package.json ├── packages ├── lib │ ├── package.json │ ├── src │ │ ├── JotaiDevTools.tsx │ │ ├── compactFormat.ts │ │ ├── createPolkadotApi.ts │ │ ├── getDecimalPattern.ts │ │ ├── index.ts │ │ ├── signAndSend.ts │ │ ├── sleep.ts │ │ ├── toCurrency.ts │ │ ├── toFixed.ts │ │ ├── toPercentage.ts │ │ ├── transformSs58Format.ts │ │ ├── trimAddress.ts │ │ ├── useConnectPolkadotWallet.ts │ │ ├── useInterval.ts │ │ ├── useTimeout.ts │ │ ├── validateAddress.ts │ │ └── weightedAverage.ts │ └── tsconfig.json ├── polkadot-types │ ├── interfaces │ │ ├── augment-api-consts.ts │ │ ├── augment-api-errors.ts │ │ ├── augment-api-events.ts │ │ ├── augment-api-query.ts │ │ ├── augment-api-rpc.ts │ │ ├── augment-api-runtime.ts │ │ ├── augment-api-tx.ts │ │ ├── augment-api.ts │ │ ├── augment-types.ts │ │ ├── definitions.ts │ │ ├── index.ts │ │ ├── lookup.ts │ │ ├── registry.ts │ │ ├── types-lookup.ts │ │ └── types.ts │ ├── metadata.json │ ├── package.json │ ├── tsconfig.json │ └── updateMetadata.ts ├── store │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── polkadotAccountAtoms.ts │ │ └── walletAtoms.ts │ └── tsconfig.json └── ui │ ├── icons │ ├── asset │ │ ├── aca.png │ │ ├── astr.png │ │ ├── ausd.png │ │ ├── bnc.png │ │ ├── bsx.png │ │ ├── crab.png │ │ ├── dot.png │ │ ├── eth.png │ │ ├── glmr.png │ │ ├── hko.png │ │ ├── kar.png │ │ ├── kma.png │ │ ├── movr.png │ │ ├── para.png │ │ ├── pha.png │ │ ├── sdn.png │ │ ├── tur.png │ │ ├── vpha.png │ │ ├── wglmr.png │ │ └── zlk.png │ └── chain │ │ ├── acala.png │ │ ├── astar.png │ │ ├── astarevm.png │ │ ├── base.png │ │ ├── basilisk.png │ │ ├── bifrost.png │ │ ├── calamari.png │ │ ├── crab.png │ │ ├── ethereum.png │ │ ├── karura.png │ │ ├── khala.png │ │ ├── moonbase_alpha.png │ │ ├── moonbeam.png │ │ ├── moonriver.png │ │ ├── parallel.png │ │ ├── parallel_heiko.png │ │ ├── phala.png │ │ ├── polkadot.png │ │ ├── shiden.png │ │ └── turing.png │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── patches └── @buildwithsygma%2Fsygma-sdk-core@2.11.2.patch ├── tsconfig.base.json └── turbo.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | jobs: 13 | build: 14 | name: Test 15 | runs-on: ubuntu-latest 16 | env: 17 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 18 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 19 | 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: oven-sh/setup-bun@v2 23 | - run: bun install --frozen-lockfile 24 | - run: bunx biome ci . 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[javascript]": { 3 | "editor.defaultFormatter": "biomejs.biome" 4 | }, 5 | "[javascriptreact]": { 6 | "editor.defaultFormatter": "biomejs.biome" 7 | }, 8 | "[json]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[jsonc]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | }, 14 | "[typescript]": { 15 | "editor.defaultFormatter": "biomejs.biome" 16 | }, 17 | "[typescriptreact]": { 18 | "editor.defaultFormatter": "biomejs.biome" 19 | }, 20 | "biome.enabled": true, 21 | "cSpell.words": [ 22 | "biomejs", 23 | "bunx", 24 | "crowdloan", 25 | "fontawesome", 26 | "fortawesome", 27 | "karura", 28 | "khala", 29 | "lefthook", 30 | "moonbase", 31 | "moonriver", 32 | "movr", 33 | "orml", 34 | "parachain", 35 | "phala", 36 | "polkadot", 37 | "preinstall", 38 | "readystate", 39 | "roboto", 40 | "staker", 41 | "stakers", 42 | "subbridge", 43 | "Subscan", 44 | "subsquid", 45 | "svgs", 46 | "sygma", 47 | "talismn", 48 | "tanstack", 49 | "tokenomic", 50 | "unsub", 51 | "xtokens", 52 | "connectkit", 53 | "wagmi", 54 | "walletconnect" 55 | ], 56 | "editor.codeActionsOnSave": { 57 | "quickfix.biome": "explicit", 58 | "source.addMissingImports": "explicit", 59 | "source.organizeImports.biome": "explicit" 60 | }, 61 | "editor.formatOnSave": true, 62 | "files.associations": { 63 | "*.css": "tailwindcss" 64 | }, 65 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 66 | "javascript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 67 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingEmptyBraces": false, 68 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 69 | "typescript.tsdk": "./node_modules/typescript/lib" 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phala Apps 2 | 3 | [![Discord](https://img.shields.io/discord/697726436211163147?color=%235865F2&label=discord&style=for-the-badge)](https://discord.gg/phala-network) 4 | 5 | Monorepo for apps in phala. 6 | 7 | ## Apps 8 | 9 | ### [Phala App](/apps/app) 10 | 11 | ### [SubBridge](/apps/subbridge) 12 | -------------------------------------------------------------------------------- /apps/app/.gitignore: -------------------------------------------------------------------------------- 1 | # Sentry Config File 2 | .env.sentry-build-plugin 3 | -------------------------------------------------------------------------------- /apps/app/README.md: -------------------------------------------------------------------------------- 1 | # [Phala App](https://app.phala.network) 2 | -------------------------------------------------------------------------------- /apps/app/assets/b1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/apps/4ab19eaf2f6ba3d12a1c25c0a745b32a5f0e6a8a/apps/app/assets/b1.png -------------------------------------------------------------------------------- /apps/app/assets/nova_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/apps/4ab19eaf2f6ba3d12a1c25c0a745b32a5f0e6a8a/apps/app/assets/nova_logo.png -------------------------------------------------------------------------------- /apps/app/assets/phala_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /apps/app/assets/powered_by_slpx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/apps/4ab19eaf2f6ba3d12a1c25c0a745b32a5f0e6a8a/apps/app/assets/powered_by_slpx.png -------------------------------------------------------------------------------- /apps/app/assets/share_equation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/apps/4ab19eaf2f6ba3d12a1c25c0a745b32a5f0e6a8a/apps/app/assets/share_equation.png -------------------------------------------------------------------------------- /apps/app/assets/stake_pool.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/app/assets/subwallet_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phala-Network/apps/4ab19eaf2f6ba3d12a1c25c0a745b32a5f0e6a8a/apps/app/assets/subwallet_logo.png -------------------------------------------------------------------------------- /apps/app/assets/vault.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /apps/app/assets/vault_detailed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /apps/app/assets/whitelists.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /apps/app/components/BasePool/CreateButton.tsx: -------------------------------------------------------------------------------- 1 | import usePolkadotApi from '@/hooks/usePolkadotApi' 2 | import useSignAndSend from '@/hooks/useSignAndSend' 3 | import type {BasePoolKind} from '@/lib/subsquidQuery' 4 | import type {ButtonProps} from '@mui/material' 5 | import type {FC} from 'react' 6 | import PromiseButton from '../PromiseButton' 7 | 8 | const CreateButton: FC<{kind: BasePoolKind} & Omit> = ({ 9 | kind, 10 | ...props 11 | }) => { 12 | const api = usePolkadotApi() 13 | const signAndSend = useSignAndSend() 14 | const onClick = async (): Promise => { 15 | if (api == null) return 16 | const extrinsic = 17 | kind === 'StakePool' 18 | ? api.tx.phalaStakePoolv2.create() 19 | : api.tx.phalaVault.create() 20 | 21 | await signAndSend(extrinsic) 22 | } 23 | 24 | return ( 25 | {`Create ${kind}`} 31 | ) 32 | } 33 | 34 | export default CreateButton 35 | -------------------------------------------------------------------------------- /apps/app/components/BasePool/Whitelist/AddWhitelist.tsx: -------------------------------------------------------------------------------- 1 | import usePolkadotApi from '@/hooks/usePolkadotApi' 2 | import useSignAndSend from '@/hooks/useSignAndSend' 3 | import { 4 | Button, 5 | DialogActions, 6 | DialogContent, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material' 10 | import {validateAddress} from '@phala/lib' 11 | import {type FC, useMemo, useState} from 'react' 12 | 13 | const AddWhitelist: FC<{ 14 | pid: string 15 | onClose: () => void 16 | }> = ({pid, ...props}) => { 17 | const api = usePolkadotApi() 18 | const signAndSend = useSignAndSend() 19 | const [loading, setLoading] = useState(false) 20 | const [address, setAddress] = useState('') 21 | const isValid = useMemo(() => validateAddress(address), [address]) 22 | 23 | const onClick = (): void => { 24 | if (api == null) return 25 | const extrinsic = api.tx.phalaBasePool.addStakerToWhitelist(pid, address) 26 | 27 | setLoading(true) 28 | signAndSend(extrinsic) 29 | .then(() => { 30 | props.onClose() 31 | }) 32 | .catch(() => { 33 | setLoading(false) 34 | }) 35 | } 36 | 37 | return ( 38 | <> 39 | Add Whitelist 40 | 41 | { 51 | setAddress(e.target.value) 52 | }} 53 | /> 54 | 55 | 56 | 64 | 65 | 66 | ) 67 | } 68 | 69 | export default AddWhitelist 70 | -------------------------------------------------------------------------------- /apps/app/components/BasePool/Worker/AddWorker.tsx: -------------------------------------------------------------------------------- 1 | import usePolkadotApi from '@/hooks/usePolkadotApi' 2 | import useSignAndSend from '@/hooks/useSignAndSend' 3 | import { 4 | Button, 5 | DialogActions, 6 | DialogContent, 7 | DialogTitle, 8 | TextField, 9 | } from '@mui/material' 10 | import {type FC, useMemo, useState} from 'react' 11 | 12 | const AddWorker: FC<{ 13 | pid: string 14 | onClose: () => void 15 | }> = ({onClose, pid}) => { 16 | const api = usePolkadotApi() 17 | const signAndSend = useSignAndSend() 18 | const [loading, setLoading] = useState(false) 19 | const [workerId, setWorkerId] = useState('') 20 | const isValid = useMemo( 21 | () => /^0x[0-9a-fA-F]{64}$/.test(workerId), 22 | [workerId], 23 | ) 24 | 25 | const onClick = (): void => { 26 | if (api == null) return 27 | const extrinsic = api.tx.phalaStakePoolv2.addWorker(pid, workerId) 28 | 29 | setLoading(true) 30 | signAndSend(extrinsic) 31 | .then(() => { 32 | onClose() 33 | }) 34 | .catch(() => { 35 | setLoading(false) 36 | }) 37 | } 38 | 39 | return ( 40 | <> 41 | Add Worker 42 | 43 | { 54 | setWorkerId(e.target.value) 55 | }} 56 | /> 57 | 58 | 59 | 67 | 68 | 69 | ) 70 | } 71 | 72 | export default AddWorker 73 | -------------------------------------------------------------------------------- /apps/app/components/ClientOnly.tsx: -------------------------------------------------------------------------------- 1 | import {type FC, type ReactNode, useEffect, useState} from 'react' 2 | 3 | // MEMO: different from MUI , fallback content will be also rendered when children is falsy 4 | const ClientOnly: FC<{children: ReactNode; fallback?: ReactNode}> = ({ 5 | children, 6 | fallback, 7 | }) => { 8 | const [hasMounted, setHasMounted] = useState(false) 9 | 10 | useEffect(() => { 11 | setHasMounted(true) 12 | }, []) 13 | 14 | return ( 15 | <> 16 | {hasMounted && children != null && children !== false 17 | ? children 18 | : fallback} 19 | 20 | ) 21 | } 22 | 23 | export default ClientOnly 24 | -------------------------------------------------------------------------------- /apps/app/components/Delegation/Menu.tsx: -------------------------------------------------------------------------------- 1 | import usePolkadotApi from '@/hooks/usePolkadotApi' 2 | import useSignAndSend from '@/hooks/useSignAndSend' 3 | import type {DelegationCommonFragment} from '@/lib/subsquidQuery' 4 | import MoreVert from '@mui/icons-material/MoreVert' 5 | import { 6 | IconButton, 7 | Menu, 8 | MenuItem, 9 | type SxProps, 10 | type Theme, 11 | } from '@mui/material' 12 | import {type FC, useRef, useState} from 'react' 13 | import type {OnAction} from './List' 14 | 15 | const DelegationMenu: FC<{ 16 | onAction?: OnAction 17 | delegation: DelegationCommonFragment 18 | sx?: SxProps 19 | }> = ({delegation, onAction, sx}) => { 20 | const api = usePolkadotApi() 21 | const signAndSend = useSignAndSend() 22 | const {basePool} = delegation 23 | const [menuOpen, setMenuOpen] = useState(false) 24 | const moreRef = useRef(null) 25 | const poolHasWithdrawal = delegation.basePool.withdrawingShares !== '0' 26 | const isVault = basePool.kind === 'Vault' 27 | const reclaim = async (): Promise => { 28 | if (api == null) return 29 | await signAndSend( 30 | isVault 31 | ? api.tx.phalaVault.checkAndMaybeForceWithdraw(basePool.id) 32 | : api.tx.phalaStakePoolv2.checkAndMaybeForceWithdraw(basePool.id), 33 | ) 34 | } 35 | 36 | return ( 37 | <> 38 | { 41 | setMenuOpen(true) 42 | }} 43 | sx={sx} 44 | > 45 | 46 | 47 | { 51 | setMenuOpen(false) 52 | }} 53 | > 54 | { 56 | onAction?.(delegation, 'withdraw') 57 | setMenuOpen(false) 58 | }} 59 | > 60 | Withdraw 61 | 62 | { 65 | void reclaim() 66 | setMenuOpen(false) 67 | }} 68 | > 69 | Reclaim 70 | 71 | { 73 | location.href = `/api/phala/snapshots/delegation/${delegation.id}` 74 | setMenuOpen(false) 75 | }} 76 | > 77 | Download historical data 78 | 79 | 80 | 81 | ) 82 | } 83 | 84 | export default DelegationMenu 85 | -------------------------------------------------------------------------------- /apps/app/components/DownloadHistory.tsx: -------------------------------------------------------------------------------- 1 | import {Button, type ButtonProps} from '@mui/material' 2 | import type {FC} from 'react' 3 | 4 | const DownloadHistory: FC< 5 | { 6 | kind: 'pool' | 'account' | 'delegation' 7 | id: string 8 | } & ButtonProps 9 | > = ({kind, id, ...props}) => { 10 | return ( 11 | 19 | ) 20 | } 21 | 22 | export default DownloadHistory 23 | -------------------------------------------------------------------------------- /apps/app/components/Empty.tsx: -------------------------------------------------------------------------------- 1 | import {Stack, type SxProps, Typography} from '@mui/material' 2 | import type {FC} from 'react' 3 | 4 | const Empty: FC<{message?: string; sx?: SxProps}> = ({ 5 | message = 'No Results', 6 | sx, 7 | }) => { 8 | return ( 9 | 10 | 11 | {message} 12 | 13 | 14 | ) 15 | } 16 | 17 | export default Empty 18 | -------------------------------------------------------------------------------- /apps/app/components/HydrateAtoms.tsx: -------------------------------------------------------------------------------- 1 | import type {WritableAtom} from 'jotai' 2 | import {useHydrateAtoms} from 'jotai/utils' 3 | import type {FC, ReactNode} from 'react' 4 | 5 | const HydrateAtoms: FC<{ 6 | // biome-ignore lint/suspicious/noExplicitAny: 7 | initialValues: Map, any> 8 | children: ReactNode 9 | }> = ({initialValues, children}) => { 10 | useHydrateAtoms(initialValues) 11 | return <>{children} 12 | } 13 | 14 | export default HydrateAtoms 15 | -------------------------------------------------------------------------------- /apps/app/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import useListenBlockHeight from '@/hooks/useListenBlockHeight' 2 | import {useNotice} from '@/hooks/useNotice' 3 | import useResetVault from '@/hooks/useResetVault' 4 | import useShowWalletDialog from '@/hooks/useShowWalletDialog' 5 | import {Container} from '@mui/material' 6 | import {useConnectPolkadotWallet} from '@phala/lib' 7 | import {useRouter} from 'next/router' 8 | import type {FC, ReactNode} from 'react' 9 | import ScrollTop from './ScrollTop' 10 | import TopBar from './TopBar' 11 | import WalletDialog from './WalletDialog' 12 | import WikiDialog from './Wiki/Dialog' 13 | const lgWidthPathnames = ['/staking', '/wiki'] 14 | 15 | const Layout: FC<{children: ReactNode}> = ({children}) => { 16 | const router = useRouter() 17 | 18 | useConnectPolkadotWallet('Phala App', 30) 19 | useListenBlockHeight() 20 | useResetVault() 21 | useShowWalletDialog() 22 | useNotice() 23 | 24 | return ( 25 | <> 26 | 27 | 31 | {children} 32 | 33 | 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | export default Layout 41 | -------------------------------------------------------------------------------- /apps/app/components/ListSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import {Skeleton, type SkeletonProps} from '@mui/material' 2 | import {forwardRef} from 'react' 3 | 4 | const ListSkeleton = forwardRef< 5 | HTMLSpanElement, 6 | SkeletonProps & {count?: number} 7 | >(({count = 5, ...props}, ref) => ( 8 | <> 9 | {Array.from({length: count}).map((_, index) => ( 10 | 17 | ))} 18 | 19 | )) 20 | 21 | ListSkeleton.displayName = 'ListSkeleton' 22 | 23 | export default ListSkeleton 24 | -------------------------------------------------------------------------------- /apps/app/components/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import {Stack, Typography} from '@mui/material' 2 | import type {FC, ReactNode} from 'react' 3 | import Title from './Title' 4 | 5 | const PageHeader: FC<{ 6 | title: string 7 | pageTitle?: ReactNode 8 | children?: ReactNode 9 | }> = ({title, pageTitle, children}) => { 10 | return ( 11 | <> 12 | {title} 13 | 20 | 26 | {pageTitle ?? title} 27 | 28 | 29 | {pageTitle ?? title} 30 | 31 | {children} 32 | 33 | 34 | ) 35 | } 36 | 37 | export default PageHeader 38 | -------------------------------------------------------------------------------- /apps/app/components/PromiseButton.tsx: -------------------------------------------------------------------------------- 1 | import {Button, type ButtonProps} from '@mui/material' 2 | import {type FC, useState} from 'react' 3 | 4 | const PromiseButton: FC< 5 | Omit & { 6 | onClick?: ( 7 | ...args: Parameters> 8 | ) => Promise 9 | } 10 | > = ({onClick, ...props}) => { 11 | const [loading, setLoading] = useState(false) 12 | 13 | const handleClick: ButtonProps['onClick'] = (e) => { 14 | if (onClick != null) { 15 | setLoading(true) 16 | void onClick(e).finally(() => { 17 | setLoading(false) 18 | }) 19 | } 20 | } 21 | 22 | return 20 | ) 21 | } 22 | 23 | export default SwitchChainButton 24 | -------------------------------------------------------------------------------- /apps/app/components/TextSkeleton.tsx: -------------------------------------------------------------------------------- 1 | import {Skeleton, Stack, type StackProps} from '@mui/material' 2 | import type {FC} from 'react' 3 | 4 | const TextSkeleton: FC = (props) => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | export default TextSkeleton 15 | -------------------------------------------------------------------------------- /apps/app/components/Title.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import type {FC} from 'react' 3 | 4 | const Title: FC<{children: string}> = ({children}) => { 5 | return ( 6 | 7 | {`${children} | Phala App`} 8 | 9 | ) 10 | } 11 | 12 | export default Title 13 | -------------------------------------------------------------------------------- /apps/app/components/TopBar/Account.tsx: -------------------------------------------------------------------------------- 1 | import {walletDialogOpenAtom} from '@/store/ui' 2 | import {Box, Button, useTheme} from '@mui/material' 3 | import {polkadotAccountAtom, walletAtom} from '@phala/store' 4 | import {useAtom} from 'jotai' 5 | import Image from 'next/image' 6 | import type {FC} from 'react' 7 | 8 | const Account: FC = () => { 9 | const theme = useTheme() 10 | const [wallet] = useAtom(walletAtom) 11 | const [polkadotAccount] = useAtom(polkadotAccountAtom) 12 | const [, setWalletDialogOpen] = useAtom(walletDialogOpenAtom) 13 | const isConnected = wallet != null && polkadotAccount != null 14 | return ( 15 | 44 | ) 45 | } 46 | 47 | export default Account 48 | -------------------------------------------------------------------------------- /apps/app/components/TopBar/ChainStatus.tsx: -------------------------------------------------------------------------------- 1 | import {subsquidClient} from '@/config' 2 | import usePolkadotApi from '@/hooks/usePolkadotApi' 3 | import {useGlobalStateQuery} from '@/lib/subsquidQuery' 4 | import CircleIcon from '@mui/icons-material/Circle' 5 | import {Tooltip} from '@mui/material' 6 | import {toCurrency} from '@phala/lib' 7 | import {useQuery} from '@tanstack/react-query' 8 | import {type FC, useMemo} from 'react' 9 | import Property from '../Property' 10 | 11 | const ChainStatus: FC = () => { 12 | const api = usePolkadotApi() 13 | 14 | const {data: globalStateData} = useGlobalStateQuery( 15 | subsquidClient, 16 | undefined, 17 | {select: (data) => data.globalStateById}, 18 | ) 19 | const subsquidHeight = globalStateData?.height 20 | const blockTime = globalStateData?.averageBlockTime 21 | const apiConnected = api?.isConnected 22 | const {data: chainHeight} = useQuery({ 23 | queryKey: ['chainHeight'], 24 | queryFn: async () => { 25 | if (api == null) return null 26 | const lastHeader = await api.rpc.chain.getHeader() 27 | return lastHeader.number.toNumber() 28 | }, 29 | refetchInterval: 3000, 30 | }) 31 | const status: 'success' | 'warning' | 'error' = useMemo(() => { 32 | if (apiConnected === true) { 33 | if ( 34 | subsquidHeight != null && 35 | chainHeight != null && 36 | chainHeight - subsquidHeight < 10 37 | ) { 38 | return 'success' 39 | } 40 | return 'warning' 41 | } 42 | 43 | return 'error' 44 | }, [apiConnected, chainHeight, subsquidHeight]) 45 | 46 | return ( 47 | { 52 | e.stopPropagation() 53 | }, 54 | }, 55 | }} 56 | title={ 57 | <> 58 | 59 | {apiConnected === true ? 'Connected' : 'Disconnected'} 60 | 61 | 62 | {subsquidHeight == null ? '-' : toCurrency(subsquidHeight)} 63 | 64 | 65 | {chainHeight == null ? '-' : toCurrency(chainHeight)} 66 | 67 | 68 | {blockTime == null ? '-' : `${blockTime / 1000}s`} 69 | 70 | 71 | } 72 | > 73 | 74 | 75 | ) 76 | } 77 | 78 | export default ChainStatus 79 | -------------------------------------------------------------------------------- /apps/app/components/Vest.tsx: -------------------------------------------------------------------------------- 1 | import usePolkadotApi from '@/hooks/usePolkadotApi' 2 | import useSignAndSend from '@/hooks/useSignAndSend' 3 | import { 4 | DialogActions, 5 | DialogContent, 6 | DialogTitle, 7 | Skeleton, 8 | Stack, 9 | } from '@mui/material' 10 | import {toCurrency} from '@phala/lib' 11 | import {polkadotAccountAtom} from '@phala/store' 12 | import type {DeriveBalancesAll} from '@polkadot/api-derive/types' 13 | import BN from 'bn.js' 14 | import Decimal from 'decimal.js' 15 | import {useAtom} from 'jotai' 16 | import {type FC, type ReactElement, useEffect, useState} from 'react' 17 | import PromiseButton from './PromiseButton' 18 | import Property from './Property' 19 | 20 | const format = (bn?: BN): ReactElement => { 21 | if (bn !== undefined) { 22 | // HACK: derived vestedClaimable may be negative 23 | const value = bn.isNeg() ? new BN(0) : bn 24 | return <>{`${toCurrency(new Decimal(value.toString()).div(1e12))} PHA`} 25 | } 26 | return 27 | } 28 | 29 | const Vest: FC<{onClose: () => void}> = ({onClose}) => { 30 | const api = usePolkadotApi() 31 | const signAndSend = useSignAndSend() 32 | const [allBalances, setAllBalances] = useState() 33 | const [account] = useAtom(polkadotAccountAtom) 34 | 35 | useEffect(() => { 36 | let unmounted = false 37 | if (api != null && account != null) { 38 | void api.derive.balances.all(account.address).then((all) => { 39 | if (!unmounted) { 40 | setAllBalances(all) 41 | } 42 | }) 43 | } 44 | return () => { 45 | unmounted = true 46 | } 47 | }, [api, account]) 48 | 49 | const vest = async (): Promise => { 50 | if (api !== undefined) { 51 | await signAndSend(api.tx.vesting.vest()).then(onClose) 52 | } 53 | } 54 | 55 | return ( 56 | <> 57 | Claim Crowdloan Reward 58 | 59 | 60 | 61 | {format( 62 | allBalances?.vestedBalance.sub(allBalances.vestedClaimable), 63 | )} 64 | 65 | 66 | {format(allBalances?.vestingLocked)} 67 | 68 | 69 | {format(allBalances?.vestedClaimable)} 70 | 71 | 72 | 73 | 74 | 83 | Claim 84 | 85 | 86 | 87 | ) 88 | } 89 | 90 | export default Vest 91 | -------------------------------------------------------------------------------- /apps/app/components/Web3Provider.tsx: -------------------------------------------------------------------------------- 1 | import {ethChain} from '@/config' 2 | import { 3 | RainbowKitProvider, 4 | darkTheme, 5 | getDefaultConfig, 6 | } from '@rainbow-me/rainbowkit' 7 | import type {ReactNode} from 'react' 8 | import type {Chain} from 'viem' 9 | import {WagmiProvider} from 'wagmi' 10 | import '@rainbow-me/rainbowkit/styles.css' 11 | 12 | const chains: [Chain, ...Chain[]] = [ethChain] 13 | 14 | const config = getDefaultConfig({ 15 | chains, 16 | projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID as string, 17 | appName: 'Phala App', 18 | appUrl: 'https://app.phala.network', 19 | appIcon: 'https://app.phala.network/apple-touch-icon.png', 20 | ssr: true, 21 | }) 22 | 23 | export const Web3Provider = ({ 24 | children, 25 | }: { 26 | children: ReactNode 27 | }) => { 28 | return ( 29 | 30 | 39 | {children} 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /apps/app/components/Wiki/Button.tsx: -------------------------------------------------------------------------------- 1 | import type {WikiEntry} from '@/assets/wikiData' 2 | import {wikiExpandEntryAtom} from '@/store/common' 3 | import {wikiDialogOpenAtom} from '@/store/ui' 4 | import InfoIcon from '@mui/icons-material/Info' 5 | import {IconButton, Stack} from '@mui/material' 6 | import {useAtom} from 'jotai' 7 | import type {FC, ReactNode} from 'react' 8 | 9 | const WikiButton: FC<{entry: WikiEntry; children?: ReactNode}> = ({ 10 | entry, 11 | children, 12 | }) => { 13 | const [, setOpen] = useAtom(wikiDialogOpenAtom) 14 | const [, setExpand] = useAtom(wikiExpandEntryAtom) 15 | 16 | return ( 17 | 18 | {children} 19 | { 22 | e.stopPropagation() 23 | setOpen(true) 24 | setExpand(entry) 25 | }} 26 | sx={{p: 0.5, mr: -0.5, mb: -1, mt: '-9px', ml: 0}} 27 | > 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | export default WikiButton 35 | -------------------------------------------------------------------------------- /apps/app/components/Wiki/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import {wikiDialogOpenAtom} from '@/store/ui' 2 | import {Dialog, DialogContent, useMediaQuery, useTheme} from '@mui/material' 3 | import {useAtom} from 'jotai' 4 | import type {FC} from 'react' 5 | import Wiki from '.' 6 | 7 | const WikiDialog: FC = () => { 8 | const [open, setOpen] = useAtom(wikiDialogOpenAtom) 9 | const theme = useTheme() 10 | const fullScreen = useMediaQuery(theme.breakpoints.down('sm')) 11 | 12 | return ( 13 | { 17 | setOpen(false) 18 | }} 19 | PaperProps={{sx: {height: fullScreen ? undefined : '700px'}}} 20 | > 21 | 22 | 23 | 24 | 25 | ) 26 | } 27 | 28 | export default WikiDialog 29 | -------------------------------------------------------------------------------- /apps/app/components/WrapDecimal.tsx: -------------------------------------------------------------------------------- 1 | import {Box} from '@mui/material' 2 | import {type FC, Fragment, type ReactNode, useMemo} from 'react' 3 | 4 | const regex = /(\d+\.\d+)/g 5 | 6 | const WrapDecimal: FC<{children: ReactNode}> = ({children}) => { 7 | const parts = useMemo(() => { 8 | if (typeof children === 'string') { 9 | return children.split(regex) 10 | } 11 | return [] 12 | }, [children]) 13 | 14 | if (parts.length === 0) { 15 | return children 16 | } 17 | 18 | return ( 19 | <> 20 | {parts.map((part, index) => { 21 | if (regex.test(part)) { 22 | const [integer, decimal] = part.split('.') 23 | return ( 24 | 28 | {integer}. 29 | 30 | {decimal} 31 | 32 | 33 | ) 34 | } 35 | // biome-ignore lint/suspicious/noArrayIndexKey: 36 | return {part} 37 | })} 38 | 39 | ) 40 | } 41 | 42 | export default WrapDecimal 43 | -------------------------------------------------------------------------------- /apps/app/components/ZendeskWidget.tsx: -------------------------------------------------------------------------------- 1 | import Script from 'next/script' 2 | import type {FC} from 'react' 3 | 4 | declare global { 5 | interface Window { 6 | zESettings: unknown 7 | } 8 | } 9 | 10 | const ZendeskWidget: FC = () => { 11 | return ( 12 |