├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ └── bug_report.md ├── images │ └── MULTIX_LOGO_FULL_BLUE_1200px.png ├── pull_request_template.md └── workflows │ ├── cypress-tests.yaml │ ├── deploy-ui-cf-pages-multix-cloud.yaml │ ├── formating.yaml │ ├── lint-squid.yaml │ └── lint-ui.yaml ├── .gitignore ├── .node-version ├── .prettierignore ├── .prettierrc.js ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-workspace-tools.cjs └── releases │ └── yarn-3.6.3.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── chopsticks-config.yml ├── docker-compose.yml ├── package.json ├── packages └── ui │ ├── .env.example │ ├── .env.staging │ ├── .eslintrc │ ├── .papi │ ├── descriptors │ │ ├── .gitignore │ │ └── package.json │ ├── metadata │ │ ├── acala.scale │ │ ├── bifrostDot.scale │ │ ├── coretimeDot.scale │ │ ├── dancelight.scale │ │ ├── dot.scale │ │ ├── dotAssetHub.scale │ │ ├── dotPpl.scale │ │ ├── hydration.scale │ │ ├── ksm-asset-hub.scale │ │ ├── ksm.scale │ │ ├── ksmAssetHub.scale │ │ ├── ksmPpl.scale │ │ ├── pasPpl.scale │ │ ├── paseo.scale │ │ ├── phala.scale │ │ ├── polimec.scale │ │ ├── rhala.scale │ │ ├── wesAssetHub.scale │ │ ├── wesPpl.scale │ │ └── westend.scale │ └── polkadot-api.json │ ├── README.md │ ├── cypress.config.ts │ ├── cypress │ ├── fixtures │ │ ├── extrinsicsDisplayAccounts.ts │ │ ├── knownMultisigs.ts │ │ ├── landingData.ts │ │ ├── nameDisplay.ts │ │ ├── polkadotAccounts.ts │ │ ├── polkadotAssetHub.ts │ │ ├── testAccounts.ts │ │ └── westendAccounts.ts │ ├── support │ │ ├── commands.ts │ │ ├── e2e.ts │ │ └── page-objects │ │ │ ├── components │ │ │ ├── accountDisplay.ts │ │ │ └── expander.ts │ │ │ ├── landingPage.ts │ │ │ ├── modals │ │ │ ├── editNamesModal.ts │ │ │ ├── hiddenAccountInfoModal.ts │ │ │ └── txSigningModal.ts │ │ │ ├── multisigPage.ts │ │ │ ├── newMultisigPage.ts │ │ │ ├── notifications.ts │ │ │ ├── sendTxModal.ts │ │ │ ├── settingsPage.ts │ │ │ └── topMenuItems.ts │ ├── tests │ │ ├── address-bar.cy.ts │ │ ├── balances-transfer-batch-multi-asset.cy.ts │ │ ├── default-multisigs.cy.ts │ │ ├── hidden-accounts.cy.ts │ │ ├── hydration-extrinsic-display.cy.ts │ │ ├── identity.cy.ts │ │ ├── landing-messaging.cy.ts │ │ ├── login.cy.ts │ │ ├── multisig-creation.cy.ts │ │ ├── multisig-pure-display.cy.ts │ │ ├── name-edition-display.cy.ts │ │ ├── network-switch.cy.ts │ │ ├── transaction-display.cy.ts │ │ ├── transactions.cy.ts │ │ ├── walletconnect.cy.ts │ │ └── watched-accounts.cy.ts │ └── utils │ │ ├── clickOnConnect.ts │ │ ├── getShortAddress.ts │ │ ├── rejectCurrentMultisigTxs.ts │ │ ├── waitForAuthRequests.ts │ │ ├── waitForMultisigLength.ts │ │ └── waitForTxRequests.ts │ ├── graphql.config.json │ ├── index.html │ ├── package.json │ ├── public │ ├── .well-known │ │ └── walletconnect.txt │ ├── OG_IMAGE_BLUE_600px.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── manifest.json │ └── robots.txt │ ├── src │ ├── App.tsx │ ├── components │ │ ├── AccountDisplay │ │ │ ├── AccountDisplay.tsx │ │ │ └── EditInput.tsx │ │ ├── AccountEditName.tsx │ │ ├── AccountSelect │ │ │ └── index.tsx │ │ ├── CallInfo.tsx │ │ ├── ConnectCreateOrWatch.tsx │ │ ├── CurrentReferendumBanner.tsx │ │ ├── DomainMoveBanner.tsx │ │ ├── Drawer │ │ │ ├── Drawer.tsx │ │ │ └── DrawerMenu.tsx │ │ ├── EasySetup │ │ │ ├── BalancesTransfer.tsx │ │ │ ├── FromCallData.tsx │ │ │ ├── ManualExtrinsic.tsx │ │ │ └── SetIdentity.tsx │ │ ├── ErrorFallback │ │ │ └── ErrorFallback.tsx │ │ ├── Expander.tsx │ │ ├── ExportedData.tsx │ │ ├── Header │ │ │ └── Header.tsx │ │ ├── IdenticonBadge.tsx │ │ ├── IdentityIcon.tsx │ │ ├── LoadingBox.tsx │ │ ├── MultisigCompactDisplay.tsx │ │ ├── MultixIdenticon.tsx │ │ ├── NewMulisigAlert.tsx │ │ ├── OptionsMenu.tsx │ │ ├── SuccessCreation.tsx │ │ ├── TeleportFundsAlert.tsx │ │ ├── Toasts │ │ │ ├── Snackbar.tsx │ │ │ ├── ToastBar.tsx │ │ │ └── ToastContent.tsx │ │ ├── Transactions │ │ │ ├── Transaction.tsx │ │ │ ├── TransactionList.tsx │ │ │ └── TransactionProgress.tsx │ │ ├── TransferAsset.tsx │ │ ├── WalletConnect │ │ │ ├── WalletConnectActiveSessions.tsx │ │ │ └── WalletConnectSession.tsx │ │ ├── layout │ │ │ ├── Center.tsx │ │ │ └── Main.tsx │ │ ├── library │ │ │ ├── AssetBalance.tsx │ │ │ ├── Autocomplete.tsx │ │ │ ├── Balance.tsx │ │ │ ├── Button.tsx │ │ │ ├── InputField.tsx │ │ │ ├── Link.tsx │ │ │ ├── ModalCloseButton.tsx │ │ │ ├── Select.tsx │ │ │ ├── TextFieldLargeStyled.tsx │ │ │ ├── TextFieldStyled.tsx │ │ │ └── index.tsx │ │ ├── modals │ │ │ ├── ChangeMultisig.tsx │ │ │ ├── EditNames.tsx │ │ │ ├── HiddenAccountInfo.tsx │ │ │ ├── ProposalSigning.tsx │ │ │ ├── Send.tsx │ │ │ ├── WalletConnectSessionProposal.tsx │ │ │ └── WalletConnectSigning.tsx │ │ └── select │ │ │ ├── AccountSelection.tsx │ │ │ ├── GenericAccountSelection.tsx │ │ │ ├── MultiProxySelection.tsx │ │ │ ├── NetworkSelection.tsx │ │ │ ├── OptionMenuItem.tsx │ │ │ ├── SignatorySelection.tsx │ │ │ └── SignerSelection.tsx │ ├── constants.ts │ ├── contexts │ │ ├── AccountNamesContext.tsx │ │ ├── AccountsContext.tsx │ │ ├── ApiContext.tsx │ │ ├── AssetsContext.tsx │ │ ├── HiddenAccountsContext.tsx │ │ ├── ModalsContext.tsx │ │ ├── MultiProxyContext.tsx │ │ ├── NetworkContext.tsx │ │ ├── PendingTxContext.tsx │ │ ├── PeopleChainApiContext.tsx │ │ ├── ToastContext.tsx │ │ ├── WalletConnectContext.tsx │ │ └── WatchedAccountsContext.tsx │ ├── gql │ │ ├── fragment-masking.ts │ │ ├── gql.ts │ │ ├── graphql.ts │ │ └── index.ts │ ├── hooks │ │ ├── useAccountBaseFromAccountList.tsx │ │ ├── useAccountDisplayInfo.tsx │ │ ├── useAccountId.ts │ │ ├── useAnyApi.tsx │ │ ├── useCallInfoFromCallData.tsx │ │ ├── useCheckTransferableBalance.tsx │ │ ├── useDisplayError.tsx │ │ ├── useDisplayLoader.tsx │ │ ├── useFetchData.ts │ │ ├── useGetAssetBalance.tsx │ │ ├── useGetAssetBalances.tsx │ │ ├── useGetBalance.tsx │ │ ├── useGetED.tsx │ │ ├── useGetEncodedAddress.tsx │ │ ├── useGetIdentity.tsx │ │ ├── useGetMultisigAddress.tsx │ │ ├── useGetMultisigTx.tsx │ │ ├── useGetSortAddress.tsx │ │ ├── useHasIdentityFeature.tsx │ │ ├── useIdentityApi.tsx │ │ ├── useImportExportLocalData.tsx │ │ ├── useMultisigProposalNeededFunds.tsx │ │ ├── usePjsLinks.tsx │ │ ├── useProxyAdditionNeededFunds.tsx │ │ ├── usePureProxyCreationNeededFunds.tsx │ │ ├── useQueryMultisigsAndPureByAccounts.tsx │ │ ├── useSetIdentityReservedFunds.tsx │ │ ├── useSigningCallback.tsx │ │ ├── useSubscanLink.tsx │ │ ├── useSwitchAddress.tsx │ │ ├── useWalletConnectEventsManager.ts │ │ └── useWalletConnectNamespace.tsx │ ├── index.tsx │ ├── logos │ │ ├── acalaSVG.ts │ │ ├── amplitudeSVG.ts │ │ ├── assetHubSVG.ts │ │ ├── astarPNG.ts │ │ ├── bifrostSVG.ts │ │ ├── chainsafeSVG.ts │ │ ├── coretimeSVG.ts │ │ ├── dancelightSVG.svg │ │ ├── hydrationSVG.ts │ │ ├── interlaySVG.ts │ │ ├── joystreamSVG.ts │ │ ├── khalaSVG.ts │ │ ├── kiltPNG.ts │ │ ├── kusamaSVG .ts │ │ ├── localSVG.ts │ │ ├── moonbeamSVG.ts │ │ ├── moonriverSVG.ts │ │ ├── multix-logo.svg │ │ ├── multixLogo.ts │ │ ├── paseoSVG.svg │ │ ├── pendulumSVG.ts │ │ ├── phalaSVG.ts │ │ ├── polimecSVG.ts │ │ ├── polkadot-circleSVG.ts │ │ ├── rococoSVG.ts │ │ ├── usdc.svg │ │ ├── usdt.svg │ │ ├── w3fSVG.ts │ │ ├── walletConnectSVG.svg │ │ ├── watrPNG.ts │ │ └── westend_colourSVG.ts │ ├── pages │ │ ├── About.tsx │ │ ├── Creation │ │ │ ├── NameSelection.tsx │ │ │ ├── Summary.tsx │ │ │ ├── ThresholdSelection.tsx │ │ │ ├── WithProxySelection.tsx │ │ │ └── index.tsx │ │ ├── Home │ │ │ ├── HeaderView.tsx │ │ │ ├── Home.tsx │ │ │ ├── MultisigAccordion.tsx │ │ │ ├── MultisigActionMenu.tsx │ │ │ └── MultisigView.tsx │ │ ├── Import │ │ │ └── Import.tsx │ │ ├── Overview │ │ │ ├── CustomNode.tsx │ │ │ ├── OverviewHeaderView.tsx │ │ │ └── index.tsx │ │ ├── Settings │ │ │ ├── Export.tsx │ │ │ ├── HiddenAccounts.tsx │ │ │ ├── Settings.tsx │ │ │ └── WatchedAccounts.tsx │ │ ├── index.ts │ │ ├── multisigHelpers.ts │ │ └── routes.tsx │ ├── queries │ │ ├── multisigById.graphql │ │ └── multisigsAndPureByAccount.graphql │ ├── styles │ │ ├── App.css │ │ ├── index.css │ │ └── theme.ts │ ├── types.ts │ ├── utils │ │ ├── arrayUtils.ts │ │ ├── bnUtils.ts │ │ ├── camelcasetoString.ts │ │ ├── copyToClipboard.ts │ │ ├── debounce.ts │ │ ├── encodeAccounts.ts │ │ ├── encodeSubstrateAddress.ts │ │ ├── ethereumChains.ts │ │ ├── formatBnBalance.ts │ │ ├── getAllNetworkWalletConnectNameSpaces.ts │ │ ├── getApproveAsMultiTx.ts │ │ ├── getAsMultiTx.ts │ │ ├── getByteCount.ts │ │ ├── getDisplayAddress.ts │ │ ├── getDisplayName.ts │ │ ├── getEncodedCallFromDecodedTx.ts │ │ ├── getErrorMessageReservedFunds.tsx │ │ ├── getExtrinsicName.ts │ │ ├── getIdentityName.ts │ │ ├── getImportUrl.ts │ │ ├── getMultiProxyAddress.ts │ │ ├── getOptionLabel.ts │ │ ├── getPapiHowLink.ts │ │ ├── getPubKeyFromAddress.ts │ │ ├── getSubscanAccountLink.ts │ │ ├── getSubscanExtrinsicLink.ts │ │ ├── getWalletConnectErrorResponse.ts │ │ ├── getWalletConnectId.ts │ │ ├── getWalletConnectNameSpace.ts │ │ ├── isProxyCall.ts │ │ ├── isValidAddress.ts │ │ ├── jsonPrint.ts │ │ ├── namesUtil.ts │ │ ├── paramConversion.ts │ │ ├── translateError.ts │ │ ├── txHash.ts │ │ └── wsStatusChangeCallback.ts │ └── walletConfigs.ts │ ├── tsconfig.json │ ├── types-and-hooks.tsx │ └── vite.config.ts ├── squid ├── .env.example ├── .eslintrc ├── .prettierignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── assets │ └── envs │ │ ├── .env.acala │ │ ├── .env.amplitude │ │ ├── .env.asset-hub-kusama │ │ ├── .env.asset-hub-polkadot │ │ ├── .env.asset-hub-westend │ │ ├── .env.astar │ │ ├── .env.bifrost-polkadot │ │ ├── .env.chopsticks-ci │ │ ├── .env.chopsticks-local │ │ ├── .env.coretime-kusama │ │ ├── .env.coretime-polkadot │ │ ├── .env.dancelight │ │ ├── .env.hydration │ │ ├── .env.interlay │ │ ├── .env.joystream │ │ ├── .env.khala │ │ ├── .env.kilt │ │ ├── .env.kusama │ │ ├── .env.moonbeam │ │ ├── .env.moonriver │ │ ├── .env.paseo │ │ ├── .env.pendulum │ │ ├── .env.phala │ │ ├── .env.polimec │ │ ├── .env.polkadot │ │ ├── .env.rhala │ │ ├── .env.rococo │ │ ├── .env.watr │ │ └── .env.westend ├── commands.json ├── db │ └── migrations │ │ └── 1738584086606-Data.js ├── package-lock.json ├── package.json ├── schema.graphql ├── squid-manifests │ ├── large-squid.yaml │ └── test-squid.yaml ├── src │ ├── constants.ts │ ├── main.ts │ ├── model │ │ ├── generated │ │ │ ├── _proxyType.ts │ │ │ ├── account.model.ts │ │ │ ├── accountMultisig.model.ts │ │ │ ├── index.ts │ │ │ ├── marshal.ts │ │ │ ├── multisigCall.model.ts │ │ │ └── proxyAccount.model.ts │ │ └── index.ts │ ├── multisigCalls.ts │ ├── processorHandlers │ │ ├── handleNewMultisigCalls.ts │ │ ├── handleNewMultisigs.ts │ │ ├── handleNewProxies.ts │ │ ├── handleNewPureProxies.ts │ │ ├── handleProxyKillPure.ts │ │ ├── handleProxyRemovals.ts │ │ └── index.ts │ ├── typegens │ │ └── typegen.json │ ├── types │ │ ├── calls.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── multisig │ │ │ └── calls.ts │ │ ├── proxy │ │ │ ├── calls.ts │ │ │ └── events.ts │ │ ├── support.ts │ │ ├── v1000000.ts │ │ ├── v1001000.ts │ │ ├── v1002000.ts │ │ ├── v1002004.ts │ │ ├── v1002005.ts │ │ ├── v1002006.ts │ │ ├── v1003000.ts │ │ ├── v1004000.ts │ │ ├── v1004001.ts │ │ ├── v1005000.ts │ │ ├── v2005.ts │ │ ├── v2007.ts │ │ ├── v2011.ts │ │ ├── v2013.ts │ │ ├── v2015.ts │ │ ├── v2022.ts │ │ ├── v2023.ts │ │ ├── v2024.ts │ │ ├── v2025.ts │ │ ├── v2026.ts │ │ ├── v2028.ts │ │ ├── v2029.ts │ │ ├── v2030.ts │ │ ├── v9010.ts │ │ ├── v9030.ts │ │ ├── v9040.ts │ │ ├── v9050.ts │ │ ├── v9080.ts │ │ ├── v9090.ts │ │ ├── v9100.ts │ │ ├── v9111.ts │ │ ├── v9122.ts │ │ ├── v9130.ts │ │ ├── v9160.ts │ │ ├── v9170.ts │ │ ├── v9180.ts │ │ ├── v9190.ts │ │ ├── v9220.ts │ │ ├── v9230.ts │ │ ├── v9250.ts │ │ ├── v9271.ts │ │ ├── v9291.ts │ │ ├── v9300.ts │ │ ├── v9320.ts │ │ ├── v9340.ts │ │ ├── v9350.ts │ │ ├── v9370.ts │ │ ├── v9381.ts │ │ ├── v9420.ts │ │ └── v9430.ts │ └── util │ │ ├── Env.ts │ │ ├── JsonLog.ts │ │ ├── entities.ts │ │ ├── getAccountId.ts │ │ ├── getAccountMultisigId.ts │ │ ├── getMultisigCallId.ts │ │ ├── getMultisigPubKey.ts │ │ ├── getOriginAccount.ts │ │ ├── getProxyAccountIByDelegatorIds.ts │ │ ├── getProxyAccountId.ts │ │ ├── getProxyInfoFromArgs.ts │ │ ├── getProxyKillPureArgs.ts │ │ ├── getProxyTypeFromRaw.ts │ │ ├── getPureProxyInfoFromArgs.ts │ │ ├── index.ts │ │ └── shouldReplicate.ts └── tsconfig.json └── yarn.lock /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # @tbaut owns any files in the /.github/ 2 | 3 | # directory at the root of the repository and any of its 4 | 5 | # subdirectories. 6 | 7 | /.github/ @tbaut 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve Multix 4 | title: '' 5 | labels: 'Issue: Bug 🐛' 6 | assignees: '' 7 | --- 8 | 9 | **Description** 10 | A clear and concise description of what the bug is. 11 | 12 | **Prerequisites** 13 | 14 | Add any setup info / environment / account specifics that need to be in place prior to the test 15 | 16 | **Steps To Reproduce** 17 | 18 | Add clear steps on how to reproduce the bug 19 | 20 | 1. Go to Multix at 'https://multix.chainsafe.io/' 21 | 2. Click on '....' 22 | 3. Scroll down to '....' 23 | 4. See error 24 | 25 | **Expected Behaviour** 26 | A clear and concise description of what you expected to happen. 27 | 28 | **Console Error** 29 | If possible, please copy/paste or provide a screenshot of the browser console. To do so, press `Ctrl` + `Shift` + `J` (or `Cmd` + `Shift` + `J` on a Mac) 30 | 31 | **Screenshots / Video** 32 | If applicable, please add screenshots or video to help explain or demonstrate the problem you are experiencing. 33 | 34 | **Test Host Specifics** 35 | 36 | - OS: [e.g. Linux Mint 21.1, macOS 13, Windows 11] 37 | - Browser [e.g. Chrome, Safari] 38 | - Version [e.g. 111] 39 | - Wallet [e.g. Polkadot.js, Talisman] 40 | 41 | **Additional context** 42 | Add any other context about the problem here. 43 | -------------------------------------------------------------------------------- /.github/images/MULTIX_LOGO_FULL_BLUE_1200px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/.github/images/MULTIX_LOGO_FULL_BLUE_1200px.png -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | closes # 2 | 3 | --- 4 | 5 | Submission checklist: 6 | 7 | #### Layout 8 | 9 | - [ ] Change inspected in the desktop web ui 10 | - [ ] Change inspected in the mobile web ui 11 | 12 | #### Compatibility 13 | 14 | - [ ] Functionality of change validated with a connected account with multisig 15 | - [ ] Applicable elements hidden / disabled for watched multisigs / pure 16 | - [ ] Looks good for solo multisig 17 | - [ ] Looks good for multisig with proxy 18 | -------------------------------------------------------------------------------- /.github/workflows/cypress-tests.yaml: -------------------------------------------------------------------------------- 1 | name: Cypress tests 2 | on: 3 | pull_request: 4 | paths: 5 | - 'packages/ui/**/*' 6 | - '.github/workflows/cypress-tests.yaml' 7 | - 'chopsticks-config.yml' 8 | jobs: 9 | cypress-run: 10 | runs-on: ubuntu-latest 11 | container: 12 | image: cypress/browsers:latest 13 | services: 14 | postgres: 15 | image: postgres:14 16 | env: 17 | POSTGRES_USER: postgres 18 | POSTGRES_PASSWORD: postgres 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | - name: Install indexer deps 23 | working-directory: squid 24 | run: npm ci 25 | - name: run indexer routine 26 | working-directory: squid 27 | run: | 28 | npm run codegen 29 | npm run typegen 30 | npm run build 31 | npm run db:migrate 32 | env: 33 | DB_HOST: postgres 34 | 35 | # Install NPM dependencies, cache them correctly 36 | # and run all Cypress tests 37 | - name: Cypress run 38 | uses: cypress-io/github-action@v6 39 | env: 40 | DB_HOST: postgres 41 | GQL_PORT: 4350 42 | DB_PORT: 5432 43 | with: 44 | install-command: yarn install 45 | start: | 46 | npm run start:chopsticks 47 | yarn ui:start-with-chopsticks 48 | npm run indexer:start:chopsticks-ci 49 | npm run start:graphql-server 50 | wait-on: 'http://localhost:3333' 51 | # custom test command to run 52 | command: yarn test:ci 53 | # after the test run completes store videos and any screenshots 54 | - name: Store screenshots 55 | uses: actions/upload-artifact@v4 56 | if: failure() 57 | with: 58 | name: cypress-screenshots 59 | path: packages/ui/cypress/screenshots 60 | # # store the videos if the tests fail 61 | # - name: Store videos 62 | # uses: actions/upload-artifact@v4 63 | # if: failure() 64 | # with: 65 | # name: cypress-videos 66 | # path: packages/ui/cypress/videos 67 | -------------------------------------------------------------------------------- /.github/workflows/deploy-ui-cf-pages-multix-cloud.yaml: -------------------------------------------------------------------------------- 1 | name: UI deployment Multix.cloud 2 | on: 3 | push: 4 | branches: ['main'] 5 | paths: ['packages/ui/**/*'] 6 | pull_request: 7 | branches: ['main'] 8 | paths: ['packages/ui/**/*'] 9 | 10 | jobs: 11 | deploy: 12 | runs-on: ubuntu-latest 13 | defaults: 14 | run: 15 | working-directory: ./packages/ui/ 16 | permissions: 17 | contents: read 18 | deployments: write 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: actions/setup-node@v3 22 | with: 23 | cache: yarn 24 | node-version: 20 25 | cache-dependency-path: 'yarn.lock' 26 | - run: corepack enable 27 | - run: yarn install --immutable 28 | - run: yarn build 29 | - name: Publish to Cloudflare Pages 30 | uses: cloudflare/pages-action@v1 31 | with: 32 | apiToken: ${{ secrets.CLOUDFLARE_MULTIX_CLOUD }} 33 | accountId: 8c558f9b31e53240659ee45069b7ec2b 34 | projectName: multix-cloud 35 | directory: ./packages/ui/build 36 | gitHubToken: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/formating.yaml: -------------------------------------------------------------------------------- 1 | name: Formating 2 | on: pull_request 3 | jobs: 4 | extract: 5 | name: prettier 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v3 9 | - name: set user 10 | run: | 11 | git config --global user.name 'GitHub Actions' 12 | git config --global user.email 'actions@github.com' 13 | # use node module caching 14 | - uses: actions/cache@v4 15 | with: 16 | path: '**/node_modules' 17 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 18 | 19 | - name: install packages 20 | run: corepack enable && yarn install --immutable 21 | 22 | - name: prettier 23 | run: yarn prettier --check . 24 | -------------------------------------------------------------------------------- /.github/workflows/lint-squid.yaml: -------------------------------------------------------------------------------- 1 | name: Lint squid 2 | on: 3 | pull_request: 4 | paths: ['squid/**/*', '.github/workflows/lint-squid.yaml'] 5 | jobs: 6 | extract: 7 | name: eslint 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | working-directory: ./squid/ 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - name: set user 16 | run: | 17 | git config --global user.name 'GitHub Actions' 18 | git config --global user.email 'actions@github.com' 19 | # use node module caching 20 | - uses: actions/cache@v4 21 | with: 22 | path: '**/node_modules' 23 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 24 | 25 | - name: install packages 26 | run: npm ci 27 | 28 | - name: lint 29 | run: npm run lint 30 | -------------------------------------------------------------------------------- /.github/workflows/lint-ui.yaml: -------------------------------------------------------------------------------- 1 | name: Lint UI 2 | on: 3 | pull_request: 4 | paths: ['packages/ui/**/*', '.github/workflows/lint-ui.yaml'] 5 | jobs: 6 | extract: 7 | name: eslint 8 | runs-on: ubuntu-latest 9 | defaults: 10 | run: 11 | working-directory: ./packages/ui/ 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: set user 15 | run: | 16 | git config --global user.name 'GitHub Actions' 17 | git config --global user.email 'actions@github.com' 18 | # use node module caching 19 | - uses: actions/cache@v4 20 | with: 21 | path: '**/node_modules' 22 | key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }} 23 | 24 | - name: install packages 25 | run: corepack enable && yarn install --immutable 26 | 27 | - name: lint 28 | run: yarn ui:lint 29 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v19.9.0 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | /squid/db/**/* 3 | /squid/src/types/**/* 4 | /squid/src/model/**/* 5 | /squid/lib/**/* 6 | /packages/ui/src/gql/**/* 7 | /packages/ui/src/interfaces/**/* 8 | /packages/ui/types-and-hooks.tsx 9 | /packages/ui/build/**/* 10 | .papi -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | const config = { 2 | tabWidth: 2, 3 | semi: false, 4 | singleQuote: true, 5 | arrowParens: 'always', 6 | singleAttributePerLine: true, 7 | printWidth: 100, 8 | trailingComma: 'none' 9 | } 10 | 11 | module.exports = { ...config } 12 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: 0 2 | 3 | enableGlobalCache: true 4 | 5 | nmMode: hardlinks-local 6 | 7 | nodeLinker: node-modules 8 | 9 | plugins: 10 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 11 | spec: '@yarnpkg/plugin-workspace-tools' 12 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 13 | spec: '@yarnpkg/plugin-interactive-tools' 14 | 15 | yarnPath: .yarn/releases/yarn-3.6.3.cjs 16 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | db: 5 | container_name: subsquid_db 6 | image: postgres:14 7 | environment: 8 | POSTGRES_DB: postgres 9 | POSTGRES_PASSWORD: postgres 10 | shm_size: 1gb 11 | ports: 12 | - '5432:5432' 13 | # command: ["postgres", "-c", "log_statement=all"] 14 | # volumes: 15 | # - ./data/db:/var/lib/postgresql/data 16 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chainsafe/multix", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*" 6 | ], 7 | "description": "An interface to easily manage complex multisigs.", 8 | "engines": { 9 | "node": ">=16.10" 10 | }, 11 | "packageManager": "yarn@3.6.3", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/ChainSafe/Multix" 15 | }, 16 | "keywords": [ 17 | "polkadot", 18 | "multisig" 19 | ], 20 | "license": "Apache-2.0", 21 | "scripts": { 22 | "docker:db": "docker compose up -d db", 23 | "docker:down": "docker compose down", 24 | "build": "yarn workspaces foreach run build", 25 | "build:indexer": "cd squid && npm run codegen && npm run typegen && npm run build && npm run db:migrate", 26 | "lint": "yarn workspaces foreach run lint", 27 | "lint:fix": "yarn workspaces foreach run lint:fix", 28 | "formatAll": "prettier --write .", 29 | "start:chopsticks-test-build-and-launch-all": "concurrently --kill-others 'npm run start:chopsticks' 'npm run ui:start-with-chopsticks' 'npm run docker:down && npm run docker:db && npm run build:indexer && npm run indexer:start:chopsticks-local' 'npm run start:graphql-server'", 30 | "start:chopsticks": "npx --yes @acala-network/chopsticks@1.0.6 --config chopsticks-config.yml", 31 | "start:graphql-server": "cd squid && npm run start:graphql-server", 32 | "indexer:start:chopsticks-ci": "cd squid && npm run start:chopsticks-ci", 33 | "indexer:start:chopsticks-local": "cd squid && npm run start:chopsticks-local", 34 | "ui:start": "yarn workspace multix-ui start", 35 | "ui:lint": "yarn workspace multix-ui lint", 36 | "ui:test": "yarn workspace multix-ui test", 37 | "ui:start-with-chopsticks": "yarn workspace multix-ui start-with-chopsticks" 38 | }, 39 | "devDependencies": { 40 | "concurrently": "^9.1.2", 41 | "eslint-config-prettier": "^10.1.5", 42 | "eslint-plugin-prettier": "^5.4.1", 43 | "prettier": "^3.5.3" 44 | }, 45 | "resolutions": { 46 | "graphql": "^16.0.0", 47 | "@polkadot/util-crypto": "12.5.1", 48 | "@polkadot/util": "12.5.1" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/ui/.env.example: -------------------------------------------------------------------------------- 1 | VITE_CHAIN_ID="kusama" 2 | VITE_EXPLORER_NETWORK_NAME="kusama" 3 | VITE_WS_PROVIDER="wss://rpc.ibp.network/kusama" 4 | VITE_GRAPHQL_HTTP_PROVIDER="http://localhost:4350/graphql" -------------------------------------------------------------------------------- /packages/ui/.env.staging: -------------------------------------------------------------------------------- 1 | VITE_CHAIN_ID="kusama" 2 | VITE_EXPLORER_NETWORK_NAME="kusama" 3 | VITE_WS_PROVIDER="ws://localhost:8000" 4 | VITE_GRAPHQL_HTTP_PROVIDER="http://localhost:4350/graphql" -------------------------------------------------------------------------------- /packages/ui/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react-hooks/recommended", 9 | "plugin:react/recommended", 10 | "prettier", 11 | "plugin:@typescript-eslint/eslint-recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "ecmaFeatures": { 16 | "jsx": true 17 | }, 18 | "ecmaVersion": 12, 19 | "sourceType": "module", 20 | "tsconfigRootDir": "./", 21 | "project": "./tsconfig.json" 22 | }, 23 | "plugins": ["react", "prettier", "@typescript-eslint", "no-only-tests"], 24 | "rules": { 25 | "no-only-tests/no-only-tests": "error", 26 | "no-unused-vars": "off", 27 | "@typescript-eslint/no-unused-vars": "error", 28 | "react/jsx-max-props-per-line": [ 29 | "error", 30 | { 31 | "maximum": { 32 | "single": 1, 33 | "multi": 1 34 | } 35 | } 36 | ], 37 | "trailingComma": "off", 38 | "object-curly-spacing": ["error", "always"], 39 | "react/jsx-tag-spacing": "error", 40 | "prettier/prettier": "error", 41 | "react-hooks/exhaustive-deps": "error", 42 | "react/react-in-jsx-scope": "off" 43 | 44 | }, 45 | "overrides": [ 46 | { 47 | "files": ["*.ts", "*tsx", "*.js", "*.jsx"], 48 | "processor": "@graphql-eslint/graphql" 49 | }, 50 | { 51 | "files": ["*.graphql"], 52 | "extends": "plugin:@graphql-eslint/schema-recommended", 53 | "rules": { 54 | "@graphql-eslint/known-type-names": "error" 55 | } 56 | }, 57 | { 58 | "files": ["*.graphql"], 59 | "extends": ["plugin:@graphql-eslint/operations-recommended"], 60 | "rules": { 61 | "@graphql-eslint/require-selections": "error" 62 | } 63 | } 64 | ], 65 | "ignorePatterns": ["src/interfaces/**/*", "types-and-hooks.tsx", "build", "src/gql"] 66 | } 67 | -------------------------------------------------------------------------------- /packages/ui/.papi/descriptors/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !package.json -------------------------------------------------------------------------------- /packages/ui/.papi/descriptors/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0-autogenerated.17231857916183545855", 3 | "name": "@polkadot-api/descriptors", 4 | "files": [ 5 | "dist" 6 | ], 7 | "exports": { 8 | ".": { 9 | "types": "./dist/index.d.ts", 10 | "module": "./dist/index.mjs", 11 | "import": "./dist/index.mjs", 12 | "require": "./dist/index.js" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "main": "./dist/index.js", 17 | "module": "./dist/index.mjs", 18 | "browser": "./dist/index.mjs", 19 | "types": "./dist/index.d.ts", 20 | "sideEffects": false, 21 | "peerDependencies": { 22 | "polkadot-api": ">=1.11.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/acala.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/acala.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/bifrostDot.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/bifrostDot.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/coretimeDot.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/coretimeDot.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/dancelight.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/dancelight.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/dot.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/dot.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/dotAssetHub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/dotAssetHub.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/dotPpl.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/dotPpl.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/hydration.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/hydration.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/ksm-asset-hub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/ksm-asset-hub.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/ksm.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/ksm.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/ksmAssetHub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/ksmAssetHub.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/ksmPpl.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/ksmPpl.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/pasPpl.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/pasPpl.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/paseo.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/paseo.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/phala.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/phala.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/polimec.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/polimec.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/rhala.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/rhala.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/wesAssetHub.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/wesAssetHub.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/wesPpl.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/wesPpl.scale -------------------------------------------------------------------------------- /packages/ui/.papi/metadata/westend.scale: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/.papi/metadata/westend.scale -------------------------------------------------------------------------------- /packages/ui/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | specPattern: 'cypress/tests/**/*.cy.ts', 6 | setupNodeEvents() { 7 | // implement node event listeners here 8 | }, 9 | retries: { 10 | runMode: 3 11 | } 12 | } 13 | }) 14 | -------------------------------------------------------------------------------- /packages/ui/cypress/fixtures/extrinsicsDisplayAccounts.ts: -------------------------------------------------------------------------------- 1 | import { InjectedAccountWitMnemonic } from './testAccounts' 2 | 3 | export const expectedMultisigAddress = '1ZJA1iLT5dei9XB9myMJYyw4d24aCfiniHBxQJPWsvCUoV9' 4 | 5 | export const extrinsicsDisplayAccounts = { 6 | // it has no token but is part of a multisig 7 | Alice: { 8 | address: '15oF4uVJwmo4TdGW7VfQxNLavjCXviqxT9S1MgbjMNHr6Sp5', 9 | publicKey: '0xb4b72576a091c5d691c2fd37f6eaa3d51c7480c2baaeab48737e5a209db4a431', 10 | name: 'Alice', 11 | type: 'sr25519', 12 | mnemonic: 'bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice' 13 | } as InjectedAccountWitMnemonic 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/cypress/fixtures/landingData.ts: -------------------------------------------------------------------------------- 1 | export const baseUrl = 'http://localhost:3333' 2 | export const defaultNetwork = 'paseo' 3 | const WATCH_ACCOUNT_ANCHOR = 'watched-accounts' 4 | const HIDDEN_ACCOUNTS_ANCHOR = 'hidden-accounts' 5 | 6 | export const landingPageNetwork = (networkName: string) => `${baseUrl}?network=${networkName}` 7 | export const landingPageUrl = landingPageNetwork(defaultNetwork) 8 | export const getSettingsPageUrl = (network = defaultNetwork) => 9 | `${baseUrl}/settings?network=${network}` 10 | export const getSettingsPageWatchAccountUrl = (network = defaultNetwork) => 11 | `${getSettingsPageUrl(network)}#${WATCH_ACCOUNT_ANCHOR}` 12 | export const getSettingsPageHiddenAccountUrl = (network = defaultNetwork) => 13 | `${getSettingsPageUrl(network)}#${HIDDEN_ACCOUNTS_ANCHOR}` 14 | export const landingPageNetworkAddress = ({ 15 | network, 16 | address 17 | }: { 18 | network: string 19 | address: string 20 | }) => `${landingPageNetwork(network)}&address=${address}` 21 | export const landingPageAddressUrl = (address: string) => 22 | landingPageNetworkAddress({ network: defaultNetwork, address }) 23 | -------------------------------------------------------------------------------- /packages/ui/cypress/fixtures/nameDisplay.ts: -------------------------------------------------------------------------------- 1 | export const multisigWithKusamaIdentity = { 2 | publicKey: '0x905f923a67cec79db9e1415567822f2c440e794c4a38b43144bfb1a044b2a2f2', 3 | address: 'Fqcoa6z2T8QJkNWMr8M7LcVmPonv7wwARsvciiL7HyUnJc4', 4 | identityMain: 'ChainSafe', 5 | subIdentity: 'ChainSafe Validator 0' 6 | } 7 | -------------------------------------------------------------------------------- /packages/ui/cypress/fixtures/polkadotAccounts.ts: -------------------------------------------------------------------------------- 1 | import { InjectedAccountWitMnemonic } from './testAccounts' 2 | 3 | export const expectedPolkadotMultisigAddressClow = 4 | '12niVg1BaFNjWDgtsneoiUQqZ1KB6YBGG68JLrNomYScckrQ' 5 | 6 | export const polkadotMemberAccount = { 7 | // this is the member of a multisig on Polkadot 8 | Clow: { 9 | address: '15BERoWxrWC61cAb4JjpUdM7sy8FAS9uduismDbZ7PURZLto', 10 | publicKey: '0xb8be96d986897797d117987e4368640ffdf32bc967ba7467d012136c22dca33c', 11 | name: 'Clow', 12 | type: 'sr25519', 13 | mnemonic: '' 14 | } as InjectedAccountWitMnemonic, 15 | // this is a pure on Polkadot 16 | CultureDot: { 17 | address: '158sHoKeq7ZesHMLcp2MkxFQpd7caEW6Rcy1md3AK1fuX8ak', 18 | publicKey: '0xb6f0f10eec993f3e6806eb6cc4d2f13d5f5a90a17b855a7bf9847a87e07ee322', 19 | name: 'CultureDot', 20 | type: 'sr25519', 21 | mnemonic: '' 22 | } as InjectedAccountWitMnemonic, 23 | // this is a pure on Polkadot 24 | unknown: { 25 | address: '12QZUSm3c2HMjKFDdsY5XVT4gTJeUCiupRA5zUaQmSmnhToK', 26 | publicKey: '0x3e3431343e95883c170859a65d5c00e5bf15113ff92b57ea7a2adbf6cdf90629', 27 | name: 'unknown', 28 | type: 'sr25519', 29 | mnemonic: '' 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/cypress/fixtures/polkadotAssetHub.ts: -------------------------------------------------------------------------------- 1 | import { InjectedAccountWitMnemonic } from './testAccounts' 2 | 3 | export const expectedPolkadotAHMultisigAddress = '13cJVjVjHpabhge4XEjdXmVPgD6w2UaSp8bpnYX9sDcgwp45' 4 | 5 | export const polkadotAHMemberAccount = { 6 | // this is the member of a multisig on Polkadot Asset hub with 1 DOT and 1 USDC 7 | Nikos: { 8 | address: '15DCZocYEM2ThYCAj22QE4QENRvUNVrDtoLBVbCm5x4EQncr', 9 | publicKey: '0xba3ecfd7483cdcdad1132af7d1e8067816009cbd77fc0bc30eafe8d2218a1971', 10 | name: 'Nikos', 11 | type: 'sr25519', 12 | mnemonic: '' 13 | } as InjectedAccountWitMnemonic, 14 | Gato: { 15 | address: '14b9HXT8zq3uTqUPPhVFLCYfJjLgKFAdbLTStTPWTUEqrXHc', 16 | publicKey: '0x9ebeef0150a33357023e678bfff549602e6943b5b85d8bfdb58473992fcfaf63', 17 | name: 'Gato', 18 | type: 'sr25519', 19 | mnemonic: '' 20 | } as InjectedAccountWitMnemonic 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | Cypress.on('uncaught:exception', (e, runnable) => { 23 | console.log('Error:', e) 24 | console.log('Test', runnable) 25 | if (e.name.includes('RpcError') && e.message.includes('Method not found')) return false 26 | }) 27 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/components/accountDisplay.ts: -------------------------------------------------------------------------------- 1 | export const accountDisplay = { 2 | identicon: () => cy.get('[data-cy=icon-identicon]'), 3 | pureBadge: () => cy.get('[data-cy=badge-pure]'), 4 | multisigBadge: () => cy.get('[data-cy=badge-multi]'), 5 | nameLabel: () => cy.get('[data-cy=label-account-name]'), 6 | noNameLabel: () => cy.get('[data-cy=label-no-name]'), 7 | addressLabel: () => cy.get('[data-cy=label-account-address]'), 8 | watchedIcon: () => cy.get('[data-cy=icon-watched]'), 9 | nameEditButton: () => cy.get('[data-cy=button-name-edit]'), 10 | validateEditButton: () => cy.get('[data-cy=button-edition-submit]'), 11 | cancelEditButton: () => cy.get('[data-cy=button-edition-cancel]'), 12 | nameEditionInput: () => cy.get('[data-cy=input-name-edition]'), 13 | identityIcon: () => cy.get('[data-cy=icon-identity]'), 14 | subIdentityLabel: () => cy.get('[data-cy=label-sub-identity]') 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/components/expander.ts: -------------------------------------------------------------------------------- 1 | export const expander = { 2 | paramExpander: () => cy.get('[data-cy=label-expander]'), 3 | contentExpander: () => cy.get('[data-cy=content-expander]') 4 | } 5 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/modals/editNamesModal.ts: -------------------------------------------------------------------------------- 1 | export const editNamesModal = { 2 | body: () => cy.get('[data-cy=modal-edit-names]'), 3 | inputEditPureName: () => cy.get('[data-cy=input-edit-pure-name]'), 4 | inputEditMultisigName: () => cy.get('[data-cy=input-edit-multisig-name]'), 5 | inputEditSignatoryName: () => cy.get('[data-cy=input-edit-signatory-name]'), 6 | saveButton: () => cy.get('[data-cy=button-save-edited-names]') 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/modals/hiddenAccountInfoModal.ts: -------------------------------------------------------------------------------- 1 | export const hiddenAccountInfoModal = { 2 | body: () => cy.get('[data-cy=modal-hidden-account-info]'), 3 | gotItButton: () => cy.get('[data-cy=button-hidden-account-info-gotit]'), 4 | checkBoxMessage: () => cy.get('[data-cy=checkbox-dont-show-again]') 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/modals/txSigningModal.ts: -------------------------------------------------------------------------------- 1 | export const txSigningModal = { 2 | body: () => cy.get('[data-cy=modal-tx-signing]'), 3 | callHashLabel: () => cy.get('[data-cy=label-call-hash]'), 4 | approveButton: () => cy.get('[data-cy=button-approve-tx]'), 5 | executeButton: () => cy.get('[data-cy=button-execute-tx]'), 6 | rejectButton: () => cy.get('[data-cy=button-reject-tx]'), 7 | callDataInput: () => cy.get('[data-cy=input-call-data]'), 8 | callInfoContainer: () => cy.get('[data-cy=container-call-info]'), 9 | signerInput: () => cy.get('[data-cy=input-account-address]') 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/multisigPage.ts: -------------------------------------------------------------------------------- 1 | export const multisigPage = { 2 | // header elements 3 | accountHeader: (timeout = 4000) => cy.get('[data-cy=header-account]', { timeout }), 4 | seeOverviewButton: () => cy.get('[data-cy=button-see-overview]'), 5 | newTransactionButton: () => cy.get('[data-cy=button-new-transaction]'), 6 | optionsMenuButton: () => cy.get('[data-cy=button-options-menu]'), 7 | editNamesMenuOption: () => cy.get('[data-cy=menu-option-edit-names]'), 8 | subscanMenuOption: () => cy.get('[data-cy=menu-option-subscan]'), 9 | reviewButton: () => cy.get('[data-cy=button-review-tx]'), 10 | setIdentityMenuOption: () => cy.get('[data-cy=menu-option-set-identity]'), 11 | hideAccountMenuOption: () => cy.get('[data-cy=menu-option-hide-this-account]'), 12 | assetHubBalance: (id: string) => cy.get(`[data-cy=asset-balance-${id}]`), 13 | nativeBalance: () => cy.get('[data-cy=asset-balance-native]'), 14 | 15 | // multisig details elements 16 | multisigDetailsContainer: () => cy.get('[data-cy=container-multisig-details]'), 17 | multisigAccountSummary: () => cy.get('[data-cy=container-multisig-account-summary]'), 18 | thresholdListItem: () => cy.get('[data-cy=list-item-threshold]'), 19 | proxyTypeListItem: () => cy.get('[data-cy=list-item-proxy-type]]'), 20 | balanceListItem: () => cy.get('[data-cy=list-item-balance]'), 21 | signatoriesAccordion: () => cy.get('[data-cy=accordion-signatories]'), 22 | expandSignatoriesIcon: () => cy.get('[data-cy=icon-expand-signatories-summary]'), 23 | signatoriesList: () => cy.get('[data-cy=list-item-signatory]'), 24 | 25 | // transaction list elements 26 | transactionList: () => cy.get('[data-cy=container-transaction-list]', { timeout: 20000 }), 27 | pendingTransactionItem: (timeout = 4000) => 28 | cy.get('[data-cy=container-pending-tx-item]', { timeout }), 29 | dateLabel: () => cy.get('[data-cy=label-date]'), 30 | pendingTransactionCallName: () => cy.get('[data-cy=label-call-name]'), 31 | unknownCallIcon: () => cy.get('[data-cy=icon-unknown-call]'), 32 | unknownCallAlert: () => cy.get('[data-cy=alert-no-call-data]'), 33 | batchItem: () => cy.get('[data-cy=batch-call-item]') 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/newMultisigPage.ts: -------------------------------------------------------------------------------- 1 | export const newMultisigPage = { 2 | addButton: () => cy.get('[data-cy=button-add-account]'), 3 | nextButton: () => cy.get('[data-cy=button-next]'), 4 | creatingLoader: () => cy.get('[data-cy=button-creating-loader'), 5 | addressSelector: () => cy.get('[data-cy=input-account-address]'), 6 | 7 | step1: { 8 | accountNameInput: () => cy.get('[data-cy=input-account-name]'), 9 | addAccountError: () => cy.get('[data-cy=label-add-account-error]'), 10 | createMutlisigError: () => cy.get('[data-cy=container-create-multisig-error]'), 11 | signatoryItem: (address: string) => cy.get(`[data-cy=item-signatory-${address}]`) 12 | }, 13 | step2: { 14 | thresholdWarning: () => cy.get('[data-cy=input-warning-message]'), 15 | thresholdInput: () => cy.get('[data-cy=input-threshold-creation'), 16 | nameInput: () => cy.get('[data-cy=input-name-creation'), 17 | checkboxUsePureProxy: () => cy.get('[data-cy=checkbox-use-pure-proxy]') 18 | }, 19 | step3: { 20 | infoBox: () => cy.get('[data-cy=label-creation-info]'), 21 | errorNotEnoughFunds: () => cy.get('[data-cy=alert-insufficient-funds]') 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/notifications.ts: -------------------------------------------------------------------------------- 1 | export const notifications = { 2 | successNotificationIcon: (timeout = 4000) => 3 | cy.get('[data-cy=notification-icon-success]', { timeout }), 4 | errorNotificationIcon: () => cy.get('[data-cy=notification-icon-error]'), 5 | loadingNotificationIcon: () => cy.get('[data-cy=notification-icon-loading]'), 6 | notificationWrapper: (timeout = 4000) => cy.get('[data-cy=notification-wrapper]', { timeout }) 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/settingsPage.ts: -------------------------------------------------------------------------------- 1 | export const settingsPage = { 2 | // watch account section 3 | watchedAccountsAccordion: () => cy.get('[data-cy=accordion-title-watched-accounts]'), 4 | watchedAccountsInputsWrapper: () => cy.get('[data-cy=wrapper-watched-accounts-inputs]'), 5 | hiddenAccountsAccordion: () => cy.get('[data-cy=accordion-title-hidden-accounts]'), 6 | hiddenAccountsInputsWrapper: () => cy.get('[data-cy=wrapper-hidden-accounts-inputs]'), 7 | accountAddressInput: () => cy.get('[data-cy=input-account-address]'), 8 | accountNameInput: () => cy.get('[data-cy=input-account-name]'), 9 | addButton: () => cy.get('[data-cy=button-add-account]'), 10 | watchedAccountsContainer: () => cy.get('[data-cy=container-account-details]', { timeout: 20000 }), 11 | hiddenAccountsContainer: () => 12 | cy.get('[data-cy=container-hidden-account-details]', { timeout: 20000 }), 13 | watchedAccountDeleteButton: () => cy.get('[data-cy=button-delete-watched-account]'), 14 | hiddenAccountDeleteButton: () => cy.get('[data-cy=button-delete-hidden-account]'), 15 | errorLabel: () => cy.get('[data-cy=label-add-account-error]'), 16 | // wallet connect section 17 | wallectConnectAccordion: () => cy.get('[data-cy=accordion-title-wallet-connect]'), 18 | walletConnectAlert: () => cy.get('[data-cy=alert-wallet-connect-warning]'), 19 | connectDappButton: () => cy.get('[data-cy=button-connect-dapp]'), 20 | walletConnectKeyInput: () => cy.get('[data-cy=input-wallet-connect-key]'), 21 | tooltipInfoIcon: () => cy.get('[data-cy=tooltip-wallet-connect-info]'), 22 | hiddenAccountWatchedWarning: () => cy.get('[data-cy=alert-removed-watched-account]') 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/cypress/support/page-objects/topMenuItems.ts: -------------------------------------------------------------------------------- 1 | export const topMenuItems = { 2 | desktopMenu: () => cy.get('[data-cy=menu-desktop]'), 3 | homeButton: () => cy.get('[data-cy=button-navigate-home]'), 4 | newMultisigButton: () => cy.get('[data-cy=button-navigate-new-multisig]'), 5 | settingsButton: () => cy.get('[data-cy=button-navigate-settings]'), 6 | overviewButton: () => cy.get('[data-cy=button-navigate-overview]'), 7 | aboutButton: () => cy.get('[data-cy=button-navigate-about]'), 8 | connectButton: () => cy.get('[data-cy=button-menu-connect]'), 9 | multiproxySelectorDesktop: () => 10 | cy.get('[data-cy=select-multiproxy-desktop]', { timeout: 20000 }), 11 | multiproxySelectorInputDesktop: () => 12 | cy.get('[data-cy=input-select-multiproxy-desktop]', { timeout: 10000 }), 13 | multiproxySelectorOptionDesktop: () => cy.get('[data-cy=select-multiproxy-option-desktop]'), 14 | networkSelector: () => cy.get('[data-cy=select-networks]'), 15 | networkSelectorOption: (networkName: string) => 16 | cy.get(`[data-cy=select-network-option-${networkName}]`) 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/cypress/tests/login.cy.ts: -------------------------------------------------------------------------------- 1 | import { testAccounts } from '../fixtures/testAccounts' 2 | import { landingPageUrl } from '../fixtures/landingData' 3 | import { landingPage } from '../support/page-objects/landingPage' 4 | import { topMenuItems } from '../support/page-objects/topMenuItems' 5 | import { clickOnConnect } from '../utils/clickOnConnect' 6 | import { newMultisigPage } from '../support/page-objects/newMultisigPage' 7 | import { accountDisplay } from '../support/page-objects/components/accountDisplay' 8 | import { MULTIX_DAPP_NAME } from '../support/commands' 9 | 10 | describe('Connect Account', () => { 11 | beforeEach(() => { 12 | cy.visit(landingPageUrl) 13 | cy.initWallet(Object.values(testAccounts)) 14 | clickOnConnect() 15 | }) 16 | 17 | it('Reject connection', () => { 18 | cy.getAuthRequests().then((authRequests) => { 19 | const requests = Object.values(authRequests) 20 | // we should have 1 connection request to the extension 21 | cy.wrap(requests.length).should('eq', 1) 22 | // this request should be from the application Multix 23 | cy.wrap(requests[0].origin).should('eq', MULTIX_DAPP_NAME) 24 | 25 | // auth is rejected 26 | cy.rejectAuth(requests[0].id, 'Cancelled') 27 | landingPage 28 | .noAccountFoundError() 29 | .should( 30 | 'contain.text', 31 | 'No account found. Please connect at least one in a wallet extension.' 32 | ) 33 | }) 34 | }) 35 | 36 | it('Connect Accounts', () => { 37 | const address1 = testAccounts['Multisig Member Account 1'].address 38 | const address2 = testAccounts['Multisig Member Account 2'].address 39 | 40 | cy.connectAccounts([address1, address2]) 41 | 42 | topMenuItems.newMultisigButton().click() 43 | // Click on the account address selector to show the list of accounts 44 | newMultisigPage.addressSelector().click() 45 | 46 | accountDisplay.nameLabel().each((el, index) => { 47 | const expectedName = Object.values(testAccounts)[index].name 48 | cy.wrap(el).should('have.text', expectedName) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /packages/ui/cypress/utils/clickOnConnect.ts: -------------------------------------------------------------------------------- 1 | import { landingPage } from '../support/page-objects/landingPage' 2 | import { topMenuItems } from '../support/page-objects/topMenuItems' 3 | import { waitForAuthRequest } from './waitForAuthRequests' 4 | 5 | export const clickOnConnect = () => { 6 | topMenuItems.connectButton().click() 7 | landingPage.connectionDialog().should('exist') 8 | landingPage 9 | .connectionDialog() 10 | .within(() => cy.get('button', { includeShadowDom: true }).contains('Connect').click()) 11 | waitForAuthRequest() 12 | landingPage 13 | .connectionDialog() 14 | .within(() => cy.get('#close-button', { includeShadowDom: true }).click()) 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/cypress/utils/getShortAddress.ts: -------------------------------------------------------------------------------- 1 | export const getShortAddress = (address: string) => `${address.slice(0, 6)}..${address.slice(-6)}` 2 | -------------------------------------------------------------------------------- /packages/ui/cypress/utils/waitForAuthRequests.ts: -------------------------------------------------------------------------------- 1 | export const waitForAuthRequest = () => 2 | cy.waitUntil(() => cy.getAuthRequests().then((req) => Object.entries(req).length > 0)) 3 | -------------------------------------------------------------------------------- /packages/ui/cypress/utils/waitForMultisigLength.ts: -------------------------------------------------------------------------------- 1 | import { multisigPage } from '../support/page-objects/multisigPage' 2 | import { topMenuItems } from '../support/page-objects/topMenuItems' 3 | 4 | export const waitForMultisigLength = (length: number) => 5 | cy.waitUntil(() => { 6 | multisigPage.accountHeader().click() 7 | return topMenuItems 8 | .multiproxySelectorDesktop() 9 | .click() 10 | .then(() => { 11 | return topMenuItems.multiproxySelectorOptionDesktop().then((el) => el.length === length) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /packages/ui/cypress/utils/waitForTxRequests.ts: -------------------------------------------------------------------------------- 1 | export const waitForTxRequest = () => 2 | cy.waitUntil(() => cy.getTxRequests().then((req) => Object.entries(req).length > 0)) 3 | -------------------------------------------------------------------------------- /packages/ui/graphql.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "overwrite": true, 3 | "schema": "https://chainsafe.squids.live/multix-arrow@v7/api/graphql", 4 | "documents": "src/**/*.graphql", 5 | "generates": { 6 | "src/gql/": { 7 | "preset": "client", 8 | "plugins": [] 9 | }, 10 | "types-and-hooks.tsx": { 11 | "plugins": ["typescript", "typescript-operations", "typescript-react-query"], 12 | "config": { 13 | "reactQueryVersion": 5, 14 | "fetcher": { 15 | "func": "./src/hooks/useFetchData#useFetchData", 16 | "isReactHook": true 17 | } 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/public/.well-known/walletconnect.txt: -------------------------------------------------------------------------------- 1 | 73a5cd8f-4b73-4645-9869-679f9e267f5d=e74a09bd674190b7113efb1a83f0ab1f12586ae27f99a72418123ada405c7a55 -------------------------------------------------------------------------------- /packages/ui/public/OG_IMAGE_BLUE_600px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/OG_IMAGE_BLUE_600px.png -------------------------------------------------------------------------------- /packages/ui/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /packages/ui/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/ui/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/apple-touch-icon.png -------------------------------------------------------------------------------- /packages/ui/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/favicon-16x16.png -------------------------------------------------------------------------------- /packages/ui/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/favicon-32x32.png -------------------------------------------------------------------------------- /packages/ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tbaut/Multix/870bac702eea07d446b29e96fbdcf2eba1e5310d/packages/ui/public/favicon.ico -------------------------------------------------------------------------------- /packages/ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Multix", 3 | "name": "Multix", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#4964a8", 19 | "background_color": "#ffffff" 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/ui/src/components/AccountSelect/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { CardHeader, IconButton, Menu, MenuItem } from '@mui/material' 3 | import { MenuProps } from '@mui/material/Menu/Menu' 4 | import { useAccounts } from '../../contexts/AccountsContext' 5 | import MultixIdenticon from '../MultixIdenticon' 6 | 7 | interface Props extends Omit { 8 | anchorEl: null | HTMLElement 9 | onClose: () => void 10 | } 11 | 12 | export const AccountSelect: React.FC = ({ anchorEl, onClose, ...props }) => { 13 | const { ownAccountList, selectAccount } = useAccounts() 14 | 15 | if (!ownAccountList) { 16 | return null 17 | } 18 | 19 | const handleSelect = (account: (typeof ownAccountList)[0]) => () => { 20 | selectAccount(account) 21 | onClose() 22 | } 23 | 24 | return ( 25 | 31 | {ownAccountList.map((account) => ( 32 | 36 | 39 | 40 | 41 | } 42 | title={account.name} 43 | subheader={account.address} 44 | /> 45 | 46 | ))} 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /packages/ui/src/components/Drawer/Drawer.tsx: -------------------------------------------------------------------------------- 1 | import Drawer from '@mui/material/Drawer' 2 | import DrawerMenu from './DrawerMenu' 3 | import { styled } from '@mui/material/styles' 4 | 5 | const DRAWER_WIDTH = 240 6 | 7 | interface DrawerComponentProps { 8 | open: boolean 9 | handleDrawerClose: () => void 10 | } 11 | 12 | function MobileDrawerComponent({ open, handleDrawerClose }: DrawerComponentProps) { 13 | return ( 14 | 23 | 24 | 25 | ) 26 | } 27 | 28 | const DrawerStyled = styled(Drawer)` 29 | width: ${DRAWER_WIDTH}px; 30 | flex-shrink: 0; 31 | 32 | & .MuiDrawer-paper { 33 | width: ${DRAWER_WIDTH}px; 34 | } 35 | 36 | .MuiIconButton-root { 37 | color: black; 38 | } 39 | ` 40 | 41 | export default MobileDrawerComponent 42 | -------------------------------------------------------------------------------- /packages/ui/src/components/ErrorFallback/ErrorFallback.tsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, useRouteError } from 'react-router' 2 | import { Alert, AlertTitle, Grid2 as Grid } from '@mui/material' 3 | import { theme } from '../../styles/theme' 4 | import { styled, ThemeProvider as MuiThemeProvider } from '@mui/material/styles' 5 | import { Button } from '../library' 6 | 7 | export const ErrorFallback = () => { 8 | const routeError = useRouteError() as Error 9 | const navigate = useNavigate() 10 | 11 | return ( 12 | 13 | 18 | 19 | 23 | Oy yoi yoi! Something went wrong: 24 |
{routeError && routeError.message}
25 | 28 | navigate({ 29 | pathname: '/' 30 | }) 31 | } 32 | > 33 | Go to Home 34 | 35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | 42 | const AlertStyled = styled(Alert)` 43 | margin-top: 2rem; 44 | border-radius: ${({ theme }) => theme.custom.borderRadius}; 45 | 46 | .MuiAlert-icon { 47 | flex: 0; 48 | } 49 | 50 | .MuiAlert-message { 51 | flex: 1; 52 | flex-direction: column; 53 | display: flex; 54 | align-items: center; 55 | justify-content: center; 56 | } 57 | ` 58 | 59 | const ButtonStyled = styled(Button)` 60 | margin-top: 1rem; 61 | ` 62 | 63 | export default ErrorFallback 64 | -------------------------------------------------------------------------------- /packages/ui/src/components/Expander.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Collapse } from '@mui/material' 2 | import { ReactNode, useState } from 'react' 3 | import { styled } from '@mui/material/styles' 4 | import { HiChevronRight as ChevronRightIcon } from 'react-icons/hi2' 5 | 6 | interface Props { 7 | className?: string 8 | title: ReactNode 9 | content: ReactNode 10 | expanded?: boolean 11 | } 12 | 13 | const Expander = ({ className = '', title, content, expanded = false }: Props) => { 14 | const [open, setOpen] = useState(expanded) 15 | 16 | return ( 17 | 18 |
setOpen(!open)} 21 | className="titleWrapper" 22 | > 23 | 24 | {title} 25 |
26 | 30 | {content} 31 | 32 |
33 | ) 34 | } 35 | 36 | export default styled(Expander)` 37 | display: flex; 38 | flex-direction: column; 39 | min-width: 0; 40 | 41 | .titleWrapper { 42 | cursor: pointer; 43 | display: flex; 44 | align-items: center; 45 | } 46 | 47 | .expanderIcon { 48 | margin-right: 0.5rem; 49 | transition: transform 0.2s ease-in-out; 50 | &.rotated { 51 | transform: rotate(90deg); 52 | } 53 | } 54 | ` 55 | -------------------------------------------------------------------------------- /packages/ui/src/components/LoadingBox.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { Box, CircularProgress } from '@mui/material' 3 | 4 | interface Props { 5 | message: string 6 | testId?: string 7 | } 8 | 9 | const LoadingBox = ({ message, testId }: Props) => ( 10 | 11 | 12 |
{message}
13 |
14 | ) 15 | 16 | export const MessageBox = styled(Box)` 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | flex-direction: column; 21 | width: 100%; 22 | padding: 1rem; 23 | ` 24 | 25 | export default LoadingBox 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/MultixIdenticon.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react' 2 | import Tooltip from '@mui/material/Tooltip' 3 | import { ICON_SIZE_MEDIUM, DEFAULT_ICON_THEME } from '../constants' 4 | import Identicon from '@polkadot/react-identicon' 5 | import { styled } from '@mui/material/styles' 6 | import { useApi } from '../contexts/ApiContext' 7 | 8 | const DEFAULT_PLACEMENT = 'top' 9 | const DEFAULT_TITLE = 'Address copied!' 10 | const DEFAULT_AUTO_HIDE_DURATION = 2000 11 | 12 | interface Props { 13 | value?: string 14 | size?: number 15 | className?: string 16 | } 17 | const MultixIdenticon = ({ value, size = ICON_SIZE_MEDIUM, className }: Props) => { 18 | const [open, setOpen] = useState(false) 19 | const handleTooltipClose = useCallback(() => setOpen(false), []) 20 | const handleTooltipOpen = useCallback(() => setOpen(true), []) 21 | const { chainInfo } = useApi() 22 | 23 | return ( 24 | 31 | 32 | 39 | 40 | 41 | ) 42 | } 43 | 44 | const TooltipIconStyled = styled('div')` 45 | display: inherit; 46 | line-height: 0; 47 | 48 | img { 49 | border-radius: 50%; 50 | } 51 | ` 52 | 53 | export default MultixIdenticon 54 | -------------------------------------------------------------------------------- /packages/ui/src/components/NewMulisigAlert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, IconButton } from '@mui/material' 2 | import { styled } from '@mui/material/styles' 3 | import { HiOutlineXMark as CloseIcon } from 'react-icons/hi2' 4 | 5 | interface Props { 6 | className?: string 7 | onClose: () => void 8 | } 9 | 10 | const NewMulisigAlert = ({ className = '', onClose }: Props) => { 11 | return ( 12 | 17 |
21 | Your new multisig is being created. It will be available in ~1min from the dropdown. 22 |
23 | 31 | 32 | 33 |
34 | ) 35 | } 36 | 37 | export default styled(NewMulisigAlert)` 38 | width: 100%; 39 | margin-top: 1rem; 40 | margin-bottom: 0.5rem; 41 | 42 | .infoText { 43 | flex: 1; 44 | } 45 | 46 | .MuiAlert-message { 47 | display: flex; 48 | align-items: center; 49 | width: 100%; 50 | } 51 | 52 | .MuiAlert-icon { 53 | align-items: center; 54 | } 55 | ` 56 | -------------------------------------------------------------------------------- /packages/ui/src/components/SuccessCreation.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid2 as Grid } from '@mui/material' 2 | import { styled } from '@mui/material/styles' 3 | import { HiOutlineClock as AccessTimeIcon } from 'react-icons/hi2' 4 | 5 | interface Props { 6 | className?: string 7 | } 8 | 9 | const SuccessCreation = ({ className }: Props) => { 10 | return ( 11 | 12 |

Multisig creation in progress...

13 | 17 | 18 | 19 | 20 | 21 |

22 | It shouldn't take more than 30s. 23 |
24 | This page will refresh automatically. 25 |

26 |
27 |
28 |
29 | ) 30 | } 31 | 32 | export default styled(SuccessCreation)` 33 | display: flex; 34 | flex-direction: column; 35 | align-content: center; 36 | align-items: center; 37 | 38 | .icon { 39 | animation: spin 10s linear infinite; 40 | @keyframes spin { 41 | 0% { 42 | transform: rotate(-360deg); 43 | } 44 | 100% { 45 | transform: rotate(0deg); 46 | } 47 | } 48 | 49 | font-size: 10rem; 50 | color: ${({ theme }) => theme.custom.text.addressColorLightGray}; 51 | text-align: center; 52 | } 53 | 54 | .explainer { 55 | margin-left: 1rem; 56 | } 57 | ` 58 | -------------------------------------------------------------------------------- /packages/ui/src/components/Toasts/Snackbar.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, styled } from '@mui/material' 2 | import { useToasts } from '../../contexts/ToastContext' 3 | import ToastBar from './ToastBar' 4 | 5 | interface Props { 6 | className?: string 7 | } 8 | 9 | const Snackbar = ({ className }: Props) => { 10 | const { toasts } = useToasts() 11 | 12 | return ( 13 | 18 | {toasts.map((toast) => ( 19 | 23 | ))} 24 | 25 | ) 26 | } 27 | 28 | export default styled(Snackbar)` 29 | position: fixed; 30 | bottom: 1rem; 31 | left: 1rem; 32 | z-index: 9999; 33 | ` 34 | -------------------------------------------------------------------------------- /packages/ui/src/components/Toasts/ToastBar.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, Snackbar } from '@mui/material' 2 | import { Toast, useToasts } from '../../contexts/ToastContext' 3 | import { HiOutlineXMark as CloseIcon } from 'react-icons/hi2' 4 | import ToastContent from './ToastContent' 5 | import React, { useCallback } from 'react' 6 | import { styled } from '@mui/material/styles' 7 | 8 | const HORIZONTAL_POSITION = 'left' 9 | const VERTICAL_POSITION = 'bottom' 10 | const DEFAULT_AUTO_HIDE_DURATION = 6000 11 | const ERROR_AUTO_HIDE_DURATION = 600000 12 | 13 | interface Props { 14 | toast: Toast 15 | className?: string 16 | } 17 | 18 | const ToastBar = ({ toast, className }: Props) => { 19 | const { id, duration } = toast 20 | const { removeToast } = useToasts() 21 | 22 | const handleClose = useCallback( 23 | (_: React.SyntheticEvent | Event, reason?: string) => { 24 | if (reason === 'clickaway') { 25 | return 26 | } 27 | 28 | removeToast(id) 29 | }, 30 | [removeToast, id] 31 | ) 32 | 33 | return ( 34 | 50 | 51 | 52 | } 53 | message={} 54 | /> 55 | ) 56 | } 57 | 58 | export default styled(ToastBar)` 59 | position: relative; 60 | bottom: 0; 61 | left: 0; 62 | ` 63 | -------------------------------------------------------------------------------- /packages/ui/src/components/layout/Center.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Grid2 as Grid } from '@mui/material' 3 | 4 | interface Props { 5 | className?: string 6 | children: React.ReactNode 7 | } 8 | export const Center = ({ children, className }: Props) => ( 9 | 18 | {children} 19 | 20 | ) 21 | -------------------------------------------------------------------------------- /packages/ui/src/components/layout/Main.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Box } from '@mui/material' 3 | import Header from '../Header/Header' 4 | import Container from '@mui/material/Container' 5 | import { Outlet } from 'react-router' 6 | import MobileMenu from '../Drawer/Drawer' 7 | import { styled } from '@mui/material/styles' 8 | 9 | function MainLayout() { 10 | const [open, setOpen] = React.useState(false) 11 | 12 | return ( 13 | 14 |
setOpen(true)} /> 15 | 19 | 20 | 21 | setOpen(false)} 24 | /> 25 | 26 | ) 27 | } 28 | 29 | const BoxStyled = styled(Box)` 30 | display: flex; 31 | flex-direction: column; 32 | ` 33 | 34 | export default MainLayout 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/AssetBalance.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { useGetAssetBalance } from '../../hooks/useGetAssetBalance' 3 | 4 | interface BalanceProps { 5 | address: string 6 | assetId: number 7 | logo: string 8 | } 9 | 10 | const AssetBalance = ({ address, assetId, logo }: BalanceProps) => { 11 | const { balanceFormatted } = useGetAssetBalance({ address, assetId }) 12 | 13 | if (!balanceFormatted) return null 14 | 15 | return ( 16 | 17 | {balanceFormatted} 18 | 22 | 23 | ) 24 | } 25 | 26 | const BalanceStyled = styled('div')` 27 | display: flex; 28 | color: ${({ theme }) => theme.custom.gray[700]}; 29 | font-size: 1rem; 30 | margin-top: 0.5rem; 31 | justify-content: flex-end; 32 | ` 33 | 34 | const ImgStyled = styled('img')` 35 | margin-left: 0.5rem; 36 | width: 1.5rem; 37 | border-radius: 50%; 38 | ` 39 | export default AssetBalance 40 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/Balance.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { useGetBalance } from '../../hooks/useGetBalance' 3 | import { useNetwork } from '../../contexts/NetworkContext' 4 | 5 | interface BalanceProps { 6 | address: string 7 | } 8 | 9 | const Balance = ({ address }: BalanceProps) => { 10 | const { balanceFormatted } = useGetBalance({ address }) 11 | const { selectedNetworkInfo } = useNetwork() 12 | 13 | return ( 14 | 15 | {balanceFormatted} 16 | 20 | 21 | ) 22 | } 23 | 24 | const BalanceStyled = styled('div')` 25 | display: flex; 26 | color: ${({ theme }) => theme.custom.gray[700]}; 27 | font-size: 1rem; 28 | justify-content: flex-end; 29 | ` 30 | 31 | const ImgStyled = styled('img')` 32 | margin-left: 0.5rem; 33 | width: 1.5rem; 34 | border-radius: 50%; 35 | ` 36 | export default Balance 37 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/InputField.tsx: -------------------------------------------------------------------------------- 1 | import { css, styled } from '@mui/material/styles' 2 | import React from 'react' 3 | import { theme } from '../../styles/theme' 4 | 5 | interface InputFieldProps { 6 | value?: string 7 | label?: string 8 | onChange?: (e: React.ChangeEvent) => void 9 | onKeyDown?: (e: React.KeyboardEvent) => void 10 | disabled?: boolean 11 | inputRef?: React.Ref 12 | } 13 | 14 | export const InputField = ({ 15 | label, 16 | value, 17 | onChange, 18 | onKeyDown, 19 | disabled, 20 | inputRef, 21 | ...props 22 | }: InputFieldProps) => ( 23 | 24 | {label} 25 | 33 | 34 | ) 35 | 36 | const LabelStyled = styled('label')` 37 | display: flex; 38 | flex-direction: column; 39 | 40 | span { 41 | margin-bottom: 4px; 42 | font-size: 1.125rem; 43 | font-weight: 500; 44 | color: ${(props) => props.theme.custom.text.primary}; 45 | } 46 | ` 47 | export const InputStyledBaseCss = css` 48 | width: 100%; 49 | min-height: 41px; 50 | color: ${theme.custom.text.black}; 51 | padding: 0.5rem 1.25rem; 52 | border: none; 53 | outline: 1.5px solid ${theme.custom.text.borderColor}; 54 | border-radius: ${theme.custom.borderRadius}; 55 | font-size: 1rem; 56 | font-family: 'Jost', sans-serif; 57 | 58 | &:focus-visible { 59 | outline: 3px solid ${theme.custom.text.borderColor}; 60 | } 61 | 62 | &:disabled { 63 | cursor: not-allowed; 64 | background: #f3f6f9; 65 | outline: 1.5px solid #e7e7e7; 66 | border-radius: ${theme.custom.borderRadius}; 67 | } 68 | ` 69 | 70 | const InputStyled = styled('input')` 71 | ${InputStyledBaseCss}; 72 | background: ${(props) => props.theme.palette.primary.white}; 73 | ` 74 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/Link.tsx: -------------------------------------------------------------------------------- 1 | import { css, styled } from '@mui/material/styles' 2 | import { Link as RouterLinkDom, LinkProps, NavLink as RouterNavLink } from 'react-router' 3 | 4 | const BaseLinkStyles = css` 5 | color: #3a3b3b; 6 | text-decoration: none; 7 | font-weight: 500; 8 | cursor: pointer; 9 | padding: 0.5rem 1rem; 10 | transition: color 0.2s linear; 11 | 12 | &:focus, 13 | &:hover { 14 | color: #1244f5; 15 | } 16 | 17 | &:disabled { 18 | background: #d2d2d2; 19 | box-shadow: none; 20 | } 21 | ` 22 | 23 | export const Link = styled('a')` 24 | ${BaseLinkStyles}; 25 | ` 26 | 27 | export const RouterLink = styled(RouterLinkDom)` 28 | ${BaseLinkStyles}; 29 | ` 30 | 31 | export const NavLink = styled(RouterNavLink)` 32 | ${BaseLinkStyles}; 33 | 34 | display: inline-block; 35 | color: ${({ theme }) => theme.custom.gray[900]}; 36 | transition: all 0.2s ease-in-out; 37 | 38 | &:hover { 39 | background: ${({ theme }) => theme.custom.gray[300]}; 40 | border-radius: ${({ theme }) => theme.custom.borderRadius}; 41 | color: ${({ theme }) => theme.custom.gray[900]}; 42 | } 43 | 44 | &:active { 45 | background: ${({ theme }) => theme.custom.gray[500]}; 46 | } 47 | 48 | &.active { 49 | color: ${({ theme }) => theme.custom.brand[400]}; 50 | } 51 | ` 52 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/ModalCloseButton.tsx: -------------------------------------------------------------------------------- 1 | import { IconButton, styled } from '@mui/material' 2 | import { HiOutlineXMark as CloseIcon } from 'react-icons/hi2' 3 | 4 | interface Props { 5 | onClose: () => void 6 | className?: string 7 | } 8 | 9 | const CloseButton = ({ onClose, className }: Props) => ( 10 | 17 | 18 | 19 | ) 20 | 21 | export const ModalCloseButton = styled(CloseButton)` 22 | position: absolute; 23 | right: 0.5rem; 24 | top: 0.5rem; 25 | ` 26 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/TextFieldLargeStyled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { TextField } from '@mui/material' 3 | 4 | const TextFieldLargeStyled = styled(TextField)` 5 | .MuiInputBase-root { 6 | height: 3.5rem; 7 | padding: 0.5rem 0.75rem 0.5rem 1rem; 8 | border: none; 9 | border-radius: ${({ theme }) => theme.custom.borderRadius}; 10 | outline: 1.5px solid ${({ theme }) => theme.custom.text.borderColor}; 11 | 12 | &:hover { 13 | border: none; 14 | } 15 | } 16 | 17 | .MuiOutlinedInput-notchedOutline { 18 | border: none; 19 | } 20 | 21 | fieldset { 22 | &:hover { 23 | border: none; 24 | } 25 | } 26 | 27 | input { 28 | font-size: 1rem; 29 | font-weight: 400; 30 | color: ${({ theme }) => theme.custom.text.primary}; 31 | } 32 | ` 33 | 34 | export default TextFieldLargeStyled 35 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/TextFieldStyled.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { TextField } from '@mui/material' 3 | import { InputStyledBaseCss } from './InputField' 4 | 5 | const TextFieldStyled = styled(TextField)` 6 | label { 7 | display: block; 8 | transform: none; 9 | position: static; 10 | margin-bottom: 4px; 11 | font-size: 1.125rem; 12 | font-weight: 500; 13 | color: ${({ theme }) => theme.custom.text.primary}; 14 | 15 | &.Mui-focused { 16 | color: ${({ theme }) => theme.custom.text.primary}; 17 | } 18 | } 19 | 20 | fieldset { 21 | display: none; 22 | } 23 | 24 | .MuiInputBase-root { 25 | ${InputStyledBaseCss}; 26 | max-height: 41px; 27 | padding: 0 1.25rem 0 1rem; 28 | 29 | .MuiInputBase-input { 30 | padding: 0; 31 | } 32 | 33 | .MuiAutocomplete-input { 34 | font-size: 1rem; 35 | font-weight: 500; 36 | color: ${({ theme }) => theme.custom.gray[900]}; 37 | border: none; 38 | height: 41px; 39 | box-sizing: border-box; 40 | padding: 0.5rem 0 0.5rem 0; 41 | } 42 | 43 | .MuiInputBase-inputAdornedStart { 44 | padding: 0.5rem 0.25rem; 45 | } 46 | 47 | &.Mui-focused { 48 | outline: 3px solid ${({ theme }) => theme.custom.text.borderColor}; 49 | } 50 | 51 | &.Mui-error { 52 | outline: 3px solid ${({ theme }) => theme.custom.error}; 53 | margin-bottom: 1rem; 54 | } 55 | } 56 | 57 | .MuiFormHelperText-root { 58 | &.Mui-error { 59 | position: absolute; 60 | bottom: -1rem; 61 | } 62 | } 63 | ` 64 | 65 | export default TextFieldStyled 66 | -------------------------------------------------------------------------------- /packages/ui/src/components/library/index.tsx: -------------------------------------------------------------------------------- 1 | import { Button, ButtonWithIcon } from './Button' 2 | import { Link, RouterLink, NavLink } from './Link' 3 | import { InputField } from './InputField' 4 | import TextField from './TextFieldStyled' 5 | import TextFieldLargeStyled from './TextFieldLargeStyled' 6 | import Select from './Select' 7 | import Autocomplete from './Autocomplete' 8 | import Balance from './Balance' 9 | 10 | export { 11 | Autocomplete, 12 | Balance, 13 | Button, 14 | ButtonWithIcon, 15 | Link, 16 | NavLink, 17 | RouterLink, 18 | InputField, 19 | TextField, 20 | TextFieldLargeStyled, 21 | Select 22 | } 23 | -------------------------------------------------------------------------------- /packages/ui/src/components/select/OptionMenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material' 2 | import { styled } from '@mui/material/styles' 3 | import React from 'react' 4 | 5 | interface OptionMenuProps { 6 | keyValue: string 7 | children: React.ReactNode[] | React.ReactNode 8 | } 9 | 10 | const OptionMenuItem = ({ keyValue, children, ...props }: OptionMenuProps) => ( 11 | 15 | {children} 16 | 17 | ) 18 | 19 | const BoxStyled = styled(Box)` 20 | &.MuiAutocomplete-option { 21 | background-color: transparent; 22 | cursor: pointer; 23 | padding: 0.75rem; 24 | border-bottom: 1px solid ${({ theme }) => theme.custom.text.borderColor}; 25 | font-size: 1rem; 26 | color: ${({ theme }) => theme.custom.gray[900]}; 27 | transition: background-color 0.2s ease-in-out; 28 | 29 | &:last-child { 30 | border-bottom: none; 31 | } 32 | 33 | &[aria-selected='true'].Mui-focused { 34 | background-color: transparent; 35 | } 36 | 37 | &:hover { 38 | background-color: ${({ theme }) => theme.custom.gray[400]} !important; 39 | } 40 | } 41 | 42 | .MuiBox-root { 43 | div { 44 | font-size: 1rem; 45 | color: ${({ theme }) => theme.custom.gray[900]}; 46 | } 47 | } 48 | ` 49 | 50 | export default OptionMenuItem 51 | -------------------------------------------------------------------------------- /packages/ui/src/components/select/SignerSelection.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo } from 'react' 2 | import { useAccounts } from '../../contexts/AccountsContext' 3 | import GenericAccountSelection, { AccountBaseInfo } from './GenericAccountSelection' 4 | import { useAccountBaseFromAccountList } from '../../hooks/useAccountBaseFromAccountList' 5 | 6 | interface SignerSelectionProps { 7 | className?: string 8 | possibleSigners: string[] 9 | onChange?: () => void 10 | label?: string 11 | } 12 | 13 | const SignerSelection = ({ possibleSigners, onChange, label }: SignerSelectionProps) => { 14 | const { selectAccount, selectedAccount, getAccountByAddress } = useAccounts() 15 | const accountBase = useAccountBaseFromAccountList() 16 | const selectedAccountBaseInfo = useMemo( 17 | () => accountBase.find(({ address }) => selectedAccount?.address === address), 18 | [accountBase, selectedAccount?.address] 19 | ) 20 | const signersList = useMemo(() => { 21 | return accountBase?.filter((account) => possibleSigners.includes(account.address)) || [] 22 | }, [accountBase, possibleSigners]) 23 | 24 | useEffect(() => { 25 | if (!selectedAccount || signersList.length === 0) { 26 | return 27 | } 28 | 29 | if (!possibleSigners.includes(selectedAccount.address)) { 30 | const account = getAccountByAddress(signersList[0].address) 31 | account && selectAccount(account) 32 | } 33 | }, [getAccountByAddress, possibleSigners, selectAccount, selectedAccount, signersList]) 34 | 35 | const onChangeSigner = useCallback( 36 | (newAccount?: AccountBaseInfo) => { 37 | const account = newAccount && getAccountByAddress(newAccount.address) 38 | account && selectAccount(account) 39 | onChange && onChange() 40 | }, 41 | [getAccountByAddress, onChange, selectAccount] 42 | ) 43 | 44 | if (signersList.length === 0) { 45 | return null 46 | } 47 | 48 | return ( 49 | 56 | ) 57 | } 58 | 59 | export default SignerSelection 60 | -------------------------------------------------------------------------------- /packages/ui/src/contexts/ToastContext.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useContext, createContext } from 'react' 2 | import { ToastType } from '../components/Toasts/ToastContent' 3 | import Snackbar from '../components/Toasts/Snackbar' 4 | 5 | export type Toast = { 6 | id: number 7 | title?: string 8 | link?: string 9 | duration?: number 10 | type: ToastType 11 | } 12 | 13 | const MAX_VISIBLE_TOASTS = 6 14 | 15 | export type ToastContextProps = { 16 | toasts: Toast[] 17 | addToast: (toast: Omit) => void 18 | removeToast: (id: Toast['id']) => void 19 | } 20 | 21 | const ToastContext = createContext(undefined) 22 | 23 | const ToastProvider = ({ children }: React.PropsWithChildren) => { 24 | const [toasts, setToasts] = useState([]) 25 | 26 | const addToast = (toast: Omit) => { 27 | const id = Date.now() 28 | 29 | setToasts((prev) => { 30 | const rest = prev.length < MAX_VISIBLE_TOASTS ? prev : prev.slice(0, -1) 31 | return [{ ...toast, id }, ...rest] 32 | }) 33 | } 34 | 35 | const removeToast = (key: Toast['id']) => { 36 | setToasts((prev) => prev.filter((toast) => toast.id !== key)) 37 | } 38 | 39 | return ( 40 | 41 | 42 | {children} 43 | 44 | ) 45 | } 46 | 47 | export const useToasts = () => { 48 | const context = useContext(ToastContext) 49 | if (context === undefined) { 50 | throw new Error('useToast must be used within a ToastContextProvider') 51 | } 52 | return context 53 | } 54 | 55 | export default ToastProvider 56 | -------------------------------------------------------------------------------- /packages/ui/src/gql/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./fragment-masking"; 2 | export * from "./gql"; -------------------------------------------------------------------------------- /packages/ui/src/hooks/useAccountBaseFromAccountList.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { AccountBaseInfo } from '../components/select/GenericAccountSelection' 3 | import { useAccounts } from '../contexts/AccountsContext' 4 | import { useAccountNames } from '../contexts/AccountNamesContext' 5 | 6 | interface Params { 7 | withAccountsFromAddressBook: boolean 8 | } 9 | 10 | export const useAccountBaseFromAccountList = (params?: Params) => { 11 | const { withAccountsFromAddressBook = false } = params || {} 12 | const { ownAccountList } = useAccounts() 13 | const { accountNames } = useAccountNames() 14 | 15 | const accountBase = useMemo((): AccountBaseInfo[] => { 16 | const ownAccountListAddresses = 17 | (ownAccountList && ownAccountList.map(({ address }) => address)) || [] 18 | const accountFromNameRegistry = 19 | (withAccountsFromAddressBook && Object.keys(accountNames).map((address) => address)) || [] 20 | const addressesSet = new Set([...accountFromNameRegistry, ...ownAccountListAddresses]) 21 | 22 | return Array.from(addressesSet).map((address) => ({ address })) 23 | }, [accountNames, ownAccountList, withAccountsFromAddressBook]) 24 | 25 | return accountBase 26 | } 27 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useAccountDisplayInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | import { IdentityInfo, useGetIdentity } from './useGetIdentity' 3 | import { useAccountNames } from '../contexts/AccountNamesContext' 4 | import { getDisplayName } from '../utils/getDisplayName' 5 | import { getIdentityName } from '../utils/getIdentityName' 6 | import { isValidAddress } from '../utils/isValidAddress' 7 | 8 | interface Props { 9 | address?: string 10 | } 11 | 12 | export const useAccountDisplayInfo = ({ address }: Props) => { 13 | const getIdentity = useGetIdentity() 14 | const { getNamesWithExtension } = useAccountNames() 15 | const localName = useMemo( 16 | () => (address ? getNamesWithExtension(address) : undefined), 17 | [address, getNamesWithExtension] 18 | ) 19 | const isLocalNameDisplayed = useMemo(() => !!localName, [localName]) 20 | const [identity, setIdentity] = useState() 21 | const { identityName, sub } = useMemo(() => getIdentityName(identity), [identity]) 22 | 23 | useEffect(() => { 24 | if (!address || !isValidAddress(address)) return 25 | 26 | getIdentity(address).then(setIdentity).catch(console.error) 27 | }, [address, getIdentity]) 28 | 29 | const displayName = useMemo( 30 | () => getDisplayName(localName || '', identityName), 31 | [localName, identityName] 32 | ) 33 | 34 | return { 35 | identity, 36 | isLocalNameDisplayed, 37 | localName, 38 | displayName, 39 | subIdentity: sub, 40 | identityName 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useAccountId.ts: -------------------------------------------------------------------------------- 1 | import { useNetwork } from '../contexts/NetworkContext' 2 | 3 | const getId = (pubKey: string, chainId: string) => `${chainId}-${pubKey}` 4 | 5 | export function useAccountId(pubKey: string[]): string[] 6 | export function useAccountId(pubKey: string): string 7 | export function useAccountId(pubKey: string | string[]) { 8 | const { selectedNetworkInfo } = useNetwork() 9 | 10 | if (Array.isArray(pubKey)) { 11 | return pubKey.map((pk) => getId(pk, selectedNetworkInfo?.chainId || '')) 12 | } 13 | 14 | return getId(pubKey, selectedNetworkInfo?.chainId || '') 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useAnyApi.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useApi } from '../contexts/ApiContext' 3 | import { usePplApi } from '../contexts/PeopleChainApiContext' 4 | 5 | interface Props { 6 | withPplApi: boolean 7 | } 8 | 9 | export const useAnyApi = ({ withPplApi }: Props) => { 10 | const normalCtx = useApi() 11 | const { 12 | api: normalApi, 13 | compatibilityToken: normalCompatibilityToken, 14 | chainInfo: normalChainInfo, 15 | client: normalClient 16 | } = normalCtx 17 | const pplCtx = usePplApi() 18 | const { pplApi, pplCompatibilityToken, pplChainInfo, pplClient } = pplCtx 19 | 20 | const api = useMemo(() => (withPplApi ? pplApi : normalApi), [withPplApi, normalApi, pplApi]) 21 | const compatibilityToken = useMemo( 22 | () => (withPplApi ? pplCompatibilityToken : normalCompatibilityToken), 23 | [withPplApi, normalCompatibilityToken, pplCompatibilityToken] 24 | ) 25 | const chainInfo = useMemo( 26 | () => (withPplApi ? pplChainInfo : normalChainInfo), 27 | [withPplApi, normalChainInfo, pplChainInfo] 28 | ) 29 | const client = useMemo( 30 | () => (withPplApi ? pplClient : normalClient), 31 | [withPplApi, normalClient, pplClient] 32 | ) 33 | 34 | return { api, compatibilityToken, chainInfo, client } 35 | } 36 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useCallInfoFromCallData.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { SubmittingCall } from '../types' 3 | import { PAYMENT_INFO_ACCOUNT } from '../constants' 4 | import { Binary, HexString } from 'polkadot-api' 5 | import { hashFromTx } from '../utils/txHash' 6 | import { useAnyApi } from './useAnyApi' 7 | 8 | export const useCallInfoFromCallData = ({ 9 | isPplTx, 10 | callData 11 | }: { 12 | isPplTx: boolean 13 | callData?: HexString 14 | }) => { 15 | const { api, compatibilityToken } = useAnyApi({ withPplApi: isPplTx }) 16 | const [callInfo, setCallInfo] = useState(undefined) 17 | const [isGettingCallInfo, setIsGettingCallInfo] = useState(false) 18 | 19 | useEffect(() => { 20 | if (!callData) { 21 | setCallInfo(undefined) 22 | setIsGettingCallInfo(false) 23 | return 24 | } 25 | 26 | if (!api || !compatibilityToken) { 27 | setCallInfo(undefined) 28 | setIsGettingCallInfo(false) 29 | return 30 | } 31 | 32 | setIsGettingCallInfo(true) 33 | 34 | try { 35 | const tx = api.txFromCallData(Binary.fromHex(callData), compatibilityToken) 36 | 37 | tx.getPaymentInfo(PAYMENT_INFO_ACCOUNT, { at: 'best' }) 38 | .then(({ weight, partial_fee }) => { 39 | setCallInfo({ 40 | decodedCall: tx?.decodedCall, 41 | call: tx, 42 | hash: hashFromTx(callData), 43 | weight, 44 | section: tx?.decodedCall.type, 45 | method: tx?.decodedCall.value.type, 46 | partialFee: partial_fee 47 | }) 48 | setIsGettingCallInfo(false) 49 | }) 50 | .catch((e) => { 51 | console.error(e) 52 | setIsGettingCallInfo(false) 53 | setCallInfo(undefined) 54 | }) 55 | } catch (e) { 56 | console.error(e) 57 | setIsGettingCallInfo(false) 58 | setCallInfo(undefined) 59 | } 60 | }, [api, callData, compatibilityToken]) 61 | 62 | return { callInfo, isGettingCallInfo } 63 | } 64 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useCheckTransferableBalance.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useGetBalance } from './useGetBalance' 3 | 4 | export interface Props { 5 | min?: bigint 6 | address?: string 7 | withPplApi: boolean 8 | } 9 | 10 | export const useCheckTransferableBalance = ({ min, address, withPplApi }: Props) => { 11 | const { balance } = useGetBalance({ address, withPplApi }) 12 | const hasEnoughFreeBalance = useMemo(() => { 13 | if (!address || !balance || min === undefined) { 14 | return false 15 | } 16 | return balance > min 17 | }, [address, min, balance]) 18 | 19 | return { hasEnoughFreeBalance } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useDisplayLoader.tsx: -------------------------------------------------------------------------------- 1 | import { useMultiProxy } from '../contexts/MultiProxyContext' 2 | import { useApi } from '../contexts/ApiContext' 3 | import { useNetwork } from '../contexts/NetworkContext' 4 | import { useWatchedAccounts } from '../contexts/WatchedAccountsContext' 5 | import LoadingBox from '../components/LoadingBox' 6 | 7 | export const useDisplayLoader = () => { 8 | const { isLoading: isLoadingMultisigs } = useMultiProxy() 9 | const { api } = useApi() 10 | const { selectedNetworkInfo } = useNetwork() 11 | const { isInitialized: isWatchAddressInitialized } = useWatchedAccounts() 12 | 13 | if (!isWatchAddressInitialized) { 14 | return ( 15 | 19 | ) 20 | } 21 | 22 | if (!api) { 23 | return ( 24 | 28 | ) 29 | } 30 | 31 | if (isLoadingMultisigs) { 32 | return ( 33 | 37 | ) 38 | } 39 | 40 | return null 41 | } 42 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useFetchData.ts: -------------------------------------------------------------------------------- 1 | import { HTTP_GRAPHQL_URL } from '../constants' 2 | import { useNetwork } from '../contexts/NetworkContext' 3 | 4 | export const useFetchData = ( 5 | query: string, 6 | options?: RequestInit['headers'] 7 | ): ((variables?: TVariables) => Promise) => { 8 | // it is safe to call React Hooks here. 9 | const { selectedNetworkInfo } = useNetwork() 10 | 11 | return async (variables?: TVariables) => { 12 | const res = await fetch(selectedNetworkInfo?.httpGraphqlUrl || HTTP_GRAPHQL_URL, { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | ...options 17 | }, 18 | body: JSON.stringify({ 19 | query, 20 | variables 21 | }) 22 | }) 23 | 24 | const json = await res.json() 25 | 26 | if (json.errors) { 27 | const { message } = json.errors[0] || {} 28 | throw new Error(message || 'Error…') 29 | } 30 | 31 | return json.data 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetAssetBalance.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { isContextIn, useApi } from '../contexts/ApiContext' 3 | import { formatBigIntBalance } from '../utils/formatBnBalance' 4 | import { assetHubKeys } from '../types' 5 | import { useAssets } from '../contexts/AssetsContext' 6 | 7 | interface useGetBalanceProps { 8 | address?: string 9 | numberAfterComma?: number 10 | assetId: number 11 | } 12 | 13 | export const useGetAssetBalance = ({ 14 | address, 15 | numberAfterComma = 2, 16 | assetId 17 | }: useGetBalanceProps) => { 18 | const ctx = useApi() 19 | const [balance, setBalance] = useState(null) 20 | const [balanceFormatted, setFormattedBalance] = useState(null) 21 | const { getAssetMetadata } = useAssets() 22 | 23 | useEffect(() => { 24 | if (!ctx?.api || !isContextIn(ctx, assetHubKeys) || !address || !assetId) return 25 | 26 | const assetMetadata = getAssetMetadata(assetId) 27 | 28 | if (!assetMetadata) return 29 | 30 | const unsub = ctx.api.query.Assets.Account.watchValue(assetId, address, 'best').subscribe( 31 | (res) => { 32 | const balance = res?.balance || 0n 33 | 34 | setBalance(balance) 35 | setFormattedBalance( 36 | formatBigIntBalance(balance, assetMetadata.decimals, { 37 | numberAfterComma, 38 | tokenSymbol: assetMetadata.symbol 39 | }) 40 | ) 41 | } 42 | ) 43 | 44 | return () => unsub && unsub.unsubscribe() 45 | }, [address, assetId, ctx, getAssetMetadata, numberAfterComma]) 46 | 47 | return { balance, balanceFormatted } 48 | } 49 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetAssetBalances.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react' 2 | import { isContextIn, useApi } from '../contexts/ApiContext' 3 | import { assetHubKeys } from '../types' 4 | 5 | interface useGetAssetBalancesProps { 6 | address?: string 7 | assetIds: number[] 8 | } 9 | 10 | type AssetBalanceInfo = Record 11 | 12 | const POLL_INTERVAL = 6000 13 | 14 | export const useGetAssetBalances = ({ address, assetIds }: useGetAssetBalancesProps) => { 15 | const ctx = useApi() 16 | const [balances, setBalances] = useState(null) 17 | 18 | const getBalances = useCallback(async () => { 19 | if (!ctx?.api || !isContextIn(ctx, assetHubKeys) || !address || assetIds.length === 0) return 20 | 21 | const params = assetIds.map((assetId) => [assetId, address]) as [number, string][] 22 | 23 | return ctx.api.query.Assets.Account.getValues(params, { 24 | at: 'best' 25 | }).then((res) => { 26 | const balances: AssetBalanceInfo = {} 27 | 28 | res.forEach((value, index) => { 29 | const assetId = assetIds[index] 30 | const bal = value?.balance || 0n 31 | 32 | balances[assetId] = bal 33 | }) 34 | 35 | return balances 36 | }) 37 | }, [address, assetIds, ctx]) 38 | 39 | const cacheBalances = useCallback(async () => { 40 | const newBalances = await getBalances() 41 | 42 | if (!newBalances) return 43 | 44 | const shouldUpdateBalances = 45 | balances === null || 46 | Object.entries(newBalances).some(([id, bal]) => { 47 | return bal !== balances[Number(id)] 48 | }) 49 | 50 | if (shouldUpdateBalances) { 51 | setBalances(newBalances) 52 | } 53 | }, [balances, getBalances]) 54 | 55 | useEffect(() => { 56 | if (balances === null) { 57 | cacheBalances() 58 | } 59 | 60 | const inter = setInterval(cacheBalances, POLL_INTERVAL) 61 | 62 | return () => { 63 | clearInterval(inter) 64 | } 65 | }, [address, assetIds, balances, cacheBalances, ctx, getBalances]) 66 | 67 | return { balances } 68 | } 69 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetBalance.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { formatBigIntBalance } from '../utils/formatBnBalance' 3 | import { bigIntMax } from '../utils/bnUtils' 4 | import { useAnyApi } from './useAnyApi' 5 | import { useGetED } from './useGetED' 6 | 7 | interface useGetBalanceProps { 8 | address?: string 9 | numberAfterComma?: number 10 | withPplApi?: boolean 11 | } 12 | 13 | export const useGetBalance = ({ 14 | address, 15 | numberAfterComma = 2, 16 | withPplApi = false 17 | }: useGetBalanceProps) => { 18 | const { api, chainInfo } = useAnyApi({ withPplApi }) 19 | const [balance, setBalance] = useState(null) 20 | const [balanceFormatted, setFormattedBalance] = useState(null) 21 | const { existentialDeposit } = useGetED({ withPplApi }) 22 | 23 | useEffect(() => { 24 | if (!api || !address || !existentialDeposit) return 25 | 26 | const unsub = api.query.System.Account.watchValue(address, 'best').subscribe( 27 | ({ data: { free, frozen, reserved } }) => { 28 | const res = free - bigIntMax(frozen - reserved, existentialDeposit) 29 | const transferable = res < 0n ? 0n : res 30 | setBalance(transferable) 31 | setFormattedBalance( 32 | formatBigIntBalance(transferable, chainInfo?.tokenDecimals, { 33 | numberAfterComma, 34 | tokenSymbol: chainInfo?.tokenSymbol 35 | }) 36 | ) 37 | } 38 | ) 39 | 40 | return () => unsub && unsub.unsubscribe() 41 | }, [address, api, chainInfo, existentialDeposit, numberAfterComma]) 42 | 43 | return { balance, balanceFormatted } 44 | } 45 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetED.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useAnyApi } from './useAnyApi' 3 | 4 | interface useGetEDProps { 5 | withPplApi?: boolean 6 | } 7 | 8 | export const useGetED = ({ withPplApi = false }: useGetEDProps) => { 9 | const { api, compatibilityToken } = useAnyApi({ withPplApi }) 10 | const existentialDeposit = useMemo(() => { 11 | if (!api || !compatibilityToken) return 12 | 13 | return api.constants.Balances.ExistentialDeposit(compatibilityToken) 14 | }, [api, compatibilityToken]) 15 | 16 | return { existentialDeposit } 17 | } 18 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetEncodedAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useApi } from '../contexts/ApiContext' 2 | import { useCallback } from 'react' 3 | import { encodesubstrateAddress } from '../utils/encodeSubstrateAddress' 4 | import { u8aToHex, isU8a } from '@polkadot/util' 5 | import { isValidAddress } from '../utils/isValidAddress' 6 | 7 | export const useGetEncodedAddress = () => { 8 | const { chainInfo } = useApi() 9 | 10 | const getEncodedAddress = useCallback( 11 | (address: string | Uint8Array | undefined) => { 12 | if (!chainInfo || !address || address === 'undefined') { 13 | return 14 | } 15 | 16 | if (!isValidAddress(address)) { 17 | return 18 | } 19 | 20 | if (chainInfo?.isEthereum) { 21 | const res = isU8a(address) 22 | ? u8aToHex(address as Uint8Array).toString() 23 | : (address as string) 24 | return res.slice(0, 42) 25 | } 26 | 27 | return encodesubstrateAddress(address, chainInfo.ss58Format) 28 | }, 29 | [chainInfo] 30 | ) 31 | 32 | return getEncodedAddress 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetMultisigAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useApi } from '../contexts/ApiContext' 3 | import { createKeyMulti } from '@polkadot/util-crypto' 4 | import { useGetEncodedAddress } from './useGetEncodedAddress' 5 | 6 | export const useGetMultisigAddress = (signatories: string[], threshold?: number | null) => { 7 | const { chainInfo } = useApi() 8 | const getEncodedAddress = useGetEncodedAddress() 9 | 10 | const newMultisigPubKey = useMemo(() => { 11 | if (!threshold) return 12 | return createKeyMulti(signatories, threshold) 13 | }, [signatories, threshold]) 14 | const newMultisigAddress = useMemo( 15 | () => getEncodedAddress(newMultisigPubKey), 16 | [getEncodedAddress, newMultisigPubKey] 17 | ) 18 | 19 | if (chainInfo?.isEthereum && newMultisigAddress?.startsWith('0x')) { 20 | return newMultisigAddress.slice(0, 42) 21 | } 22 | 23 | return newMultisigAddress 24 | } 25 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useGetSortAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useApi } from '../contexts/ApiContext' 2 | import { useCallback } from 'react' 3 | import { sortAddresses } from '@polkadot/util-crypto' 4 | 5 | export const useGetSortAddress = () => { 6 | const { chainInfo } = useApi() 7 | 8 | const getSortAddress = useCallback( 9 | (addresses: string[]) => { 10 | if ( 11 | chainInfo?.isEthereum && 12 | addresses.every((add) => { 13 | return add.startsWith('0x') && add.length === 42 14 | }) 15 | ) { 16 | return addresses.sort() 17 | } 18 | 19 | return sortAddresses(addresses) 20 | }, 21 | [chainInfo] 22 | ) 23 | 24 | return { getSortAddress } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useHasIdentityFeature.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useNetwork } from '../contexts/NetworkContext' 3 | import { useIdentityApi } from './useIdentityApi' 4 | import { dotPpl } from '@polkadot-api/descriptors' 5 | import { TypedApi } from 'polkadot-api' 6 | 7 | export const useHasIdentityFeature = () => { 8 | const { api } = useIdentityApi() 9 | const { selectedNetworkInfo } = useNetwork() 10 | const hasIdentityPallet = useMemo( 11 | () => !!api && !!(api as TypedApi).tx?.Identity?.set_identity, 12 | [api] 13 | ) 14 | const hasPplChain = useMemo(() => !!selectedNetworkInfo?.pplChainRpcUrls, [selectedNetworkInfo]) 15 | return { 16 | hasPplChain, 17 | hasIdentityPallet 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useIdentityApi.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | import { IPplApiContext, usePplApi } from '../contexts/PeopleChainApiContext' 3 | import { ChainInfoHuman, IApiContext, useApi } from '../contexts/ApiContext' 4 | import { ApiDescriptors, PplDescriptorKeys } from '../types' 5 | import { CompatibilityToken } from 'polkadot-api' 6 | 7 | export const useIdentityApi = () => { 8 | const pplCtx = usePplApi() 9 | const ctx = useApi() 10 | const { pplApi, pplChainInfo, pplCompatibilityToken } = pplCtx 11 | const { api, chainInfo, compatibilityToken } = ctx 12 | const [apiToUse, setApiToUse] = useState< 13 | IPplApiContext['pplApi'] | IApiContext['api'] | null 14 | >(null) 15 | const [chainInfoToUse, setChainInfoToUse] = useState(undefined) 16 | const [compatibilityTokenToUse, setCompatibilityTokenToUse] = useState() 17 | const [ctxToUse, setCtxToUse] = useState< 18 | IPplApiContext | IApiContext 19 | >() 20 | 21 | useEffect(() => { 22 | if (pplApi) { 23 | setApiToUse(pplApi) 24 | setChainInfoToUse(pplChainInfo) 25 | setCompatibilityTokenToUse(pplCompatibilityToken) 26 | setCtxToUse(pplCtx) 27 | } else if (api) { 28 | setApiToUse(api) 29 | setChainInfoToUse(chainInfo) 30 | setCompatibilityTokenToUse(compatibilityToken) 31 | setCtxToUse(ctx) 32 | } 33 | }, [api, chainInfo, compatibilityToken, ctx, pplApi, pplChainInfo, pplCompatibilityToken, pplCtx]) 34 | 35 | return { 36 | api: apiToUse, 37 | chainInfo: chainInfoToUse, 38 | compatibilityToken: compatibilityTokenToUse, 39 | ctx: ctxToUse 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useMultisigProposalNeededFunds.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { Transaction } from 'polkadot-api' 3 | import { useAnyApi } from './useAnyApi' 4 | 5 | interface Props { 6 | threshold?: number | null 7 | signatories?: string[] 8 | call?: Transaction 9 | withPplApi?: boolean 10 | } 11 | 12 | export const useMultisigProposalNeededFunds = ({ 13 | threshold, 14 | signatories, 15 | call, 16 | withPplApi = false 17 | }: Props) => { 18 | const { api, compatibilityToken, chainInfo } = useAnyApi({ withPplApi }) 19 | const [min, setMin] = useState(0n) 20 | const [reserved, setReserved] = useState(0n) 21 | 22 | useEffect(() => { 23 | if (!api || !signatories || signatories.length < 2 || !compatibilityToken) return 24 | 25 | if (!chainInfo?.tokenDecimals) return 26 | 27 | if (!threshold) return 28 | 29 | if (!call) return 30 | 31 | const multisigDepositBase = api.constants.Multisig.DepositBase(compatibilityToken) 32 | const multisigDepositFactor = api.constants.Multisig.DepositFactor(compatibilityToken) 33 | 34 | if (!multisigDepositFactor || !multisigDepositBase) return 35 | 36 | call 37 | .getEstimatedFees('5CXQZrh1MSgnGGCdJu3tqvRfCv7t5iQXGGV9UKotrbfhkavs') 38 | .then((info) => { 39 | const reservedTemp = multisigDepositFactor * BigInt(threshold) + multisigDepositBase 40 | setMin(reservedTemp + info) 41 | setReserved(reservedTemp) 42 | }) 43 | .catch(console.error) 44 | }, [api, call, chainInfo, compatibilityToken, signatories, threshold]) 45 | 46 | return { multisigProposalNeededFunds: min, reserved } 47 | } 48 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/usePjsLinks.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react' 2 | import { useNetwork } from '../contexts/NetworkContext' 3 | 4 | interface Params { 5 | isPplChain: boolean 6 | } 7 | 8 | export const usePjsLinks = ({ isPplChain }: Params) => { 9 | const { selectedNetworkInfo } = useNetwork() 10 | const urlBase = useMemo(() => { 11 | if (!selectedNetworkInfo?.rpcUrls) return '' 12 | 13 | const encodedRpc = encodeURIComponent( 14 | isPplChain && selectedNetworkInfo?.pplChainRpcUrls 15 | ? selectedNetworkInfo?.pplChainRpcUrls[0] 16 | : selectedNetworkInfo?.rpcUrls[0] 17 | ) 18 | return `https://polkadot.js.org/apps/?rpc=${encodedRpc}#` 19 | }, [isPplChain, selectedNetworkInfo?.pplChainRpcUrls, selectedNetworkInfo?.rpcUrls]) 20 | 21 | return { 22 | getDecodeUrl: (hex: string) => `${urlBase}/extrinsics/decode/${hex}`, 23 | extrinsicUrl: `${urlBase}/extrinsics` 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useProxyAdditionNeededFunds.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useApi } from '../contexts/ApiContext' 3 | 4 | export const useProxyAdditionNeededFunds = () => { 5 | const { api, chainInfo } = useApi() 6 | const [min, setMin] = useState(0n) 7 | 8 | useEffect(() => { 9 | if (!api) return 10 | 11 | if (!chainInfo?.tokenDecimals) return 12 | 13 | if (!api.constants?.Proxy?.ProxyDepositFactor) return // the proxy is already created (with proxyDepositBase already deposited by the first account 14 | // that added the first proxy) we are only adding one account as proxy 15 | api.constants.Proxy.ProxyDepositBase().then(setMin).catch(console.error) 16 | }, [api, chainInfo]) 17 | 18 | return { proxyAdditionNeededFunds: min } 19 | } 20 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/usePureProxyCreationNeededFunds.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { useApi } from '../contexts/ApiContext' 3 | 4 | export const usePureProxyCreationNeededFunds = () => { 5 | const { api, chainInfo, compatibilityToken } = useApi() 6 | const [min, setMin] = useState(0n) 7 | const [reserved, setReserved] = useState(0n) 8 | 9 | useEffect(() => { 10 | if (!api || !compatibilityToken) return 11 | 12 | const existentialDeposit = api.constants.Balances.ExistentialDeposit(compatibilityToken) 13 | const depositBase = api.constants.Proxy.ProxyDepositBase(compatibilityToken) 14 | const depositFactor = api.constants.Proxy.ProxyDepositFactor(compatibilityToken) 15 | 16 | if (!existentialDeposit || !depositBase || !depositFactor) return 17 | // if (!chainInfo?.tokenDecimals) return 18 | 19 | // we only create one proxy here 20 | const reserved = depositFactor * 1n + depositBase 21 | 22 | // the signer should survive and have at least the existential deposit 23 | // play safe and add the existential deposit twice which should suffice 24 | const survive = existentialDeposit * 2n 25 | 26 | setReserved(reserved) 27 | setMin(reserved + survive) 28 | // console.log('reserved Pure Creation', formatBnBalance(reserved.add(survive), chainInfo.tokenDecimals, { tokenSymbol: chainInfo?.tokenSymbol, numberAfterComma: 3 })) 29 | }, [api, chainInfo, compatibilityToken]) 30 | 31 | return { pureProxyCreationNeededFunds: min, reserved } 32 | } 33 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useQueryMultisigsAndPureByAccounts.tsx: -------------------------------------------------------------------------------- 1 | import { useMultisigsAndPureByAccountQuery } from '../../types-and-hooks' 2 | import { useMemo } from 'react' 3 | import { useNetwork } from '../contexts/NetworkContext' 4 | 5 | const DEFAULT_REFETCH_INTERVAL = 5000 6 | 7 | interface Args { 8 | accountIds: string[] 9 | watchedAccountIds: string[] 10 | shouldRefetch?: boolean 11 | } 12 | 13 | export const useQueryMultisigsAndPureByAccounts = ({ 14 | accountIds, 15 | watchedAccountIds, 16 | shouldRefetch = false 17 | }: Args) => { 18 | const { selectedNetwork } = useNetwork() 19 | const hasSomethingToQuery = useMemo( 20 | () => accountIds.length > 0 || watchedAccountIds.length > 0, 21 | [accountIds, watchedAccountIds] 22 | ) 23 | const { error, data, isLoading, refetch } = useMultisigsAndPureByAccountQuery( 24 | { accountIds, watchedAccountIds }, 25 | { 26 | enabled: hasSomethingToQuery, 27 | queryKey: [ 28 | `KeyMultisigsBySignatoriesOrWatched-${accountIds}-${watchedAccountIds}-${selectedNetwork}` 29 | ], 30 | refetchInterval: !!shouldRefetch && DEFAULT_REFETCH_INTERVAL 31 | } 32 | ) 33 | 34 | return { 35 | data, 36 | isLoading: isLoading && hasSomethingToQuery, 37 | error, 38 | refetch 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSetIdentityReservedFunds.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react' 2 | // import { formatBigIntBalance } from '../utils/formatBnBalance' 3 | import { IdentityFields } from '../components/EasySetup/SetIdentity' 4 | import { getByteCount } from '../utils/getByteCount' 5 | import { useIdentityApi } from './useIdentityApi' 6 | import { isPplContextIn } from '../contexts/PeopleChainApiContext' 7 | import { pplDescriptorKeys } from '../types' 8 | 9 | export const useSetIdentityReservedFunds = (identityFields?: IdentityFields) => { 10 | const { ctx } = useIdentityApi() 11 | const [reserved, setReserved] = useState(0n) 12 | 13 | const fieldBytes = useMemo(() => { 14 | if (!identityFields) return 0 15 | 16 | const allfields = Object.values(identityFields) 17 | .filter((value) => !!value) 18 | .join('') 19 | 20 | return getByteCount(allfields) 21 | }, [identityFields]) 22 | 23 | useEffect(() => { 24 | if (isPplContextIn(ctx, pplDescriptorKeys)) { 25 | const { pplApi, pplCompatibilityToken, pplChainInfo } = ctx 26 | if (!pplApi || !identityFields || !pplCompatibilityToken) return 27 | 28 | if (!pplChainInfo?.tokenDecimals) return 29 | 30 | const byteDeposit = pplApi.constants?.Identity?.ByteDeposit(pplCompatibilityToken) 31 | 32 | const basicDeposit = pplApi.constants?.Identity.BasicDeposit(pplCompatibilityToken) 33 | 34 | if (!basicDeposit || !byteDeposit) return 35 | 36 | const reservedFields = byteDeposit * BigInt(fieldBytes) 37 | 38 | const res = reservedFields + basicDeposit 39 | 40 | // console.log( 41 | // 'res', 42 | // formatBigIntBalance(res, chainInfo.tokenDecimals, { 43 | // tokenSymbol: chainInfo?.tokenSymbol, 44 | // numberAfterComma: 6 45 | // }) 46 | // ) 47 | setReserved(res) 48 | } 49 | }, [ctx, fieldBytes, identityFields]) 50 | 51 | return { reserved } 52 | } 53 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSubscanLink.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react' 2 | import { useNetwork } from '../contexts/NetworkContext' 3 | import { getSubscanExtrinsicLink } from '../utils/getSubscanExtrinsicLink' 4 | import { getSubscanAccountLink } from '../utils/getSubscanAccountLink' 5 | 6 | export const useGetSubscanLinks = () => { 7 | const { selectedNetworkInfo } = useNetwork() 8 | 9 | const _getSubscanExtrinsicLink = useCallback( 10 | (txHash: string) => getSubscanExtrinsicLink(selectedNetworkInfo?.explorerNetworkName, txHash), 11 | [selectedNetworkInfo] 12 | ) 13 | 14 | const _getSubscanAccountLink = useCallback( 15 | (account?: string, multisig = false) => 16 | getSubscanAccountLink(selectedNetworkInfo?.explorerNetworkName, account, multisig), 17 | [selectedNetworkInfo] 18 | ) 19 | return { 20 | getSubscanExtrinsicLink: _getSubscanExtrinsicLink, 21 | getSubscanAccountLink: _getSubscanAccountLink 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useSwitchAddress.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo } from 'react' 2 | import { useSearchParams } from 'react-router' 3 | import { useMultiProxy } from '../contexts/MultiProxyContext' 4 | 5 | export const useSwitchAddress = () => { 6 | const [searchParams] = useSearchParams({ 7 | address: '', 8 | network: '' 9 | }) 10 | const urlAddress = useMemo(() => { 11 | return searchParams.get('address') || '' 12 | }, [searchParams]) 13 | 14 | const { 15 | multiProxyList, 16 | isLoading: isMultiproxyLoading, 17 | selectMultiProxy, 18 | selectedMultiProxyAddress, 19 | setCanFindMultiProxyFromUrl, 20 | defaultAddress 21 | } = useMultiProxy() 22 | 23 | useEffect(() => { 24 | if (isMultiproxyLoading) { 25 | // we're not yet initialized 26 | return 27 | } 28 | 29 | if (!!urlAddress && !selectedMultiProxyAddress) { 30 | // this looks like a first load with an address 31 | const isSuccess = selectMultiProxy(urlAddress) 32 | setCanFindMultiProxyFromUrl(isSuccess) 33 | 34 | return 35 | } 36 | 37 | if (!urlAddress && !!defaultAddress) { 38 | // no address in the url, init with the default 39 | const isSuccess = selectMultiProxy(defaultAddress) 40 | setCanFindMultiProxyFromUrl(isSuccess) 41 | 42 | return 43 | } 44 | }, [ 45 | defaultAddress, 46 | isMultiproxyLoading, 47 | multiProxyList, 48 | selectMultiProxy, 49 | selectedMultiProxyAddress, 50 | setCanFindMultiProxyFromUrl, 51 | urlAddress 52 | ]) 53 | } 54 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/useWalletConnectNamespace.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react' 2 | import { useApi } from '../contexts/ApiContext' 3 | import { getWalletConnectId } from '../utils/getWalletConnectId' 4 | import { getWalletConnectNameSpace } from '../utils/getWalletConnectNameSpace' 5 | 6 | export const useGetWalletConnectNamespace = () => { 7 | const { client } = useApi() 8 | const [genesisHash, setGenesisHash] = useState('') 9 | const [isLoading, setIsLoading] = useState(true) 10 | 11 | useEffect(() => { 12 | if (!client) return 13 | 14 | client 15 | .getChainSpecData() 16 | .then((data) => setGenesisHash(data.genesisHash)) 17 | .catch(console.error) 18 | .finally(() => setIsLoading(false)) 19 | }, [client]) 20 | 21 | const walletConnectId = useMemo(() => getWalletConnectId(genesisHash), [genesisHash]) 22 | 23 | const namespace = useMemo(() => getWalletConnectNameSpace(walletConnectId), [walletConnectId]) 24 | 25 | const getAccountsWithNamespace = useCallback( 26 | (accounts: string[]) => { 27 | return accounts.map((address) => `${namespace}:${address}`) 28 | }, 29 | [namespace] 30 | ) 31 | 32 | return { walletConnectId, currentNamespace: namespace, getAccountsWithNamespace, isLoading } 33 | } 34 | -------------------------------------------------------------------------------- /packages/ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import './styles/index.css' 2 | import '@fontsource/jost/400.css' 3 | import '@fontsource/jost/500.css' 4 | import '@fontsource/jost/700.css' 5 | import { createRoot } from 'react-dom/client' 6 | import { RouterProvider } from 'react-router/dom' 7 | import { router } from './pages/routes' 8 | import { StrictMode } from 'react' 9 | 10 | const container = document.getElementById('root') 11 | const root = createRoot(container!) 12 | 13 | root.render( 14 | 15 | 16 | 17 | ) 18 | -------------------------------------------------------------------------------- /packages/ui/src/logos/amplitudeSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const chainsAmplitudeSVG = 7 | 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MDciIGhlaWdodD0iNTk3IiB2aWV3Qm94PSIwIDAgNjA3IDU5NyI+CiAgPGcgaWQ9IkFtcGxpdHVkZS1JY29uLUJsYWNrLXdiZyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTMzMDQgLTc2MDIpIj4KICAgIDxyZWN0IGlkPSJSZWN0YW5nbGVfNzIiIGRhdGEtbmFtZT0iUmVjdGFuZ2xlIDcyIiB3aWR0aD0iNjA3IiBoZWlnaHQ9IjU5NyIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMzMwNCA3NjAyKSIgZmlsbD0iIzEzMTMxMyIvPgogICAgPGcgaWQ9Ikdyb3VwXzY1IiBkYXRhLW5hbWU9Ikdyb3VwIDY1IiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgzMzM4LjY0MyA3NjMxLjYxNykiPgogICAgICA8cGF0aCBpZD0iVW5pb25fMSIgZGF0YS1uYW1lPSJVbmlvbiAxIiBkPSJNMCwyNjkuMDM2QzAsMTIwLjQ1LDEyMC40NSwwLDI2OS4wMzEsMFM1MzguMDY2LDEyMC40NSw1MzguMDY2LDI2OS4wMzYsNDE3LjYxNiw1MzguMDcsMjY5LjAzMSw1MzguMDcsMCw0MTcuNjE3LDAsMjY5LjAzNlptNTIuOCwwYzAsMTE5LjQxNyw5Ni44MSwyMTYuMjI3LDIxNi4yMjgsMjE2LjIyN3MyMTYuMjI3LTk2LjgxLDIxNi4yMjctMjE2LjIyN1MzODguNDUyLDUyLjgwOSwyNjkuMDMxLDUyLjgwOSw1Mi44LDE0OS42MTQsNTIuOCwyNjkuMDM2Wk0yNzMuOTU2LDM4Ny4yMWMtOC4xNDktMTYuMzE1LTYyLjYyNS0xMTguMDU1LTYyLjYyNS0xMTguMDU1SDQzMC43cy01OC4wOCwxMDMuODgyLTY0LjgxMywxMTguMDU1LTE3LjYyOSwzOS43NDYtNDQuMTc5LDM5Ljc1UzI4Mi4xMDYsNDAzLjUzLDI3My45NTYsMzg3LjIxWk0xMzAuNjM1LDMzMC42NDZjLTcuNjUtMjAuMjY3LTIxLjMwNy02MS40OS0yMS4zMDctNjEuNDlzMTIuNTUtMzIuNzIzLDIzLjU4Ni02MS40OWM5LjQzLTIyLjEsMTcuODY0LTMyLjYxMSwyNS45NDMtMzIuNjI3LDE5LjEtLjA0MiwyMy41MDgsMTIuMzYsMzEuMTU4LDMyLjYyN3MyMS4zMTIsNjEuNDksMjEuMzEyLDYxLjQ5LTEyLjU1MSwzMi43MjctMjMuNTg3LDYxLjQ5Yy05LjQzNCwyMi4xMDgtMTcuODY4LDMyLjYxMS0yNS45NDMsMzIuNjMyaC0uMDg1QzE0Mi42ODcsMzYzLjI3NiwxMzguMjc3LDM1MC44ODYsMTMwLjYzNSwzMzAuNjQ2Wm0yMjYuMTgxLTYxLjQ5SDIxMS4zMjdzNTguMDgtMTAzLjg4Miw2NC44MTMtMTE4LjA1NSwxNy42MjktMzkuNzQ1LDQ0LjE3OS0zOS43NDUsMzkuNiwyMy40MjksNDcuNzQ3LDM5Ljc0NSw2Mi42MjUsMTE4LjA1NSw2Mi42MjUsMTE4LjA1NVoiIGZpbGw9IiM0ZWU1OWEiLz4KICAgIDwvZz4KICA8L2c+Cjwvc3ZnPgo=' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/assetHubSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const nodesAssetHubSVG = 7 | 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI3LjIuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCA2NDAgNjQwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA2NDAgNjQwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6IzMyMUQ0Nzt9Cgkuc3Qxe2ZpbGw6I0ZGRkZGRjt9Cgkuc3Qye2ZpbGw6I0U2MDA3QTt9Cjwvc3R5bGU+CjxnPgoJPHBhdGggY2xhc3M9InN0MCIgZD0iTTYzNy4zLDMxOS4zYzAsMTc1LjItMTQyLDMxNy4zLTMxNy4zLDMxNy4zUzIuNyw0OTQuNiwyLjcsMzE5LjNTMTQ0LjgsMi4xLDMyMCwyLjFTNjM3LjMsMTQ0LjEsNjM3LjMsMzE5LjN6IgoJCS8+Cgk8cGF0aCBjbGFzcz0ic3QxIiBkPSJNNDQ0LjIsMzkyLjRoLTY3LjZsLTEyLjctMzFoLTg1LjhsLTEyLjcsMzFoLTY3LjZsODAuOS0xODQuM2g4NC41TDQ0NC4yLDM5Mi40eiBNMzIxLjEsMjU2bC0yMi40LDU1aDQ0LjcKCQlMMzIxLjEsMjU2eiIvPgoJPGNpcmNsZSBjbGFzcz0ic3QyIiBjeD0iMzIxIiBjeT0iMTIyLjEiIHI9IjQ2LjkiLz4KCTxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjMyMSIgY3k9IjUxNy4xIiByPSI0Ni45Ii8+Cgk8Y2lyY2xlIGNsYXNzPSJzdDIiIGN4PSIxNDcuOCIgY3k9IjIxNiIgcj0iNDYuOSIvPgoJPGNpcmNsZSBjbGFzcz0ic3QyIiBjeD0iNDk0LjMiIGN5PSIyMTYiIHI9IjQ2LjkiLz4KCTxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjE0Ny44IiBjeT0iNDI0LjgiIHI9IjQ2LjkiLz4KCTxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjQ5NC4zIiBjeT0iNDI0LjgiIHI9IjQ2LjkiLz4KPC9nPgo8L3N2Zz4K' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/bifrostSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const nodesBifrostSVG = 7 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0wIDUwQzAgMjIuMzg1OCAyMi4zODU4IDAgNTAgMFYwQzc3LjYxNDIgMCAxMDAgMjIuMzg1OCAxMDAgNTBWNTBDMTAwIDc3LjYxNDIgNzcuNjE0MiAxMDAgNTAgMTAwVjEwMEMyMi4zODU4IDEwMCAwIDc3LjYxNDIgMCA1MFY1MFoiIGZpbGw9ImJsYWNrIi8+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiPgo8cGF0aCBkPSJNNTAgNzIuMzY4NEgxOS43MzY4TDY1LjEzMTYgMjYuOTczNkg4MC4yNjMxTDUwIDcyLjM2ODRaIiBmaWxsPSJ1cmwoI3BhaW50MF9saW5lYXIpIi8+CjwvZz4KPGRlZnM+CjxsaW5lYXJHcmFkaWVudCBpZD0icGFpbnQwX2xpbmVhciIgeDE9IjUwIiB5MT0iMjYuOTczNiIgeDI9IjUwIiB5Mj0iNzIuMzY4NCIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiPgo8c3RvcCBzdG9wLWNvbG9yPSIjN0FFRENGIi8+CjxzdG9wIG9mZnNldD0iMC4yMDEzMzMiIHN0b3AtY29sb3I9IiM2OENFRkEiLz4KPHN0b3Agb2Zmc2V0PSIwLjQwMzI0NCIgc3RvcC1jb2xvcj0iIzY4OUNGOCIvPgo8c3RvcCBvZmZzZXQ9IjAuNjAyMDc2IiBzdG9wLWNvbG9yPSIjQUM1N0MwIi8+CjxzdG9wIG9mZnNldD0iMC44MDE4NjciIHN0b3AtY29sb3I9IiNFNjU2NTkiLz4KPHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjRjJDMjQxIi8+CjwvbGluZWFyR3JhZGllbnQ+CjxjbGlwUGF0aCBpZD0iY2xpcDAiPgo8cmVjdCB3aWR0aD0iNjAuNTI2MyIgaGVpZ2h0PSI0NS4zOTQ3IiBmaWxsPSJ3aGl0ZSIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTkuNzM2OCAyNi45NzM2KSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo=' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/hydrationSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const hydrationSVG = 7 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzQ5IiBoZWlnaHQ9IjM2MSIgdmlld0JveD0iMCAwIDM0OSAzNjEiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxwYXRoIGQ9Ik0zMjQuODU2IDIwNS4wMzlDMzI4Ljk4NyAyMDAuOTEzIDMzNC4zMDQgMTk1LjYwMyAzMzkuNTgzIDE5MC4zMzZDMzUxLjM1OSAxNzguNTggMzUxLjM2NSAxNTkuNTEzIDMzOS41OSAxNDcuNzUyTDMyNy4xMTUgMTM1LjI5MkMyODIuNDUyIDE3OS45MDIgMjE1LjYxNiAxODguODY1IDE2Mi4wMzUgMTYyLjE4N0MxOTguMjQ5IDE2OC44NzUgMjM1Ljc3OSAxNjMuNjY1IDI2OS4wMzUgMTQ1LjgwN0MyOTEuMjgyIDEzMy44NTggMjk1LjUwNyAxMDMuNzIyIDI3Ny42NDYgODUuODgzTDIwNC4wMDIgMTIuMzI3M0MxODcuNTQ2IC00LjEwOTExIDE2MC44NjggLTQuMTA5MTEgMTQ0LjQxOCAxMi4zMjczTDY1LjAzOTIgOTEuNjExMUMxMTcuMzE2IDY5LjQyNzggMTgwLjExNCA3OS43MTIzIDIyMi42MyAxMjIuNDcxQzE1Ny43NjcgOTEuMDI1MiA3Ny40MTQxIDEwMi4xODIgMjMuNTU4NiAxNTUuOTY3QzE5LjQyNzQgMTYwLjA5MyAxNC4xMDQzIDE2NS40MSA4LjgyNDgzIDE3MC42ODNDLTIuOTQ0NzMgMTgyLjQ0NSAtMi45Mzg0OSAyMDEuNDk5IDguODI0ODMgMjEzLjI1NEwyMS4yOTMzIDIyNS43MDhDNjUuOTU2NSAxODEuMDk4IDEzMi43OTIgMTcyLjEzNSAxODYuMzczIDE5OC44MTNDMTUwLjE1OSAxOTIuMTI1IDExMi42MjkgMTk3LjMzNSA3OS4zNzM2IDIxNS4xOTNDNTcuMTI2MiAyMjcuMTQyIDUyLjkwMTQgMjU3LjI3OCA3MC43NjE3IDI3NS4xMTdMMTQ0LjQwNiAzNDguNjczQzE2MC44NjIgMzY1LjEwOSAxODcuNTQgMzY1LjEwOSAyMDMuOTkgMzQ4LjY3M0wyODMuMzY5IDI2OS4zODlDMjMxLjA5MiAyOTEuNTcyIDE2OC4yOTQgMjgxLjI4OCAxMjUuNzcyIDIzOC41MjlDMTkwLjYzNSAyNjkuOTc1IDI3MC45ODggMjU4LjgxOCAzMjQuODQzIDIwNS4wMzNMMzI0Ljg1NiAyMDUuMDM5WiIgZmlsbD0iI0U1M0U3NiIvPgo8L3N2Zz4=' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/joystreamSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const nodesJoystreamSVG = 7 | 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNDAgMjQwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQwMzhmZjt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPkljb24tYmFzaWMtMWJnPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMTMiIGRhdGEtbmFtZT0iTGF5ZXIgMTMiPjxyZWN0IGNsYXNzPSJjbHMtMSIgd2lkdGg9IjI0MCIgaGVpZ2h0PSIyNDAiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMzUuMjgsNDkuNzNsMTIuNywwLS4xNSw1OS42N2E1Nyw1NywwLDAsMS0xNC40OSwzNy44Niw2Ny43Niw2Ny43NiwwLDAsMCwxLjcyLTE1WiIvPjxwYXRoIGNsYXNzPSJjbHMtMiIgZD0iTTk0LjI4LDE1My43OHYwYTM0LjE5LDM0LjE5LDAsMCwxLTI2LjE1LDEyLjYxTDcyLDE1My43M1oiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMDIsMTMwLjk0djEuMjhhMzQsMzQsMCwwLDEtMiwxMS40MWwtMjUtLjA2LDMuODMtMTIuNjlaIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMTU4LjE0LDQ5Ljc4bDEyLjcsMC0uMDksMzYuODRhNTcsNTcsMCwwLDEtMTQuNDksMzcuODYsNjcuNzYsNjcuNzYsMCwwLDAsMS43Mi0xNVoiLz48cGF0aCBjbGFzcz0iY2xzLTIiIGQ9Ik0xMjUuMTEsNDkuNjlsLS4yMSw4Mi41OWE1Ny4yMiw1Ny4yMiwwLDAsMS01Ny4zMiw1N0g2MS4yM2wzLjgzLTEyLjY5aDIuNTZhNDQuNSw0NC41LDAsMCwwLDQ0LjU3LTQ0LjM1bC4yMi04Mi41OFoiLz48L2c+PC9nPjwvc3ZnPg==' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/khalaSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const nodesKhalaSVG = 5 | 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KIDxyZWN0IHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9ImJsYWNrIi8+CiA8ZyBpZD0iTGF5ZXJfMSI+CiAgPHRpdGxlPkxheWVyIDE8L3RpdGxlPgogIDxnIHRyYW5zZm9ybT0ibWF0cml4KDUuMjExMDEgMCAwIDUuMjExMDEgLTEwNTgwLjYgLTExNzE5LjcpIiBpZD0ic3ZnXzEwMCI+CiAgIDxnIGlkPSJzdmdfNiI+CiAgICA8cmVjdCBpZD0ic3ZnXzEiIGZpbGw9IiMwM0ZGRkYiIGhlaWdodD0iNDkuNTA0IiB3aWR0aD0iMTIuOTI1IiB5PSIyMjczLjQwMTQiIHg9IjIwNTUuNjQ2MzMiLz4KICAgIDxyZWN0IGlkPSJzdmdfMiIgZmlsbD0iIzAzRkZGRiIgaGVpZ2h0PSIxMy4wMjQiIHdpZHRoPSIxMi45MzkiIHk9IjIyODYuOTk4NCIgeD0iMjA2OC40NzMzMyIvPgogICAgPHJlY3QgaWQ9InN2Z18zIiBmaWxsPSIjMDNGRkZGIiBoZWlnaHQ9IjEzLjAyNCIgd2lkdGg9IjEyLjkzOSIgeT0iMjMwMC4wNTE0IiB4PSIyMDgxLjI0MDMzIi8+CiAgICA8cmVjdCBpZD0ic3ZnXzQiIGZpbGw9IiMwM0ZGRkYiIGhlaWdodD0iMTMuMDI0IiB3aWR0aD0iMTIuOTM5IiB5PSIyMzA5LjY2MTQiIHg9IjIwOTAuNTMxMzMiLz4KICAgIDxyZWN0IGlkPSJzdmdfNSIgZmlsbD0iIzAzRkZGRiIgaGVpZ2h0PSIxMy4wMjQiIHdpZHRoPSIyMS44NjMiIHk9IjIyNzMuOTM1NCIgeD0iMjA4MS4wOTQzMyIvPgogICA8L2c+CiAgPC9nPgogPC9nPgo8L3N2Zz4=' 6 | -------------------------------------------------------------------------------- /packages/ui/src/logos/kusamaSVG .ts: -------------------------------------------------------------------------------- 1 | export const chainsKusamaSVG = 2 | 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0NDEgNDQxIj48ZGVmcz48c3R5bGU+LmNscy0xe3N0cm9rZTojMDAwO3N0cm9rZS1taXRlcmxpbWl0OjEwO30uY2xzLTJ7ZmlsbDojZmZmO308L3N0eWxlPjwvZGVmcz48dGl0bGU+a3VzYW1hLWtzbS1sb2dvPC90aXRsZT48ZyBpZD0iTGF5ZXJfMiIgZGF0YS1uYW1lPSJMYXllciAyIj48ZyBpZD0iTGF5ZXJfMS0yIiBkYXRhLW5hbWU9IkxheWVyIDEiPjxyZWN0IGNsYXNzPSJjbHMtMSIgeD0iMC41IiB5PSIwLjUiIHdpZHRoPSI0NDAiIGhlaWdodD0iNDQwIi8+PHBhdGggY2xhc3M9ImNscy0yIiBkPSJNMzczLjYsMTI3LjRjLTUuMi00LjEtMTEuNC05LjctMjIuNy0xMS4xLTEwLjYtMS40LTIxLjQsNS43LTI4LjcsMTAuNHMtMjEuMSwxOC41LTI2LjgsMjIuNy0yMC4zLDguMS00My44LDIyLjItMTE1LjcsNzMuMy0xMTUuNyw3My4zbDI0LC4zLTEwNyw1NS4xSDYzLjZMNDguMiwzMTJzMTMuNiwzLjYsMjUtMy42djMuM3MxMjcuNC01MC4yLDE1Mi0zNy4ybC0xNSw0LjRjMS4zLDAsMjUuNSwxLjYsMjUuNSwxLjZhMzQuMzQsMzQuMzQsMCwwLDAsMTUuNCwyNC44YzE0LjYsOS42LDE0LjksMTQuOSwxNC45LDE0LjlzLTcuNiwzLjEtNy42LDdjMCwwLDExLjItMy40LDIxLjYtMy4xYTgyLjY0LDgyLjY0LDAsMCwxLDE5LjUsMy4xcy0uOC00LjItMTAuOS03LTIwLjEtMTMuOC0yNS0xOS44YTI4LDI4LDAsMCwxLTQuMS0yNy40YzMuNS05LjEsMTUuNy0xNC4xLDQwLjktMjcuMSwyOS43LTE1LjQsMzYuNS0yNi44LDQwLjctMzUuN3MxMC40LTI2LjYsMTMuOS0zNC45YzQuNC0xMC43LDkuOC0xNi40LDE0LjMtMTkuOHMyNC41LTEwLjksMjQuNS0xMC45UzM3OC41LDEzMS4zLDM3My42LDEyNy40WiIvPjwvZz48L2c+PC9zdmc+' 3 | -------------------------------------------------------------------------------- /packages/ui/src/logos/localSVG.ts: -------------------------------------------------------------------------------- 1 | export const localSVG = 2 | 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIgdmlld0JveD0iMTUgMTUgMTQwIDE0MCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMTcwIDE3MDsiIHhtbDpzcGFjZT0icHJlc2VydmUiPgo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPi5iZ3tmaWxsOiByZ2JhKDE2MCwgMTYwLCAxNjAsIDAuMjUpfTwvc3R5bGU+CjxnPjxjaXJjbGUgY2xhc3M9ImJnIiBjeD0iODUiIGN5PSI4NSIgcj0iNzAiLz48L2c+Cjwvc3ZnPgo=' 3 | -------------------------------------------------------------------------------- /packages/ui/src/logos/paseoSVG.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /packages/ui/src/logos/pendulumSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // Do not edit. Auto-generated via node scripts/imgConvert.mjs 5 | 6 | export const chainsPendulumSVG = 7 | 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDI3LjAuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPgo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkxheWVyXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IgoJIHZpZXdCb3g9IjAgMCAzODQgMzg0IiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAzODQgMzg0OyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CjxzdHlsZSB0eXBlPSJ0ZXh0L2NzcyI+Cgkuc3Qwe2ZpbGw6I0ZGRkZGRjt9Cgkuc3Qxe2ZpbGw6I0RBRTlFQjt9Cgkuc3Qye2ZpbGw6IzMyMjUzRTt9Cjwvc3R5bGU+CjxnPgoJPGc+CgkJPHBhdGggY2xhc3M9InN0MiIgZD0iTTE5My44LDY0Yy01Mi0wLjktOTYuNSw0MS42LTk3LjcsOTMuNmMtMC45LDM1LjMsMTcuMyw2Ni40LDQ0LjksODMuOGMxLjMsMC44LDMuMSwwLjMsMy44LTEuMWwyMS42LTQzLjEKCQkJYzMuMi02LjMsMi43LTEzLjktMS4yLTE5LjhjLTMuMy01LjEtNS4yLTExLjItNS4xLTE3LjdjMC4yLTE2LjgsMTMuNS0zMC43LDMwLjItMzEuNmMxOS4zLTEsMzUuMiwxNS4xLDMzLjcsMzQuNQoJCQljLTEuMiwxNS42LTEzLjgsMjguMS0yOS4zLDI5LjRjLTAuMSwwLTAuMiwwLTAuMywwYy03LjEsMC41LTEzLjQsNC42LTE2LjYsMTFMMTU2LjIsMjQ2Yy0wLjcsMS40LDAsMy4xLDEuNCwzLjYKCQkJYzEwLjksNC4yLDIyLjcsNi40LDM1LjEsNi40YzUyLjUtMC4zLDk1LjQtNDMuNCw5NS40LTk1LjlDMjg4LjIsMTA3LjYsMjQ2LjEsNjQuOSwxOTMuOCw2NHoiLz4KCQk8Y2lyY2xlIGNsYXNzPSJzdDIiIGN4PSIxMjguMSIgY3k9IjI4OCIgcj0iMzIiLz4KCTwvZz4KPC9nPgo8L3N2Zz4K' 8 | -------------------------------------------------------------------------------- /packages/ui/src/logos/phalaSVG.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | export const phalaSVG = 5 | 'data:image/svg+xml;base64,PHN2ZyBpZD0ibGF5ZXJfMSIgZGF0YS1uYW1lPSJsYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MDAgNTAwIj4KICA8ZGVmcz4KICAgIDxzdHlsZT4KICAgICAgLmNscy0xIHsKICAgICAgZmlsbDogI2QxZmY1MjsKICAgICAgfQogICAgPC9zdHlsZT4KICA8L2RlZnM+CiAgPHRpdGxlPmJvYXJkIDE0PC90aXRsZT4KICA8Y2lyY2xlIGN4PSIyNTAiIGN5PSIyNTAiIHI9IjI1MCIgLz4KICA8cmVjdCBjbGFzcz0iY2xzLTEiIHg9IjE4MCIgeT0iMjU5Ljg1IiB3aWR0aD0iMTcxLjUiIGhlaWdodD0iNTcuMTciIC8+CiAgPHJlY3QgY2xhc3M9ImNscy0xIiB4PSIzNTEuNTEiIHk9IjE3NC4xIiB3aWR0aD0iNTcuMTciIGhlaWdodD0iODUuNzYiIC8+CiAgPHBvbHlnb24gY2xhc3M9ImNscy0xIgogICAgICAgICAgIHBvaW50cz0iMTgwIDMxNy4wMyAxMjIuODIgMzE3LjAzIDEyMi44MiAzMzEuMzIgMTIyLjgyIDM0NS42MiAxMjIuODIgNDAyLjc4IDE3OS45OSA0MDIuNzggMTc5Ljk5IDM0NS42MiAxODAgMzQ1LjYyIDE4MCAzMTcuMDMiIC8+CiAgPHBvbHlnb24gY2xhc3M9ImNscy0xIgogICAgICAgICAgIHBvaW50cz0iMzUxLjUxIDExNi45MyAxODAgMTE2LjkzIDE3MS43OCAxMTYuOTMgMTIyLjgyIDExNi45MyAxMjIuODIgMjMxLjI3IDEyMi44MiAyNDkuNTIgMTIyLjgyIDI1OS44NiAxODAgMjU5Ljg2IDE4MCAyNDkuNTIgMTgwIDIzMS4yNyAxODAgMTc0LjEgMzUxLjUxIDE3NC4xIDM1MS41MSAxMTYuOTMiIC8+CiAgPHJlY3QgY2xhc3M9ImNscy0xIiB4PSIxODAiIHk9IjI1OS44NSIgd2lkdGg9IjE3MS41IiBoZWlnaHQ9IjU3LjE3IiAvPgo8L3N2Zz4K' 6 | -------------------------------------------------------------------------------- /packages/ui/src/logos/usdc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ui/src/logos/usdt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/ui/src/logos/walletConnectSVG.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /packages/ui/src/pages/Creation/NameSelection.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Box, styled } from '@mui/material' 2 | import React, { useCallback } from 'react' 3 | import { TextField } from '../../components/library' 4 | 5 | interface Props { 6 | className?: string 7 | name?: string 8 | setName: React.Dispatch> 9 | originalName?: string 10 | } 11 | 12 | const NameSelection = ({ className, name, setName, originalName = '' }: Props) => { 13 | const handleChange = useCallback( 14 | (event: React.ChangeEvent) => { 15 | const value = event.target.value 16 | 17 | setName(value) 18 | }, 19 | [setName] 20 | ) 21 | 22 | return ( 23 | 24 | 33 | {!!originalName && ( 34 | 35 | The address book contains the name "{originalName}" for this multisig 36 | already. Any change will override it. 37 | 38 | )} 39 | 40 | ) 41 | } 42 | 43 | const AlertStyled = styled(Alert)` 44 | margin-top: 1rem; 45 | ` 46 | 47 | export default NameSelection 48 | -------------------------------------------------------------------------------- /packages/ui/src/pages/Creation/WithProxySelection.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Checkbox, FormControlLabel, Tooltip, styled } from '@mui/material' 2 | import { HiOutlineInformationCircle } from 'react-icons/hi2' 3 | 4 | interface Props { 5 | setWithProxy: (withProxy: boolean) => void 6 | withProxy: boolean 7 | className?: string 8 | } 9 | const WithProxySelection = ({ setWithProxy, withProxy, className }: Props) => { 10 | return ( 11 | 12 | 13 | Pure proxy 14 | 15 | 18 | Use a pure proxy (not cross-chain{' '} 19 | 24 | see here 25 | 26 | ) 27 | 28 | } 29 | control={ 30 | setWithProxy(!withProxy)} 33 | // @ts-expect-error 34 | inputProps={{ 'data-cy': 'checkbox-use-pure-proxy' }} 35 | /> 36 | } 37 | /> 38 | 39 | ) 40 | } 41 | 42 | const InfoBox = ({ className = '' }: { className?: string }) => ( 43 | 47 | Using a pure proxy has advantages and drawbacks see when it makes sense to use it{' '} 48 | 53 | in the docs 54 | 55 | . 56 | 57 | } 58 | > 59 | 60 | 61 | 62 | 63 | ) 64 | 65 | const TitleBoxStyled = styled(Box)` 66 | font-size: 1.125rem; 67 | font-weight: 500; 68 | color: ${({ theme }) => theme.custom.text.primary}; 69 | ` 70 | 71 | export default styled(WithProxySelection)` 72 | margin-top: 1rem; 73 | ` 74 | -------------------------------------------------------------------------------- /packages/ui/src/pages/Overview/CustomNode.tsx: -------------------------------------------------------------------------------- 1 | import { styled } from '@mui/material/styles' 2 | import { Handle, Position } from 'reactflow' 3 | import { Box } from '@mui/material' 4 | import AccountDisplay from '../../components/AccountDisplay/AccountDisplay' 5 | import { AccountBadge } from '../../types' 6 | 7 | export type NodeData = { 8 | address: string 9 | badge?: AccountBadge 10 | handle: 'right' | 'left' | 'both' 11 | } 12 | 13 | interface Props { 14 | data: NodeData 15 | className?: string 16 | } 17 | 18 | const CustomNode = ({ data, className = '' }: Props) => { 19 | const { address, badge, handle } = data 20 | 21 | return ( 22 | 23 | {(handle === 'both' || handle === 'right') && ( 24 | 29 | )} 30 | {(handle === 'both' || handle === 'left') && ( 31 | 36 | )} 37 | 42 | 43 | ) 44 | } 45 | 46 | export default styled(CustomNode)` 47 | padding-right: 1rem; 48 | padding-left: 1rem; 49 | max-width: 15rem; 50 | ` 51 | -------------------------------------------------------------------------------- /packages/ui/src/pages/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Creation } from './Creation' 2 | export { default as Home } from './Home/Home' 3 | export { default as About } from './About' 4 | export { default as Overview } from './Overview' 5 | export { default as Settings } from './Settings/Settings' 6 | -------------------------------------------------------------------------------- /packages/ui/src/pages/multisigHelpers.ts: -------------------------------------------------------------------------------- 1 | export function renderMultisigHeading(hasSeveralMultisigs: boolean | undefined): string { 2 | return hasSeveralMultisigs ? 'Multisigs' : 'Multisig' 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/pages/routes.tsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter } from 'react-router' 2 | import { About, Creation, Home, Overview, Settings } from './index' 3 | import React from 'react' 4 | import App from '../App' 5 | import ErrorFallback from '../components/ErrorFallback/ErrorFallback' 6 | import Import from './Import/Import' 7 | 8 | interface Route { 9 | path: string 10 | element: React.ReactNode 11 | name: string 12 | isDisplayWhenNoWallet: boolean 13 | } 14 | 15 | export const MENU_ROUTES: Route[] = [ 16 | { 17 | path: '/', 18 | element: , 19 | name: 'Home', 20 | isDisplayWhenNoWallet: true 21 | }, 22 | { 23 | path: 'create', 24 | element: , 25 | name: 'New Multisig', 26 | isDisplayWhenNoWallet: false 27 | }, 28 | { 29 | path: 'settings', 30 | element: , 31 | name: 'Settings', 32 | isDisplayWhenNoWallet: true 33 | }, 34 | { 35 | path: 'overview', 36 | element: , 37 | name: 'Overview', 38 | isDisplayWhenNoWallet: true 39 | }, 40 | { 41 | path: 'about', 42 | element: , 43 | name: 'About', 44 | isDisplayWhenNoWallet: true 45 | } 46 | ] 47 | 48 | export const HIDDEN_ROUTES: Route[] = [ 49 | { 50 | path: 'migrate', 51 | element: , 52 | name: 'Import', 53 | isDisplayWhenNoWallet: true 54 | }, 55 | { 56 | path: 'import', 57 | element: , 58 | name: 'Import', 59 | isDisplayWhenNoWallet: true 60 | } 61 | ] 62 | 63 | export const router = createBrowserRouter([ 64 | { 65 | path: '/', 66 | element: , 67 | errorElement: , 68 | children: MENU_ROUTES.concat(HIDDEN_ROUTES) 69 | } 70 | ]) 71 | -------------------------------------------------------------------------------- /packages/ui/src/queries/multisigById.graphql: -------------------------------------------------------------------------------- 1 | query MultisigById($id: String!) { 2 | accounts(where: { id_eq: $id, isMultisig_eq: true }) { 3 | id 4 | threshold 5 | signatories(limit: 50) { 6 | id 7 | signatory { 8 | id 9 | pubKey 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/queries/multisigsAndPureByAccount.graphql: -------------------------------------------------------------------------------- 1 | query MultisigsAndPureByAccount($accountIds: [String!], $watchedAccountIds: [String!]) { 2 | accounts( 3 | where: { 4 | AND: [ 5 | { 6 | OR: [ 7 | { id_in: $watchedAccountIds } 8 | { signatories_some: { signatory: { id_in: $accountIds } } } 9 | { signatories_some: { signatory: { id_in: $watchedAccountIds } } } 10 | ] 11 | } 12 | { OR: [{ isMultisig_eq: true }, { isPureProxy_eq: true }] } 13 | ] 14 | } 15 | ) { 16 | # either it's a multisig or proxy, with direct address match from a watch account 17 | # or one of accounts or watch accounts is a signatory 18 | id 19 | pubKey 20 | isMultisig 21 | isPureProxy 22 | threshold 23 | signatories { 24 | id 25 | signatory { 26 | id 27 | pubKey 28 | } 29 | } 30 | delegateeFor { 31 | id 32 | type 33 | delegator { 34 | id 35 | pubKey 36 | isPureProxy 37 | } 38 | delegatee { 39 | id 40 | pubKey 41 | isPureProxy 42 | } 43 | } 44 | delegatorFor { 45 | id 46 | type 47 | delegatee { 48 | id 49 | pubKey 50 | isMultisig 51 | threshold 52 | signatories { 53 | id 54 | signatory { 55 | id 56 | pubKey 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/ui/src/styles/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | @keyframes App-logo-spin { 17 | from { 18 | transform: rotate(0deg); 19 | } 20 | 21 | to { 22 | transform: rotate(360deg); 23 | } 24 | } 25 | 26 | ul { 27 | padding-left: 20px; 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/styles/index.css: -------------------------------------------------------------------------------- 1 | code { 2 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; 3 | } 4 | 5 | #root { 6 | min-height: 100vh; 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/utils/arrayUtils.ts: -------------------------------------------------------------------------------- 1 | export const getIntersection = (array1 = [] as string[], array2 = [] as string[]) => 2 | array1.filter((v1) => array2.includes(v1)) 3 | export const getDifference = (array1 = [] as string[], array2 = [] as string[]) => 4 | array1.filter((v1) => !array2.includes(v1)) 5 | 6 | export const isEmptyArray = (array: any[]) => Array.isArray(array) && array.length === 0 7 | -------------------------------------------------------------------------------- /packages/ui/src/utils/bnUtils.ts: -------------------------------------------------------------------------------- 1 | const DEFAULT_BITLENGTH = 32 as BitLength 2 | 3 | export type BitLength = 8 | 16 | 32 | 64 | 128 | 256 4 | 5 | export function getGlobalMaxValue(bitLength?: number): bigint { 6 | return 2n ** BigInt(bitLength || DEFAULT_BITLENGTH) - 1n 7 | } 8 | 9 | export function isValidNumber( 10 | bn: bigint, 11 | bitLength: BitLength, 12 | isSigned: boolean, 13 | isZeroable: boolean, 14 | maxValue?: bigint | null 15 | ): boolean { 16 | if ( 17 | // cannot be negative 18 | (!isSigned && bn < 0n) || 19 | // cannot be > than allowed max 20 | bn > getGlobalMaxValue(bitLength) || 21 | // check if 0 and it should be a value 22 | (!isZeroable && bn === 0n) || 23 | // check that the bitlengths fit 24 | bn.toString(2).length > (bitLength || DEFAULT_BITLENGTH) || 25 | // cannot be > max (if specified) 26 | (maxValue && maxValue > 0n && bn > maxValue) 27 | ) { 28 | return false 29 | } 30 | 31 | return true 32 | } 33 | 34 | export function inputToBigInt(chainDecimals: number, input: string, isSigned = false) { 35 | const isDecimalValue = input.match(/^(\d+)\.(\d+)$/) 36 | let result 37 | 38 | if (isDecimalValue) { 39 | const div = BigInt(input.replace(/\.\d*$/, '')) 40 | const modString = input.replace(/^\d+\./, '').substring(0, chainDecimals) 41 | const mod = BigInt(modString) 42 | 43 | result = 44 | div * 10n ** BigInt(chainDecimals) + mod * 10n ** BigInt(chainDecimals - modString.length) 45 | } else { 46 | result = 47 | BigInt(input.replace(/[^\d]/g, '')) * 48 | 10n ** BigInt(chainDecimals) * 49 | (isSigned && input.startsWith('-') ? -1n : 1n) 50 | } 51 | 52 | return result 53 | } 54 | 55 | export const bigIntMax = (a: bigint, b: bigint) => (a > b ? a : b) 56 | -------------------------------------------------------------------------------- /packages/ui/src/utils/camelcasetoString.ts: -------------------------------------------------------------------------------- 1 | export const camelcaseToString = (proxy: string) => 2 | proxy.replace(/[A-Z]/g, function (match, index) { 3 | return index === 0 ? match : ` ${match}` 4 | }) 5 | -------------------------------------------------------------------------------- /packages/ui/src/utils/copyToClipboard.ts: -------------------------------------------------------------------------------- 1 | export const copyTextToClipboard = async (text: string) => { 2 | if ('clipboard' in navigator) { 3 | return await navigator.clipboard.writeText(text) 4 | } else { 5 | return document.execCommand('copy', true, text) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | export const debounce = any>(func: F, waitForMs: number) => { 2 | let timeout: any = 0 3 | 4 | const debounced = (...args: any[]) => { 5 | clearTimeout(timeout) 6 | timeout = setTimeout(() => func(...args), waitForMs) 7 | } 8 | 9 | return debounced as unknown as (...args: Parameters) => ReturnType 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/utils/encodeAccounts.ts: -------------------------------------------------------------------------------- 1 | import { encodesubstrateAddress } from './encodeSubstrateAddress' 2 | import { InjectedPolkadotAccount } from 'polkadot-api/pjs-signer' 3 | 4 | export function encodeAccounts(accounts: string[], ss58Format: number): string[] 5 | export function encodeAccounts( 6 | accounts: InjectedPolkadotAccount[], 7 | ss58Format: number 8 | ): InjectedPolkadotAccount[] 9 | export function encodeAccounts(accounts: unknown[], ss58Format: number): unknown[] { 10 | if (!accounts || accounts.length === 0) return [] 11 | 12 | if (typeof accounts[0] === 'string') { 13 | return (accounts as string[]) 14 | .map((account) => encodesubstrateAddress(account, ss58Format)) 15 | .filter(Boolean) as string[] 16 | } 17 | 18 | return (accounts as InjectedPolkadotAccount[]) 19 | .map((account) => { 20 | const addressToEncode = account.address 21 | 22 | const encodedAddress = encodesubstrateAddress(addressToEncode, ss58Format) 23 | 24 | if (!encodedAddress) { 25 | return null 26 | } 27 | 28 | return { 29 | ...account, 30 | address: encodedAddress 31 | } as InjectedPolkadotAccount 32 | }) 33 | .filter(Boolean) as InjectedPolkadotAccount[] 34 | } 35 | -------------------------------------------------------------------------------- /packages/ui/src/utils/encodeSubstrateAddress.ts: -------------------------------------------------------------------------------- 1 | import { encodeAddress } from '@polkadot/util-crypto' 2 | 3 | export const encodesubstrateAddress = (address: string | Uint8Array, ss58Format: number) => { 4 | // this looks like an ethereum account, do not encode 5 | if (typeof address === 'string' && address.startsWith('0x') && address.length === 42) { 6 | // console.log('Ethereum address detected, not encoding', address) 7 | return address.toLowerCase() 8 | } 9 | 10 | try { 11 | return encodeAddress(address, ss58Format) 12 | } catch (e) { 13 | console.error(`Error encoding the address ${address}, skipping`, e) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/ui/src/utils/ethereumChains.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2023 @polkadot/apps-config authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | // The list of Ethereum networks, for these the UI will default to Ethereum-only accounts 5 | 6 | export const ethereumChains = [ 7 | 'mythical-devnet', 8 | 'frontier-template', 9 | 'jaz', 10 | 'moonbase', 11 | 'moonbeam', 12 | 'moonriver', 13 | 'moonsama', 14 | 'moonshadow', 15 | 'altbeacon', 16 | 'alt-producer', 17 | 'flash-layer', 18 | 'armonia-eva', 19 | 'armonia-wall-e', 20 | 'root', 21 | 'Darwinia2', 22 | 'Crab2', 23 | 'Pangolin2', 24 | 'Pangoro2', 25 | 'thebifrost-dev', 26 | 'thebifrost-testnet', 27 | 'thebifrost-mainnet', 28 | 'dracones', 29 | 'dracones-dwarf', 30 | 'subspace-evm-domain', 31 | 'ferrum-parachain', 32 | 'quantum-portal-network-parachain', 33 | 'peerplays' 34 | ] 35 | -------------------------------------------------------------------------------- /packages/ui/src/utils/formatBnBalance.ts: -------------------------------------------------------------------------------- 1 | interface Options { 2 | numberAfterComma?: number 3 | withThousandDelimiter?: boolean 4 | tokenSymbol?: string 5 | } 6 | 7 | function removeTrailingZeros(value: string) { 8 | return value.replace(/0+$/, '') 9 | } 10 | 11 | function countLeadingZeros(numberString: string): number { 12 | const match = numberString.match(/^0+/) 13 | return match ? match[0].length : 0 14 | } 15 | 16 | export const formatBigIntBalance = ( 17 | value: bigint | string, 18 | tokenDecimals = 0, 19 | { numberAfterComma = 4, withThousandDelimiter = true, tokenSymbol }: Options 20 | ): string => { 21 | const valueString = value.toString() 22 | 23 | let suffix = '' 24 | let prefix = '' 25 | 26 | if (valueString.length > tokenDecimals) { 27 | suffix = valueString.slice(-tokenDecimals) 28 | prefix = valueString.slice(0, valueString.length - tokenDecimals) 29 | } else { 30 | prefix = '0' 31 | suffix = valueString.padStart(tokenDecimals, '0') 32 | } 33 | 34 | suffix = removeTrailingZeros(suffix) 35 | let comma = '.' 36 | const countLeadingZerosSuffix = countLeadingZeros(suffix) 37 | const numberAfterCommaLtZero = numberAfterComma && numberAfterComma < 0 38 | 39 | if (numberAfterCommaLtZero || numberAfterComma === 0 || suffix === '') { 40 | comma = '' 41 | suffix = '' 42 | } else if (countLeadingZerosSuffix > 0) { 43 | suffix = suffix.slice(0, numberAfterComma + countLeadingZerosSuffix) 44 | } else if (numberAfterComma && numberAfterComma > 0) { 45 | suffix = suffix.slice(0, numberAfterComma) 46 | } 47 | 48 | if (withThousandDelimiter) { 49 | prefix = prefix.replace(/\B(?=(\d{3})+(?!\d))/g, ' ') 50 | } 51 | 52 | const unit = tokenSymbol ? ` ${tokenSymbol}` : '' 53 | 54 | return `${prefix}${comma}${suffix}${unit}` 55 | } 56 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getAllNetworkWalletConnectNameSpaces.ts: -------------------------------------------------------------------------------- 1 | import { networkList } from '../constants' 2 | import { getWalletConnectId } from './getWalletConnectId' 3 | import { getWalletConnectNameSpace } from './getWalletConnectNameSpace' 4 | 5 | export const getAllNetworkWalletConnectNameSpaces = (): string[] => { 6 | return ( 7 | Object.values(networkList) 8 | .map((network) => network.genesisHash) 9 | .filter(Boolean) as string[] 10 | ).map((genesisHash) => { 11 | const id = getWalletConnectId(genesisHash) 12 | return getWalletConnectNameSpace(id) 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getApproveAsMultiTx.ts: -------------------------------------------------------------------------------- 1 | import { FixedSizeBinary, HexString } from 'polkadot-api' 2 | import { IApiContext } from '../contexts/ApiContext' 3 | import { ApiDescriptors, MultisigStorageInfo } from '../types' 4 | 5 | interface Params { 6 | api: IApiContext['api'] 7 | threshold: number 8 | otherSignatories: string[] 9 | when?: MultisigStorageInfo['when'] 10 | hash?: HexString 11 | } 12 | 13 | export const getApproveAsMultiTx = ({ api, threshold, otherSignatories, hash, when }: Params) => { 14 | if (!hash || !api) return 15 | 16 | return api.tx.Multisig.approve_as_multi({ 17 | threshold, 18 | other_signatories: otherSignatories, 19 | maybe_timepoint: when, 20 | call_hash: FixedSizeBinary.fromHex(hash), 21 | max_weight: { 22 | ref_time: 0n, 23 | proof_size: 0n 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getAsMultiTx.ts: -------------------------------------------------------------------------------- 1 | import { ApiDescriptors, MultisigStorageInfo, Weight } from '../types' 2 | import { Binary, HexString, Transaction } from 'polkadot-api' 3 | import { IApiContext } from '../contexts/ApiContext' 4 | 5 | interface Params { 6 | api: IApiContext['api'] 7 | threshold: number 8 | otherSignatories: string[] 9 | tx?: Transaction 10 | callData?: HexString 11 | weight?: Weight 12 | when?: MultisigStorageInfo['when'] 13 | compatibilityToken: IApiContext['compatibilityToken'] 14 | } 15 | 16 | // TODO check if we can do this with papi 17 | // const LEGACY_ASMULTI_PARAM_LENGTH = 6 18 | 19 | export const getAsMultiTx = ({ 20 | api, 21 | threshold, 22 | otherSignatories, 23 | callData, 24 | tx, 25 | weight, 26 | when, 27 | compatibilityToken 28 | }: Params): Transaction | undefined => { 29 | // we can pass either the tx, or the callData 30 | if (!callData && !tx) return 31 | if (!compatibilityToken) return 32 | if (!api) return 33 | 34 | let txToSend: Transaction | undefined = tx 35 | 36 | if (!txToSend && callData) { 37 | txToSend = api.txFromCallData(Binary.fromHex(callData), compatibilityToken) 38 | } 39 | 40 | if (!txToSend) return 41 | 42 | return api.tx.Multisig.as_multi({ 43 | threshold, 44 | other_signatories: otherSignatories, 45 | maybe_timepoint: when, 46 | max_weight: weight || { proof_size: 0n, ref_time: 0n }, 47 | call: txToSend.decodedCall 48 | }) 49 | 50 | // return api.tx.multisig.asMulti.meta.args.length === LEGACY_ASMULTI_PARAM_LENGTH 51 | // ? api.tx.multisig.asMulti( 52 | // threshold, 53 | // otherSignatories, 54 | // when || null, 55 | // tx, 56 | // false, 57 | // // @ts-ignore 58 | // weight || 0 59 | // ) 60 | // : api.tx.multisig.asMulti( 61 | // threshold, 62 | // otherSignatories, 63 | // when || null, 64 | // tx, 65 | // weight || { 66 | // refTime: 0, 67 | // proofSize: 0 68 | // } 69 | // ) 70 | } 71 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getByteCount.ts: -------------------------------------------------------------------------------- 1 | export function getByteCount(s: string) { 2 | return encodeURI(s).split(/%(?:u[0-9A-F]{2})?[0-9A-F]{2}|./).length - 1 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getDisplayAddress.ts: -------------------------------------------------------------------------------- 1 | export const getDisplayAddress = (address?: string) => { 2 | if (!address) return '' 3 | 4 | return `${address.slice(0, 6)}..${address.slice(-6)}` 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getDisplayName.ts: -------------------------------------------------------------------------------- 1 | export const getDisplayName = (localName: string, identityName: string) => localName || identityName 2 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getEncodedCallFromDecodedTx.ts: -------------------------------------------------------------------------------- 1 | import { CompatibilityToken, Transaction } from 'polkadot-api' 2 | import { IApiContext } from '../contexts/ApiContext' 3 | import { ApiDescriptors } from '../types' 4 | 5 | export const getEncodedCallFromDecodedTx = ( 6 | api: IApiContext['api'], 7 | decodedTx: Transaction['decodedCall'], 8 | compatibilityToken: CompatibilityToken 9 | ) => { 10 | if (!api) return 11 | 12 | const batch = api.tx.Utility.batch({ 13 | calls: [decodedTx] 14 | }).getEncodedData(compatibilityToken) 15 | 16 | // we slice the 3 bytes from utility batch 17 | return batch.asHex().replace('0x', '').slice(6) 18 | } 19 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getErrorMessageReservedFunds.tsx: -------------------------------------------------------------------------------- 1 | export const wikiLinkReservedFunds = 2 | 'https://github.com/ChainSafe/Multix/wiki/Why-are-funds-reserved%3F' 3 | 4 | export const getErrorMessageReservedFunds = ({ 5 | identifier, 6 | requiredBalanceString, 7 | reservedString, 8 | withPpleChain 9 | }: { 10 | identifier: string 11 | requiredBalanceString?: string 12 | reservedString?: string 13 | withPpleChain?: boolean 14 | }) => { 15 | if (!requiredBalanceString) return '' 16 | 17 | return ( 18 | 19 | The {identifier} doesn't have the required {requiredBalanceString} 20 | {withPpleChain ? ' on the People Chain' : ''} to submit this transaction.{' '} 21 | 22 | 23 | ) 24 | } 25 | 26 | const ReservedMessage = ({ reservedAmount }: { reservedAmount?: string }) => { 27 | if (!reservedAmount) return null 28 | 29 | return ( 30 | 31 | Note that it includes {reservedAmount} that will be reserved.{' '} 32 | 37 | More info. 38 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getExtrinsicName.ts: -------------------------------------------------------------------------------- 1 | export const getExtrinsicName = (section = '', method = '') => { 2 | if (!section || !method) return '' 3 | 4 | return `${section}.${method}` 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getIdentityName.ts: -------------------------------------------------------------------------------- 1 | import { IdentityInfo } from '../hooks/useGetIdentity' 2 | 3 | export interface IdentityNameResults { 4 | identityName: string 5 | sub: string 6 | } 7 | 8 | export const getIdentityName = (identity?: IdentityInfo): IdentityNameResults => { 9 | if (!identity) return { identityName: '', sub: '' } 10 | 11 | if (identity.sub && identity.display) { 12 | // when an identity is a sub identity, `sub` is set 13 | // and `display` is the parent identity 14 | return { 15 | identityName: identity.display, 16 | sub: identity.sub 17 | } 18 | } else { 19 | // There there is no sub. 20 | return { 21 | identityName: identity.displayParent || identity.display || '', 22 | sub: '' 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getImportUrl.ts: -------------------------------------------------------------------------------- 1 | export const getImportUrl = (encodedData: string) => { 2 | return `https://multix.cloud/import?d=${encodedData}` 3 | } 4 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getMultiProxyAddress.ts: -------------------------------------------------------------------------------- 1 | import { MultiProxy } from '../contexts/MultiProxyContext' 2 | 3 | export function getMultiProxyAddress(multi: undefined): undefined 4 | export function getMultiProxyAddress(multi: MultiProxy): string 5 | export function getMultiProxyAddress(multi?: MultiProxy): string | undefined 6 | export function getMultiProxyAddress(multi?: MultiProxy) { 7 | if (!multi) return 8 | 9 | return multi.proxy || multi.multisigs[0].address 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getOptionLabel.ts: -------------------------------------------------------------------------------- 1 | import { AccountBaseInfo } from '../components/select/GenericAccountSelection' 2 | 3 | export const getOptionLabel = (option: string | AccountBaseInfo | null) => { 4 | if (!option) return '' 5 | 6 | return typeof option === 'string' ? option : option.address 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getPapiHowLink.ts: -------------------------------------------------------------------------------- 1 | export const getPapiHowLink = () => 'https://dev.papi.how' 2 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getPubKeyFromAddress.ts: -------------------------------------------------------------------------------- 1 | import { u8aToHex } from '@polkadot/util' 2 | import { decodeAddress } from '@polkadot/util-crypto' 3 | import { HexString } from 'polkadot-api' 4 | 5 | const decode = (address: string) => { 6 | // if it's an ethereum address pass just return it 7 | if (address.startsWith('0x') && address.length === 42) { 8 | return address.toLowerCase() 9 | } 10 | 11 | try { 12 | return u8aToHex(decodeAddress(address)) 13 | } catch (e) { 14 | console.error(`Error decoding the address ${address}, skipping`, e) 15 | return null 16 | } 17 | } 18 | 19 | export function getPubKeyFromAddress(address: string[]): HexString[] 20 | export function getPubKeyFromAddress(address: string): HexString | null 21 | export function getPubKeyFromAddress(address: string | string[]) { 22 | if (Array.isArray(address)) { 23 | return address.map(decode).filter(Boolean) as HexString[] 24 | } 25 | 26 | return decode(address) 27 | } 28 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getSubscanAccountLink.ts: -------------------------------------------------------------------------------- 1 | export const getSubscanAccountLink = ( 2 | network?: string, 3 | account?: string, 4 | multisigTab?: boolean 5 | ) => { 6 | if (!network || !account) return 7 | 8 | return `https://${network}.subscan.io/account/${account}${ 9 | multisigTab ? '?tab=multisig_extrinsic' : '' 10 | }` 11 | } 12 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getSubscanExtrinsicLink.ts: -------------------------------------------------------------------------------- 1 | export const getSubscanExtrinsicLink = (network: string | undefined, txHash: string) => { 2 | if (!network || !txHash) return 3 | 4 | return `https://${network}.subscan.io/extrinsic/${txHash}` 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getWalletConnectErrorResponse.ts: -------------------------------------------------------------------------------- 1 | export const getWalletConnectErrorResponse = (requestId: number, message: string) => { 2 | return { 3 | id: requestId, 4 | jsonrpc: '2.0', 5 | error: { 6 | code: 5000, 7 | message 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getWalletConnectId.ts: -------------------------------------------------------------------------------- 1 | export const getWalletConnectId = (genesisHash: string) => genesisHash.substring(2, 34) 2 | -------------------------------------------------------------------------------- /packages/ui/src/utils/getWalletConnectNameSpace.ts: -------------------------------------------------------------------------------- 1 | export const getWalletConnectNameSpace = (id: string) => `polkadot:${id}` 2 | -------------------------------------------------------------------------------- /packages/ui/src/utils/isProxyCall.ts: -------------------------------------------------------------------------------- 1 | export const isProxyCall = (name?: string) => !!name && name.toLowerCase() === 'proxy.proxy' 2 | -------------------------------------------------------------------------------- /packages/ui/src/utils/isValidAddress.ts: -------------------------------------------------------------------------------- 1 | import { decodeAddress, encodeAddress } from '@polkadot/keyring' 2 | import { hexToU8a, isHex } from '@polkadot/util' 3 | 4 | export const isValidAddress = (address: string | Uint8Array | null | undefined) => { 5 | try { 6 | encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)) 7 | 8 | return true 9 | } catch { 10 | // if it's an ethereum address it can't be decoded but could still be valid 11 | if (typeof address === 'string' && address.startsWith('0x') && address.length === 42) { 12 | return true 13 | } 14 | return false 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/ui/src/utils/jsonPrint.ts: -------------------------------------------------------------------------------- 1 | import { Binary } from 'polkadot-api' 2 | import json5 from 'json5' 3 | 4 | export const JSONprint = (e: unknown) => { 5 | if (e === null || e === undefined) { 6 | return '' 7 | } 8 | return ( 9 | json5 10 | .stringify(e, { 11 | replacer: (_, v) => 12 | typeof v === 'bigint' ? v.toString() : v instanceof Binary ? v.asText() : v, 13 | space: 4 14 | }) 15 | // remove { and } 16 | .slice(1, -1) 17 | // remove trailing comma if any 18 | .replace(/,\s*$/, '') 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/src/utils/namesUtil.ts: -------------------------------------------------------------------------------- 1 | import { AccountNames } from '../contexts/AccountNamesContext' 2 | import { getPubKeyFromAddress } from './getPubKeyFromAddress' 3 | import { encodesubstrateAddress } from './encodeSubstrateAddress' 4 | 5 | export const encodeNames = (accounts: AccountNames, ss58Format: number) => { 6 | const res = {} as AccountNames 7 | 8 | Object.entries(accounts).forEach(([pubkey, name]) => { 9 | const address = encodesubstrateAddress(pubkey, ss58Format) 10 | 11 | if (address) { 12 | res[address] = name 13 | } 14 | }) 15 | return res 16 | } 17 | 18 | export const decodeNames = (accounts: AccountNames) => { 19 | const res = {} as AccountNames 20 | 21 | Object.entries(accounts).forEach(([address, name]) => { 22 | const pubkey = getPubKeyFromAddress(address) 23 | if (pubkey) { 24 | res[pubkey] = name 25 | } 26 | }) 27 | return res 28 | } 29 | -------------------------------------------------------------------------------- /packages/ui/src/utils/paramConversion.ts: -------------------------------------------------------------------------------- 1 | const paramConversion = { 2 | num: [ 3 | 'Compact', 4 | 'BalanceOf', 5 | 'u8', 6 | 'u16', 7 | 'u32', 8 | 'u64', 9 | 'u128', 10 | 'i8', 11 | 'i16', 12 | 'i32', 13 | 'i64', 14 | 'i128' 15 | ] 16 | } 17 | 18 | export default paramConversion 19 | -------------------------------------------------------------------------------- /packages/ui/src/utils/translateError.ts: -------------------------------------------------------------------------------- 1 | export const translateError = (error: any) => { 2 | // {"error":{"type":"Invalid","value":{"type":"Payment"}},"name":"InvalidTxError"} 3 | if (error.error && error.error.type === 'Invalid' && error.error.value.type === 'Payment') { 4 | return 'Not enough funds to pay for the tx' 5 | } 6 | 7 | // { "type": "Invalid", "value": { "type": "Stale" } } 8 | if (error.error && error.error.type === 'Invalid' && error.error.value.type === 'Stale') { 9 | return 'A transaction with the same nonce has not been finalized yet, please retry' 10 | } 11 | 12 | return error.message || error.toString() 13 | } 14 | 15 | export const translateErrorInfo = (errorInfo: string) => { 16 | if (errorInfo === 'NoTimepoint') { 17 | return 'The same multisig transaction is already pending' 18 | } 19 | 20 | return errorInfo 21 | } 22 | -------------------------------------------------------------------------------- /packages/ui/src/utils/txHash.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from 'polkadot-api' 2 | import { Blake2256 } from '@polkadot-api/substrate-bindings' 3 | import { fromHex, toHex } from '@polkadot-api/utils' 4 | 5 | export const hashFromTx = (tx: HexString) => toHex(Blake2256(fromHex(tx))) as HexString 6 | -------------------------------------------------------------------------------- /packages/ui/src/utils/wsStatusChangeCallback.ts: -------------------------------------------------------------------------------- 1 | import { StatusChange, WsEvent } from 'polkadot-api/ws-provider/web' 2 | 3 | export const wsStatusChangeCallback = (status: StatusChange) => { 4 | return status.type === WsEvent.CONNECTING 5 | ? console.log('⚪ Connecting to RPC:', status.uri) 6 | : status.type === WsEvent.CONNECTED 7 | ? console.log('🟢 Connected to RPC:', status.uri) 8 | : status.type === WsEvent.ERROR 9 | ? console.error('🔴 Error connecting to RPC: ', status.event) 10 | : status.type === WsEvent.CLOSE 11 | ? console.error('⚫ Connection closed: ', status.event) 12 | : undefined 13 | } 14 | -------------------------------------------------------------------------------- /packages/ui/src/walletConfigs.ts: -------------------------------------------------------------------------------- 1 | import { InjectedWalletProvider } from '@reactive-dot/core/wallets.js' 2 | import { registerDotConnect } from 'dot-connect' 3 | import { DAPP_NAME, WALLETCONNECT_PROJECT_ID } from './constants' 4 | import { WalletConnect } from '@reactive-dot/wallet-walletconnect' 5 | import { defineConfig } from '@reactive-dot/core' 6 | import { getAllNetworkWalletConnectNameSpaces } from './utils/getAllNetworkWalletConnectNameSpaces' 7 | 8 | export const config = defineConfig({ 9 | chains: {}, 10 | wallets: [ 11 | new InjectedWalletProvider({ originName: DAPP_NAME }), 12 | new WalletConnect({ 13 | projectId: WALLETCONNECT_PROJECT_ID, 14 | providerOptions: { 15 | metadata: { 16 | name: 'Multix', 17 | description: 'The best interface to create and manage multisigs on Polkadot.', 18 | url: 'https://multix.cloud', 19 | icons: ['https://multix.cloud/android-chrome-192x192.png?raw=true'] 20 | } 21 | }, 22 | optionalChainIds: getAllNetworkWalletConnectNameSpaces() 23 | }) 24 | ] 25 | }) 26 | 27 | // Register dot-connect custom elements & configure supported wallets 28 | registerDotConnect({ 29 | wallets: config.wallets ?? [] 30 | }) 31 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // commenting it out to make TS compiler happy with Papi types 4 | // "composite": true, 5 | "target": "esnext", 6 | "lib": ["dom", "dom.iterable", "esnext"], 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | "esModuleInterop": true, 10 | "allowSyntheticDefaultImports": true, 11 | "strict": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "module": "esnext", 15 | "moduleResolution": "Bundler", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "noEmit": true, 19 | "jsx": "react-jsx", 20 | "types": ["vite/client", "vite-plugin-svgr/client", "cypress", "node", "cypress-wait-until"], 21 | "disableSizeLimit": true 22 | }, 23 | "include": [ 24 | "src/**/*", 25 | "**/*.config.ts", 26 | "**/types-and-hooks.tsx", 27 | "**/codegen.ts", 28 | "graphql.config.js", 29 | "cypress/**/*" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | import svgrPlugin from 'vite-plugin-svgr' 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | // This changes the out put dir from dist to build 8 | // comment this out if that isn't relevant for your project 9 | build: { 10 | outDir: 'build' 11 | }, 12 | plugins: [ 13 | react(), 14 | svgrPlugin({ 15 | svgrOptions: { 16 | icon: true 17 | // ...svgr options (https://react-svgr.com/docs/options/) 18 | } 19 | }) 20 | ], 21 | resolve: { 22 | preserveSymlinks: true // this is the fix! 23 | }, 24 | optimizeDeps: { exclude: ['node_modules/.cache'] } 25 | }) 26 | -------------------------------------------------------------------------------- /squid/.env.example: -------------------------------------------------------------------------------- 1 | DB_PORT=5432 2 | GQL_PORT=4350 3 | SQD_DEBUG=sqd:processor:mapping 4 | 5 | 6 | #paseo 7 | BLOCK_START=0 8 | RPC_WS="wss://rpc.ibp.network/paseo" 9 | CHAIN_ID='paseo' 10 | GATEWAY_URL="https://v2.archive.subsquid.io/network/paseo" -------------------------------------------------------------------------------- /squid/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2021": true 4 | }, 5 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "prettier"], 6 | "plugins": ["prettier"], 7 | "parser": "@typescript-eslint/parser", 8 | "parserOptions": { 9 | "ecmaVersion": 12, 10 | "sourceType": "module", 11 | "tsconfigRootDir": "./", 12 | "project": "tsconfig.json" 13 | }, 14 | "rules": {}, 15 | "ignorePatterns": [ 16 | "lib/**/*", 17 | "db/**/*", 18 | "src/types/**/*", 19 | "src/model/generated/**/*", 20 | "prettierrc.js" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /squid/.prettierignore: -------------------------------------------------------------------------------- 1 | db/**/* 2 | src/types/**/* 3 | src/model/**/* 4 | lib/**/* -------------------------------------------------------------------------------- /squid/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const config = require('../.prettierrc.js') 2 | 3 | module.exports = { ...config } 4 | -------------------------------------------------------------------------------- /squid/assets/envs/.env.acala: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=10 3 | RPC_WS="wss://acala-rpc-3.aca-api.network/ws" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/acala" 5 | CHAIN_ID='acala' -------------------------------------------------------------------------------- /squid/assets/envs/.env.amplitude: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=57 3 | RPC_WS="wss://rpc-amplitude.pendulumchain.tech" 4 | # GATEWAY_URL="https://v2.archive.subsquid.io/network/amplitude" 5 | CHAIN_ID='amplitude' -------------------------------------------------------------------------------- /squid/assets/envs/.env.asset-hub-kusama: -------------------------------------------------------------------------------- 1 | #asset-hub-kusama 2 | BLOCK_START=0 3 | #PREFIX=2 4 | RPC_WS="wss://sys.ibp.network/statemine" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/asset-hub-kusama" 6 | CHAIN_ID='asset-hub-kusama' 7 | -------------------------------------------------------------------------------- /squid/assets/envs/.env.asset-hub-polkadot: -------------------------------------------------------------------------------- 1 | #asset-hub-polkadot 2 | BLOCK_START=0 3 | #PREFIX=0 4 | RPC_WS="wss://sys.ibp.network/statemint" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/asset-hub-polkadot" 6 | CHAIN_ID='asset-hub-polkadot' -------------------------------------------------------------------------------- /squid/assets/envs/.env.asset-hub-westend: -------------------------------------------------------------------------------- 1 | #asset-hub-westend 2 | BLOCK_START=0 3 | #PREFIX=42 4 | RPC_WS="wss://asset-hub-westend-rpc.dwellir.com" 5 | # GATEWAY_URL="https://v2.archive.subsquid.io/network/asset-hub-kusama" 6 | CHAIN_ID='asset-hub-westend' 7 | -------------------------------------------------------------------------------- /squid/assets/envs/.env.astar: -------------------------------------------------------------------------------- 1 | #astar 2 | BLOCK_START=0 3 | #PREFIX=5 4 | RPC_WS="wss://rpc.astar.network" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/astar-substrate" 6 | CHAIN_ID='astar' 7 | # GENESIS='0x9eb76c5184c4ab8679d2d5d819fdf90b9c001403e9e17da2e14b6d8aec4029c6' -------------------------------------------------------------------------------- /squid/assets/envs/.env.bifrost-polkadot: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=6 3 | RPC_WS="wss://eu.bifrost-polkadot-rpc.liebi.com/ws" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/bifrost-polkadot" 5 | CHAIN_ID='bifrost-polkadot' -------------------------------------------------------------------------------- /squid/assets/envs/.env.chopsticks-ci: -------------------------------------------------------------------------------- 1 | DB_HOST=postgres 2 | # DB_PORT=5432 3 | # GQL_PORT=4350 4 | # SQD_DEBUG=sqd:processor:mapping 5 | # SQD_DEBUG=* 6 | 7 | # kusama chopsticks 8 | BLOCK_START=28000000 9 | #PREFIX=2 10 | RPC_WS="http://localhost:8000" 11 | CHAIN_ID='kusama' -------------------------------------------------------------------------------- /squid/assets/envs/.env.chopsticks-local: -------------------------------------------------------------------------------- 1 | # DB_PORT=5432 2 | GQL_PORT=4350 3 | # SQD_DEBUG=sqd:processor:mapping 4 | # SQD_DEBUG=* 5 | 6 | # kusama chopsticks 7 | BLOCK_START=28000000 8 | #PREFIX=2 9 | RPC_WS="http://localhost:8000" 10 | CHAIN_ID='kusama' -------------------------------------------------------------------------------- /squid/assets/envs/.env.coretime-kusama: -------------------------------------------------------------------------------- 1 | #coretime-kusama 2 | BLOCK_START=0 3 | #PREFIX=2 4 | RPC_WS="wss://sys.ibp.network/coretime-kusama" 5 | # GATEWAY_URL="coretime-kusama" 6 | CHAIN_ID='coretime-kusama' 7 | -------------------------------------------------------------------------------- /squid/assets/envs/.env.coretime-polkadot: -------------------------------------------------------------------------------- 1 | #coretime-polkadot 2 | BLOCK_START=0 3 | #PREFIX=0 4 | RPC_WS="wss://coretime-polkadot.dotters.network" 5 | # GATEWAY_URL="coretime-polkadot" 6 | CHAIN_ID='coretime-polkadot' 7 | -------------------------------------------------------------------------------- /squid/assets/envs/.env.dancelight: -------------------------------------------------------------------------------- 1 | # dancelight 2 | BLOCK_START=1048021 # this is the starting block of the indexer 3 | PREFIX=42 # the ss58 prefix for the chain 4 | RPC_WS="wss://dancelight.tanssi-api.network" # a WS endpoint to connect to the blockchain 5 | ARCHIVE_NAME="dancelight" # the archive name from subsquid archives 6 | CHAIN_ID='dancelight' # the name that will prefix most ids in the indexer's DB -------------------------------------------------------------------------------- /squid/assets/envs/.env.hydration: -------------------------------------------------------------------------------- 1 | #hydradx 2 | BLOCK_START=0 3 | #PREFIX=63 4 | RPC_WS="wss://hydration.ibp.network" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/hydradx" 6 | CHAIN_ID='hydradx' -------------------------------------------------------------------------------- /squid/assets/envs/.env.interlay: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=2032 3 | RPC_WS="wss://interlay-rpc.dwellir.com" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/interlay" 5 | CHAIN_ID='interlay' -------------------------------------------------------------------------------- /squid/assets/envs/.env.joystream: -------------------------------------------------------------------------------- 1 | RPC_WS="wss://rpc.joystream.org" 2 | CHAIN_ID='joystream' 3 | BLOCK_START=0 4 | #PREFIX=126 5 | # GATEWAY_URL="joystream" -------------------------------------------------------------------------------- /squid/assets/envs/.env.khala: -------------------------------------------------------------------------------- 1 | #khala 2 | BLOCK_START=0 3 | #PREFIX=30 4 | RPC_WS="wss://rpc.helikon.io/khala" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/khala" 6 | CHAIN_ID='khala' -------------------------------------------------------------------------------- /squid/assets/envs/.env.kilt: -------------------------------------------------------------------------------- 1 | BLOCK_START=2000000 2 | #PREFIX=38 3 | RPC_WS="wss://spiritnet.kilt.io" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/kilt" 5 | CHAIN_ID='kilt' -------------------------------------------------------------------------------- /squid/assets/envs/.env.kusama: -------------------------------------------------------------------------------- 1 | #kusama 2 | BLOCK_START=6000000 3 | #PREFIX=2 4 | RPC_WS="wss://rpc.ibp.network/kusama" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/kusama" 6 | CHAIN_ID='kusama' -------------------------------------------------------------------------------- /squid/assets/envs/.env.moonbeam: -------------------------------------------------------------------------------- 1 | BLOCK_START=3000000 2 | #PREFIX=1284 3 | RPC_WS="wss://moonbeam-rpc.dwellir.com" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/moonbeam-substrate" 5 | CHAIN_ID='moonbeam' 6 | IS_ETHEREUM='true' -------------------------------------------------------------------------------- /squid/assets/envs/.env.moonriver: -------------------------------------------------------------------------------- 1 | BLOCK_START=3000000 2 | #PREFIX=1285 3 | RPC_WS="wss://moonriver-rpc.dwellir.com" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/moonriver-substrate" 5 | CHAIN_ID='moonriver' 6 | IS_ETHEREUM='true' -------------------------------------------------------------------------------- /squid/assets/envs/.env.paseo: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=0 3 | RPC_WS="wss://rpc.ibp.network/paseo" 4 | CHAIN_ID='paseo' 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/paseo" -------------------------------------------------------------------------------- /squid/assets/envs/.env.pendulum: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=56 3 | RPC_WS="wss://rpc-pendulum.prd.pendulumchain.tech" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/pendulum" 5 | CHAIN_ID='pendulum' -------------------------------------------------------------------------------- /squid/assets/envs/.env.phala: -------------------------------------------------------------------------------- 1 | #phala 2 | BLOCK_START=2400000 3 | #PREFIX=30 4 | RPC_WS="wss://priv-api.phala.network/phala/ws" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/phala" 6 | CHAIN_ID='phala' -------------------------------------------------------------------------------- /squid/assets/envs/.env.polimec: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=41 3 | RPC_WS="wss://polimec.rpc.amforc.com" 4 | GATEWAY_URL="https://v2.archive.subsquid.io/network/polimec" 5 | CHAIN_ID='polimec' -------------------------------------------------------------------------------- /squid/assets/envs/.env.polkadot: -------------------------------------------------------------------------------- 1 | #polkadot 2 | BLOCK_START=12000000 3 | #PREFIX=0 4 | RPC_WS="wss://rpc.ibp.network/polkadot" 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/polkadot" 6 | CHAIN_ID='polkadot' -------------------------------------------------------------------------------- /squid/assets/envs/.env.rhala: -------------------------------------------------------------------------------- 1 | #rhala 2 | BLOCK_START=0 3 | #PREFIX=30 4 | RPC_WS="wss://rhala-node.phala.network/ws" 5 | # GATEWAY_URL="" 6 | CHAIN_ID='rhala' -------------------------------------------------------------------------------- /squid/assets/envs/.env.rococo: -------------------------------------------------------------------------------- 1 | #rococo 2 | # BLOCK_START=6585633 3 | BLOCK_START=3000000 4 | #PREFIX=42 5 | RPC_WS="wss://rococo-rpc.polkadot.io" 6 | GATEWAY_URL="https://v2.archive.subsquid.io/network/rococo" 7 | CHAIN_ID='rococo' -------------------------------------------------------------------------------- /squid/assets/envs/.env.watr: -------------------------------------------------------------------------------- 1 | BLOCK_START=0 2 | #PREFIX=19 3 | RPC_WS="wss://watr-rpc.watr-api.network" 4 | CHAIN_ID='watr' -------------------------------------------------------------------------------- /squid/assets/envs/.env.westend: -------------------------------------------------------------------------------- 1 | BLOCK_START=18000000 2 | #PREFIX=42 3 | RPC_WS="wss://westend-rpc.polkadot.io" 4 | CHAIN_ID='westend' 5 | GATEWAY_URL="https://v2.archive.subsquid.io/network/westend" -------------------------------------------------------------------------------- /squid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multix-squid", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "engines": { 7 | "node": ">=16" 8 | }, 9 | "scripts": { 10 | "build": "rm -rf lib && tsc", 11 | "db:migrate": "npx squid-typeorm-migration apply", 12 | "db:generate-migration": "npx squid-typeorm-migration generate", 13 | "codegen": "npx squid-typeorm-codegen", 14 | "typegen": "npx squid-substrate-typegen src/typegens/*.json", 15 | "start": "node -r dotenv/config lib/main.js", 16 | "start:chopsticks-ci": "node -r dotenv/config lib/main dotenv_config_path=assets/envs/.env.chopsticks-ci", 17 | "start:chopsticks-local": "node -r dotenv/config lib/main dotenv_config_path=assets/envs/.env.chopsticks-local", 18 | "start:graphql-server": "npx squid-graphql-server --sql-statement-timeout 3000 --dumb-cache in-memory --dumb-cache-ttl 1000 --dumb-cache-size 100 --dumb-cache-max-age 1000", 19 | "lint": "eslint 'src/**/*.{js,ts,tsx}'", 20 | "lint:fix": "eslint 'src/**/*.{js,ts,tsx}' --fix", 21 | "format": " npx prettier --write ." 22 | }, 23 | "dependencies": { 24 | "@polkadot/util-crypto": "^13.3.1", 25 | "@subsquid/graphql-server": "^4.9.0", 26 | "@subsquid/ss58": "^2.0.2", 27 | "@subsquid/substrate-processor": "^8.5.2", 28 | "@subsquid/typeorm-migration": "^1.3.0", 29 | "@subsquid/typeorm-store": "^1.5.1", 30 | "dotenv": "^16.4.7", 31 | "pg": "8.13.1", 32 | "typeorm": "0.3.20", 33 | "typescript": "5.7.3" 34 | }, 35 | "devDependencies": { 36 | "@subsquid/substrate-metadata-explorer": "^3.2.0", 37 | "@subsquid/substrate-typegen": "^8.1.0", 38 | "@subsquid/typeorm-codegen": "^2.0.2", 39 | "@types/node": "22.13.0", 40 | "@typescript-eslint/eslint-plugin": "^8.22.0", 41 | "@typescript-eslint/parser": "^8.22.0", 42 | "eslint": "^8.57.0", 43 | "eslint-config-prettier": "^9.1.0", 44 | "eslint-plugin-prettier": "^5.2.3", 45 | "prettier": "3.4.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /squid/schema.graphql: -------------------------------------------------------------------------------- 1 | type Account @entity { 2 | id: ID! 3 | pubKey: String! 4 | multisigs: [AccountMultisig] @derivedFrom(field: "signatory") 5 | isPureProxy: Boolean 6 | # it's the origin/delegator for another proxy accounts, a pure must have some 7 | delegatorFor: [ProxyAccount!] @derivedFrom(field: "delegator") 8 | # it's the delegatee/doing stuff on behalf of another account 9 | delegateeFor: [ProxyAccount!] @derivedFrom(field: "delegatee") 10 | isMultisig: Boolean 11 | signatories: [AccountMultisig!] @derivedFrom(field: "multisig") 12 | threshold: Int 13 | multisigsCalls: [MultisigCall!] @derivedFrom(field: "multisig") 14 | } 15 | 16 | type MultisigCall @entity { 17 | id: ID! 18 | blockHash: String! 19 | timestamp: DateTime! 20 | multisig: Account 21 | callIndex: Int! 22 | } 23 | 24 | # entity for linking accounts and multisigs 25 | type AccountMultisig @entity { 26 | id: ID! 27 | multisig: Account! 28 | signatory: Account! 29 | } 30 | 31 | # entity for linking proxies 32 | type ProxyAccount @entity { 33 | id: ID! 34 | delegator: Account! 35 | delegatee: Account! 36 | type: ProxyType! 37 | delay: Int! 38 | createdAt: DateTime! 39 | extrinsicIndex: Int # only useful for pure proxies 40 | creationBlockNumber: Int # only useful for pure proxies 41 | } 42 | 43 | # from https://github.com/paritytech/polkadot/blob/476d3ddddf7a8f7361edac92228d0200abac0895/runtime/polkadot/src/lib.rs#L918 44 | # and https://github.com/paritytech/polkadot/blob/476d3ddddf7a8f7361edac92228d0200abac0895/runtime/kusama/src/lib.rs#L934 45 | enum ProxyType { 46 | Any 47 | Governance 48 | NonTransfer 49 | IdentityJudgement 50 | CancelProxy 51 | Auction 52 | Staking 53 | SudoBalances 54 | NominationPools 55 | Society 56 | Unknown # this is added in case a new proxy type comes up and isn't supported yet 57 | } 58 | -------------------------------------------------------------------------------- /squid/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const MULTI_PREFIX = 'MULTI' 2 | 3 | export const polkadotChainIds = ['polkadot', 'asset-hub-polkadot', 'coretime-polkadot'] 4 | export const kusamaChainIds = ['kusama', 'asset-hub-kusama', 'coretime-kusama'] 5 | export const paseoChainIds = ['paseo', 'asset-hub-paseo', 'coretime-paseo'] 6 | export const westendChainIds = ['westend', 'asset-hub-westend', 'coretime-westend'] 7 | 8 | export const replicationGroups = { 9 | polkadot: polkadotChainIds, 10 | kusama: kusamaChainIds, 11 | paseo: paseoChainIds, 12 | westend: westendChainIds 13 | } 14 | // Manually add asset hub pure proxies 15 | // https://polkadot.subsquare.io/referenda/1308 16 | export const PURE_PROXIEs_MIGRATION_BLOCK = 7903349 // <-- this needs to be a block where something happens 17 | export const PURE_PROXIEs_MIGRATION_CHAIN = 'asset-hub-polkadot' 18 | export const PURE_PROXIES_MIGRATION_ARRAY = [ 19 | { 20 | entity: 'Heroic', 21 | who: '16Cf2SMFkWMApL7fiaiu3nSFiVk3wZoNHn7QTzTL1KvLDMcT', 22 | pure: '12RP5AAF8TEb4qVBgiAgJXMVF8NzYZZPD8XftcKD7sM153E7', 23 | signatories: [ 24 | '17L1abEGisdmSQamtrYvsdsEWhBQiAqSQfjuNYAADYNeivp', 25 | '12EeAYWN52HcmCwjPmxyGBZ5H4tXnTL4CZ7z63FBZYWM64mQ', 26 | '14H4NwJn122wNmMJaQErbNWZuyNdMAuUrh5W7BpNSDpgiWAj' 27 | ], 28 | threshold: 2 29 | }, 30 | { 31 | entity: 'PBA', 32 | who: '1kJLyFPntELGnaawpDLzidR7rXaX4wQMPbd9ShQQZ3LK1Nh', 33 | pure: '15UQ1nhCRRJoWf1C4LBraqCVXS91qPiWLiWQfnS3k8Dk4R79' 34 | }, 35 | 36 | { 37 | entity: 'Polytope Labs', 38 | who: '12w4jGrxQuWRqHr1dCoJZS8Ez8eoUdqVbCA7n1ze584umJoy', 39 | pure: '1tCybVtS7otBAK5CnbDwJQumWmfEWTCXRaYZM9UtihTQ1Dt' 40 | }, 41 | 42 | { 43 | entity: 'Social Media Editorial Board', 44 | who: '12k979BFp7JczuaHEvg7KpPwNkpHSNu5NYQRSL9cFjr7rdhh', 45 | pure: '1VNSqFCX4Gk7R8kKBbEaYhXSioBGLrrL4kHgLwDkoTLkgqB' 46 | } 47 | ] 48 | -------------------------------------------------------------------------------- /squid/src/model/generated/_proxyType.ts: -------------------------------------------------------------------------------- 1 | export enum ProxyType { 2 | Any = "Any", 3 | Governance = "Governance", 4 | NonTransfer = "NonTransfer", 5 | IdentityJudgement = "IdentityJudgement", 6 | CancelProxy = "CancelProxy", 7 | Auction = "Auction", 8 | Staking = "Staking", 9 | SudoBalances = "SudoBalances", 10 | NominationPools = "NominationPools", 11 | Society = "Society", 12 | Unknown = "Unknown", 13 | } 14 | -------------------------------------------------------------------------------- /squid/src/model/generated/account.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, StringColumn as StringColumn_, OneToMany as OneToMany_, BooleanColumn as BooleanColumn_, IntColumn as IntColumn_} from "@subsquid/typeorm-store" 2 | import {AccountMultisig} from "./accountMultisig.model" 3 | import {ProxyAccount} from "./proxyAccount.model" 4 | import {MultisigCall} from "./multisigCall.model" 5 | 6 | @Entity_() 7 | export class Account { 8 | constructor(props?: Partial) { 9 | Object.assign(this, props) 10 | } 11 | 12 | @PrimaryColumn_() 13 | id!: string 14 | 15 | @StringColumn_({nullable: false}) 16 | pubKey!: string 17 | 18 | @OneToMany_(() => AccountMultisig, e => e.signatory) 19 | multisigs!: AccountMultisig[] 20 | 21 | @BooleanColumn_({nullable: true}) 22 | isPureProxy!: boolean | undefined | null 23 | 24 | @OneToMany_(() => ProxyAccount, e => e.delegator) 25 | delegatorFor!: ProxyAccount[] 26 | 27 | @OneToMany_(() => ProxyAccount, e => e.delegatee) 28 | delegateeFor!: ProxyAccount[] 29 | 30 | @BooleanColumn_({nullable: true}) 31 | isMultisig!: boolean | undefined | null 32 | 33 | @OneToMany_(() => AccountMultisig, e => e.multisig) 34 | signatories!: AccountMultisig[] 35 | 36 | @IntColumn_({nullable: true}) 37 | threshold!: number | undefined | null 38 | 39 | @OneToMany_(() => MultisigCall, e => e.multisig) 40 | multisigsCalls!: MultisigCall[] 41 | } 42 | -------------------------------------------------------------------------------- /squid/src/model/generated/accountMultisig.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, ManyToOne as ManyToOne_, Index as Index_} from "@subsquid/typeorm-store" 2 | import {Account} from "./account.model" 3 | 4 | @Entity_() 5 | export class AccountMultisig { 6 | constructor(props?: Partial) { 7 | Object.assign(this, props) 8 | } 9 | 10 | @PrimaryColumn_() 11 | id!: string 12 | 13 | @Index_() 14 | @ManyToOne_(() => Account, {nullable: true}) 15 | multisig!: Account 16 | 17 | @Index_() 18 | @ManyToOne_(() => Account, {nullable: true}) 19 | signatory!: Account 20 | } 21 | -------------------------------------------------------------------------------- /squid/src/model/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./account.model" 2 | export * from "./multisigCall.model" 3 | export * from "./accountMultisig.model" 4 | export * from "./proxyAccount.model" 5 | export * from "./_proxyType" 6 | -------------------------------------------------------------------------------- /squid/src/model/generated/multisigCall.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, StringColumn as StringColumn_, DateTimeColumn as DateTimeColumn_, ManyToOne as ManyToOne_, Index as Index_, IntColumn as IntColumn_} from "@subsquid/typeorm-store" 2 | import {Account} from "./account.model" 3 | 4 | @Entity_() 5 | export class MultisigCall { 6 | constructor(props?: Partial) { 7 | Object.assign(this, props) 8 | } 9 | 10 | @PrimaryColumn_() 11 | id!: string 12 | 13 | @StringColumn_({nullable: false}) 14 | blockHash!: string 15 | 16 | @DateTimeColumn_({nullable: false}) 17 | timestamp!: Date 18 | 19 | @Index_() 20 | @ManyToOne_(() => Account, {nullable: true}) 21 | multisig!: Account | undefined | null 22 | 23 | @IntColumn_({nullable: false}) 24 | callIndex!: number 25 | } 26 | -------------------------------------------------------------------------------- /squid/src/model/generated/proxyAccount.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, ManyToOne as ManyToOne_, Index as Index_, IntColumn as IntColumn_, DateTimeColumn as DateTimeColumn_} from "@subsquid/typeorm-store" 2 | import {Account} from "./account.model" 3 | import {ProxyType} from "./_proxyType" 4 | 5 | @Entity_() 6 | export class ProxyAccount { 7 | constructor(props?: Partial) { 8 | Object.assign(this, props) 9 | } 10 | 11 | @PrimaryColumn_() 12 | id!: string 13 | 14 | @Index_() 15 | @ManyToOne_(() => Account, {nullable: true}) 16 | delegator!: Account 17 | 18 | @Index_() 19 | @ManyToOne_(() => Account, {nullable: true}) 20 | delegatee!: Account 21 | 22 | @Column_("varchar", {length: 17, nullable: false}) 23 | type!: ProxyType 24 | 25 | @IntColumn_({nullable: false}) 26 | delay!: number 27 | 28 | @DateTimeColumn_({nullable: false}) 29 | createdAt!: Date 30 | 31 | @IntColumn_({nullable: true}) 32 | extrinsicIndex!: number | undefined | null 33 | 34 | @IntColumn_({nullable: true}) 35 | creationBlockNumber!: number | undefined | null 36 | } 37 | -------------------------------------------------------------------------------- /squid/src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from './generated' 2 | -------------------------------------------------------------------------------- /squid/src/multisigCalls.ts: -------------------------------------------------------------------------------- 1 | import { Call } from '@subsquid/substrate-processor' 2 | 3 | // "args": { 4 | // "call": { 5 | // "__kind": "Proxy", 6 | // "value": { 7 | // "__kind": "create_pure", 8 | // "delay": 0, 9 | // "index": 0, 10 | // "proxyType": { 11 | // "__kind": "Any" 12 | // } 13 | // } 14 | // }, 15 | // "maxWeight": { 16 | // "proofSize": "0", 17 | // "refTime": "182470554" 18 | // }, 19 | // "maybeTimepoint": { 20 | // "height": 3152562, 21 | // "index": 2 22 | // }, 23 | // "otherSignatories": [ 24 | // "0x4e66461fed55e8de6988270d17e18f29a5c3fb0fc6ca39f9a9f41bff01510665", 25 | // "0x7c6bb0cfc976a5a68c6493c963ac05427423d37d4a21f3d5a589bbe0756b3b59" 26 | // ], 27 | // "threshold": 2 28 | // }, 29 | 30 | export const handleMultisigCall = (multisigArgs: Call['args']) => { 31 | return { 32 | otherSignatories: multisigArgs['otherSignatories'], 33 | threshold: multisigArgs['threshold'] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/handleNewMultisigCalls.ts: -------------------------------------------------------------------------------- 1 | import { In } from 'typeorm' 2 | import { Account, MultisigCall } from '../model' 3 | import { Ctx } from '../main' 4 | import { getAccountId } from '../util/getAccountId' 5 | 6 | export interface MultisigCallInfo extends Omit { 7 | multisigPubKey: string 8 | } 9 | 10 | export const handleNewMultisigCalls = async ( 11 | ctx: Ctx, 12 | newMultisigCalls: MultisigCallInfo[], 13 | chainId: string 14 | ) => { 15 | const multisigIds = newMultisigCalls.map((multi) => getAccountId(multi.multisigPubKey, chainId)) 16 | const multisigCalls: MultisigCall[] = [] 17 | 18 | const multisigAccountsMap = await ctx.store 19 | .findBy(Account, { id: In([...multisigIds]) }) 20 | .then((q) => new Map(q.map((i) => [i.pubKey, i]))) 21 | 22 | for (const { blockHash, id, callIndex, multisigPubKey, timestamp } of newMultisigCalls) { 23 | multisigCalls.push( 24 | new MultisigCall({ 25 | id, 26 | blockHash, 27 | callIndex, 28 | multisig: multisigAccountsMap.get(multisigPubKey), 29 | timestamp 30 | }) 31 | ) 32 | } 33 | 34 | await ctx.store.save(multisigCalls) 35 | } 36 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/handleNewMultisigs.ts: -------------------------------------------------------------------------------- 1 | import { Account, AccountMultisig } from '../model' 2 | import { Ctx } from '../main' 3 | import { getOrCreateAccounts, getAccountMultisigId } from '../util' 4 | import { shouldReplicateOn } from '../util/shouldReplicate' 5 | import { getAccountId } from '../util/getAccountId' 6 | 7 | export interface NewMultisigsInfo extends Omit { 8 | newSignatories: string[] 9 | } 10 | 11 | export const handleNewMultisigs = async ( 12 | ctx: Ctx, 13 | multisigs: NewMultisigsInfo[], 14 | chainId: string 15 | ) => { 16 | const newMultisigs: Map = new Map() 17 | const newAccountMultisigs: Map = new Map() 18 | 19 | const replicatedNetworks = shouldReplicateOn(chainId) || [chainId] 20 | 21 | for (const { pubKey, newSignatories, threshold, isMultisig, isPureProxy } of multisigs) { 22 | for (const network of replicatedNetworks) { 23 | const signatoriesAccounts = await getOrCreateAccounts(ctx, newSignatories, network) 24 | 25 | const multisigId = getAccountId(pubKey, network) 26 | const newMultisig = new Account({ 27 | id: multisigId, 28 | pubKey, 29 | threshold, 30 | isMultisig, 31 | isPureProxy 32 | }) 33 | 34 | newMultisigs.set(multisigId, newMultisig) 35 | 36 | signatoriesAccounts.forEach((account) => { 37 | const newAccountMultisigId = getAccountMultisigId(newMultisig.id, account.id, network) 38 | 39 | const newAccountMultisig = new AccountMultisig({ 40 | id: newAccountMultisigId, 41 | multisig: newMultisig, 42 | signatory: account 43 | }) 44 | newAccountMultisigs.set(newAccountMultisigId, newAccountMultisig) 45 | }) 46 | } 47 | 48 | // ctx.log.info(`--> New multisig ${newMultisigs.size}`) 49 | // ctx.log.info(JsonLog(Array.from(newMultisigs.keys()))) 50 | // ctx.log.info(`--> New accountMultisigs ${newAccountMultisigs.size}`) 51 | // ctx.log.info(JsonLog(Array.from(newAccountMultisigs.values()))) 52 | 53 | await ctx.store.save(Array.from(newMultisigs.values())) 54 | await ctx.store.save(Array.from(newAccountMultisigs.values())) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/handleNewProxies.ts: -------------------------------------------------------------------------------- 1 | import { Account, ProxyAccount, ProxyType } from '../model' 2 | import { Ctx } from '../main' 3 | import { getOrCreateAccounts } from '../util' 4 | import { getAccountId } from '../util/getAccountId' 5 | 6 | export interface NewProxy { 7 | id: string 8 | delegator: string 9 | delegatee: string 10 | type: ProxyType 11 | delay: number 12 | createdAt: Date 13 | } 14 | 15 | export const handleNewProxies = async (ctx: Ctx, newProxies: NewProxy[], chainId: string) => { 16 | // Aggregate all accounts we deal with using a set to make sure we don't have dublicates 17 | const allAccountsStringSet = new Set() 18 | 19 | newProxies.forEach(({ delegatee, delegator }) => { 20 | allAccountsStringSet.add(delegatee) 21 | allAccountsStringSet.add(delegator) 22 | }) 23 | 24 | const accountsToUpdate = await getOrCreateAccounts( 25 | ctx, 26 | Array.from(allAccountsStringSet.values()), 27 | chainId 28 | ) 29 | 30 | const accountMap = new Map() 31 | accountsToUpdate.forEach((account) => accountMap.set(account.id, account)) 32 | const proxyAccounts: ProxyAccount[] = [] 33 | 34 | for (const { id, delegatee, delegator, delay, type, createdAt } of newProxies) { 35 | // ctx.log.info(`---> type ${type}`) 36 | const delegatorAccount = accountMap.get(getAccountId(delegator, chainId)) 37 | const delegateeAccount = accountMap.get(getAccountId(delegatee, chainId)) 38 | 39 | proxyAccounts.push( 40 | new ProxyAccount({ 41 | id, 42 | delegator: delegatorAccount, 43 | delegatee: delegateeAccount, 44 | type, 45 | delay, 46 | createdAt, 47 | creationBlockNumber: null, 48 | extrinsicIndex: null 49 | }) 50 | ) 51 | } 52 | 53 | // ctx.log.info(`new proxy account to save ${Array.from(proxyAccounts.values())}`) 54 | await ctx.store.save(proxyAccounts) 55 | } 56 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/handleProxyKillPure.ts: -------------------------------------------------------------------------------- 1 | import { ProxyAccount } from '../model' 2 | import { Ctx } from '../main' 3 | import { KillPureCallInfo } from '../util/getProxyKillPureArgs' 4 | 5 | export const handleProxyKillPure = async (ctx: Ctx, proxyKillPureArgs: KillPureCallInfo[]) => { 6 | const proxyAccountsToRemove: ProxyAccount[] = [] 7 | 8 | for (const { blockNumber, extrinsicIndex, spawnerPubKey } of proxyKillPureArgs) { 9 | const matchingProxyAcccount = await ctx.store.findOne(ProxyAccount, { 10 | where: { 11 | creationBlockNumber: blockNumber, 12 | extrinsicIndex: extrinsicIndex, 13 | delegatee: { 14 | pubKey: spawnerPubKey 15 | } 16 | } 17 | }) 18 | 19 | matchingProxyAcccount && proxyAccountsToRemove.push(matchingProxyAcccount) 20 | } 21 | 22 | await ctx.store.remove(proxyAccountsToRemove) 23 | } 24 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/handleProxyRemovals.ts: -------------------------------------------------------------------------------- 1 | import { In } from 'typeorm' 2 | import { ProxyAccount } from '../model' 3 | import { Ctx } from '../main' 4 | 5 | export const handleProxyRemovals = async (ctx: Ctx, proxyRemovals: string[]) => { 6 | const toRemove = await ctx.store.findBy(ProxyAccount, { 7 | id: In(proxyRemovals) 8 | }) 9 | 10 | // ctx.log.info(`--> Remove ${toRemove.map((proxyAccount) => JSON.stringify(proxyAccount))}`) 11 | 12 | await ctx.store.remove(toRemove) 13 | } 14 | -------------------------------------------------------------------------------- /squid/src/processorHandlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './handleNewMultisigCalls' 2 | export * from './handleNewMultisigs' 3 | export * from './handleNewPureProxies' 4 | export * from './handleNewProxies' 5 | export * from './handleProxyRemovals' 6 | -------------------------------------------------------------------------------- /squid/src/typegens/typegen.json: -------------------------------------------------------------------------------- 1 | { 2 | "outDir": "../types", 3 | "specVersions": "https://v2.archive.subsquid.io/metadata/kusama", 4 | "events": [ 5 | "Proxy.AnonymousCreated", 6 | "Proxy.PureCreated", 7 | "Proxy.ProxyAdded", 8 | "Proxy.ProxyRemoved" 9 | ], 10 | "calls": [ 11 | "Proxy.proxy", 12 | "Proxy.remove_proxies", 13 | "Proxy.kill_pure", 14 | "Multisig.as_multi", 15 | "Multisig.approve_as_multi", 16 | "Multisig.cancel_as_multi", 17 | "Multisig.as_multi_threshold_1" 18 | ], 19 | "storage": [] 20 | } 21 | -------------------------------------------------------------------------------- /squid/src/types/calls.ts: -------------------------------------------------------------------------------- 1 | export * as proxy from './proxy/calls' 2 | export * as multisig from './multisig/calls' 3 | -------------------------------------------------------------------------------- /squid/src/types/events.ts: -------------------------------------------------------------------------------- 1 | export * as proxy from './proxy/events' 2 | -------------------------------------------------------------------------------- /squid/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * as v2005 from './v2005' 2 | export * as v9130 from './v9130' 3 | export * as v9180 from './v9180' 4 | export * as v9111 from './v9111' 5 | export * as v9420 from './v9420' 6 | export * as v1002006 from './v1002006' 7 | export * as v1003000 from './v1003000' 8 | export * as v1004000 from './v1004000' 9 | export * as v9190 from './v9190' 10 | export * as v9300 from './v9300' 11 | export * as v2007 from './v2007' 12 | export * as v2011 from './v2011' 13 | export * as v2013 from './v2013' 14 | export * as v2015 from './v2015' 15 | export * as v2022 from './v2022' 16 | export * as v2023 from './v2023' 17 | export * as v2024 from './v2024' 18 | export * as v2025 from './v2025' 19 | export * as v2026 from './v2026' 20 | export * as v2028 from './v2028' 21 | export * as v2029 from './v2029' 22 | export * as v2030 from './v2030' 23 | export * as v9010 from './v9010' 24 | export * as v9030 from './v9030' 25 | export * as v9040 from './v9040' 26 | export * as v9050 from './v9050' 27 | export * as v9080 from './v9080' 28 | export * as v9090 from './v9090' 29 | export * as v9100 from './v9100' 30 | export * as v9122 from './v9122' 31 | export * as v9160 from './v9160' 32 | export * as v9170 from './v9170' 33 | export * as v9220 from './v9220' 34 | export * as v9230 from './v9230' 35 | export * as v9250 from './v9250' 36 | export * as v9271 from './v9271' 37 | export * as v9291 from './v9291' 38 | export * as v9320 from './v9320' 39 | export * as v9340 from './v9340' 40 | export * as v9350 from './v9350' 41 | export * as v9370 from './v9370' 42 | export * as v9381 from './v9381' 43 | export * as v9430 from './v9430' 44 | export * as v1000000 from './v1000000' 45 | export * as v1001000 from './v1001000' 46 | export * as v1002000 from './v1002000' 47 | export * as v1002004 from './v1002004' 48 | export * as v1002005 from './v1002005' 49 | export * as v1004001 from './v1004001' 50 | export * as v1005000 from './v1005000' 51 | export * as events from './events' 52 | export * as calls from './calls' 53 | -------------------------------------------------------------------------------- /squid/src/util/Env.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config' 2 | 3 | interface EnvValues { 4 | blockstart: string 5 | rpcWs: string 6 | gatewayUrl: string 7 | chainId: string 8 | isEthereum?: boolean 9 | } 10 | 11 | export class Env { 12 | env: EnvValues 13 | 14 | constructor() { 15 | this.env = { 16 | blockstart: process.env.BLOCK_START || '', 17 | rpcWs: process.env.RPC_WS || '', 18 | gatewayUrl: process.env.GATEWAY_URL || '', 19 | chainId: process.env.CHAIN_ID || '', 20 | isEthereum: process.env.IS_ETHEREUM === 'true' || false 21 | } 22 | 23 | this.checkForUndefined() 24 | } 25 | 26 | checkForUndefined = () => { 27 | Object.entries(this.env).forEach(([key, value]) => { 28 | // a prefix can be 0 and it is a valid value 29 | if (!value && value !== 0 && value !== false) { 30 | console.warn(`ℹ️ No env variable set for ${key} - (may be optional)`) 31 | } 32 | }) 33 | } 34 | 35 | getEnv = () => { 36 | return this.env 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /squid/src/util/JsonLog.ts: -------------------------------------------------------------------------------- 1 | export const JsonLog = (val: any): string => { 2 | return JSON.stringify( 3 | val, 4 | (_, value) => (typeof value === 'bigint' ? value.toString() : value), 5 | 4 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /squid/src/util/entities.ts: -------------------------------------------------------------------------------- 1 | import { In } from 'typeorm' 2 | import { Account } from '../model' 3 | import { Ctx } from '../main' 4 | import { getAccountId } from './getAccountId' 5 | // import { JsonLog } from "./JsonLog" 6 | 7 | export async function getOrCreateAccounts( 8 | ctx: Ctx, 9 | pubKeys: string[], 10 | chainId: string 11 | ): Promise { 12 | const ids = pubKeys.map((pubKey) => getAccountId(pubKey, chainId)) 13 | const dbAccounts = await ctx.store.findBy(Account, { id: In([...ids]) }) 14 | 15 | // ctx.log.info(`db accounts: ${JsonLog(dbAccounts)}`) 16 | const accountsMap: Map = new Map() 17 | for (const account of dbAccounts) accountsMap.set(account.id, account) 18 | const newAccounts: Set = new Set() 19 | for (const pubKey of pubKeys) { 20 | const id = getAccountId(pubKey, chainId) 21 | if (accountsMap.has(id)) continue 22 | 23 | const account = new Account({ 24 | id, 25 | pubKey, 26 | isMultisig: false, 27 | isPureProxy: false 28 | }) 29 | newAccounts.add(account) 30 | } 31 | 32 | if (newAccounts.size > 0) await ctx.store.save([...newAccounts]) 33 | 34 | return [...accountsMap.values(), ...newAccounts] 35 | } 36 | -------------------------------------------------------------------------------- /squid/src/util/getAccountId.ts: -------------------------------------------------------------------------------- 1 | export const getAccountId = (pubKey: string, chainId: string) => { 2 | return `${chainId}-${pubKey}` 3 | } 4 | -------------------------------------------------------------------------------- /squid/src/util/getAccountMultisigId.ts: -------------------------------------------------------------------------------- 1 | export const getAccountMultisigId = (multiSigId: string, accountId: string, chainId: string) => { 2 | return `${chainId}-${multiSigId.substring(20)}-${accountId.substring(20)}` 3 | } 4 | -------------------------------------------------------------------------------- /squid/src/util/getMultisigCallId.ts: -------------------------------------------------------------------------------- 1 | export const getMultisigCallId = ( 2 | pubKey: string, 3 | blockNumber: number, 4 | extrinsicIndex: number, 5 | callId: string, 6 | chainId: string 7 | ) => { 8 | return `${chainId}-${pubKey}-${blockNumber}-${extrinsicIndex}-${callId}` 9 | } 10 | -------------------------------------------------------------------------------- /squid/src/util/getMultisigPubKey.ts: -------------------------------------------------------------------------------- 1 | import { createKeyMulti } from '@polkadot/util-crypto' 2 | import { u8aToHex } from '@polkadot/util' 3 | 4 | export const getMultisigPubKey = (signatories: (string | Uint8Array)[], threshold: number) => { 5 | const key = createKeyMulti(signatories, threshold) 6 | 7 | return u8aToHex(key) 8 | } 9 | -------------------------------------------------------------------------------- /squid/src/util/getOriginAccount.ts: -------------------------------------------------------------------------------- 1 | export function getOriginAccount(origin: any): string { 2 | if (origin && origin.__kind === 'system' && origin.value.__kind === 'Signed') { 3 | const id = origin.value.value 4 | if (id.__kind === 'Id') { 5 | return id.value 6 | } else { 7 | return id 8 | } 9 | } else { 10 | throw new Error('Unexpected origin') 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /squid/src/util/getProxyAccountIByDelegatorIds.ts: -------------------------------------------------------------------------------- 1 | import { ProxyAccount } from '../model' 2 | import { Ctx } from '../main' 3 | 4 | export const getProxyAccountIByDelegatorIds = async (ctx: Ctx, delegatorIds: string[]) => { 5 | const proxyAccountIds: Set = new Set() 6 | 7 | for (const id of delegatorIds) { 8 | // Find all the ProxyAccounts that this account is a delegator for 9 | const proxyAccountToAdd = await ctx.store.findBy(ProxyAccount, { delegator: { id: id } }) 10 | // ctx.log.info(`---> Found one: ${proxyAccounts.map(a => JsonLog(a))} `) 11 | proxyAccountToAdd.forEach((proxyAccount) => proxyAccountIds.add(proxyAccount.id)) 12 | } 13 | 14 | return Array.from(proxyAccountIds) 15 | } 16 | -------------------------------------------------------------------------------- /squid/src/util/getProxyAccountId.ts: -------------------------------------------------------------------------------- 1 | import { ProxyType } from '../model' 2 | 3 | export const getProxyAccountId = ( 4 | delegateePubKey?: string, 5 | delegatorPubKey?: string, 6 | type?: ProxyType, 7 | delay = 0, 8 | chainId?: string 9 | ) => { 10 | if (!delegateePubKey || !delegatorPubKey || !type || !chainId) { 11 | throw new Error( 12 | `getProxyAccountId error - one of these is undefined: delegatee ${delegateePubKey}, delegator ${delegatorPubKey}, type: ${type}, chainId:${chainId}` 13 | ) 14 | } 15 | 16 | return `${chainId}-${delegateePubKey.substring(20)}-${delegatorPubKey.substring(20)}-${type}-${delay}` 17 | } 18 | -------------------------------------------------------------------------------- /squid/src/util/getProxyInfoFromArgs.ts: -------------------------------------------------------------------------------- 1 | import { getProxyTypeFromRaw } from './getProxyTypeFromRaw' 2 | import { getProxyAccountId } from './getProxyAccountId' 3 | import { Ctx, fields } from '../main' 4 | import { ProxyType } from '../types/v9111' 5 | import { JsonLog } from './JsonLog' 6 | import { Event } from '@subsquid/substrate-processor' 7 | 8 | interface Params { 9 | event: Event 10 | chainId: string 11 | ctx: Ctx 12 | } 13 | export const getProxyInfoFromArgs = ({ event, chainId, ctx }: Params) => { 14 | let delegator: Uint8Array | undefined 15 | let delegatee: Uint8Array | undefined 16 | let proxyType: ProxyType 17 | let delay: number = 0 18 | 19 | const args = event.args 20 | 21 | if (Array.isArray(args)) { 22 | ;[delegator, delegatee, proxyType, delay] = args 23 | } else if (args.delegator) { 24 | ;({ delegator, delegatee, proxyType, delay } = args) 25 | } else { 26 | ctx.log.error(`The proxy could not be determined ${JsonLog(event)}`) 27 | return 28 | } 29 | const _delegator = delegator?.toString() || '' 30 | const _delegatee = delegatee?.toString() || '' 31 | const _type = getProxyTypeFromRaw(proxyType.__kind) 32 | const _delay = Number(delay) 33 | const _id = getProxyAccountId(_delegatee, _delegator, _type, _delay, chainId) 34 | 35 | return { 36 | id: _id, 37 | delegator: _delegator, 38 | delegatee: _delegatee, 39 | type: _type, 40 | delay: _delay 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /squid/src/util/getProxyKillPureArgs.ts: -------------------------------------------------------------------------------- 1 | import { Call } from '@subsquid/substrate-processor' 2 | 3 | export interface KillPureCallInfo { 4 | spawnerPubKey: string 5 | extrinsicIndex: number 6 | blockNumber: number 7 | } 8 | 9 | export const getProxyKillPureArgs = (proxyKillArgs: Call['args']) => { 10 | return { 11 | extrinsicIndex: proxyKillArgs.extIndex, 12 | blockNumber: proxyKillArgs.height, 13 | spawnerPubKey: proxyKillArgs.spawner.value || proxyKillArgs.spawner.id || proxyKillArgs.spawner 14 | } as KillPureCallInfo 15 | } 16 | -------------------------------------------------------------------------------- /squid/src/util/getProxyTypeFromRaw.ts: -------------------------------------------------------------------------------- 1 | import { ProxyType } from '../model' 2 | 3 | export const getProxyTypeFromRaw = (proxyType: string) => { 4 | if (Object.values(ProxyType).some((type: string) => type === proxyType)) 5 | return proxyType 6 | else return ProxyType.Unknown 7 | } 8 | -------------------------------------------------------------------------------- /squid/src/util/getPureProxyInfoFromArgs.ts: -------------------------------------------------------------------------------- 1 | import { Event } from '@subsquid/substrate-processor' 2 | import { getProxyAccountId } from './getProxyAccountId' 3 | import { Ctx, fields } from '../main' 4 | import { getProxyTypeFromRaw } from './getProxyTypeFromRaw' 5 | import { ProxyType as ProxyTypeV2005 } from '../types/v2005' 6 | import { JsonLog } from './JsonLog' 7 | 8 | interface Params { 9 | event: Event 10 | chainId: string 11 | isAnonymous: boolean 12 | ctx: Ctx 13 | } 14 | 15 | export const getPureProxyInfoFromArgs = ({ event, chainId, isAnonymous, ctx }: Params) => { 16 | let pure: Uint8Array | undefined 17 | let who: Uint8Array | undefined 18 | let proxyType: ProxyTypeV2005 19 | let disambiguationIndex: number = 0 20 | 21 | const args = event.args 22 | if (isAnonymous && Array.isArray(args)) { 23 | ;[pure, who, proxyType, disambiguationIndex] = args 24 | } else if (isAnonymous && !!args?.anonymous) { 25 | ;({ anonymous: pure, who, proxyType, disambiguationIndex } = args) 26 | } else if (!isAnonymous && !!args?.pure) { 27 | ;({ pure, who, proxyType, disambiguationIndex } = args) 28 | } else { 29 | ctx.log.error(`The pure proxy could not be determined ${JsonLog(event)}`) 30 | return 31 | } 32 | 33 | const _who = who?.toString() || '' 34 | const _pure = pure?.toString() || '' 35 | const _type = getProxyTypeFromRaw(proxyType.__kind) 36 | const id = getProxyAccountId(_who, _pure, _type, disambiguationIndex, chainId) 37 | 38 | return { 39 | id, 40 | who: _who, 41 | pure: _pure, 42 | delay: disambiguationIndex, 43 | type: _type 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /squid/src/util/index.ts: -------------------------------------------------------------------------------- 1 | export { getAccountMultisigId } from './getAccountMultisigId' 2 | export { getMultisigPubKey } from './getMultisigPubKey' 3 | export { getMultisigCallId } from './getMultisigCallId' 4 | export { getOrCreateAccounts } from './entities' 5 | export { JsonLog } from './JsonLog' 6 | export { getOriginAccount } from './getOriginAccount' 7 | export { getProxyInfoFromArgs } from './getProxyInfoFromArgs' 8 | export { getPureProxyInfoFromArgs } from './getPureProxyInfoFromArgs' 9 | -------------------------------------------------------------------------------- /squid/src/util/shouldReplicate.ts: -------------------------------------------------------------------------------- 1 | import { replicationGroups } from '../constants' 2 | 3 | export const shouldReplicateOn = (chainId: string) => { 4 | const result = Object.entries(replicationGroups).find(([, group]) => { 5 | return group.find((id) => id === chainId) 6 | }) 7 | 8 | // if we found 1 network containing the chainId 9 | // then the multisig should be replicated among all 10 | // the chains in the group 11 | if (result) { 12 | return result[1] 13 | } 14 | 15 | return 16 | } 17 | -------------------------------------------------------------------------------- /squid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "outDir": "lib", 6 | "rootDir": "src", 7 | "strict": true, 8 | "sourceMap": true, 9 | "esModuleInterop": true, 10 | "experimentalDecorators": true, 11 | "emitDecoratorMetadata": true, 12 | "skipLibCheck": true 13 | }, 14 | "include": ["src", ".prettierrc.js"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------