├── .eslintignore ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── custom.md │ └── feature_request.md ├── actions │ └── prepare │ │ └── action.yml └── workflows │ ├── build.yml │ ├── codeql-analysis.yml │ ├── e2e-tests.yml │ ├── unit-tests.yml │ └── validate-pr-title.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .prettierignore ├── .prettierrc ├── .yarn └── releases │ └── yarn-4.6.0.cjs ├── .yarnrc.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FAUCETS.md ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress ├── e2e │ ├── contracts │ │ ├── erc20.spec.ts │ │ ├── flipper.spec.ts │ │ ├── mother.spec.ts │ │ └── storage_types.spec.ts │ ├── extension.spec.ts │ ├── instantiateDryRun.spec.ts │ └── updateMetadata.spec.ts ├── fixtures │ ├── 4.0.1 │ │ ├── erc20.contract │ │ ├── flipper.contract │ │ ├── mother.contract │ │ └── multisig.contract │ ├── erc20.contract │ ├── flipper.contract │ ├── mother.contract │ ├── multisig.contract │ └── storage_types.contract ├── plugins │ └── index.cjs ├── support │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ ├── e2e.ts │ └── util.ts └── tsconfig.json ├── icons ├── app.webmanifest ├── apple-touch-icon.png ├── icon-192x192.png ├── icon-32x32.png └── icon-512x512.png ├── index.html ├── netlify.toml ├── package.json ├── postcss.config.cjs ├── snapshots.js ├── src ├── constants │ └── index.ts ├── lib │ ├── blockTime.ts │ ├── bn.ts │ ├── callOptions.ts │ ├── fileToFileState.ts │ ├── formatBalance.test.ts │ ├── formatBalance.ts │ ├── formatUInt.ts │ ├── formatWeight.test.ts │ ├── formatWeight.ts │ ├── getContractFromPatron.ts │ ├── hasRevertFlag.ts │ ├── initValue.ts │ ├── output.ts │ └── util.ts ├── services │ ├── chain │ │ ├── chainProps.ts │ │ ├── contract.ts │ │ └── index.ts │ └── db │ │ └── index.ts ├── types │ ├── db.ts │ ├── index.ts │ ├── substrate.ts │ └── ui │ │ ├── components.ts │ │ ├── contexts.ts │ │ ├── contract.ts │ │ ├── hooks.ts │ │ ├── index.ts │ │ └── util.ts └── ui │ ├── components │ ├── App.tsx │ ├── AwaitApis.tsx │ ├── CheckBrowserSupport.tsx │ ├── Transactions.tsx │ ├── account │ │ ├── Account.tsx │ │ ├── Identicon.tsx │ │ ├── Select.tsx │ │ └── index.ts │ ├── common │ │ ├── AccountsError.tsx │ │ ├── Button.tsx │ │ ├── ConnectionError.tsx │ │ ├── CopyButton.tsx │ │ ├── Dropdown.tsx │ │ ├── Error.tsx │ │ ├── HeaderButtons.tsx │ │ ├── Info.tsx │ │ ├── Loader.tsx │ │ ├── Meter.tsx │ │ ├── NoticeBanner.tsx │ │ ├── NotificationIcon.tsx │ │ ├── ObservedBalance.tsx │ │ ├── SearchResults.tsx │ │ ├── SidePanel.tsx │ │ ├── Spinner.tsx │ │ ├── Switch.tsx │ │ ├── Tabs.tsx │ │ ├── UnsupportedBrowserMessage.tsx │ │ └── index.ts │ ├── contract │ │ ├── ContractRow.tsx │ │ ├── DryRunError.tsx │ │ ├── DryRunResult.tsx │ │ ├── Interact.tsx │ │ ├── MetadataTab.tsx │ │ ├── OutcomeItem.tsx │ │ ├── ResultsOutput.tsx │ │ ├── TransactionResult.tsx │ │ └── index.ts │ ├── form │ │ ├── ArgumentForm.tsx │ │ ├── Bool.tsx │ │ ├── Enum.tsx │ │ ├── FormField.tsx │ │ ├── Input.tsx │ │ ├── InputBalance.tsx │ │ ├── InputBn.tsx │ │ ├── InputBytes.tsx │ │ ├── InputFile.tsx │ │ ├── InputHash.tsx │ │ ├── InputHex.tsx │ │ ├── InputNumber.tsx │ │ ├── InputSalt.tsx │ │ ├── InputStorageDepositLimit.tsx │ │ ├── InputWeight.tsx │ │ ├── Option.tsx │ │ ├── OptionsForm.tsx │ │ ├── Struct.tsx │ │ ├── SubForm.tsx │ │ ├── Tuple.tsx │ │ ├── Vector.tsx │ │ ├── VectorFixed.tsx │ │ ├── findComponent.tsx │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useMetadataField.tsx │ │ └── index.ts │ ├── homepage │ │ ├── Contracts.tsx │ │ ├── HelpBox.tsx │ │ ├── Statistics.tsx │ │ └── index.ts │ ├── index.ts │ ├── instantiate │ │ ├── AvailableCodeBundles.tsx │ │ ├── CodeHash.tsx │ │ ├── DryRun.tsx │ │ ├── LookUpCodeHash.tsx │ │ ├── Step1.tsx │ │ ├── Step2.tsx │ │ ├── Step3.tsx │ │ ├── Wizard.tsx │ │ └── index.ts │ ├── message │ │ ├── ArgSignature.tsx │ │ ├── MessageDocs.tsx │ │ ├── MessageSignature.tsx │ │ └── index.ts │ ├── metadata │ │ ├── GetPatronMetadata.ts │ │ ├── Metadata.tsx │ │ └── index.ts │ ├── modal │ │ ├── ForgetAllContractsModal.tsx │ │ ├── ForgetContractModal.tsx │ │ ├── HelpModal.tsx │ │ ├── Logos.tsx │ │ ├── ModalBase.tsx │ │ ├── SettingsModal.tsx │ │ └── index.ts │ └── settings │ │ ├── CustomEndpoint.tsx │ │ ├── ThemeMode.tsx │ │ └── index.ts │ ├── contexts │ ├── ApiContext.tsx │ ├── DatabaseContext.tsx │ ├── InstantiateContext.tsx │ ├── ThemeContext.tsx │ ├── TransactionsContext.tsx │ └── index.tsx │ ├── hooks │ ├── index.ts │ ├── useAccountAvailable.ts │ ├── useArgValues.ts │ ├── useBalance.ts │ ├── useDbQuery.ts │ ├── useFormField.ts │ ├── useIsMounted.ts │ ├── useLocalStorage.ts │ ├── useMetadata.ts │ ├── useNewContract.ts │ ├── useNonEmptyString.ts │ ├── useStorageDepositLimit.ts │ ├── useStoredContract.ts │ ├── useStoredMetadata.tsx │ ├── useToggle.ts │ └── useWeight.ts │ ├── index.tsx │ ├── layout │ ├── RootLayout.tsx │ ├── index.ts │ └── sidebar │ │ ├── Footer.tsx │ │ ├── MobileMenu.tsx │ │ ├── NavLink.tsx │ │ ├── Navigation.tsx │ │ ├── NetworkAndUser.tsx │ │ ├── QuickLinks.tsx │ │ └── index.tsx │ ├── pages │ ├── AddContract.tsx │ ├── AddressLookup.tsx │ ├── Contract.tsx │ ├── ContractHeader.tsx │ ├── Homepage.tsx │ ├── Instantiate.tsx │ ├── NotFound.tsx │ ├── SelectCodeHash.tsx │ └── index.ts │ ├── styles │ ├── base-typography.css │ ├── base.css │ ├── button.css │ ├── call-results.css │ ├── collapsible.css │ ├── dropdown.css │ ├── form.css │ ├── instantiate.css │ ├── link.css │ ├── main.css │ ├── search.css │ ├── sidebar.css │ └── tabs.css │ └── util │ └── dropdown.tsx ├── tailwind.config.cjs ├── tsconfig.json ├── vite.config.ts └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | *.cjs 2 | dist 3 | node_modules 4 | cypress 5 | *.config.ts 6 | *.html 7 | coverage 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Environment (please complete the following information where it makes sense):** 33 | 34 | - contract version: [e.g. ink! v3.4.0] 35 | - cargo-contract version: [e.g. cargo-contract v1.5.1] 36 | - chain name or dev node version [e.g. "Contracts on Rococo" or substrate-contracts-node v0.23.0] 37 | - rust toolchain [e.g. stable-x86_64-apple-darwin] 38 | - rustc version [e. g. rustc 1.66.0] 39 | 40 | **Additional context** 41 | Add any other context about the problem here. 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/actions/prepare/action.yml: -------------------------------------------------------------------------------- 1 | name: Prepare 2 | description: Prepare test environment 3 | runs: 4 | using: composite 5 | steps: 6 | - name: Download node artifact 7 | uses: actions/download-artifact@v4 8 | with: 9 | name: substrate-contracts-node 10 | path: ./ 11 | 12 | - name: Start local node 13 | shell: bash 14 | run: | 15 | tar -xvzf substrate-contracts-node-linux.tar.gz 16 | cd substrate-contracts-node-linux/ 17 | chmod +x ./substrate-contracts-node 18 | ./substrate-contracts-node --dev & -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build project' 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | install: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | 20 | - name: Install and build 21 | run: | 22 | yarn 23 | yarn build 24 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: 'Unit Tests' 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | vitest-tests: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Setup node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: 20 19 | 20 | - name: Install and test 21 | run: | 22 | yarn 23 | yarn vitest run 24 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr-title.yml: -------------------------------------------------------------------------------- 1 | name: 'Validate PR title' 2 | 3 | on: 4 | pull_request: 5 | branches: [master] 6 | pull_request_target: 7 | types: 8 | - opened 9 | - edited 10 | - synchronize 11 | 12 | jobs: 13 | main: 14 | name: Validate PR title 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: amannn/action-semantic-pull-request@v5 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | .idea 121 | 122 | # SQL 123 | *.sqlite 124 | *.sqlite-journal 125 | .fuse_* 126 | .DS_Store 127 | cypress/screenshots 128 | cypress/videos -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | .idea 121 | .vscode 122 | 123 | # SQL 124 | *.sqlite 125 | *.sqlite-journal 126 | .fuse_* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "printWidth": 100, 4 | "arrowParens": "avoid", 5 | "singleQuote": true, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "jsxBracketSameLine": false, 9 | "jsxSingleQuote": false, 10 | "semi": true, 11 | "bracketSpacing": true, 12 | "plugins": ["prettier-plugin-tailwindcss"] 13 | } 14 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: mixed 2 | 3 | enableGlobalCache: false 4 | 5 | nodeLinker: node-modules 6 | 7 | yarnPath: .yarn/releases/yarn-4.6.0.cjs 8 | -------------------------------------------------------------------------------- /FAUCETS.md: -------------------------------------------------------------------------------- 1 | # Testnet Token Faucets 2 | 3 | Rococo: 4 | 5 | - https://paritytech.github.io/polkadot-testnet-faucet/ 6 | 7 | - https://github.com/paritytech/cumulus/blob/master/parachains/runtimes/contracts/contracts-rococo/README.md#rococo-deployment 8 | 9 | - https://app.element.io/#/room/#rococo-faucet:matrix.org 10 | 11 | Paseo: 12 | 13 | - https://faucet.polkadot.io/paseo 14 | 15 | Aleph Zero Testnet: 16 | 17 | - https://faucet.test.azero.dev/ 18 | 19 | - https://docs.alephzero.org/aleph-zero/build/setting-up-testnet-account 20 | 21 | Shibuya: 22 | 23 | - https://docs.astar.network/docs/build/environment/faucet/ 24 | 25 | - https://faucet.triangleplatform.com/astar/shibuya 26 | 27 | - https://www.as-faucet.xyz/en 28 | 29 | Phala PoC 5: 30 | 31 | - https://wiki.phala.network/en-us/build/getting-started/deploy-contract/#claim-test-tokens 32 | 33 | Pop Network: 34 | 35 | - https://onboard.popnetwork.xyz/ 36 | 37 | T3RN t0rn: 38 | 39 | - https://faucet.t0rn.io/ 40 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { defineConfig } from 'cypress'; 5 | 6 | export default defineConfig({ 7 | projectId: 'eup7bh', 8 | e2e: { 9 | specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', 10 | baseUrl: 'http://127.0.0.1:8081/', 11 | testIsolation: false, 12 | async setupNodeEvents(on, config) { 13 | try { 14 | const task = await import('@cypress/code-coverage/task'); 15 | task.default(on, config); 16 | } catch (err) { 17 | console.warn('[WARN] Code coverage task not loaded:', err?.message ?? err); 18 | } 19 | 20 | return config; 21 | }, 22 | }, 23 | 24 | component: { 25 | devServer: { 26 | framework: 'react', 27 | bundler: 'vite', 28 | }, 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /cypress/e2e/contracts/flipper.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2021 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: Apache-2.0 3 | 4 | import { 5 | beforeAllContracts, 6 | assertUpload, 7 | assertMoveToStep2, 8 | assertMoveToStep3, 9 | assertContractRedirect, 10 | assertInstantiate, 11 | assertReturnValue, 12 | selectMessage, 13 | assertDryRun, 14 | } from '../../support/util'; 15 | 16 | describe('Flipper Contract ', () => { 17 | const timeout = 25000; 18 | 19 | before(() => { 20 | beforeAllContracts(); 21 | }); 22 | 23 | it('contract file uploads', () => { 24 | assertUpload('flipper.contract'); 25 | }); 26 | 27 | it('moves to step 2', () => { 28 | assertMoveToStep2(); 29 | }); 30 | 31 | it('sets the init value to true', () => { 32 | cy.get('.form-field.initValue').click().find('.dropdown__option').eq(1).click(); 33 | cy.get('.form-field.initValue').find('.dropdown__single-value').should('contain', 'true'); 34 | }); 35 | 36 | it('moves to step 3', () => { 37 | assertMoveToStep3(); 38 | }); 39 | 40 | it(`submits instantiate transaction`, () => { 41 | assertInstantiate(); 42 | }); 43 | it('redirects to contract page after instantiation', () => { 44 | assertContractRedirect(); 45 | }); 46 | it('calling get() returns true', () => { 47 | selectMessage('get', 1); 48 | assertReturnValue('get', 'true'); 49 | }); 50 | it(`submits flip() transaction`, () => { 51 | selectMessage('flip', 0); 52 | assertDryRun(); 53 | cy.contains('Call contract').click(); 54 | cy.get('[data-cy="transaction-complete"]', { timeout }) 55 | .should('be.visible') 56 | .and('contain', 'system:ExtrinsicSuccess') 57 | .and('contain', 'balances:Withdraw'); 58 | }); 59 | it('calling get() returns false', () => { 60 | selectMessage('get', 1); 61 | assertReturnValue('get', 'false'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /cypress/e2e/contracts/storage_types.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { 5 | beforeAllContracts, 6 | assertUpload, 7 | assertMoveToStep2, 8 | assertMoveToStep3, 9 | assertContractRedirect, 10 | assertInstantiate, 11 | selectMessage, 12 | } from '../../support/util'; 13 | 14 | describe('Storage Types Contract', () => { 15 | before(() => { 16 | beforeAllContracts(); 17 | }); 18 | 19 | it('contract file uploads', () => { 20 | assertUpload('storage_types.contract'); 21 | }); 22 | 23 | it('moves to step 2', () => { 24 | assertMoveToStep2(); 25 | }); 26 | 27 | it('moves to step 3', () => { 28 | assertMoveToStep3(); 29 | }); 30 | 31 | it('submits instantiate transaction', () => { 32 | assertInstantiate(); 33 | }); 34 | 35 | it('redirects to contract page after instantiation', () => { 36 | assertContractRedirect(); 37 | }); 38 | 39 | [ 40 | 'getUnsignedIntegers', 41 | 'getSignedIntegers', 42 | 'getInkPreludeTypes', 43 | 'getSubstrateTypes', 44 | 'getPrimitiveTypes', 45 | 'getOptionSome', 46 | 'getOptionNone', 47 | 'getResultOk', 48 | 'getResultError', 49 | 'getPanic', 50 | ].forEach((message, index) => { 51 | it(`DryRun ${message}`, () => { 52 | cy.get('.form-field.caller').click().find('.dropdown__option').eq(2).click(); 53 | selectMessage(message, index); 54 | 55 | cy.get('[data-cy="output"]').find('code').snapshot(); 56 | }); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /cypress/e2e/extension.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | interface Window { 5 | injectedWeb3: any; 6 | } 7 | 8 | describe('Signer extension flow on live networks', () => { 9 | before(() => { 10 | cy.visit(`/instantiate/?rpc=wss://rpc2.paseo.popnetwork.xyz`); 11 | }); 12 | 13 | it('connects to Rococo', () => { 14 | cy.contains('Connecting to wss://rpc2.paseo.popnetwork.xyz').should('not.exist', { 15 | timeout: 25000, 16 | }); 17 | }); 18 | 19 | it('Rococo is selected in the network connection dropdown', () => { 20 | cy.get('.dropdown.chain') 21 | .find('.dropdown__single-value') 22 | .should('contain', 'Pop Network Testnet'); 23 | }); 24 | 25 | it('Displays help text for no extension installed', () => { 26 | cy.get('[data-cy="error-card"]').within(() => { 27 | cy.contains('No signer extension found.').should('be.visible'); 28 | cy.contains('New to Substrate?').should('be.visible'); 29 | cy.contains( 30 | 'Install the a compatible wallet like Polkadot.js Extension to create and manage Substrate accounts.', 31 | ).should('be.visible'); 32 | cy.contains( 33 | 'If the extension is installed and you are seeing this, make sure it allows Contracts UI to use your accounts for signing.', 34 | ).should('be.visible'); 35 | }); 36 | }); 37 | it('Displays help text for no accounts found', () => { 38 | cy.visit('/').then(window => { 39 | window.injectedWeb3 = { 40 | 'polkadot-js': { 41 | version: '123', 42 | enable: () => Promise.resolve({ accounts: [] }), 43 | }, 44 | }; 45 | }); 46 | cy.get('[data-cy="error-card"]').within(() => { 47 | cy.contains('No accounts found.').should('be.visible'); 48 | cy.contains( 49 | '1. Follow this guide to create your first account in the Polkadot.js extension.', 50 | ).should('be.visible'); 51 | cy.contains( 52 | '2. Drip some funds into your account via the faucets of our supported networks.', 53 | ).should('be.visible'); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /cypress/e2e/instantiateDryRun.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | import { beforeAllContracts, assertUpload, assertMoveToStep2 } from '../support/util'; 4 | 5 | describe('Instantiate dry run', () => { 6 | before(() => { 7 | beforeAllContracts(); 8 | }); 9 | 10 | it('multisig contract uploads', () => { 11 | assertUpload('multisig.contract'); 12 | }); 13 | 14 | it('moves to step 2', () => { 15 | assertMoveToStep2(); 16 | }); 17 | 18 | it('displays dry run error and debug message', () => { 19 | // initial multisig dry run is expected to return an error because requirement input value = 0 20 | cy.get('[data-cy="dry-run-result"]').within(() => { 21 | cy.contains('ContractTrapped').should('be.visible'); 22 | cy.contains('Contract trapped during execution.').should('be.visible'); 23 | cy.contains( 24 | "panicked at 'assertion failed: 0 < requirement && requirement <= owners && owners <= MAX_OWNERS", 25 | ).should('be.visible'); 26 | }); 27 | }); 28 | 29 | it('next button is disabled', () => { 30 | cy.get('[data-cy="next-btn"]').should('be.disabled'); 31 | }); 32 | 33 | it('displays dry run estimations after adjusting ', () => { 34 | cy.get('.form-field.requirement').find('input[type="number"]').type('1'); 35 | cy.get('[data-cy="dry-run-result"]').within(() => { 36 | cy.contains('The instantiation will be successful.').should('be.visible'); 37 | cy.get('[data-cy="estimated-storage-deposit"]') 38 | .should('not.contain', 'None') 39 | .and('not.be.empty'); 40 | cy.get('[data-cy="dry-run-estimations"]').should('not.contain', 'None').and('not.be.empty'); 41 | cy.get('[data-cy="dry-run-account"]') 42 | .find('[data-cy="identicon"]') 43 | .should('have.lengthOf', 1); 44 | }); 45 | cy.get('[data-cy="account-address"]').should('not.be.empty').and('contain', '...'); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /cypress/e2e/updateMetadata.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { beforeAllContracts, deploy } from '../support/util'; 5 | 6 | describe('Update contract metadata', () => { 7 | const messages1 = ['new', 'newDefault', 'failedNew', 'echoAuction', 'revertOrTrap', 'debugLog']; 8 | const messages2 = ['new', 'newDefault', 'flip', 'get']; 9 | 10 | before(() => { 11 | beforeAllContracts(); 12 | deploy('mother.contract'); 13 | }); 14 | it('displays a list of docs for the contract messages', () => { 15 | cy.get('[data-cy="contract-page-tabs"]').within(() => { 16 | cy.contains('Metadata').click(); 17 | }); 18 | cy.get('[data-cy="message-docs"]').each((item, i, list) => { 19 | expect(list).to.have.length(6); 20 | expect(Cypress.$(item).text()).to.contain(messages1[i]); 21 | }); 22 | cy.contains('No documentation provided').should('be.visible'); 23 | cy.contains('Demonstrates the ability to fail a constructor safely').should('be.visible'); 24 | cy.contains('Takes an auction data struct as input and returns it back.').should('be.visible'); 25 | cy.contains('Update metadata').should('be.disabled'); 26 | }); 27 | it('uploads a different metadata file', () => { 28 | cy.get('[data-cy="file-input"]').attachFile('flipper.contract'); 29 | cy.contains('Update metadata').should('not.be.disabled').click(); 30 | }); 31 | it('displays the docs for the new metadata', () => { 32 | cy.get('[data-cy="message-docs"]').each((item, i, list) => { 33 | expect(list).to.have.length(4); 34 | expect(Cypress.$(item).text()).to.contain(messages2[i]); 35 | }); 36 | }); 37 | it('clears the file input', () => { 38 | cy.contains('Click to select or drag and drop to upload file.') 39 | .scrollIntoView() 40 | .should('be.visible'); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /cypress/plugins/index.cjs: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | }; 23 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import 'cypress-file-upload'; 3 | require('@cypress/snapshot').register(); 4 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | 3 | import { mount } from 'cypress/react'; 4 | declare global { 5 | namespace Cypress { 6 | interface Chainable { 7 | mount: typeof mount; 8 | } 9 | } 10 | } 11 | 12 | Cypress.Commands.add('mount', mount); 13 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | import '@cypress/code-coverage/support'; 3 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress", "node", "cypress-file-upload"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /icons/app.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contracts UI", 3 | "short_name": "ContractsUI", 4 | "display": "standalone", 5 | "background_color": "#1a1d1f", 6 | "description": "Substrate Contracts UI.", 7 | "icons": [ 8 | { 9 | "src": "icon-32x32.png", 10 | "type": "image/png", 11 | "sizes": "32x32" 12 | }, 13 | { 14 | "src": "icon-192x192.png", 15 | "type": "image/png", 16 | "sizes": "192x192" 17 | }, 18 | { 19 | "src": "icon-512x512.png", 20 | "type": "image/png", 21 | "sizes": "512x512" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/contracts-ui/88477b8aee6e09c36e090f91bf5ed9bb91a66cc1/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/contracts-ui/88477b8aee6e09c36e090f91bf5ed9bb91a66cc1/icons/icon-192x192.png -------------------------------------------------------------------------------- /icons/icon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/contracts-ui/88477b8aee6e09c36e090f91bf5ed9bb91a66cc1/icons/icon-32x32.png -------------------------------------------------------------------------------- /icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/use-ink/contracts-ui/88477b8aee6e09c36e090f91bf5ed9bb91a66cc1/icons/icon-512x512.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Substrate Contracts UI 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | status = 200 4 | to = "/index.html" 5 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [require('postcss-import'), require('tailwindcss'), require('autoprefixer')], 3 | }; 4 | -------------------------------------------------------------------------------- /snapshots.js: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | module.exports = { 5 | __version: '13.13.3', 6 | 'Storage Types Contract': { 7 | 'DryRun getUnsignedIntegers': { 8 | 1: "{\n u128ValueMax: '340,282,366,920,938,463,463,374,607,431,768,211,455',\n u128ValueMin: '0',\n u16ValueMax: '65,535',\n u16ValueMin: '0',\n u32ValueMax: '4,294,967,295',\n u32ValueMin: '0',\n u64ValueMax: '18,446,744,073,709,551,615',\n u64ValueMin: '0',\n u8ValueMax: '255',\n u8ValueMin: '0',\n }", 9 | }, 10 | 'DryRun getSignedIntegers': { 11 | 1: "{\n i128ValueMax: '170,141,183,460,469,231,731,687,303,715,884,105,727',\n i128ValueMin: '-170,141,183,460,469,231,731,687,303,715,884,105,728',\n i16ValueMax: '32,767',\n i16ValueMin: '-32,768',\n i32ValueMax: '2,147,483,647',\n i32ValueMin: '-2,147,483,648',\n i64ValueMax: '9,223,372,036,854,775,807',\n i64ValueMin: '-9,223,372,036,854,775,808',\n i8ValueMax: '127',\n i8ValueMin: '-128',\n }", 12 | }, 13 | 'DryRun getInkPreludeTypes': { 14 | 1: "{\n stringValue: 'This is a string',\n vecStringValue: [\n 'This is a String',\n 'This is another String',\n ],\n vecVecStringValue: [\n [\n 'This is a String',\n 'This is another String',\n ],\n ],\n }", 15 | }, 16 | 'DryRun getSubstrateTypes': { 17 | 1: "{\n accountIdValue: '5C4hrfjw9DjXZTzV3MwzrrAr9P1MJhSrvWGWqi1eSuyUpnhM',\n balanceValueMax: '340,282,366,920,938,463,463,374,607,431,768,211,455',\n balanceValueMin: '0',\n hashValue:\n '0x0000000000000000000000000000000000000000000000000000000000000000',\n }", 18 | }, 19 | 'DryRun getPrimitiveTypes': { 20 | 1: "{\n boolValue: true,\n enumWithoutValues: 'A',\n enumWithValues: {\n ThreeValues: [\n '1',\n '2',\n '3',\n ],\n },\n arrayValue: [\n '3',\n '2',\n '1',\n ],\n tupleValue: [\n '7',\n '8',\n ],\n }", 21 | }, 22 | 'DryRun getOptionNone': { 23 | 1: 'null', 24 | }, 25 | 'DryRun getResultError': { 26 | 1: "{\n ErrorWithMessage: 'This is the Error Message.',\n }", 27 | }, 28 | 'DryRun getPanic': { 29 | 1: 'ContractTrapped', 30 | }, 31 | 'DryRun getOptionSome': { 32 | 1: 'true', 33 | }, 34 | 'DryRun getResultOk': { 35 | 1: '{\n Ok: true,\n }', 36 | }, 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/lib/blockTime.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { BN } from 'bn.js'; 5 | import { BN_TWO } from './bn'; 6 | import { ApiPromise } from 'types'; 7 | 8 | const DEFAULT_TIME = new BN(6_000); 9 | 10 | export function blockTimeMs(a: ApiPromise) { 11 | return a.query.parachainSystem 12 | ? // default guess for a parachain 13 | DEFAULT_TIME.mul(BN_TWO) 14 | : // default guess for others 15 | DEFAULT_TIME; 16 | } 17 | -------------------------------------------------------------------------------- /src/lib/bn.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import BN from 'bn.js'; 5 | import { isNumber } from './util'; 6 | import { ApiPromise } from 'types'; 7 | 8 | export const BN_ZERO = new BN(0); 9 | 10 | export const BN_ONE = new BN(1); 11 | 12 | export const BN_TWO = new BN(2); 13 | 14 | export const BN_TEN = new BN(10); 15 | 16 | export const BN_HUNDRED = new BN(100); 17 | 18 | export const BN_THOUSAND = new BN(1000); 19 | 20 | export const BN_MILLION = new BN(1000000); 21 | 22 | export function isBn(value: unknown): value is typeof BN_ZERO { 23 | return BN.isBN(value); 24 | } 25 | export function fromBalance(value: BN | number | string | null): string { 26 | if (!value) { 27 | return ''; 28 | } 29 | 30 | return value.toString(); 31 | } 32 | 33 | export function toBalance(api: ApiPromise, value: string | number): BN { 34 | const asString = isNumber(value) ? value.toString() : value; 35 | const siPower = new BN(api.registry.chainDecimals[0]); 36 | 37 | const isDecimalValue = /^(\d+)\.(\d+)$/.exec(asString); 38 | 39 | if (isDecimalValue) { 40 | const div = new BN(asString.replace(/\.\d*$/, '')); 41 | const modString = asString.replace(/^\d+\./, '').substring(0, api.registry.chainDecimals[0]); 42 | const mod = new BN(modString); 43 | 44 | return div 45 | .mul(BN_TEN.pow(siPower)) 46 | .add(mod.mul(BN_TEN.pow(new BN(siPower.subn(modString.length))))); 47 | } else { 48 | return new BN(asString.replace(/[^\d]/g, '')).mul(BN_TEN.pow(siPower)); 49 | } 50 | } 51 | 52 | export function toSats(api: ApiPromise, balance: BN | number): BN { 53 | let bn: BN; 54 | 55 | if (isNumber(balance)) { 56 | bn = new BN(balance); 57 | } else { 58 | bn = balance; 59 | } 60 | 61 | return bn.mul(BN_TEN.pow(new BN(api.registry.chainDecimals[0]))); 62 | } 63 | 64 | export function fromSats(api: ApiPromise, sats: BN): string { 65 | const pow = BN_TEN.pow(new BN(api.registry.chainDecimals[0])); 66 | const [div, mod] = [sats.div(pow), sats.mod(pow)]; 67 | 68 | return `${div.toString()}${!mod.eqn(0) ? `.${mod.toString()}` : ''}`; 69 | } 70 | 71 | export function printBN(num: number | BN | bigint) { 72 | return new Intl.NumberFormat('en-US').format(num as bigint); 73 | } 74 | -------------------------------------------------------------------------------- /src/lib/callOptions.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { BN_ZERO } from './bn'; 5 | import { AbiParam, BN, ContractCallOutcome, Registry, UIStorageDeposit, WeightV2 } from 'types'; 6 | 7 | export function decodeStorageDeposit( 8 | storageDeposit: ContractCallOutcome['storageDeposit'], 9 | ): UIStorageDeposit { 10 | if (storageDeposit.isCharge) { 11 | return { value: storageDeposit.asCharge, type: 'charge' }; 12 | } else if (storageDeposit.isRefund) { 13 | return { value: storageDeposit.asRefund, type: 'refund' }; 14 | } 15 | return { 16 | type: 'empty', 17 | }; 18 | } 19 | 20 | export function getPredictedCharge(dryRun: UIStorageDeposit) { 21 | return dryRun.type === 'charge' 22 | ? !dryRun.value?.eq(BN_ZERO) 23 | ? dryRun.value ?? null 24 | : null 25 | : null; 26 | } 27 | 28 | export function getStorageDepositLimit( 29 | switchOn: boolean, 30 | userInput: BN, 31 | registry: Registry, 32 | dryRunValue?: UIStorageDeposit, 33 | ) { 34 | return switchOn 35 | ? registry.createType('Balance', userInput) 36 | : dryRunValue 37 | ? getPredictedCharge(dryRunValue) 38 | : null; 39 | } 40 | 41 | export function getGasLimit( 42 | switchOn: boolean, 43 | refTimeLimit: BN, 44 | proofSizeLimit: BN, 45 | registry: Registry, 46 | ): WeightV2 | null { 47 | return switchOn 48 | ? registry.createType('WeightV2', { 49 | refTime: refTimeLimit, 50 | proofSize: proofSizeLimit, 51 | }) 52 | : null; 53 | } 54 | 55 | export function transformUserInput( 56 | registry: Registry, 57 | messageArgs: AbiParam[], 58 | values?: Record, 59 | ): unknown[] { 60 | return messageArgs.map(({ name, type: { type } }) => { 61 | const value = values ? values[name] : null; 62 | 63 | if (type === 'Balance') { 64 | return registry.createType('Balance', value); 65 | } 66 | 67 | return value; 68 | }); 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/fileToFileState.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { FileState } from '../types'; 5 | 6 | export const fileToFileState = (file: File): Promise => 7 | new Promise((resolve, reject) => { 8 | const reader = new FileReader(); 9 | 10 | reader.onabort = reject; 11 | reader.onerror = reject; 12 | 13 | reader.onload = ({ target }: ProgressEvent): void => { 14 | if (target && target.result) { 15 | const name = file.name; 16 | const data = new Uint8Array(target.result as ArrayBuffer); 17 | const size = data.length; 18 | 19 | resolve({ 20 | data, 21 | name, 22 | size, 23 | } as FileState); 24 | } 25 | }; 26 | 27 | reader.readAsArrayBuffer(file); 28 | }); 29 | -------------------------------------------------------------------------------- /src/lib/formatBalance.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Balance } from '@polkadot/types/interfaces'; 5 | import type { FormattingOptions } from './formatUInt'; 6 | import { formatUInt } from './formatUInt'; 7 | 8 | const DEFAULT_OPTIONS: FormattingOptions = { 9 | decimals: 12, 10 | fractionDigits: 2, 11 | symbol: undefined, 12 | digitGrouping: true, 13 | }; 14 | 15 | export const formatBalance = (balance: Balance, partialOptions?: Partial) => { 16 | return formatUInt(balance, { ...DEFAULT_OPTIONS, ...partialOptions }); 17 | }; 18 | -------------------------------------------------------------------------------- /src/lib/formatUInt.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import type { Compact, UInt } from '@polkadot/types-codec'; 5 | 6 | export type FormattingOptions = { 7 | decimals: number; 8 | symbol: string | undefined; 9 | fractionDigits: number; 10 | digitGrouping: boolean; 11 | }; 12 | 13 | export const formatUInt = (value: UInt | Compact, options: FormattingOptions) => { 14 | if (options.decimals < 0) throw new Error('Decimals must be positive'); 15 | if (options.fractionDigits < 0) throw new Error('Fraction digits must be positive'); 16 | if (options.decimals < options.fractionDigits) 17 | throw new Error('Decimals must be greater than fraction digits'); 18 | 19 | const valueString = value.toString(); 20 | const integerDigits = valueString.split(''); 21 | 22 | let fractionalPart = ''.padStart(options.decimals, '0'); 23 | if (options.decimals !== 0) { 24 | const fractionDigits = integerDigits.splice(-options.decimals); 25 | fractionalPart = fractionDigits.join('').padStart(options.decimals, '0'); 26 | } 27 | 28 | let integerPart = integerDigits.length ? integerDigits.join('') : '0'; 29 | 30 | if (options.digitGrouping) { 31 | integerPart = Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format( 32 | BigInt(integerPart), 33 | ); 34 | } 35 | 36 | if (options.fractionDigits === 0) { 37 | return integerPart + (options.symbol ? ` ${options.symbol}` : ''); 38 | } else { 39 | return ( 40 | integerPart + 41 | '.' + 42 | fractionalPart.slice(0, options.fractionDigits) + 43 | (options.symbol ? ` ${options.symbol}` : '') 44 | ); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/lib/formatWeight.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { TypeRegistry } from '@polkadot/types'; 5 | import { beforeAll, describe, expect, it } from 'vitest'; 6 | import { formatProofSize, formatRefTime } from './formatWeight'; 7 | 8 | describe('formatProofSize', () => { 9 | let registry: TypeRegistry; 10 | 11 | beforeAll(() => { 12 | registry = new TypeRegistry(); 13 | }); 14 | 15 | it('should format edge cases correctly', () => { 16 | [ 17 | { value: 0n, expected: '0.00 MB' }, 18 | { value: 1_000_000n, expected: '1.00 MB' }, 19 | // u64::MAX value 20 | { 21 | value: 18_446_744_073_709_551_615n, 22 | expected: '18446744073709.55 MB', 23 | }, 24 | ].forEach(({ value, expected }) => { 25 | const proofSize = registry.createType('Compact', value); 26 | expect(formatProofSize(proofSize, 'MB')).toBe(expected); 27 | }); 28 | }); 29 | 30 | it('should format edge cases correctly', () => { 31 | [ 32 | { value: 0n, expected: '0 bytes' }, 33 | { value: 1_000_000n, expected: '1000000 bytes' }, 34 | // u64::MAX value 35 | { 36 | value: 18_446_744_073_709_551_615n, 37 | expected: '18446744073709551615 bytes', 38 | }, 39 | ].forEach(({ value, expected }) => { 40 | const proofSize = registry.createType('Compact', value); 41 | expect(formatProofSize(proofSize, 'bytes')).toBe(expected); 42 | }); 43 | }); 44 | }); 45 | 46 | describe('formatRefTime', () => { 47 | let registry: TypeRegistry; 48 | 49 | beforeAll(() => { 50 | registry = new TypeRegistry(); 51 | }); 52 | 53 | it('should format edge cases correctly', () => { 54 | [ 55 | { value: 0n, expected: '0.00 ms' }, 56 | { value: 123n, expected: '0.00 ms' }, 57 | { value: 123_000_000n, expected: '0.12 ms' }, 58 | { value: 1_000_000_000n, expected: '1.00 ms' }, 59 | // u64::MAX value 60 | { 61 | value: 18_446_744_073_709_551_615n, 62 | expected: '18446744073.70 ms', 63 | }, 64 | ].forEach(({ value, expected }) => { 65 | const refTime = registry.createType('Compact', value); 66 | expect(formatRefTime(refTime, 'ms')).toBe(expected); 67 | }); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /src/lib/formatWeight.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { WeightV2 } from '@polkadot/types/interfaces'; 5 | import { formatUInt } from './formatUInt'; 6 | 7 | /** 8 | * Formats given Reference Time value from expected picoseconds to milliseconds. 9 | * @param refTime 10 | * @returns formatted refTime in milliseconds, with 2 decimal places 11 | */ 12 | export function formatRefTime(refTime: WeightV2['refTime'], unit: 'ms' | 'ps' = 'ps'): string { 13 | switch (unit) { 14 | case 'ps': 15 | return formatUInt(refTime, { 16 | decimals: 0, 17 | digitGrouping: false, 18 | fractionDigits: 0, 19 | symbol: unit, 20 | }); 21 | case 'ms': 22 | return formatUInt(refTime, { 23 | decimals: 9, 24 | digitGrouping: false, 25 | fractionDigits: 2, 26 | symbol: unit, 27 | }); 28 | default: 29 | throw new Error('Unsupported unit'); 30 | } 31 | } 32 | 33 | /** 34 | * Formats given Proof Size value from expected bytes to megabytes. 35 | * @param refTime 36 | * @returns formatted refTime in megabytes, with 2 decimal places 37 | */ 38 | export function formatProofSize( 39 | proofSize: WeightV2['proofSize'], 40 | unit: 'MB' | 'bytes' | 'kb' = 'bytes', 41 | ): string { 42 | switch (unit) { 43 | case 'bytes': 44 | return formatUInt(proofSize, { 45 | decimals: 0, 46 | digitGrouping: false, 47 | fractionDigits: 0, 48 | symbol: unit, 49 | }); 50 | case 'kb': 51 | return formatUInt(proofSize, { 52 | decimals: 3, 53 | digitGrouping: false, 54 | fractionDigits: 2, 55 | symbol: unit, 56 | }); 57 | case 'MB': 58 | return formatUInt(proofSize, { 59 | decimals: 6, 60 | digitGrouping: false, 61 | fractionDigits: 2, 62 | symbol: unit, 63 | }); 64 | default: 65 | throw new Error('Unsupported unit'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/getContractFromPatron.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Buffer } from 'buffer'; 5 | 6 | function getFromPatron(field: string, hash: string) { 7 | const options = { 8 | method: 'GET', 9 | headers: 10 | field === 'metadata' 11 | ? { 12 | 'Content-Type': 'application/json', 13 | } 14 | : { 15 | 'Content-Type': 'text/plain; charset=UTF-8', 16 | }, 17 | mode: 'cors' as RequestMode, 18 | }; 19 | 20 | return fetch('https://api.patron.works/buildSessions/' + field + '/' + hash, options).then( 21 | response => { 22 | if (!response.ok) { 23 | throw new Error(`HTTP error! Status: ${response.status}`); 24 | } 25 | return field === 'metadata' ? response.json() : response.arrayBuffer(); 26 | }, 27 | ); 28 | } 29 | 30 | export function getContractFromPatron(codeHash: string): Promise { 31 | const metadataPromise = getFromPatron('metadata', codeHash); 32 | const wasmPromise = getFromPatron('wasm', codeHash); 33 | return Promise.all([metadataPromise, wasmPromise]).then(([metadataResponse, wasmResponse]) => { 34 | const result = Buffer.from(wasmResponse as ArrayBuffer).toString('hex'); 35 | 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 37 | metadataResponse.source.wasm = '0x' + result; 38 | 39 | const metadataString = JSON.stringify(metadataResponse); 40 | 41 | const blob = new Blob([metadataString], { type: 'application/json' }); 42 | const patronFile = new File([blob], 'patron-contract.json', { 43 | lastModified: new Date().getTime(), 44 | type: 'json', 45 | }); 46 | return patronFile; 47 | }); 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/hasRevertFlag.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { ContractInstantiateResult } from 'types'; 5 | 6 | export function hasRevertFlag(dryRunResult: ContractInstantiateResult | undefined) { 7 | return dryRunResult?.result.isOk ? dryRunResult.result.asOk.result.flags.isRevert : false; 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { decodeAddress, encodeAddress } from '@polkadot/keyring'; 5 | import { hexToU8a, isHex } from '@polkadot/util'; 6 | import { keyring } from '@polkadot/ui-keyring'; 7 | import format from 'date-fns/format'; 8 | import parseISO from 'date-fns/parseISO'; 9 | import { twMerge } from 'tailwind-merge'; 10 | 11 | export const classes = twMerge; 12 | 13 | export function truncate(value: string | undefined, sideLength = 6): string { 14 | return value 15 | ? value.length > sideLength * 2 16 | ? `${value.substring(0, sideLength)}...${value.substring(value.length - sideLength)}` 17 | : value 18 | : ''; 19 | } 20 | 21 | export function displayDate(isoDateString: string, formatString = 'd MMM'): string { 22 | return format(parseISO(isoDateString), formatString); 23 | } 24 | 25 | export function isValidCodeHash(value: string): boolean { 26 | return /^0x[0-9a-fA-F]{64}$/.test(value); 27 | } 28 | 29 | export function isEmptyObj(value: unknown) { 30 | return JSON.stringify(value) === '{}'; 31 | } 32 | export function randomAsU8a(length = 32) { 33 | return crypto.getRandomValues(new Uint8Array(length)); 34 | } 35 | 36 | export const NOOP = (): void => undefined; 37 | 38 | export function isValidWsUrl(s: unknown) { 39 | if (typeof s === 'string') { 40 | let url; 41 | try { 42 | url = new URL(s); 43 | } catch (_) { 44 | return false; 45 | } 46 | 47 | return url.protocol === 'ws:' || url.protocol === 'wss:'; 48 | } 49 | return false; 50 | } 51 | 52 | export const genRanHex: (size?: number) => `0x${string}` = (size = 32) => 53 | `0x${[...Array(size)].map(() => Math.floor(Math.random() * 16).toString(16)).join('')}`; 54 | 55 | export function isKeyringLoaded() { 56 | try { 57 | return !!keyring.keyring; 58 | } catch { 59 | return false; 60 | } 61 | } 62 | 63 | export function isNumber(value: unknown): value is number { 64 | return typeof value === 'number'; 65 | } 66 | 67 | export function isNull(value: unknown): value is null { 68 | return value === null; 69 | } 70 | 71 | export function isUndefined(value: unknown): value is undefined { 72 | return value === undefined; 73 | } 74 | 75 | export function isValidAddress(address: string | Uint8Array | null | undefined) { 76 | try { 77 | encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address)); 78 | return true; 79 | } catch (error) { 80 | return false; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/services/chain/chainProps.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { TypeRegistry } from '@polkadot/types/create'; 5 | import { DEFAULT_DECIMALS } from '../../constants'; 6 | import { ChainProperties, ApiPromise } from 'types'; 7 | 8 | const registry = new TypeRegistry(); 9 | 10 | export async function getChainProperties(api: ApiPromise): Promise { 11 | const [chainProperties, systemName, systemVersion, systemChain, systemChainType] = 12 | await Promise.all([ 13 | api.rpc.system.properties(), 14 | api.rpc.system.name(), 15 | api.rpc.system.version(), 16 | (await api.rpc.system.chain()).toString(), 17 | api.rpc.system.chainType 18 | ? api.rpc.system.chainType() 19 | : Promise.resolve(registry.createType('ChainType', 'Live')), 20 | api.rpc.system, 21 | ]); 22 | 23 | const result = { 24 | genesisHash: api.genesisHash.toHex(), 25 | systemName: systemName.toString(), 26 | systemVersion: systemVersion.toString(), 27 | systemChainType, 28 | systemChain, 29 | tokenDecimals: chainProperties.tokenDecimals.isSome 30 | ? chainProperties.tokenDecimals.unwrap().toArray()[0].toNumber() 31 | : DEFAULT_DECIMALS, 32 | tokenSymbol: chainProperties.tokenSymbol.isSome 33 | ? chainProperties.tokenSymbol 34 | .unwrap() 35 | .toArray() 36 | .map(s => s.toString())[0] 37 | : 'Unit', 38 | }; 39 | 40 | return result; 41 | } 42 | -------------------------------------------------------------------------------- /src/services/chain/contract.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { BlueprintPromise, CodePromise } from '@polkadot/api-contract'; 5 | import { isValidAddress, isValidCodeHash, isNumber } from 'lib/util'; 6 | import { transformUserInput } from 'lib/callOptions'; 7 | import { 8 | ApiPromise, 9 | CodeBundleDocument, 10 | BlueprintOptions, 11 | InstantiateData, 12 | SubmittableExtrinsic, 13 | } from 'types'; 14 | 15 | export function createInstantiateTx( 16 | api: ApiPromise, 17 | { 18 | argValues, 19 | codeHash, 20 | constructorIndex, 21 | gasLimit, 22 | value, 23 | metadata, 24 | salt, 25 | storageDepositLimit, 26 | }: Omit, 27 | ): SubmittableExtrinsic<'promise'> { 28 | const wasm = metadata?.info.source.wasm; 29 | const isValid = codeHash || !!wasm; 30 | 31 | if (isValid && metadata && isNumber(constructorIndex) && metadata && argValues) { 32 | const constructor = metadata.findConstructor(constructorIndex); 33 | 34 | const options: BlueprintOptions = { 35 | gasLimit, 36 | salt: salt || null, 37 | storageDepositLimit, 38 | value, 39 | }; 40 | 41 | const codeOrBlueprint = codeHash 42 | ? new BlueprintPromise(api, metadata, codeHash) 43 | : new CodePromise(api, metadata, wasm && wasm.toU8a()); 44 | 45 | const transformed = transformUserInput(api.registry, constructor.args, argValues); 46 | 47 | return constructor.args.length > 0 48 | ? codeOrBlueprint.tx[constructor.method](options, ...transformed) 49 | : codeOrBlueprint.tx[constructor.method](options); 50 | } else { 51 | throw new Error('Error creating instantiate tx'); 52 | } 53 | } 54 | 55 | export async function getContractInfo(api: ApiPromise, address: string) { 56 | if (isValidAddress(address)) { 57 | return (await api.query.contracts.contractInfoOf(address)).unwrapOr(null); 58 | } 59 | } 60 | 61 | export async function checkOnChainCode(api: ApiPromise, codeHash: string): Promise { 62 | return isValidCodeHash(codeHash) 63 | ? (await api.query.contracts.pristineCode(codeHash)).isSome 64 | : false; 65 | } 66 | 67 | export async function filterOnChainCode(api: ApiPromise, items: CodeBundleDocument[]) { 68 | const codes: CodeBundleDocument[] = []; 69 | for (const item of items) { 70 | const isOnChain = await checkOnChainCode(api, item.codeHash); 71 | isOnChain && codes.push(item); 72 | } 73 | return codes; 74 | } 75 | -------------------------------------------------------------------------------- /src/services/chain/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './chainProps'; 5 | export * from './contract'; 6 | -------------------------------------------------------------------------------- /src/services/db/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import Dexie, { Table } from 'dexie'; 5 | 6 | export interface CodeBundleDocument { 7 | abi: Record; 8 | codeHash: string; 9 | date: string; 10 | id?: number; 11 | name: string; 12 | } 13 | 14 | export interface ContractDocument extends CodeBundleDocument { 15 | abi: Record; 16 | address: string; 17 | external?: boolean; 18 | } 19 | 20 | export class Database extends Dexie { 21 | codeBundles!: Table; 22 | contracts!: Table; 23 | 24 | constructor(genesisHash: string) { 25 | super(`contracts-ui__${genesisHash}`); 26 | 27 | this.version(1).stores({ 28 | codeBundles: '++id, codeHash, name, date', 29 | contracts: '++id, address, codeHash, name, date', 30 | }); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/types/db.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Database, CodeBundleDocument, ContractDocument } from 'src/services/db'; 5 | 6 | export type { CodeBundleDocument, ContractDocument, Database }; 7 | 8 | export interface DbState { 9 | db: Database; 10 | } 11 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './db'; 5 | export * from './substrate'; 6 | export * from './ui'; 7 | -------------------------------------------------------------------------------- /src/types/substrate.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | // types & interfaces 5 | export type { AnyJson, Codec, Registry, RegistryError, TypeDef } from '@polkadot/types/types'; 6 | export type { 7 | ContractInstantiateResult, 8 | DispatchError, 9 | EventRecord, 10 | Weight, 11 | WeightV2, 12 | ChainType, 13 | Hash, 14 | ContractExecResult, 15 | Balance, 16 | ContractReturnFlags, 17 | } from '@polkadot/types/interfaces'; 18 | export type { KeyringPair } from '@polkadot/keyring/types'; 19 | export type { 20 | AbiConstructor, 21 | AbiMessage, 22 | AbiParam, 23 | BlueprintOptions, 24 | ContractOptions, 25 | ContractCallOutcome, 26 | } from '@polkadot/api-contract/types'; 27 | export type { ContractQuery, ContractTx } from '@polkadot/api-contract/base/types'; 28 | export type { SubmittableExtrinsic, VoidFn } from '@polkadot/api/types'; 29 | export type { StorageDeposit } from '@polkadot/types/interfaces/contracts'; 30 | 31 | // classes 32 | export { Bytes, Raw, TypeDefInfo } from '@polkadot/types'; 33 | export { Keyring } from '@polkadot/ui-keyring'; 34 | export { Abi, ContractPromise, BlueprintPromise } from '@polkadot/api-contract'; 35 | export { BlueprintSubmittableResult, CodeSubmittableResult } from '@polkadot/api-contract/base'; 36 | export { ContractSubmittableResult } from '@polkadot/api-contract/base/Contract'; 37 | export { ApiPromise, SubmittableResult } from '@polkadot/api'; 38 | -------------------------------------------------------------------------------- /src/types/ui/components.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Props as ReactSelectProps } from 'react-select'; 5 | import { Registry, TypeDef } from '../substrate'; 6 | import { ValidFormField } from './hooks'; 7 | import { FileState, OrFalsy, SimpleSpread } from './util'; 8 | 9 | export interface DropdownOption { 10 | value: T; 11 | label: React.ReactNode; 12 | } 13 | 14 | export type ArgComponentProps = SimpleSpread< 15 | React.HTMLAttributes, 16 | ValidFormField 17 | > & { 18 | nestingNumber: number; 19 | registry: Registry; 20 | typeDef: TypeDef; 21 | }; 22 | 23 | export type DropdownProps = SimpleSpread< 24 | React.HTMLAttributes, 25 | ValidFormField & 26 | Pick< 27 | ReactSelectProps, false>, 28 | 'components' | 'formatOptionLabel' | 'isDisabled' | 'isSearchable' | 'options' | 'placeholder' 29 | > & { onCreate?: (input: string) => void } 30 | >; 31 | 32 | export type InputFileProps = SimpleSpread< 33 | React.InputHTMLAttributes, 34 | { 35 | errorMessage?: React.ReactNode; 36 | isDisabled?: boolean; 37 | isSupplied?: boolean; 38 | isError?: boolean; 39 | onChange: (_: FileState) => void; 40 | onRemove: () => void; 41 | successMessage?: React.ReactNode; 42 | value: OrFalsy; 43 | } 44 | >; 45 | -------------------------------------------------------------------------------- /src/types/ui/contract.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { DecodedEvent } from '@polkadot/api-contract/types'; 5 | import BN from 'bn.js'; 6 | import type { 7 | AbiMessage, 8 | ContractPromise, 9 | EventRecord, 10 | RegistryError, 11 | Balance, 12 | } from '../substrate'; 13 | 14 | export interface ContractDryRunParams { 15 | contract: ContractPromise; 16 | message: AbiMessage; 17 | payment: BN; 18 | address: string; 19 | argValues?: Record; 20 | } 21 | 22 | export interface CallResult { 23 | id: number; 24 | events: EventRecord[]; 25 | contractEvents?: DecodedEvent[]; 26 | message: AbiMessage; 27 | error?: RegistryError; 28 | time: number; 29 | } 30 | 31 | export type UIStorageDeposit = { 32 | value?: Balance; 33 | type: 'charge' | 'refund' | 'empty'; 34 | }; 35 | -------------------------------------------------------------------------------- /src/types/ui/hooks.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { ContractPromise, VoidFn } from '../substrate'; 5 | import { BN, FileState, MetadataState, Validation } from './util'; 6 | 7 | export type InputMode = 'estimation' | 'custom'; 8 | 9 | export type UIGas = { 10 | isValid: boolean; 11 | setIsValid: (value: boolean) => void; 12 | limit: BN; 13 | setLimit: React.Dispatch; 14 | mode?: InputMode; 15 | setMode: (m: InputMode) => void; 16 | errorMsg: string; 17 | setErrorMsg: (m: string) => void; 18 | text: string; 19 | setText: (m: string) => void; 20 | }; 21 | 22 | export interface ValidFormField extends Validation { 23 | value: T; 24 | onChange: (_: T) => void; 25 | } 26 | 27 | export type UseBalance = ValidFormField; 28 | 29 | export interface UseMetadata extends MetadataState { 30 | onChange: (_: FileState) => void; 31 | onRemove: () => void; 32 | } 33 | 34 | export type UseStepper = [number, VoidFn, VoidFn, React.Dispatch]; 35 | 36 | export type UseToggle = [boolean, () => void, (value: boolean) => void]; 37 | 38 | export interface UseStorageDepositLimit extends ValidFormField { 39 | maximum: BN | undefined; 40 | isActive: boolean; 41 | toggleIsActive: () => void; 42 | } 43 | 44 | export interface UIContract extends Pick { 45 | name: string; 46 | displayName: string; 47 | date: string; 48 | id: number | undefined; 49 | type: 'added' | 'instantiated'; 50 | codeHash: string; 51 | address: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/types/ui/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './components'; 5 | export * from './contexts'; 6 | export * from './contract'; 7 | export * from './hooks'; 8 | export * from './util'; 9 | -------------------------------------------------------------------------------- /src/types/ui/util.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import BN from 'bn.js'; 5 | import type { Abi } from '../substrate'; 6 | 7 | export type { BN }; 8 | 9 | export type OrFalsy = T | null | undefined; 10 | 11 | export type OrNull = T | null; 12 | 13 | export type OrUndef = T | undefined; 14 | 15 | export type SetState = React.Dispatch>; 16 | 17 | export type UseState = [T, SetState]; 18 | 19 | export type SimpleSpread = R & Pick>; 20 | 21 | export type ValidateFn = (_: OrFalsy) => Validation; 22 | 23 | export interface Validation { 24 | isError?: boolean; 25 | isSuccess?: boolean; 26 | isTouched?: boolean; 27 | isValid?: boolean; 28 | isWarning?: boolean; 29 | message?: React.ReactNode; 30 | } 31 | 32 | export interface FileState { 33 | data: Uint8Array; 34 | name: string; 35 | size: number; 36 | } 37 | 38 | export interface MetadataState extends Validation { 39 | source?: Record; 40 | name: string; 41 | value?: Abi; 42 | isSupplied: boolean; 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/components/App.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Outlet } from 'react-router'; 5 | import { AwaitApis, CheckBrowserSupport } from 'ui/components'; 6 | import { 7 | ApiContextProvider, 8 | DatabaseContextProvider, 9 | ThemeContextProvider, 10 | TransactionsContextProvider, 11 | } from 'ui/contexts'; 12 | import { Sidebar } from 'ui/layout/sidebar'; 13 | 14 | export default function App() { 15 | return ( 16 | 17 | 18 | 19 | 20 | {/* we want the sidebar outside the outlet to prevent flickering in quicklinks */} 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 |
31 |
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/components/AwaitApis.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useState } from 'react'; 5 | import type { HTMLAttributes } from 'react'; 6 | import { isWeb3Injected } from '@polkadot/extension-dapp'; 7 | import { AccountsError, ExtensionError } from './common/AccountsError'; 8 | import { useApi, useDatabase } from 'ui/contexts'; 9 | import { Loader, ConnectionError } from 'ui/components/common'; 10 | import { isKeyringLoaded } from 'lib/util'; 11 | 12 | export function AwaitApis({ children }: HTMLAttributes): React.ReactElement { 13 | const { accounts, api, endpoint, status, systemChainType } = useApi(); 14 | 15 | const { db } = useDatabase(); 16 | const [message, setMessage] = useState(''); 17 | 18 | useEffect(() => { 19 | !db && setMessage('Loading data...'); 20 | status === 'loading' && setMessage(`Connecting to ${endpoint}...`); 21 | !isKeyringLoaded() && setMessage(`Loading accounts...`); 22 | }, [db, endpoint, api, status]); 23 | 24 | if (status === 'error') { 25 | return ; 26 | } 27 | 28 | if ( 29 | !isWeb3Injected && 30 | status === 'connected' && 31 | !systemChainType.isDevelopment && 32 | isKeyringLoaded() 33 | ) { 34 | return ; 35 | } 36 | 37 | if (isKeyringLoaded() && accounts?.length === 0) { 38 | return ; 39 | } 40 | 41 | return ( 42 | <> 43 | {status === 'loading' || !db || !isKeyringLoaded() ? ( 44 | 45 | ) : ( 46 | children 47 | )} 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/components/CheckBrowserSupport.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import type { HTMLAttributes } from 'react'; 5 | import { useMemo } from 'react'; 6 | import { UnsupportedBrowserMessage } from './common/UnsupportedBrowserMessage'; 7 | 8 | export function CheckBrowserSupport({ 9 | children, 10 | }: HTMLAttributes): React.ReactElement { 11 | const isSafari = useMemo(() => { 12 | return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 13 | }, []); 14 | 15 | if (isSafari) { 16 | return ; 17 | } else { 18 | return <>{children}; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/components/account/Account.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Identicon } from './Identicon'; 5 | import { classes, truncate } from 'lib/util'; 6 | import { OrFalsy } from 'types'; 7 | import { useApi } from 'ui/contexts'; 8 | 9 | interface Props extends React.HTMLAttributes { 10 | name?: React.ReactNode; 11 | value: OrFalsy; 12 | size?: number; 13 | } 14 | 15 | export function Account({ className, name: propsName, size = 42, value }: Props) { 16 | const { accounts } = useApi(); 17 | 18 | const account = accounts?.find(a => a.address === value); 19 | const name = propsName || account?.meta.name; 20 | 21 | if (!value) { 22 | return null; 23 | } 24 | 25 | return ( 26 |
27 | 28 |
29 | {name && ( 30 | 34 | {name} 35 | 36 | )} 37 |

38 | {truncate(value, 4)} 39 |

40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/components/account/Identicon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { polkadotIcon } from '@polkadot/ui-shared'; 5 | import type { Circle } from '@polkadot/ui-shared/icons/types'; 6 | import copy from 'copy-to-clipboard'; 7 | import React, { useCallback } from 'react'; 8 | import { Tooltip } from 'react-tooltip'; 9 | import { Button } from '../common'; 10 | import { classes } from 'lib/util'; 11 | 12 | export interface Props extends React.HTMLAttributes { 13 | value?: string | null; 14 | isAlternative?: boolean; 15 | size: number; 16 | } 17 | 18 | function renderCircle({ cx, cy, fill, r }: Circle, key: number): React.ReactNode { 19 | return ; 20 | } 21 | 22 | function IdenticonBase({ 23 | value = '', 24 | className = '', 25 | isAlternative = false, 26 | size, 27 | style, 28 | }: Props): React.ReactElement | null { 29 | const onClick = useCallback(() => { 30 | if (value) { 31 | copy(value); 32 | } 33 | }, [value]); 34 | 35 | const tooltipId = `identicon-copied-${value}`; 36 | 37 | try { 38 | return ( 39 | <> 40 | 59 | 60 | Copied to clipboard 61 | 62 | 63 | ); 64 | } catch (e) { 65 | return null; 66 | } 67 | } 68 | 69 | export const Identicon = React.memo(IdenticonBase); 70 | -------------------------------------------------------------------------------- /src/ui/components/account/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './Account'; 5 | export * from './Select'; 6 | export * from './Identicon'; 7 | -------------------------------------------------------------------------------- /src/ui/components/common/AccountsError.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Error } from './Error'; 5 | 6 | export function AccountsError() { 7 | return ( 8 | 9 |
No accounts found.
10 |

11 | 1. Follow{' '} 12 | 17 | this guide 18 | {' '} 19 | to create your first account in the Polkadot.js extension. 20 |

21 |

22 | 2. Drip some funds into your account via the faucets of our supported networks. You can find 23 | a faucets list{' '} 24 | here. 25 |

26 |
27 | ); 28 | } 29 | 30 | export function ExtensionError() { 31 | return ( 32 | 33 |
No signer extension found.
34 |
35 | New to Substrate? 36 |

37 | Install the a compatible wallet like{' '} 38 | 39 | Polkadot.js Extension 40 | {' '} 41 | to create and manage Substrate accounts. 42 |

43 |

44 | You can find out more about compatible wallets on the{' '} 45 | Polkadot documentation about 46 | wallets. 47 |

48 |

49 | If the extension is installed and you are seeing this, make sure it allows{' '} 50 | Contracts UI to use your accounts for signing. 51 |

52 |
53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/components/common/Button.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import React from 'react'; 5 | import { classes } from 'lib/util'; 6 | 7 | type Variant = 'default' | 'primary' | 'plain' | 'negative'; 8 | 9 | interface Props extends React.HTMLAttributes { 10 | isDisabled?: boolean; 11 | isLoading?: boolean; 12 | ref?: React.MutableRefObject; 13 | variant?: Variant; 14 | } 15 | 16 | export function Buttons({ children, className }: React.HTMLAttributes) { 17 | return
{children}
; 18 | } 19 | 20 | export const Button = React.forwardRef((props, ref) => { 21 | const { children, variant, className, isDisabled, isLoading, ...rest } = props; 22 | 23 | return ( 24 | 42 | ); 43 | }); 44 | -------------------------------------------------------------------------------- /src/ui/components/common/ConnectionError.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Error } from './Error'; 5 | import { useApi } from 'ui/contexts'; 6 | import { LOCAL } from 'src/constants'; 7 | 8 | function ContractsNodeHelp() { 9 | return ( 10 | <> 11 |
12 |
13 | Make sure you are running a local{' '} 14 | 20 | substrate-contracts-node 21 | 22 | . 23 |
24 |
25 | substrate-contracts-node --dev 26 |
27 |
28 | 29 | ); 30 | } 31 | 32 | export function ConnectionError() { 33 | const { endpoint } = useApi(); 34 | 35 | return ( 36 | 37 |
Could not connect to {endpoint}
38 | {endpoint === LOCAL.rpc && } 39 |
40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/ui/components/common/CopyButton.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import copy from 'copy-to-clipboard'; 5 | import { MouseEventHandler, useCallback, useRef, useState } from 'react'; 6 | import { DocumentDuplicateIcon } from '@heroicons/react/outline'; 7 | import { Tooltip } from 'react-tooltip'; 8 | import { Button } from './Button'; 9 | import { classes } from 'lib/util'; 10 | 11 | interface Props extends React.HTMLAttributes { 12 | iconClassName?: string; 13 | value: string; 14 | id: string; 15 | } 16 | 17 | export function CopyButton({ className, iconClassName, value, id }: Props) { 18 | const ref = useRef(null); 19 | const [showTooltip, setShowTooltip] = useState(false); 20 | const onClick: MouseEventHandler = useCallback( 21 | (event): void => { 22 | event.stopPropagation(); 23 | copy(value); 24 | setShowTooltip(true); 25 | setTimeout(() => { 26 | setShowTooltip(false); 27 | }, 1000); 28 | }, 29 | [value], 30 | ); 31 | 32 | return ( 33 | <> 34 | 46 | 47 | 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/components/common/Error.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { ExclamationCircleIcon } from '@heroicons/react/outline'; 5 | import { classes } from 'lib/util'; 6 | 7 | type Props = React.HTMLProps; 8 | 9 | export function Error({ children, className }: Props) { 10 | return ( 11 |
12 |
13 | 14 | {children} 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/common/Info.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { InformationCircleIcon } from '@heroicons/react/solid'; 5 | import { classes } from 'lib/util'; 6 | 7 | type Props = React.HTMLProps; 8 | 9 | export function Info({ children, className }: Props) { 10 | return ( 11 |
12 |
13 | 14 | {children} 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/common/Loader.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Spinner } from './Spinner'; 5 | 6 | interface Props extends React.HTMLAttributes { 7 | isLoading?: boolean; 8 | message?: React.ReactNode; 9 | } 10 | 11 | export function Loader({ children, isLoading, message = 'Loading...' }: Props): React.ReactElement { 12 | return isLoading ? ( 13 |
14 | 15 |
{message}
16 |
17 | ) : ( 18 | <>{children} 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/ui/components/common/Meter.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | interface Props { 5 | label?: React.ReactNode; 6 | accessory?: React.ReactNode; 7 | withAccessory?: boolean; 8 | } 9 | 10 | export function Meter({ accessory, label, withAccessory }: Props) { 11 | return ( 12 |
13 |
14 | {label} 15 | {withAccessory &&
{accessory}
} 16 |
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/components/common/NoticeBanner.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { EmojiSadIcon } from '@heroicons/react/outline'; 5 | 6 | export function NoticeBanner({ isVisible, endpoint }: { isVisible: boolean; endpoint: string }) { 7 | return isVisible ? ( 8 |
9 | 10 | 11 |

Unsupported node version.

12 |

13 | Looks like your node does not support WeightV2. 14 |

15 |

16 | Upgrade your node or{' '} 17 | 21 | click here 22 | {' '} 23 | to use an older version of Contracts UI. 24 |

25 |
26 | ) : null; 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/components/common/NotificationIcon.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { CheckIcon, ClockIcon, ExclamationCircleIcon } from '@heroicons/react/outline'; 5 | import { Spinner } from './Spinner'; 6 | import { TxStatus } from 'types'; 7 | import { classes } from 'lib/util'; 8 | 9 | interface Props { 10 | status?: TxStatus; 11 | } 12 | 13 | export const NotificationIcon = ({ status }: Props) => { 14 | switch (status) { 15 | case 'success': 16 | return ; 17 | 18 | case 'error': 19 | return ; 20 | 21 | case 'processing': 22 | return ( 23 | 29 | ); 30 | 31 | case 'queued': 32 | return ; 33 | 34 | default: 35 | return null; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /src/ui/components/common/ObservedBalance.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useState } from 'react'; 5 | import { formatBalance } from 'lib/formatBalance'; 6 | import { Balance } from 'types'; 7 | import { useApi } from 'ui/contexts'; 8 | 9 | export const ObservedBalance = ({ address }: { address: string }) => { 10 | const { api, tokenDecimals, tokenSymbol } = useApi(); 11 | 12 | const [balance, setBalance] = useState(null); 13 | useEffect(() => { 14 | const unsubscribePromise = api.query.system.account(address, result => { 15 | setBalance(result.data.free); 16 | }); 17 | 18 | return () => { 19 | unsubscribePromise 20 | .then(unsubscribe => { 21 | unsubscribe(); 22 | }) 23 | .catch(console.error); 24 | }; 25 | }, [address, api]); 26 | 27 | if (!balance) return null; 28 | 29 | return formatBalance(balance, { 30 | decimals: tokenDecimals, 31 | symbol: tokenSymbol, 32 | fractionDigits: 2, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/ui/components/common/SidePanel.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { classes } from 'lib/util'; 5 | 6 | interface Props extends React.HTMLAttributes { 7 | header: React.ReactNode; 8 | emptyView?: React.ReactNode; 9 | } 10 | 11 | export const SidePanel = ({ children, className, header, emptyView = '' }: Props) => { 12 | return ( 13 |
16 |
17 | {header} 18 |
19 |
20 | {children} 21 | {(!children || (children as unknown[]).length === 0) && ( 22 |

{emptyView || ''}

23 | )} 24 |
25 |
26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /src/ui/components/common/Spinner.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { classes } from 'lib/util'; 5 | 6 | interface Props extends React.HTMLAttributes { 7 | strokeWidth?: number; 8 | width?: number; 9 | color?: string; 10 | darkColor?: string; 11 | } 12 | 13 | export function Spinner({ 14 | className, 15 | color = 'blue-500', 16 | darkColor = color, 17 | strokeWidth = 4, 18 | width = 16, 19 | }: Props) { 20 | return ( 21 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/components/common/Switch.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Switch as HUISwitch } from '@headlessui/react'; 5 | import { classes } from 'lib/util'; 6 | import { SimpleSpread } from 'types'; 7 | 8 | type Props = SimpleSpread< 9 | React.HTMLAttributes, 10 | { 11 | value: boolean; 12 | onChange: (_: boolean) => void; 13 | } 14 | >; 15 | 16 | export function Switch({ children, className, onChange, value }: Props) { 17 | return ( 18 | 28 | {children} 29 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/components/common/Tabs.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import type { SetState } from 'types'; 5 | import { classes } from 'lib/util'; 6 | 7 | interface Tab { 8 | id: string; 9 | isDisabled?: boolean; 10 | label: React.ReactNode; 11 | } 12 | 13 | interface Props extends React.HTMLAttributes { 14 | children: React.ReactNode[]; 15 | index: number; 16 | setIndex: SetState; 17 | tabs: Tab[]; 18 | } 19 | 20 | export function Tabs({ children, index, setIndex, tabs }: Props) { 21 | return ( 22 | <> 23 |
24 |
    28 | {tabs.map(({ id, label }, tabIndex) => { 29 | return ( 30 |
  • 31 | 37 |
  • 38 | ); 39 | })} 40 |
41 |
42 | {children.map((child, i) => { 43 | return ( 44 |
45 | {child} 46 |
47 | ); 48 | })} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /src/ui/components/common/UnsupportedBrowserMessage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Info } from './Info'; 5 | 6 | const SuggestedBrowsers = [ 7 | ['Mozilla Firefox', 'https://www.mozilla.org/firefox'], 8 | ['Brave Browser', 'https://brave.com/'], 9 | ['Google Chrome', 'https://www.google.com/chrome/'], 10 | ]; 11 | 12 | export function UnsupportedBrowserMessage() { 13 | return ( 14 | 15 |
Unsupported Browser
16 | 17 |
18 |

19 | We currently do not support Apple's Safari browser. We recommend using this DApp with one 20 | of the following browsers: 21 |

22 |
23 | {SuggestedBrowsers.map(([title, url]) => ( 24 | 25 | {`> ${title}`} 26 | 27 | ))} 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/components/common/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './Button'; 5 | export * from './Dropdown'; 6 | export * from './HeaderButtons'; 7 | export * from './Loader'; 8 | export * from './Meter'; 9 | export * from './Spinner'; 10 | export * from './Switch'; 11 | export * from './Tabs'; 12 | export * from './SearchResults'; 13 | export * from './ConnectionError'; 14 | export * from './NoticeBanner'; 15 | -------------------------------------------------------------------------------- /src/ui/components/contract/ContractRow.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useState } from 'react'; 5 | import { Link } from 'react-router-dom'; 6 | import { Identicon } from '../account/Identicon'; 7 | import { ObservedBalance } from '../common/ObservedBalance'; 8 | import { ContractDocument } from 'types'; 9 | import { useApi } from 'ui/contexts'; 10 | import { displayDate, truncate } from 'lib/util'; 11 | import { getContractInfo } from 'services/chain'; 12 | 13 | interface Props { 14 | contract: ContractDocument; 15 | } 16 | 17 | export function ContractRow({ contract: { address, name, date } }: Props) { 18 | const { api } = useApi(); 19 | const [isOnChain, setIsOnChain] = useState(true); 20 | 21 | useEffect(() => { 22 | getContractInfo(api, address) 23 | .then(info => { 24 | setIsOnChain(info ? true : false); 25 | }) 26 | .catch(console.error); 27 | }, [address, api]); 28 | 29 | return ( 30 | 34 |
35 | 36 |
{name}
37 |
38 | 39 | {isOnChain ? ( 40 |
41 | {truncate(address, 4)} 42 |
43 | ) : ( 44 |
not on-chain
45 | )} 46 |
{displayDate(date)}
47 | 48 |
49 | 50 |
51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/ui/components/contract/DryRunError.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { OutcomeItem } from './OutcomeItem'; 5 | import { RegistryError } from 'types'; 6 | 7 | export function DryRunError({ error }: { error: RegistryError }) { 8 | return ( 9 |
10 | 11 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/components/contract/OutcomeItem.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { CopyButton } from 'ui/components/common/CopyButton'; 5 | 6 | export function OutcomeItem({ 7 | displayValue, 8 | copyValue = JSON.stringify(displayValue), 9 | title, 10 | id, 11 | }: { 12 | title: string; 13 | displayValue: string; 14 | copyValue?: string; 15 | id?: string; 16 | }): JSX.Element { 17 | return ( 18 |
19 |
{title}
20 |
24 |
25 |           {displayValue}
26 |         
27 | 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/components/contract/ResultsOutput.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { SidePanel } from '../common/SidePanel'; 5 | import { TransactionResult } from './TransactionResult'; 6 | import { DryRunResult } from './DryRunResult'; 7 | import { CallResult, ContractExecResult, Registry, AbiMessage } from 'types'; 8 | 9 | interface Props { 10 | results: CallResult[]; 11 | registry: Registry; 12 | message: AbiMessage; 13 | outcome?: ContractExecResult; 14 | } 15 | 16 | export const ResultsOutput = ({ registry, results, outcome, message }: Props) => { 17 | return ( 18 | <> 19 | 23 |
24 | {outcome && } 25 |
26 |
27 | 28 | {results 29 | .map(result => { 30 | const { time } = result; 31 | const date = new Date(time).toLocaleString(); 32 | return ; 33 | }) 34 | .reverse()} 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /src/ui/components/contract/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './ContractRow'; 5 | export * from './Interact'; 6 | export * from './MetadataTab'; 7 | export * from './ResultsOutput'; 8 | export * from './TransactionResult'; 9 | -------------------------------------------------------------------------------- /src/ui/components/form/ArgumentForm.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useMemo } from 'react'; 5 | import { ArgSignature } from '../message/ArgSignature'; 6 | import { Form, FormField } from './FormField'; 7 | import { findComponent } from './findComponent'; 8 | import { AbiParam, Registry, SetState } from 'types'; 9 | import { classes } from 'lib/util'; 10 | 11 | interface Props extends React.HTMLAttributes { 12 | args: AbiParam[]; 13 | argValues: Record; 14 | registry: Registry; 15 | setArgValues: SetState>; 16 | } 17 | 18 | export function ArgumentForm({ args, argValues, registry, setArgValues, className }: Props) { 19 | const components = useMemo( 20 | () => args.map(arg => ({ arg, Component: findComponent(registry, arg.type) })), 21 | [args, registry], 22 | ); 23 | return ( 24 |
25 | {components.map(({ arg, Component }) => { 26 | const onChange = (value: unknown) => { 27 | setArgValues(prev => ({ 28 | ...prev, 29 | [arg.name]: value, 30 | })); 31 | }; 32 | 33 | return ( 34 | 43 | } 44 | > 45 | 54 | 55 | ); 56 | })} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/components/form/Bool.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { SimpleSpread, ValidFormField } from 'types'; 5 | import { Dropdown } from 'ui/components/common/Dropdown'; 6 | 7 | type Props = SimpleSpread, ValidFormField>; 8 | 9 | const options = [ 10 | { 11 | value: false, 12 | label: 'false', 13 | }, 14 | { 15 | value: true, 16 | label: 'true', 17 | }, 18 | ]; 19 | 20 | export function Bool({ value, onChange, ...props }: Props) { 21 | return ; 22 | } 23 | -------------------------------------------------------------------------------- /src/ui/components/form/Enum.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useState } from 'react'; 5 | import { Dropdown } from '../common/Dropdown'; 6 | import { ArgSignature } from '../message/ArgSignature'; 7 | import { FormField, getValidation } from './FormField'; 8 | import { isNumber } from 'lib/util'; 9 | import { ArgComponentProps, OrFalsy, TypeDef } from 'types'; 10 | import { useApi } from 'ui/contexts'; 11 | import { getInitValue } from 'lib/initValue'; 12 | 13 | interface Props extends ArgComponentProps> { 14 | components: React.ComponentType>[]; 15 | } 16 | 17 | export function Enum(props: Props) { 18 | const { components, typeDef, nestingNumber, onChange: _onChange, registry, value } = props; 19 | const variants = typeDef.sub as TypeDef[]; 20 | const { accounts } = useApi(); 21 | const [variantIndex, _setVariantIndex] = useState(0); 22 | 23 | const Component = components[variantIndex]; 24 | 25 | const onChange = useCallback( 26 | (value: unknown): void => { 27 | _onChange({ [variants[variantIndex].name as string]: value }); 28 | }, 29 | [_onChange, variants, variantIndex], 30 | ); 31 | 32 | const setVariantIndex = useCallback( 33 | (value: OrFalsy) => { 34 | if (isNumber(value)) { 35 | _setVariantIndex(value); 36 | 37 | _onChange({ 38 | [variants[value].name as string]: getInitValue(registry, accounts || [], variants[value]), 39 | }); 40 | } 41 | }, 42 | [registry, accounts, _onChange, variants], 43 | ); 44 | 45 | return ( 46 | <> 47 | ({ label: name, value: index }))} 50 | value={variantIndex} 51 | /> 52 | {variants[variantIndex].type !== 'Null' && ( 53 | } 56 | {...getValidation(props)} 57 | > 58 | 65 | 66 | )} 67 | 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /src/ui/components/form/FormField.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { 5 | InformationCircleIcon, 6 | ExclamationCircleIcon, 7 | CheckCircleIcon, 8 | } from '@heroicons/react/outline'; 9 | import { useMemo } from 'react'; 10 | import { Tooltip } from 'react-tooltip'; 11 | import type { Validation } from 'types'; 12 | import { classes } from 'lib/util'; 13 | 14 | type ValidationState = 'error' | 'success' | 'warning' | null; 15 | 16 | interface Props extends React.HTMLAttributes { 17 | label?: React.ReactNode; 18 | help?: React.ReactNode; 19 | } 20 | 21 | export function getValidation({ 22 | isError, 23 | isSuccess, 24 | isValid, 25 | isWarning, 26 | message, 27 | }: Validation): Validation { 28 | return { isError, isSuccess, isValid, isWarning, message }; 29 | } 30 | 31 | export function FormField({ 32 | children, 33 | className, 34 | help, 35 | id, 36 | isError, 37 | isSuccess, 38 | isWarning, 39 | label, 40 | message, 41 | }: Props & Validation) { 42 | const validationState = useMemo((): ValidationState => { 43 | if (!message) return null; 44 | 45 | if (isError) return 'error'; 46 | if (isWarning) return 'warning'; 47 | if (isSuccess) return 'success'; 48 | 49 | return null; 50 | }, [isError, isSuccess, isWarning, message]); 51 | 52 | return ( 53 |
54 | {label && ( 55 | 64 | )} 65 | {children} 66 | {message && validationState && ( 67 |
68 | {['error', 'warning'].includes(validationState) && } 69 | {validationState === 'success' && } 70 | {message} 71 |
72 | )} 73 |
74 | ); 75 | } 76 | 77 | export function Form({ className, children, ...props }: React.HTMLAttributes) { 78 | return ( 79 |
80 | {children} 81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/components/form/Input.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { SimpleSpread } from 'types'; 5 | import { classes } from 'lib/util'; 6 | 7 | type Props = SimpleSpread< 8 | React.InputHTMLAttributes, 9 | { 10 | isDisabled?: boolean; 11 | isError?: boolean; 12 | onChange: (_: string) => void; 13 | value?: string | null; 14 | } 15 | >; 16 | 17 | export function Input({ 18 | children, 19 | className, 20 | isDisabled = false, 21 | isError = false, 22 | onChange: _onChange, 23 | placeholder, 24 | value, 25 | onFocus, 26 | type = 'text', 27 | }: Props) { 28 | function onChange(e: React.ChangeEvent): void { 29 | _onChange(e.target.value); 30 | } 31 | 32 | return ( 33 |
34 | 47 | {children} 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/components/form/InputBalance.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import React, { useState } from 'react'; 5 | import BN from 'bn.js'; 6 | 7 | import { InputNumber } from './InputNumber'; 8 | import { classes } from 'lib/util'; 9 | import { ApiPromise, OrFalsy, SimpleSpread } from 'types'; 10 | import { useApi } from 'ui/contexts'; 11 | import { BN_ZERO, fromBalance, fromSats, toBalance } from 'lib/bn'; 12 | 13 | type Props = SimpleSpread< 14 | React.InputHTMLAttributes, 15 | { 16 | value?: BN; 17 | onChange: (_: BN) => void; 18 | withUnits?: boolean; 19 | } 20 | >; 21 | 22 | function getStringValue(api: ApiPromise, value: OrFalsy) { 23 | if (!value) { 24 | return ''; 25 | } 26 | 27 | return fromBalance(fromSats(api, value || BN_ZERO)); 28 | } 29 | 30 | function InputBalanceBase({ 31 | children, 32 | className, 33 | value, 34 | onChange, 35 | withUnits = true, 36 | ...inputProps 37 | }: Props) { 38 | const { api, tokenSymbol } = useApi(); 39 | 40 | const [stringValue, setStringValue] = useState(getStringValue(api, value)); 41 | 42 | return ( 43 | <> 44 |
45 | { 49 | const val = e.target.value; 50 | if (val.length < 26) { 51 | const bn = toBalance(api, val); 52 | onChange(bn); 53 | setStringValue(val); 54 | } 55 | }} 56 | value={stringValue ?? 0} 57 | {...inputProps} 58 | /> 59 | {withUnits && ( 60 |
61 | {tokenSymbol} 62 |
63 | )} 64 | {children} 65 |
66 | 67 | ); 68 | } 69 | 70 | export const InputBalance = React.memo(InputBalanceBase); 71 | -------------------------------------------------------------------------------- /src/ui/components/form/InputBn.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useState } from 'react'; 5 | import BN from 'bn.js'; 6 | import { InputNumber } from './InputNumber'; 7 | import { ArgComponentProps } from 'types'; 8 | 9 | type Props = ArgComponentProps; 10 | 11 | function getMinMax(type: string): [bigint, bigint] { 12 | switch (type) { 13 | case 'i8': 14 | return [-128n, 127n]; 15 | case 'i16': 16 | return [-32768n, 32767n]; 17 | case 'i32': 18 | return [-2147483648n, 2147483647n]; 19 | case 'i64': 20 | return [-9223372036854775808n, 9223372036854775807n]; 21 | case 'i128': 22 | return [-170141183460469231731687303715884105728n, 170141183460469231731687303715884105727n]; 23 | case 'u8': 24 | return [0n, 255n]; 25 | case 'u16': 26 | return [0n, 65535n]; 27 | case 'u32': 28 | return [0n, 4294967295n]; 29 | case 'u64': 30 | return [0n, 18446744073709551615n]; 31 | case 'u128': 32 | return [0n, 340282366920938463463374607431768211455n]; 33 | default: 34 | return [-BigInt(Number.MAX_SAFE_INTEGER), BigInt(Number.MAX_SAFE_INTEGER)]; 35 | } 36 | } 37 | 38 | export function InputBn({ onChange, typeDef: { type } }: Props): JSX.Element { 39 | const [displayValue, setDisplayValue] = useState('0'); 40 | const [min, max] = getMinMax(type); 41 | 42 | const handleChange = useCallback( 43 | ({ target: { value } }: React.ChangeEvent) => { 44 | if (value.trim()) { 45 | const val = Number(value); 46 | if (!Number.isNaN(val) && min <= val && val <= max) { 47 | const bn = new BN(value); 48 | setDisplayValue(value); 49 | onChange(bn); 50 | } 51 | } 52 | }, 53 | [max, min, onChange], 54 | ); 55 | 56 | return ( 57 | <> 58 | 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /src/ui/components/form/InputBytes.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { hexToU8a } from '@polkadot/util'; 5 | import React, { useCallback, useMemo, useState } from 'react'; 6 | import { Input } from './Input'; 7 | import { classes } from 'lib/util'; 8 | import { ArgComponentProps } from 'types'; 9 | 10 | type Props = ArgComponentProps & { length?: number }; 11 | type Validation = { isValid: boolean; message?: string }; 12 | 13 | const isHexRegex = /^0x[A-F0-9]+$/i; 14 | const validateFn = 15 | (length = 64) => 16 | (value: string): Validation => { 17 | if (!isHexRegex.test(value)) { 18 | return { isValid: false, message: 'Not a valid hex string' }; 19 | } 20 | 21 | const expectedLength = length + 2; // +2 for 0x prefix 22 | if (value.length < expectedLength) { 23 | return { isValid: false, message: `Input too short! Expecting ${length} characters.` }; 24 | } 25 | if (value.length > expectedLength) { 26 | return { isValid: false, message: `Input too long! Expecting ${length} characters.` }; 27 | } 28 | 29 | return { 30 | isValid: true, 31 | message: '', 32 | }; 33 | }; 34 | 35 | export function InputBytes({ onChange, className, length }: Props): React.ReactElement { 36 | const [value, setValue] = useState(''); 37 | const [{ isValid, message }, setValidation] = useState({ isValid: true }); 38 | const validate = useMemo(() => validateFn(length), [length]); 39 | 40 | const handleChange = useCallback( 41 | (d: string) => { 42 | setValue(d); 43 | const validation = validate(d); 44 | setValidation(validation); 45 | if (validation.isValid) { 46 | try { 47 | onChange(hexToU8a(d)); 48 | } catch (e) { 49 | console.error(e); 50 | } 51 | } else { 52 | // TODO shouldn't this unset the value in error and invalid case to prevent form submission? 53 | } 54 | }, 55 | [onChange, validate], 56 | ); 57 | 58 | return ( 59 | <> 60 |
61 | 68 |
69 | {!isValid &&
{message}
} 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/ui/components/form/InputHash.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useState } from 'react'; 5 | import { InputHex } from './InputHex'; 6 | import { ArgComponentProps, Hash } from 'types'; 7 | 8 | type Props = ArgComponentProps; 9 | 10 | type Validation = { isValid: boolean; message?: string }; 11 | 12 | function validate(value: string): Validation { 13 | if (value.length < 64) { 14 | return { isValid: false, message: 'Input too short! Expecting 64 characters.' }; 15 | } 16 | if (value.length > 64) { 17 | return { isValid: false, message: 'Input too long! Expecting 64 characters.' }; 18 | } 19 | return { 20 | isValid: true, 21 | message: '', 22 | }; 23 | } 24 | 25 | export function InputHash({ registry, onChange, className }: Props) { 26 | const [{ isValid, message }, setValidation] = useState({ isValid: true }); 27 | const handleChange = useCallback( 28 | (d: string) => { 29 | const validation = validate(d); 30 | setValidation(validation); 31 | if (validation.isValid) { 32 | try { 33 | const x = registry.createType('H256', `0x${d}`); 34 | onChange(x); 35 | } catch (e) { 36 | console.error(e); 37 | } 38 | } 39 | }, 40 | [onChange, registry], 41 | ); 42 | return ( 43 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/components/form/InputHex.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useState } from 'react'; 5 | import { Input } from './Input'; 6 | import { classes } from 'lib/util'; 7 | 8 | interface Props { 9 | onChange: (_: string) => void; 10 | className?: string; 11 | defaultValue?: string; 12 | error?: string; 13 | } 14 | 15 | export function InputHex({ onChange, className, defaultValue, error }: Props) { 16 | const [value, setValue] = useState(defaultValue ?? ''); 17 | 18 | const handleChange = useCallback( 19 | (d: string) => { 20 | const regex = /^(0x|0X)?[a-fA-F0-9]+$/; 21 | if (!d || regex.test(d)) { 22 | setValue(d); 23 | onChange(d); 24 | } 25 | }, 26 | [onChange], 27 | ); 28 | return ( 29 | <> 30 |
31 | 0x 32 | 39 |
40 |
{error}
41 | 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/components/form/InputNumber.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { classes } from 'lib/util'; 5 | 6 | export function InputNumber({ 7 | children, 8 | className, 9 | onChange, 10 | value, 11 | disabled, 12 | min, 13 | max, 14 | placeholder, 15 | step, 16 | }: React.InputHTMLAttributes) { 17 | return ( 18 |
19 | 34 | {children} 35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/ui/components/form/InputSalt.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Switch } from '../common/Switch'; 5 | import { Input } from './Input'; 6 | import { SimpleSpread, ValidFormField } from 'types'; 7 | 8 | type Props = SimpleSpread< 9 | React.HTMLAttributes, 10 | ValidFormField & { 11 | isActive?: boolean; 12 | toggleIsActive: () => void; 13 | } 14 | >; 15 | 16 | export function InputSalt({ isError, onChange, value, isActive = false, toggleIsActive }: Props) { 17 | return ( 18 |
19 | 28 |
29 | 30 |
31 |
32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/ui/components/form/InputStorageDepositLimit.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import Big from 'big.js'; 5 | import { useMemo } from 'react'; 6 | import { Meter, Switch } from '../common'; 7 | import { InputBalance } from './InputBalance'; 8 | import { getValidation } from './FormField'; 9 | import type { SimpleSpread, UseStorageDepositLimit } from 'types'; 10 | import { classes, isNull, isNumber } from 'lib/util'; 11 | 12 | type Props = SimpleSpread< 13 | React.HTMLAttributes, 14 | UseStorageDepositLimit & { 15 | isActive?: boolean; 16 | toggleIsActive: () => void; 17 | } 18 | >; 19 | 20 | export function InputStorageDepositLimit({ 21 | className, 22 | isActive = false, 23 | maximum, 24 | onChange, 25 | toggleIsActive, 26 | value, 27 | ...props 28 | }: Props) { 29 | const percentage = useMemo((): number | null => { 30 | if (!maximum || maximum.eqn(0)) { 31 | return null; 32 | } 33 | 34 | return 100 * new Big(value.toString()).div(new Big(maximum.toString())).toNumber(); 35 | }, [maximum, value]); 36 | 37 | return ( 38 |
39 |
40 | 50 |
51 | 52 |
53 |
54 | {isActive && !isNull(percentage) && ( 55 | 56 | )} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/components/form/InputWeight.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import BN from 'bn.js'; 5 | import { Meter } from '../common/Meter'; 6 | import { InputNumber } from './InputNumber'; 7 | import { UIGas } from 'types'; 8 | import { MAX_CALL_WEIGHT } from 'src/constants'; 9 | 10 | export function InputWeight({ 11 | setLimit, 12 | mode, 13 | setMode, 14 | setErrorMsg, 15 | setIsValid, 16 | limit, 17 | name, 18 | text, 19 | setText, 20 | }: UIGas & { name: string }) { 21 | return ( 22 | <> 23 | { 29 | if (mode === 'custom') { 30 | const bn = new BN(e.target.value); 31 | if (bn.lte(MAX_CALL_WEIGHT)) { 32 | if (!bn.eq(limit)) { 33 | setText(e.target.value); 34 | setLimit(bn); 35 | setErrorMsg(''); 36 | setIsValid(true); 37 | } 38 | } else { 39 | setErrorMsg('Value exceeds maximum block weight'); 40 | setIsValid(false); 41 | } 42 | } 43 | }} 44 | placeholder="MGas" 45 | value={text} 46 | /> 47 | { 55 | e.preventDefault(); 56 | setMode('estimation'); 57 | }} 58 | > 59 | {`Use Estimation`} 60 | 61 | ) : ( 62 | <> 63 | {`Using Estimation`} 64 |  {' · '}  65 | { 70 | e.preventDefault(); 71 | setMode('custom'); 72 | }} 73 | > 74 | Use Custom 75 | 76 | 77 | ) 78 | } 79 | withAccessory={true} 80 | /> 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/components/form/Option.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useEffect, useRef } from 'react'; 5 | import { Switch } from '../common/Switch'; 6 | import { Input } from './Input'; 7 | import { ArgComponentProps, OrFalsy, Registry, TypeDef } from 'types'; 8 | import { useApi } from 'ui/contexts'; 9 | import { NOOP } from 'lib/util'; 10 | import { useToggle } from 'ui/hooks/useToggle'; 11 | import { getInitValue } from 'lib/initValue'; 12 | 13 | interface Props extends ArgComponentProps { 14 | component: React.ComponentType>; 15 | registry: Registry; 16 | typeDef: TypeDef; 17 | } 18 | 19 | export function Option({ 20 | component: Component, 21 | onChange: _onChange, 22 | nestingNumber, 23 | registry, 24 | typeDef, 25 | value = null, 26 | }: Props) { 27 | const { accounts } = useApi(); 28 | const [isSupplied, toggleIsSupplied] = useToggle(value !== null); 29 | const isSuppliedRef = useRef(isSupplied); 30 | 31 | const onChange = useCallback( 32 | (value: OrFalsy): void => { 33 | if (!isSupplied) { 34 | _onChange(null); 35 | 36 | return; 37 | } 38 | 39 | _onChange(value); 40 | }, 41 | [_onChange, isSupplied], 42 | ); 43 | 44 | useEffect((): void => { 45 | if (isSupplied && !isSuppliedRef.current && value === null && accounts) { 46 | onChange(getInitValue(registry, accounts, typeDef.sub as TypeDef)); 47 | isSuppliedRef.current = true; 48 | } else if (!isSupplied && isSuppliedRef.current && value !== null) { 49 | onChange(null); 50 | isSuppliedRef.current = false; 51 | } 52 | }, [accounts, registry, onChange, value, isSupplied, typeDef.sub]); 53 | 54 | return ( 55 |
56 | {isSupplied ? ( 57 |
58 | 65 |
66 | ) : ( 67 |
68 | 69 |
70 | )} 71 |
72 | 73 |
74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /src/ui/components/form/Struct.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { encodeTypeDef } from '@polkadot/types'; 5 | import { FormField } from './FormField'; 6 | import { TypeDefInfo, ArgComponentProps } from 'types'; 7 | 8 | type Props = ArgComponentProps> & { 9 | components: React.ComponentType>[]; 10 | }; 11 | 12 | export function Struct({ components, value, nestingNumber, onChange, registry, typeDef }: Props) { 13 | const _onChange = (name: string) => (newEntry: unknown) => { 14 | const newValue = { ...value, [name]: newEntry }; 15 | onChange(newValue); 16 | }; 17 | const subTypes = 18 | typeDef.info === TypeDefInfo.Si 19 | ? registry.lookup.getTypeDef(typeDef.type as `Lookup${number}`).sub 20 | : typeDef.sub; 21 | return ( 22 |
23 | {subTypes && 24 | components && 25 | components.map((Component, index) => { 26 | const subType = Array.isArray(subTypes) ? subTypes[index] : subTypes; 27 | const name = subType.displayName || subType.name || ''; 28 | 29 | return ( 30 | 35 | 42 | 43 | ); 44 | })} 45 |
46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/ui/components/form/SubForm.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { classes } from 'lib/util'; 5 | 6 | interface Props extends React.HTMLAttributes { 7 | nestingNumber: number; 8 | } 9 | 10 | export function SubForm({ children, className, nestingNumber }: Props) { 11 | const isOddNesting = nestingNumber % 2 != 0; 12 | 13 | return ( 14 |
21 | {children} 22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/ui/components/form/Tuple.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { encodeTypeDef } from '@polkadot/types'; 5 | import { useCallback } from 'react'; 6 | import { FormField } from './FormField'; 7 | import { ArgComponentProps, OrFalsy, TypeDef } from 'types'; 8 | 9 | interface Props extends ArgComponentProps { 10 | components: React.ComponentType>[]; 11 | } 12 | 13 | export function Tuple({ 14 | className, 15 | components, 16 | nestingNumber, 17 | onChange: _onChange, 18 | registry, 19 | typeDef, 20 | value, 21 | }: Props) { 22 | const onChange = useCallback( 23 | (index: number) => 24 | (newValue: OrFalsy): void => { 25 | _onChange(value.map((argAtIndex, atIndex) => (atIndex === index ? newValue : argAtIndex))); 26 | }, 27 | [_onChange, value], 28 | ); 29 | 30 | return ( 31 |
32 | {components.map((Component, index) => { 33 | const subType = (typeDef.sub as TypeDef[])[index]; 34 | 35 | return ( 36 | 41 | 48 | 49 | ); 50 | })} 51 |
52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/ui/components/form/VectorFixed.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback } from 'react'; 5 | import { FormField } from './FormField'; 6 | import { ArgComponentProps, OrFalsy } from 'types'; 7 | 8 | interface Props extends ArgComponentProps { 9 | component: React.ComponentType>; 10 | } 11 | 12 | export function VectorFixed({ 13 | component: Component, 14 | nestingNumber, 15 | onChange: _onChange, 16 | registry, 17 | typeDef, 18 | value, 19 | }: Props) { 20 | const length = typeDef.length; 21 | 22 | const onChange = useCallback( 23 | (index: number) => 24 | (newValue: OrFalsy): void => { 25 | _onChange(value.map((argAtIndex, atIndex) => (atIndex === index ? newValue : argAtIndex))); 26 | }, 27 | [_onChange, value], 28 | ); 29 | 30 | return ( 31 | value && ( 32 |
33 | {[...Array(length).keys()].map((_, index) => { 34 | return ( 35 | 41 | 48 | 49 | ); 50 | })} 51 |
52 | ) 53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /src/ui/components/form/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './useMetadataField'; 5 | -------------------------------------------------------------------------------- /src/ui/components/form/hooks/useMetadataField.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useMemo, useState } from 'react'; 5 | import { useParams } from 'react-router'; 6 | import { FileState, OrFalsy, UseMetadata } from 'types'; 7 | import { useDatabase } from 'ui/contexts'; 8 | import { useDbQuery } from 'ui/hooks'; 9 | import { useMetadata } from 'ui/hooks/useMetadata'; 10 | 11 | interface UseMetadataField extends UseMetadata { 12 | file: OrFalsy; 13 | isLoading: boolean; 14 | isStored: boolean; 15 | } 16 | 17 | export const useMetadataField = (): UseMetadataField => { 18 | const { db } = useDatabase(); 19 | const { codeHash: codeHashUrlParam } = useParams<{ codeHash: string }>(); 20 | 21 | const [file, setFile] = useState>(null); 22 | 23 | const [codeBundle, isLoading] = useDbQuery( 24 | () => db.codeBundles.get({ codeHash: codeHashUrlParam || '' }), 25 | [codeHashUrlParam, db], 26 | ); 27 | const metadata = useMetadata(codeBundle?.abi, { 28 | isWasmRequired: !codeHashUrlParam && window.location.pathname.includes('instantiate'), 29 | revertOnFileRemove: true, 30 | onChange: setFile, 31 | }); 32 | 33 | const isStored = useMemo((): boolean => !!codeBundle, [codeBundle]); 34 | 35 | return { 36 | file, 37 | isLoading: isLoading && !!codeHashUrlParam, 38 | isStored, 39 | ...metadata, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /src/ui/components/form/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './hooks'; 5 | export * from './FormField'; 6 | export * from './Input'; 7 | export * from './InputBalance'; 8 | export * from './InputBn'; 9 | export * from './InputFile'; 10 | export * from './InputNumber'; 11 | export * from './InputSalt'; 12 | export * from './InputWeight'; 13 | export * from './InputStorageDepositLimit'; 14 | export * from './ArgumentForm'; 15 | export * from './OptionsForm'; 16 | export * from './Bool'; 17 | -------------------------------------------------------------------------------- /src/ui/components/homepage/Contracts.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { FolderOpenIcon, TrashIcon } from '@heroicons/react/outline'; 5 | import { useCallback, useState } from 'react'; 6 | import { Link } from 'react-router-dom'; 7 | import { ContractRow } from '../contract/ContractRow'; 8 | import { ForgetAllContractsModal } from 'ui/components/modal'; 9 | import { useDatabase } from 'ui/contexts'; 10 | import { useDbQuery } from 'ui/hooks'; 11 | 12 | export function Contracts(): React.ReactElement | null { 13 | const { db } = useDatabase(); 14 | const [isOpen, setIsOpen] = useState(false); 15 | const [contracts, isLoading] = useDbQuery(() => db.contracts.toArray(), [db]); 16 | const forgetAllContracts = useCallback(() => db.contracts.clear(), [db]); 17 | 18 | if (isLoading || !contracts) { 19 | return null; 20 | } 21 | 22 | if (contracts.length === 0) { 23 | return ( 24 |
25 | 26 |
You haven't uploaded any contracts yet on this browser.
27 | 28 | Upload a new contract 29 | 30 |
31 | ); 32 | } 33 | 34 | return ( 35 | <> 36 | 37 |
38 |
39 | {contracts?.map(contract => { 40 | return ; 41 | })} 42 |
43 |
44 | 52 |
53 |
54 | 55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /src/ui/components/homepage/HelpBox.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export function HelpBox(): React.ReactElement | null { 5 | return ( 6 |
7 |
8 |
9 |
10 | New to ink!? Check out the documentation! 11 |
12 | 17 |
18 | 19 |
20 |
21 | Need some guidance? Find an example! 22 |
23 | 28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/components/homepage/Statistics.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useMemo, useState } from 'react'; 5 | import { useApi } from 'ui/contexts'; 6 | import { AnyJson, ApiPromise, ChainProperties } from 'types'; 7 | 8 | function getChainType(systemChainType: ChainProperties['systemChainType']): string { 9 | if (systemChainType.isDevelopment) return 'Development'; 10 | if (systemChainType.isLocal) return 'Local'; 11 | if (systemChainType.isLive) return 'Live'; 12 | if (systemChainType.isCustom) return 'Custom'; 13 | return 'Unknown'; 14 | } 15 | 16 | export function Statistics(): React.ReactElement | null { 17 | const { api, systemChain, systemName, systemChainType, tokenSymbol } = useApi(); 18 | 19 | const [blockNumber, setBlockNumber] = useState(''); 20 | 21 | useEffect(() => { 22 | async function listenToBlocks(api: ApiPromise) { 23 | return api.rpc.chain.subscribeNewHeads(header => { 24 | setBlockNumber(header.number.toHuman()); 25 | }); 26 | } 27 | let cleanUp: VoidFunction | undefined; 28 | listenToBlocks(api) 29 | .then(unsub => (cleanUp = unsub)) 30 | .catch(console.error); 31 | 32 | return () => cleanUp && cleanUp(); 33 | }, [api]); 34 | 35 | const entries = useMemo((): Record => { 36 | return { 37 | 'Chain Name': systemChainType.isDevelopment ? systemName : systemChain, 38 | 'Chain Type': getChainType(systemChainType), 39 | 'Highest Block': `#${blockNumber}`, 40 | Token: tokenSymbol, 41 | }; 42 | }, [blockNumber, systemChain, systemChainType, systemName, tokenSymbol]); 43 | 44 | return ( 45 | <> 46 |
50 | {Object.entries(entries).map(([label, value], i) => { 51 | return ( 52 |
53 |
{label}
54 |
{value}
55 |
56 | ); 57 | })} 58 |
59 | 60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/components/homepage/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './Contracts'; 5 | export * from './HelpBox'; 6 | export * from './Statistics'; 7 | -------------------------------------------------------------------------------- /src/ui/components/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './account'; 5 | export * from './common'; 6 | export * from './contract'; 7 | export * from './form'; 8 | export * from './homepage'; 9 | export * from './instantiate'; 10 | export * from './message'; 11 | export * from './AwaitApis'; 12 | export * from './CheckBrowserSupport'; 13 | -------------------------------------------------------------------------------- /src/ui/components/instantiate/Wizard.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { DryRun } from './DryRun'; 5 | import { Step1 } from './Step1'; 6 | import { Step2 } from './Step2'; 7 | import { Step3 } from './Step3'; 8 | import { useInstantiate } from 'ui/contexts'; 9 | 10 | export function Wizard() { 11 | const { 12 | step, 13 | data: { metadata }, 14 | } = useInstantiate(); 15 | 16 | return ( 17 |
18 |
19 | 20 | {metadata && } 21 | {step === 3 && } 22 |
23 | {step === 2 && ( 24 | 27 | )} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/ui/components/instantiate/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './AvailableCodeBundles'; 5 | export * from './LookUpCodeHash'; 6 | export * from './Wizard'; 7 | -------------------------------------------------------------------------------- /src/ui/components/message/ArgSignature.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { encodeTypeDef } from '@polkadot/types/create'; 5 | import { Registry, TypeDef } from 'types'; 6 | import { classes } from 'lib/util'; 7 | 8 | interface Props extends React.HTMLAttributes { 9 | arg: { name?: string; type: TypeDef }; 10 | registry: Registry; 11 | value?: string; 12 | } 13 | 14 | const MAX_PARAM_LENGTH = 20; 15 | 16 | function truncate(param: string): string { 17 | return param.length > MAX_PARAM_LENGTH 18 | ? `${param.substring(0, MAX_PARAM_LENGTH / 2)}…${param.substring( 19 | param.length - MAX_PARAM_LENGTH / 2, 20 | )}` 21 | : param; 22 | } 23 | 24 | export function ArgSignature({ 25 | arg: { name, type }, 26 | children, 27 | className, 28 | registry, 29 | value, 30 | ...props 31 | }: Props) { 32 | return ( 33 | 34 | {name ? `${name}: ` : ''} 35 | 36 | {value ? {truncate(value)} : type.typeName || encodeTypeDef(registry, type)} 37 | 38 | {children} 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /src/ui/components/message/MessageDocs.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Disclosure } from '@headlessui/react'; 5 | import { ChevronUpIcon } from '@heroicons/react/solid'; 6 | import ReactMarkdown from 'react-markdown'; 7 | import remarkGfm from 'remark-gfm'; 8 | import { MessageSignature } from './MessageSignature'; 9 | import type { AbiMessage, Registry } from 'types'; 10 | import { classes } from 'lib/util'; 11 | 12 | interface Props extends React.ComponentProps { 13 | message: AbiMessage; 14 | registry: Registry; 15 | className?: string; 16 | } 17 | 18 | export const MessageDocs = ({ 19 | message, 20 | message: { docs }, 21 | registry, 22 | className, 23 | ...restOfProps 24 | }: Props) => { 25 | return ( 26 | 27 | {({ open }) => ( 28 |
29 | 33 | 36 | 37 | 38 | 39 | {/* eslint-disable-next-line react/no-children-prop */} 40 | {docs.length ? ( 41 | {docs.join('\r\n')} 42 | ) : ( 43 | No documentation provided 44 | )} 45 | 46 |
47 | )} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/ui/components/message/MessageSignature.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { encodeTypeDef } from '@polkadot/types/create'; 5 | import { DatabaseIcon } from '@heroicons/react/outline'; 6 | import { ArgSignature } from './ArgSignature'; 7 | import type { AbiMessage, Registry } from 'types'; 8 | import { classes } from 'lib/util'; 9 | 10 | interface Props extends React.HTMLAttributes { 11 | message: Partial; 12 | params?: unknown[]; 13 | registry: Registry; 14 | } 15 | 16 | export function MessageSignature({ 17 | className, 18 | message: { args, isConstructor, isMutating, method, returnType }, 19 | params = [], 20 | registry, 21 | }: Props) { 22 | return ( 23 |
24 | 31 | {method} 32 | 33 | ( 34 | {args?.map((arg, index): React.ReactNode => { 35 | return ( 36 | 42 | {index < args.length - 1 && ', '} 43 | 44 | ); 45 | })} 46 | ) 47 | {!isConstructor && returnType && ( 48 | <> 49 | : {encodeTypeDef(registry, returnType)} 50 | 51 | )} 52 | {isMutating && ( 53 | <> 54 | 55 | 56 | )} 57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/components/message/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './ArgSignature'; 5 | export * from './MessageDocs'; 6 | export * from './MessageSignature'; 7 | -------------------------------------------------------------------------------- /src/ui/components/metadata/GetPatronMetadata.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Buffer } from 'buffer'; 5 | 6 | function getPatronMetadata(field: string, hash: string) { 7 | const options = { 8 | method: 'GET', 9 | headers: 10 | field === 'metadata' 11 | ? { 12 | 'Content-Type': 'application/json', 13 | } 14 | : { 15 | 'Content-Type': 'text/plain; charset=UTF-8', 16 | }, 17 | mode: 'cors' as RequestMode, 18 | }; 19 | 20 | return fetch('https://api.patron.works/buildSessions/' + field + '/' + hash, options).then( 21 | response => { 22 | if (!response.ok) { 23 | throw new Error(`HTTP error! Status: ${response.status}`); 24 | } 25 | return field === 'metadata' ? response.json() : response.arrayBuffer(); 26 | }, 27 | ); 28 | } 29 | 30 | export function onDropPatronFile(patronCodeHash: string, onDrop: (files: File[]) => void) { 31 | const metadataPromise = getPatronMetadata('metadata', patronCodeHash); 32 | const wasmPromise = getPatronMetadata('wasm', patronCodeHash); 33 | Promise.all([metadataPromise, wasmPromise]) 34 | .then(([metadataResponse, wasmResponse]) => { 35 | const result = Buffer.from(wasmResponse as ArrayBuffer).toString('hex'); 36 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access 37 | metadataResponse.source.wasm = '0x' + result; 38 | const metadataString = JSON.stringify(metadataResponse); 39 | const blob = new Blob([metadataString], { type: 'application/json' }); 40 | const patronFile = new File([blob], 'patron-contract.json', { 41 | lastModified: new Date(0).getTime(), 42 | type: 'json', 43 | }); 44 | onDrop([patronFile]); 45 | }) 46 | .catch(e => { 47 | console.error(e); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/ui/components/metadata/Metadata.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2023 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { classes } from 'lib/util'; 5 | import { Abi } from 'types'; 6 | 7 | interface Props extends React.HTMLAttributes { 8 | metadata: Abi; 9 | } 10 | 11 | export function Metadata({ metadata, className = '', ...restOfProps }: Props) { 12 | return ( 13 |
20 |
21 |
Contract Hash
22 |
23 | {metadata.info.contract.hash.toHex()} 24 |
25 |
26 |
27 |
Language
28 |
{metadata.info.source.language}
29 |
30 |
31 |
Compiler
32 |
{metadata.info.source.compiler}
33 |
34 |
35 |
Contract version
36 |
{metadata.info.contract.version}
37 |
38 |
39 |
Authors
40 |
{metadata.info.contract.authors}
41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/ui/components/metadata/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2023 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './Metadata'; 5 | -------------------------------------------------------------------------------- /src/ui/components/modal/ForgetAllContractsModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { TrashIcon } from '@heroicons/react/outline'; 5 | import { useCallback, useState } from 'react'; 6 | import type { ModalProps } from './ModalBase'; 7 | import { ModalBase as Modal } from './ModalBase'; 8 | 9 | interface Props extends ModalProps { 10 | confirm: () => Promise; 11 | } 12 | 13 | export const ForgetAllContractsModal = ({ isOpen, setIsOpen, confirm }: Omit) => { 14 | const [isBusy, setIsBusy] = useState(false); 15 | const onConfirm = useCallback(() => { 16 | setIsBusy(true); 17 | confirm() 18 | .then(() => setIsOpen(false)) 19 | .catch(console.error) 20 | .finally(() => setIsBusy(false)); 21 | }, [confirm, setIsOpen]); 22 | 23 | return ( 24 | 25 |
26 |

27 | You will remove the metadata for all contract instances from browser storage. 28 |

29 |

30 | This operation has no on-chain consequences. The forget operation only removes references 31 | to these contracts from your browser. 32 |

33 | 42 |
43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/ui/components/modal/ForgetContractModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { TrashIcon } from '@heroicons/react/outline'; 5 | import { ModalBase as Modal } from './ModalBase'; 6 | import type { ModalProps } from './ModalBase'; 7 | 8 | interface Props extends ModalProps { 9 | confirm: () => void; 10 | } 11 | 12 | export const ForgetContractModal = ({ isOpen, setIsOpen, confirm }: Omit) => { 13 | return ( 14 | 15 |
16 |

17 | You will remove the metadata for this contract instance from browser storage. This 18 | operation has no on-chain consequences.
The forget operation only limits your 19 | access to the contract on this browser. 20 |

21 | 29 |
30 |
31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /src/ui/components/modal/Logos.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export const GithubLogo = () => { 5 | return ( 6 | 13 | 14 | 15 | ); 16 | }; 17 | export const StackExchangeLogo = () => { 18 | return ( 19 | 27 | 28 | 29 | 30 | 31 | 32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /src/ui/components/modal/SettingsModal.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { CustomEndpoint } from '../settings/CustomEndpoint'; 5 | import { ThemeMode } from '../settings/ThemeMode'; 6 | import type { ModalProps } from './ModalBase'; 7 | import { ModalBase as Modal } from './ModalBase'; 8 | 9 | export const SettingsModal = ({ isOpen, setIsOpen }: Omit) => { 10 | return ( 11 | 12 |
13 |

Appearance

14 | 15 |
16 | 17 |
18 |

Local Node

19 | 20 |
21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /src/ui/components/modal/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './ForgetAllContractsModal'; 5 | export * from './ForgetContractModal'; 6 | export * from './HelpModal'; 7 | -------------------------------------------------------------------------------- /src/ui/components/settings/CustomEndpoint.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useState } from 'react'; 5 | import { useNavigate } from 'react-router'; 6 | 7 | import { LOCAL, LOCAL_STORAGE_KEY } from '../../../constants'; 8 | import { useLocalStorage } from '../../hooks/useLocalStorage'; 9 | import { Button } from '../common/Button'; 10 | import { Input } from '../form/Input'; 11 | import { isValidWsUrl } from 'lib/util'; 12 | 13 | export function CustomEndpoint() { 14 | const [customEndpoint, setCustomEndpoint] = useLocalStorage( 15 | LOCAL_STORAGE_KEY.CUSTOM_ENDPOINT, 16 | LOCAL.rpc, 17 | ); 18 | const [value, setValue] = useState(customEndpoint); 19 | const navigate = useNavigate(); 20 | 21 | const onApply = useCallback(() => { 22 | if (isValidWsUrl(value)) { 23 | setCustomEndpoint(value); 24 | navigate(`/?rpc=${value}`); 25 | } 26 | }, [value, setCustomEndpoint, navigate]); 27 | 28 | return ( 29 |
30 |
31 | Custom Endpoint 32 | 33 | Use a custom endpoint for the local nodes 34 | 35 |
36 | 37 |
38 | 39 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /src/ui/components/settings/ThemeMode.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Dropdown } from 'ui/components/common/Dropdown'; 5 | import { useTheme } from 'ui/contexts'; 6 | 7 | const options = [ 8 | { 9 | label: 'Light', 10 | value: 'light', 11 | }, 12 | { 13 | label: 'Dark', 14 | value: 'dark', 15 | }, 16 | ]; 17 | 18 | export function ThemeMode() { 19 | const { theme, setTheme } = useTheme(); 20 | return ( 21 |
22 |
23 | Theme mode 24 | Select a display theme 25 |
26 |
27 | setTheme && setTheme(e as 'light' | 'dark')} 29 | options={options} 30 | value={theme} 31 | /> 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/ui/components/settings/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './CustomEndpoint'; 5 | export * from './ThemeMode'; 6 | -------------------------------------------------------------------------------- /src/ui/contexts/DatabaseContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { createContext, useContext, useEffect, useState } from 'react'; 5 | import { useApi } from './ApiContext'; 6 | import { Database } from 'src/services/db'; 7 | import { DbState } from 'types'; 8 | 9 | export const DbContext: React.Context = createContext({} as unknown as DbState); 10 | export const DbConsumer: React.Consumer = DbContext.Consumer; 11 | export const DbProvider: React.Provider = DbContext.Provider; 12 | 13 | const INITIAL = {} as unknown as DbState; 14 | 15 | export function DatabaseContextProvider({ 16 | children, 17 | }: React.HTMLAttributes): JSX.Element | null { 18 | const { genesisHash } = useApi(); 19 | const [state, setState] = useState(INITIAL); 20 | 21 | useEffect((): void => { 22 | setState({ 23 | db: new Database(genesisHash), 24 | }); 25 | }, [genesisHash]); 26 | 27 | return {children}; 28 | } 29 | 30 | export function useDatabase(): DbState { 31 | return useContext(DbContext); 32 | } 33 | -------------------------------------------------------------------------------- /src/ui/contexts/InstantiateContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { createContext, useState, useContext } from 'react'; 5 | import { 6 | InstantiateProps, 7 | InstantiateState, 8 | CodeSubmittableResult, 9 | BlueprintSubmittableResult, 10 | InstantiateData, 11 | ContractInstantiateResult, 12 | } from 'types'; 13 | 14 | const InstantiateContext = createContext(undefined); 15 | 16 | export function isResultValid({ 17 | contract, 18 | }: CodeSubmittableResult<'promise'> | BlueprintSubmittableResult<'promise'>): boolean { 19 | return !!contract; 20 | } 21 | 22 | export function InstantiateContextProvider({ 23 | children, 24 | }: React.PropsWithChildren>) { 25 | const [step, setStep] = useState<1 | 2 | 3>(1); 26 | const [data, setData] = useState({} as InstantiateData); 27 | const [dryRunResult, setDryRunResult] = useState(); 28 | 29 | const value: InstantiateState = { 30 | data, 31 | setData, 32 | step, 33 | setStep, 34 | dryRunResult, 35 | setDryRunResult, 36 | }; 37 | 38 | return {children}; 39 | } 40 | 41 | export const useInstantiate = () => { 42 | const context = useContext(InstantiateContext); 43 | if (context === undefined) { 44 | throw new Error('useInstantiate must be used within an InstantiateProvider'); 45 | } 46 | return context; 47 | }; 48 | -------------------------------------------------------------------------------- /src/ui/contexts/ThemeContext.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { createContext, useContext, useEffect } from 'react'; 5 | import { LOCAL_STORAGE_KEY } from '../../constants'; 6 | import { useLocalStorage } from 'ui/hooks/useLocalStorage'; 7 | 8 | type Theme = 'light' | 'dark'; 9 | type Props = { 10 | theme: Theme; 11 | setTheme?: (_: Theme) => void; 12 | }; 13 | 14 | const INIT_STATE: Props = { 15 | theme: 16 | 'theme' in localStorage 17 | ? localStorage.theme === 'dark' 18 | ? 'dark' 19 | : 'light' 20 | : window.matchMedia('(prefers-color-scheme: dark)').matches 21 | ? 'dark' 22 | : 'light', 23 | }; 24 | 25 | export const ThemeContext = createContext(INIT_STATE); 26 | 27 | export const ThemeContextProvider = ({ children }: React.PropsWithChildren>) => { 28 | const [theme, setTheme] = useLocalStorage(LOCAL_STORAGE_KEY.THEME, INIT_STATE.theme); 29 | useEffect(() => { 30 | if (theme === 'dark') { 31 | document.documentElement.classList.add('dark'); 32 | } else { 33 | document.documentElement.classList.remove('dark'); 34 | } 35 | }, [theme]); 36 | return {children}; 37 | }; 38 | export const useTheme = () => useContext(ThemeContext); 39 | -------------------------------------------------------------------------------- /src/ui/contexts/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './ApiContext'; 5 | export * from './DatabaseContext'; 6 | export * from './InstantiateContext'; 7 | export * from './TransactionsContext'; 8 | export * from './ThemeContext'; 9 | -------------------------------------------------------------------------------- /src/ui/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './useLocalStorage'; 5 | export * from './useDbQuery'; 6 | export * from './useWeight'; 7 | export * from './useBalance'; 8 | export * from './useFormField'; 9 | export * from './useArgValues'; 10 | export * from './useNewContract'; 11 | export * from './useStorageDepositLimit'; 12 | export * from './useToggle'; 13 | export * from './useStoredContract'; 14 | -------------------------------------------------------------------------------- /src/ui/hooks/useAccountAvailable.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { keyring } from '@polkadot/ui-keyring'; 5 | import { useMemo } from 'react'; 6 | 7 | export const useAccountAvailable = (accountId?: string): boolean | undefined => 8 | useMemo(() => { 9 | if (accountId === '' || accountId === undefined) return undefined; 10 | try { 11 | keyring.getPair(accountId); 12 | return true; 13 | } catch { 14 | return false; 15 | } 16 | }, [accountId]); 17 | -------------------------------------------------------------------------------- /src/ui/hooks/useArgValues.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useMemo, useRef, useState } from 'react'; 5 | import { useApi } from 'ui/contexts/ApiContext'; 6 | import { AbiMessage, AbiParam, Account, Registry, SetState } from 'types'; 7 | import { getInitValue } from 'lib/initValue'; 8 | import { transformUserInput } from 'lib/callOptions'; 9 | 10 | type ArgValues = Record; 11 | 12 | function fromArgs(registry: Registry, accounts: Account[], args: AbiParam[]): ArgValues { 13 | const result: ArgValues = {}; 14 | 15 | args?.forEach(({ name, type }) => { 16 | result[name] = getInitValue(registry, accounts, type); 17 | }); 18 | 19 | return result; 20 | } 21 | 22 | export function useArgValues( 23 | message: AbiMessage | undefined, 24 | registry: Registry, 25 | ): [ArgValues, SetState, Uint8Array | undefined] { 26 | const { accounts } = useApi(); 27 | const [value, setValue] = useState( 28 | accounts && message ? fromArgs(registry, accounts, message.args) : {}, 29 | ); 30 | const argsRef = useRef(message?.args ?? []); 31 | 32 | const inputData = useMemo(() => { 33 | let data: Uint8Array | undefined; 34 | try { 35 | data = message?.toU8a(transformUserInput(registry, message.args, value)); 36 | } catch (e) { 37 | console.error(e); 38 | } 39 | return data; 40 | }, [value, registry, message]); 41 | 42 | useEffect((): void => { 43 | if (accounts && message && argsRef.current !== message.args) { 44 | setValue(fromArgs(registry, accounts, message.args)); 45 | argsRef.current = message.args; 46 | } 47 | }, [accounts, message, registry]); 48 | 49 | return [value, setValue, inputData]; 50 | } 51 | -------------------------------------------------------------------------------- /src/ui/hooks/useBalance.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import BN from 'bn.js'; 5 | import { useCallback } from 'react'; 6 | import { useFormField } from './useFormField'; 7 | import { toBalance, toSats, BN_ONE, BN_TWO, BN_ZERO, isBn } from 'lib/bn'; 8 | import { useApi } from 'ui/contexts/ApiContext'; 9 | import type { UseBalance, Validation } from 'types'; 10 | 11 | type BitLength = 8 | 16 | 32 | 64 | 128 | 256; 12 | 13 | const DEFAULT_BITLENGTH = 128; 14 | 15 | interface ValidateOptions { 16 | bitLength?: BitLength; 17 | isZeroable?: boolean; 18 | maxValue?: BN; 19 | } 20 | 21 | function getGlobalMaxValue(bitLength?: number): BN { 22 | return BN_TWO.pow(new BN(bitLength || DEFAULT_BITLENGTH)).isub(BN_ONE); 23 | } 24 | 25 | export function useBalance( 26 | initialValue: BN | string | number = 0, 27 | { bitLength = DEFAULT_BITLENGTH, isZeroable = true, maxValue }: ValidateOptions = {}, 28 | ): UseBalance { 29 | const { api } = useApi(); 30 | 31 | const validate = useCallback( 32 | (value: BN | null | undefined): Validation => { 33 | let message: React.ReactNode; 34 | let isError = false; 35 | 36 | if (!value) { 37 | isError = true; 38 | return { 39 | isError, 40 | }; 41 | } 42 | 43 | if (value?.lt(BN_ZERO)) { 44 | isError = true; 45 | message = 'Value cannot be negative'; 46 | } 47 | 48 | if (value?.gt(getGlobalMaxValue(bitLength))) { 49 | isError = true; 50 | message = 'Value exceeds global maximum'; 51 | } 52 | 53 | if (!isZeroable && value?.isZero()) { 54 | isError = true; 55 | message = 'Value cannot be zero'; 56 | } 57 | 58 | if (value && value?.bitLength() > (bitLength || DEFAULT_BITLENGTH)) { 59 | isError = true; 60 | message = "Value's bitlength is too high"; 61 | } 62 | 63 | if (maxValue && maxValue.gtn(0) && value?.gt(maxValue)) { 64 | isError = true; 65 | message = `Value exceeds available balance`; 66 | } 67 | 68 | return { 69 | isError, 70 | isValid: !isError, 71 | message, 72 | }; 73 | }, 74 | [bitLength, isZeroable, maxValue], 75 | ); 76 | 77 | const balance = useFormField( 78 | isBn(initialValue) ? toSats(api, initialValue) : toBalance(api, initialValue), 79 | validate, 80 | ); 81 | 82 | return balance; 83 | } 84 | -------------------------------------------------------------------------------- /src/ui/hooks/useDbQuery.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useLiveQuery } from 'dexie-react-hooks'; 5 | import { useMemo } from 'react'; 6 | import { OrFalsy } from 'types'; 7 | 8 | export function useDbQuery( 9 | querier: () => T | Promise, 10 | deps: unknown[] = [], 11 | ): [OrFalsy, boolean] { 12 | const liveQuery = useLiveQuery(querier, deps, null); 13 | const isLoading = useMemo(() => liveQuery === null, [liveQuery]); 14 | 15 | return [liveQuery, isLoading]; 16 | } 17 | -------------------------------------------------------------------------------- /src/ui/hooks/useFormField.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useMemo, useRef, useState } from 'react'; 5 | 6 | import { isNull, isUndefined } from 'lib/util'; 7 | import type { ValidFormField, ValidateFn, Validation } from 'types'; 8 | 9 | export function useFormField( 10 | defaultValue: T, 11 | validate: ValidateFn = value => ({ isValid: !isNull(value), message: null }), 12 | ): ValidFormField { 13 | const [value, setValue] = useState(defaultValue); 14 | const [validation, setValidation] = useState>(validate(value)); 15 | const isTouched = useRef(false); 16 | 17 | const isError = useMemo(() => { 18 | if (!isTouched.current) { 19 | return false; 20 | } 21 | 22 | return !validation.isValid; 23 | }, [validation.isValid]); 24 | 25 | const onChange = useCallback( 26 | (value?: T | null) => { 27 | if (!isUndefined(value) && !isNull(value)) { 28 | setValue(value); 29 | setValidation(validate(value)); 30 | isTouched.current = true; 31 | } 32 | }, 33 | [validate], 34 | ); 35 | 36 | return useMemo( 37 | () => ({ 38 | isError, 39 | isTouched: isTouched.current, 40 | isValid: validation.isValid, 41 | isWarning: validation.isWarning || false, 42 | message: validation.message, 43 | onChange, 44 | value, 45 | }), 46 | [value, onChange, isError, validation.isValid, validation.isWarning, validation.message], 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/ui/hooks/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useEffect, useRef } from 'react'; 5 | 6 | export function useIsMounted(): boolean { 7 | const isMounted = useRef(false); 8 | 9 | useEffect((): (() => void) => { 10 | isMounted.current = true; 11 | 12 | return (): void => { 13 | isMounted.current = false; 14 | }; 15 | }, []); 16 | 17 | return isMounted.current; 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/hooks/useLocalStorage.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useState } from 'react'; 5 | import { LocalStorageKey } from '../../constants'; 6 | 7 | export function useLocalStorage(key: LocalStorageKey, initialValue: T): [T, (_: T) => void] { 8 | const [storedValue, setStoredValue] = useState(() => { 9 | try { 10 | const item = window.localStorage.getItem(key); 11 | 12 | return item ? (JSON.parse(item) as T) : initialValue; 13 | } catch (error) { 14 | console.error(error); 15 | return initialValue; 16 | } 17 | }); 18 | 19 | const setValue = (value: T) => { 20 | try { 21 | const valueToStore = value instanceof Function ? (value(storedValue) as T) : value; 22 | setStoredValue(valueToStore); 23 | 24 | window.localStorage.setItem(key, JSON.stringify(valueToStore)); 25 | } catch (error) { 26 | console.error(error); 27 | } 28 | }; 29 | 30 | return [storedValue, setValue]; 31 | } 32 | -------------------------------------------------------------------------------- /src/ui/hooks/useNewContract.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useNavigate } from 'react-router'; 5 | import type { BlueprintSubmittableResult } from 'types'; 6 | import { useDatabase, useInstantiate } from 'ui/contexts'; 7 | 8 | export function useNewContract() { 9 | const { db } = useDatabase(); 10 | const navigate = useNavigate(); 11 | const { 12 | data: { accountId, name }, 13 | } = useInstantiate(); 14 | 15 | return async function ({ contract }: BlueprintSubmittableResult<'promise'>): Promise { 16 | if (accountId && contract?.abi.json) { 17 | const codeHash = contract.abi.info.source.wasmHash.toHex(); 18 | const document = { 19 | abi: contract.abi.json, 20 | address: contract.address.toString(), 21 | codeHash, 22 | date: new Date().toISOString(), 23 | name, 24 | }; 25 | 26 | await Promise.all([ 27 | db.contracts.add(document), 28 | db.codeBundles.get({ codeHash }).then(codeBundle => { 29 | if (!codeBundle) { 30 | return db.codeBundles.add(document); 31 | } 32 | }), 33 | ]); 34 | 35 | navigate(`/contract/${contract.address}`); 36 | } 37 | }; 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/hooks/useNonEmptyString.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback } from 'react'; 5 | import { useFormField } from './useFormField'; 6 | import type { ValidFormField, Validation } from 'types'; 7 | 8 | export function useNonEmptyString(initialValue = ''): ValidFormField { 9 | const validate = useCallback((value?: string | null): Validation => { 10 | if (!value || value.trim().length === 0) { 11 | return { isValid: false, message: 'Value cannot be empty' }; 12 | } 13 | 14 | return { isValid: true }; 15 | }, []); 16 | return useFormField(initialValue, validate); 17 | } 18 | -------------------------------------------------------------------------------- /src/ui/hooks/useStorageDepositLimit.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable header/header */ 2 | // Copyright 2017-2021 @polkadot/react-hooks authors & contributors 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import type BN from 'bn.js'; 6 | import { useEffect, useState } from 'react'; 7 | import { useBalance } from './useBalance'; 8 | import { useToggle } from './useToggle'; 9 | import { useApi } from 'ui/contexts/ApiContext'; 10 | import type { OrFalsy, UseStorageDepositLimit } from 'types'; 11 | import { BN_ZERO } from 'lib/bn'; 12 | 13 | export function useStorageDepositLimit(accountId: OrFalsy): UseStorageDepositLimit { 14 | const { api } = useApi(); 15 | const [maximum, setMaximum] = useState(); 16 | const [isActive, toggleIsActive] = useToggle(false); 17 | 18 | const storageDepositLimit = useBalance(BN_ZERO, { maxValue: maximum }); 19 | 20 | useEffect((): void => { 21 | accountId && 22 | api.derive.balances 23 | .account(accountId) 24 | .then(({ freeBalance }) => setMaximum(freeBalance)) 25 | .catch(console.error); 26 | }, [accountId, api]); 27 | 28 | return { 29 | ...storageDepositLimit, 30 | maximum, 31 | isActive, 32 | toggleIsActive, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/hooks/useStoredContract.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useLiveQuery } from 'dexie-react-hooks'; 5 | import { useNavigate } from 'react-router-dom'; 6 | import { useState } from 'react'; 7 | import { useApi, useDatabase } from 'ui/contexts'; 8 | import { ContractDocument, ContractPromise, UIContract } from 'types'; 9 | 10 | export function useStoredContract(address: string): UIContract | undefined { 11 | const navigate = useNavigate(); 12 | const { api } = useApi(); 13 | const { db } = useDatabase(); 14 | const [contract, setContract] = useState(); 15 | const [document, setDocument] = useState(); 16 | 17 | useLiveQuery(async () => { 18 | // setting to undefined to prevent metadata "leak" on route change 19 | // https://github.com/use-ink/contracts-ui/issues/359 20 | setContract(undefined); 21 | setDocument(undefined); 22 | const d = await db.contracts.get({ address }); 23 | if (!d) { 24 | navigate('/'); 25 | } else { 26 | const c = new ContractPromise(api, d.abi, address); 27 | setDocument(d); 28 | setContract(c); 29 | } 30 | }, [address]); 31 | 32 | if (!document || !contract) return undefined; 33 | 34 | return { 35 | abi: contract.abi, 36 | name: contract.abi.info.contract.name.toString(), 37 | displayName: document.name, 38 | tx: contract.tx, 39 | codeHash: document.codeHash, 40 | address: contract.address.toString(), 41 | date: document.date, 42 | id: document.id, 43 | type: document.external ? 'added' : 'instantiated', 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/ui/hooks/useStoredMetadata.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useDbQuery } from '.'; 5 | import { Abi } from 'types'; 6 | import { useDatabase } from 'ui/contexts'; 7 | 8 | export const useStoredMetadata = () => { 9 | const { db } = useDatabase(); 10 | 11 | return useDbQuery(async () => { 12 | const collectedAbis = new Set(); 13 | const storedAbis: Abi[] = []; 14 | await db.contracts.each(contract => { 15 | if (collectedAbis.has(contract.codeHash)) return; 16 | 17 | const abi = new Abi(contract.abi); 18 | storedAbis.push(abi); 19 | collectedAbis.add(contract.codeHash); 20 | }); 21 | return storedAbis; 22 | }, [db]); 23 | }; 24 | -------------------------------------------------------------------------------- /src/ui/hooks/useToggle.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useCallback, useEffect, useState } from 'react'; 5 | import { useIsMounted } from './useIsMounted'; 6 | import type { UseToggle } from 'types'; 7 | 8 | // Simple wrapper for a true/false toggle 9 | export function useToggle(defaultValue = false, onToggle?: (isActive: boolean) => void): UseToggle { 10 | const isMounted = useIsMounted(); 11 | const [isActive, setActive] = useState(defaultValue); 12 | 13 | const toggle = useCallback((): void => { 14 | setActive(isActive => !isActive); 15 | }, []); 16 | 17 | useEffect(() => { 18 | isMounted && !!onToggle && onToggle(isActive); 19 | }, [isMounted, isActive, onToggle]); 20 | 21 | return [isActive, toggle, setActive]; 22 | } 23 | -------------------------------------------------------------------------------- /src/ui/hooks/useWeight.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable header/header */ 2 | // Copyright 2017-2021 @polkadot/react-hooks authors & contributors 3 | // SPDX-License-Identifier: Apache-2.0 4 | 5 | import { useEffect, useState } from 'react'; 6 | import * as Yup from 'yup'; 7 | import { BN_HUNDRED } from 'lib/bn'; 8 | import type { BN, UIGas, InputMode } from 'types'; 9 | 10 | const schema = Yup.number().positive('Value must be positive').min(2).required(); 11 | 12 | export const useWeight = (estimation?: BN): UIGas => { 13 | const [limit, setLimit] = useState(estimation ?? BN_HUNDRED); 14 | const [mode, setMode] = useState('estimation'); 15 | const [errorMsg, setErrorMsg] = useState(''); 16 | const [isValid, setIsValid] = useState(false); 17 | const [text, setText] = useState(limit.toString()); 18 | 19 | useEffect(() => { 20 | if (mode === 'estimation' && estimation && !estimation.eq(limit) && !estimation.isZero()) { 21 | setText(estimation.toString()); 22 | setLimit(estimation); 23 | } 24 | }, [estimation, limit, mode, setLimit]); 25 | 26 | useEffect(() => { 27 | async function validate() { 28 | try { 29 | const valid = await schema.validate(text); 30 | if (valid) { 31 | setIsValid(true); 32 | setErrorMsg(''); 33 | } 34 | } catch (err) { 35 | const { errors } = (err as { errors: string[] }) ?? { errors: [''] }; 36 | setIsValid(false); 37 | setErrorMsg(errors[0]); 38 | } 39 | } 40 | validate().catch(e => console.error(e)); 41 | }, [text]); 42 | 43 | return { 44 | isValid, 45 | limit, 46 | setLimit, 47 | mode, 48 | setMode, 49 | errorMsg, 50 | setErrorMsg, 51 | setIsValid, 52 | text, 53 | setText, 54 | }; 55 | }; 56 | -------------------------------------------------------------------------------- /src/ui/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Buffer } from 'buffer'; 5 | import { createRoot } from 'react-dom/client'; 6 | import { BrowserRouter, Route, Routes } from 'react-router-dom'; 7 | import App from 'ui/components/App'; 8 | import 'react-tooltip/dist/react-tooltip.css'; 9 | import './styles/main.css'; 10 | import '@polkadot/api-augment'; 11 | import { 12 | AddContract, 13 | Contract, 14 | Homepage, 15 | Instantiate, 16 | SelectCodeHash, 17 | NotFound, 18 | AddressLookup, 19 | } from 'ui/pages'; 20 | 21 | globalThis.Buffer = Buffer; 22 | 23 | const container = document.getElementById('app-root'); 24 | // non-null assertion encouraged by react 18 upgrade guide 25 | // https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis 26 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 | const root = createRoot(container!); 28 | 29 | root.render( 30 | 31 | 32 | } path="/"> 33 | } index /> 34 | } path="add-contract" /> 35 | } path="address-lookup" /> 36 | } path="hash-lookup" /> 37 | } path="instantiate"> 38 | 39 | 40 | } path="contract/:address/" /> 41 | } path="*" /> 42 | 43 | 44 | , 45 | ); 46 | -------------------------------------------------------------------------------- /src/ui/layout/RootLayout.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { HTMLAttributes } from 'react'; 5 | import { classes } from 'lib/util'; 6 | 7 | export function RootLayout({ accessory, heading, help, children, aside }: PageProps) { 8 | return ( 9 |
15 |
16 |
17 | {accessory &&
{accessory}
} 18 |

{heading}

19 |

{help}

20 |
21 | 22 |
{children}
23 |
24 | {aside && } 25 |
26 | ); 27 | } 28 | 29 | interface PageProps extends HTMLAttributes { 30 | accessory?: React.ReactNode; 31 | heading: React.ReactNode; 32 | help?: React.ReactNode; 33 | aside?: React.ReactNode; 34 | } 35 | -------------------------------------------------------------------------------- /src/ui/layout/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './RootLayout'; 5 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/MobileMenu.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { MenuIcon, ShareIcon } from '@heroicons/react/outline'; 5 | import { useState } from 'react'; 6 | import { Footer } from './Footer'; 7 | import { Navigation } from './Navigation'; 8 | import { NetworkAndUser } from './NetworkAndUser'; 9 | import { QuickLinks } from './QuickLinks'; 10 | 11 | export function MobileMenu() { 12 | const [networkMenuOpen, setNetworkMenuOpen] = useState(false); 13 | const [mainMenuOpen, setMainMenuOpen] = useState(false); 14 | 15 | const toggleMainMenu = () => { 16 | setNetworkMenuOpen(false); 17 | mainMenuOpen === false ? setMainMenuOpen(true) : setMainMenuOpen(false); 18 | }; 19 | 20 | const toggleNetworkMenu = () => { 21 | setMainMenuOpen(false); 22 | networkMenuOpen === false ? setNetworkMenuOpen(true) : setNetworkMenuOpen(false); 23 | }; 24 | 25 | return ( 26 |
27 |
28 | 31 |
32 |

Contracts UI

33 |
34 | 40 |
41 | {mainMenuOpen && ( 42 | <> 43 |
44 |
45 | 46 | 47 |
48 |
49 |
50 | 51 | )} 52 | {networkMenuOpen && ( 53 | <> 54 |
55 |
56 | 57 |
58 | 59 | )} 60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/NavLink.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { NavLink as NavLinkBase, NavLinkProps } from 'react-router-dom'; 5 | 6 | interface Props extends NavLinkProps { 7 | icon: (_: React.ComponentProps<'svg'>) => JSX.Element; 8 | } 9 | 10 | export function NavLink({ children, icon: Icon, ...props }: Props): React.ReactElement { 11 | return ( 12 | 13 | <> 14 | 15 | {children} 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/Navigation.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { DocumentAddIcon, CollectionIcon } from '@heroicons/react/outline'; 5 | import { NavLink } from './NavLink'; 6 | 7 | export function Navigation() { 8 | return ( 9 |
10 | 11 | Add New Contract 12 | 13 | 14 | All Contracts 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/NetworkAndUser.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { useNavigate } from 'react-router'; 5 | 6 | import { MAINNETS, TESTNETS } from '../../../constants'; 7 | import { useApi } from 'ui/contexts'; 8 | import { classes } from 'lib/util'; 9 | import { Dropdown } from 'ui/components'; 10 | 11 | const testnetOptions = TESTNETS.map(network => ({ 12 | label: network.name, 13 | value: network.rpc, 14 | })); 15 | 16 | const mainnetOptions = MAINNETS.map(network => ({ 17 | label: network.name, 18 | value: network.rpc, 19 | })); 20 | 21 | const allOptions = [...testnetOptions, ...mainnetOptions]; 22 | 23 | const dropdownOptions = [ 24 | { 25 | label: 'Live Networks', 26 | options: mainnetOptions, 27 | }, 28 | { 29 | label: 'Test Networks', 30 | options: testnetOptions, 31 | }, 32 | ]; 33 | 34 | export function NetworkAndUser() { 35 | const { endpoint, status } = useApi(); 36 | const navigate = useNavigate(); 37 | 38 | return ( 39 |
40 | { 48 | navigate(`/?rpc=${e}`); 49 | }} 50 | options={dropdownOptions} 51 | value={allOptions.find(o => o.value === endpoint)?.value || allOptions[0].value} 52 | /> 53 |
54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/QuickLinks.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { DocumentTextIcon } from '@heroicons/react/outline'; 5 | import { Link } from 'react-router-dom'; 6 | import { NavLink } from './NavLink'; 7 | import { useDatabase } from 'ui/contexts'; 8 | import { useDbQuery } from 'ui/hooks'; 9 | 10 | export function QuickLinks() { 11 | const { db } = useDatabase(); 12 | const [contracts] = useDbQuery(() => db?.contracts.toArray() || Promise.resolve(null), [db]); 13 | 14 | return ( 15 |
16 |
17 |
Your Contracts
18 | {contracts && contracts.length > 0 ? ( 19 | contracts.map(({ name, address }) => { 20 | return ( 21 | 22 | {name} 23 | 24 | ); 25 | }) 26 | ) : ( 27 |
28 | None yet  29 | {' • '}  30 | 31 | Upload one 32 | 33 |
34 | )} 35 |
36 |
37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /src/ui/layout/sidebar/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Footer } from './Footer'; 5 | import { MobileMenu } from './MobileMenu'; 6 | import { Navigation } from './Navigation'; 7 | import { NetworkAndUser } from './NetworkAndUser'; 8 | import { QuickLinks } from './QuickLinks'; 9 | 10 | export function Sidebar() { 11 | return ( 12 | <> 13 |
14 |
15 |
16 | 21 |
22 |
23 |
24 |
25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/ui/pages/Contract.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { BookOpenIcon, PlayIcon } from '@heroicons/react/outline'; 5 | import { useState } from 'react'; 6 | import { useParams } from 'react-router-dom'; 7 | import { ContractHeader } from './ContractHeader'; 8 | import { InteractTab } from 'ui/components/contract/Interact'; 9 | import { MetadataTab } from 'ui/components/contract/MetadataTab'; 10 | import { Loader } from 'ui/components/common/Loader'; 11 | import { Tabs } from 'ui/components/common/Tabs'; 12 | import { HeaderButtons } from 'ui/components/common/HeaderButtons'; 13 | import { RootLayout } from 'ui/layout'; 14 | import { useStoredContract } from 'ui/hooks'; 15 | 16 | const TABS = [ 17 | { 18 | id: 'metadata', 19 | label: ( 20 | <> 21 | 22 | Metadata 23 | 24 | ), 25 | }, 26 | { 27 | id: 'interact', 28 | label: ( 29 | <> 30 | 31 | Interact 32 | 33 | ), 34 | }, 35 | ]; 36 | 37 | export function Contract() { 38 | const [tabIndex, setTabIndex] = useState(1); 39 | const { address } = useParams(); 40 | if (!address) throw new Error('No address in url'); 41 | const contract = useStoredContract(address); 42 | 43 | return ( 44 | 45 | {contract && ( 46 | } 48 | heading={contract.displayName || contract.name} 49 | help={} 50 | > 51 | 52 | 53 | 54 | 55 | 56 | )} 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /src/ui/pages/ContractHeader.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Link } from 'react-router-dom'; 5 | import { CopyButton } from '../components/common/CopyButton'; 6 | import { ObservedBalance } from '../components/common/ObservedBalance'; 7 | import { displayDate, truncate } from 'lib/util'; 8 | import { UIContract } from 'types'; 9 | 10 | interface Props { 11 | document: UIContract; 12 | } 13 | 14 | export function ContractHeader({ document: { name, type, address, date, codeHash } }: Props) { 15 | switch (type) { 16 | case 'added': 17 | return ( 18 |
19 | You added this contract from{' '} 20 |
21 | 22 | {truncate(address, 4)} 23 | 24 | 25 |
{' '} 26 | on {displayDate(date)} and holds a value of{' '} 27 | 28 | 29 | 30 |
31 | ); 32 | case 'instantiated': 33 | return ( 34 |
35 | You instantiated this contract{' '} 36 |
37 | 41 | {truncate(address, 4)} 42 | 43 | 44 |
{' '} 45 | from{' '} 46 | 51 | {name} 52 | {' '} 53 | on {displayDate(date)} and holds a value of{' '} 54 | 55 | 56 | 57 |
58 | ); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/ui/pages/Homepage.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Contracts, HelpBox, Statistics } from '../components/homepage'; 5 | import { RootLayout } from 'ui/layout'; 6 | 7 | export function Homepage() { 8 | return ( 9 | 12 | 13 | 14 | 15 | } 16 | heading="Contracts" 17 | > 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/pages/Instantiate.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Link, useParams } from 'react-router-dom'; 5 | import { RootLayout } from '../layout/RootLayout'; 6 | import { Wizard } from 'ui/components/instantiate'; 7 | import { InstantiateContextProvider } from 'ui/contexts'; 8 | 9 | export function Instantiate() { 10 | const { codeHash: codeHashUrlParam } = useParams<{ codeHash: string }>(); 11 | 12 | return ( 13 | 20 | You can upload and instantiate new contract code{' '} 21 | 22 | here 23 | 24 | . 25 | 26 | ) : ( 27 | <> 28 | You can instantiate a new contract from an existing code bundle{' '} 29 | 30 | here 31 | 32 | . 33 | 34 | ) 35 | } 36 | > 37 | 38 | 39 | 40 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /src/ui/pages/NotFound.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { EmojiSadIcon } from '@heroicons/react/outline'; 5 | import { useNavigate } from 'react-router'; 6 | import { Button } from 'ui/components'; 7 | 8 | export function NotFound() { 9 | const navigate = useNavigate(); 10 | return ( 11 |
12 |
13 | 14 |

This page does not exist.

15 | 18 |
19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /src/ui/pages/SelectCodeHash.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { Link } from 'react-router-dom'; 5 | import { LookUpCodeHash, AvailableCodeBundles } from 'ui/components/instantiate'; 6 | import { RootLayout } from 'ui/layout'; 7 | 8 | export function SelectCodeHash() { 9 | return ( 10 | 14 | You can upload and instantiate new contract code{' '} 15 | 16 | here 17 | 18 | . 19 | 20 | } 21 | > 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/ui/pages/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | export * from './AddContract'; 5 | export * from './Contract'; 6 | export * from './Homepage'; 7 | export * from './Instantiate'; 8 | export * from './SelectCodeHash'; 9 | export * from './NotFound'; 10 | export * from './AddressLookup'; 11 | -------------------------------------------------------------------------------- /src/ui/styles/base-typography.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @layer base { 4 | h1 { 5 | @apply text-2.5xl font-semibold text-gray-700 dark:text-white; 6 | } 7 | 8 | h2 { 9 | @apply text-lg font-semibold text-gray-700 dark:text-white; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/styles/base.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | body { 4 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 5 | 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 6 | font-weight: 400; 7 | @apply antialiased; 8 | } 9 | 10 | .text-mono { 11 | font-family: 'SF Mono', 'SFMono-Regular', ui-monospace, 'DejaVu Sans Mono', 'Menlo', 'Consolas', 12 | monospace; 13 | } 14 | 15 | .content { 16 | width: calc(100vw - 14rem); 17 | } 18 | 19 | .content main { 20 | width: 100%; 21 | } 22 | 23 | .content aside { 24 | width: 100%; 25 | } 26 | a:focus { 27 | outline: none; 28 | } 29 | @screen xl { 30 | .content main { 31 | width: calc(100% - 24rem); 32 | } 33 | 34 | .content aside { 35 | width: 24rem; 36 | } 37 | } 38 | .markdown { 39 | @apply leading-relaxed; 40 | } 41 | .markdown code { 42 | @apply rounded-sm bg-gray-200 text-xs dark:bg-elevation-2; 43 | padding: 2px 4px; 44 | } 45 | .markdown :last-child { 46 | @apply mb-0; 47 | } 48 | .markdown h1, 49 | .markdown h2, 50 | .markdown h3, 51 | .markdown h4, 52 | .markdown h5, 53 | .markdown h6, 54 | .markdown p, 55 | .markdown ul { 56 | @apply mb-2; 57 | } 58 | 59 | .markdown h1 { 60 | @apply font-bold; 61 | } 62 | 63 | .page-error { 64 | @apply m-2 flex w-full items-center justify-center overflow-y-auto overflow-x-hidden px-5 py-3; 65 | } 66 | 67 | .page-error > div { 68 | @apply grid max-w-lg place-content-center justify-items-center rounded-md border px-12 py-8 text-center text-gray-500 dark:border-gray-700; 69 | } 70 | 71 | .page-error > div > div { 72 | @apply mb-6 last:mb-0; 73 | } 74 | 75 | .page-error > div a { 76 | @apply text-blue-400; 77 | } 78 | -------------------------------------------------------------------------------- /src/ui/styles/button.css: -------------------------------------------------------------------------------- 1 | .btn { 2 | @apply flex items-center justify-center rounded px-4 py-2 text-xs font-semibold disabled:cursor-not-allowed disabled:opacity-60; 3 | } 4 | 5 | .btn.plain { 6 | @apply rounded-none px-0 py-0; 7 | } 8 | 9 | .btn.default:not(:disabled) { 10 | @apply border border-gray-200 bg-gray-100 text-gray-600 hover:bg-gray-200 dark:border-gray-700 dark:bg-elevation-3 dark:text-gray-300 dark:hover:bg-elevation-1; 11 | } 12 | 13 | .btn.primary { 14 | @apply border border-green-500 bg-green-500 text-gray-100; 15 | } 16 | 17 | .btn.primary:hover:not(:disabled) { 18 | @apply border border-green-450 bg-green-450; 19 | } 20 | -------------------------------------------------------------------------------- /src/ui/styles/call-results.css: -------------------------------------------------------------------------------- 1 | .event-log :last-child { 2 | @apply mb-0; 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/styles/collapsible.css: -------------------------------------------------------------------------------- 1 | .collapsible-panel { 2 | @apply overflow-hidden rounded-md border border-gray-200 text-sm text-gray-600 dark:border-gray-700 dark:text-gray-300; 3 | } 4 | -------------------------------------------------------------------------------- /src/ui/styles/dropdown.css: -------------------------------------------------------------------------------- 1 | .dropdown { 2 | @apply relative cursor-pointer; 3 | } 4 | 5 | .dropdown .dropdown__control { 6 | @apply relative flex w-full cursor-pointer items-center rounded border border-gray-200 bg-transparent bg-white p-0.5 text-left text-sm focus:outline-none dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300; 7 | } 8 | 9 | .dropdown__control--is-focused, 10 | .dropdown__control--menu-is-open { 11 | @apply dark:border-gray-300 dark:shadow-none dark:hover:border-gray-300; 12 | } 13 | 14 | .dropdown .dropdown__single-value { 15 | @apply text-gray-500 dark:text-gray-300; 16 | } 17 | 18 | .dropdown__value-container { 19 | @apply block min-w-0 flex-1 truncate; 20 | } 21 | 22 | .dropdown__dropdown-indicator svg { 23 | @apply h-5 w-5 text-gray-400; 24 | } 25 | 26 | .dropdown__indicator-separator { 27 | @apply hidden; 28 | } 29 | 30 | .dropdown__input-container { 31 | @apply m-0; 32 | } 33 | 34 | .dropdown .dropdown__menu { 35 | @apply absolute max-h-80 w-full overflow-y-auto rounded-b border border-gray-200 bg-white text-sm text-gray-500 dark:border-gray-stroke dark:bg-gray-900 dark:text-gray-300; 36 | z-index: 1000; 37 | } 38 | 39 | .dropdown .dropdown__option { 40 | @apply relative flex cursor-pointer select-none items-center px-3 py-2 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-elevation-2; 41 | } 42 | 43 | .dropdown__option > span { 44 | @apply block flex-grow truncate; 45 | } 46 | 47 | .dropdown__option svg.selected { 48 | @apply h-3.5 w-3.5; 49 | } 50 | 51 | .dropdown__option--is-selected { 52 | @apply font-bold dark:bg-elevation-1; 53 | } 54 | 55 | .dropdown__option--is-focused { 56 | @apply bg-transparent; 57 | } 58 | 59 | .dropdown.chain .dropdown__control { 60 | @apply dark:hover:bg-elevation-2; 61 | } 62 | 63 | .dropdown.chain .dropdown__single-value { 64 | @apply flex w-full flex-grow items-center text-sm font-medium; 65 | } 66 | 67 | .dropdown.chain .dropdown__single-value:before { 68 | content: ''; 69 | @apply mr-2 block h-1.5 w-1.5 rounded-full; 70 | } 71 | 72 | .dropdown.chain.isConnected .dropdown__single-value:before { 73 | @apply bg-green-400; 74 | } 75 | 76 | .dropdown.chain.isConnecting .dropdown__single-value:before { 77 | @apply bg-yellow-300; 78 | } 79 | 80 | .dropdown.chain.isError .dropdown__single-value:before { 81 | @apply bg-red-400; 82 | } 83 | 84 | .account-select .dropdown__control { 85 | min-height: 68px; 86 | } 87 | 88 | .dropdown .dropdown__control .font-mono, 89 | .dropdown .dropdown__menu .font-mono, 90 | .dropdown.chain .dropdown__single-value .font-mono { 91 | @apply text-xs; 92 | } 93 | -------------------------------------------------------------------------------- /src/ui/styles/form.css: -------------------------------------------------------------------------------- 1 | .form-field { 2 | @apply mb-6 last-of-type:mb-0; 3 | } 4 | 5 | .form-field > label { 6 | @apply mb-1.5 inline-flex items-center text-sm font-semibold text-gray-600 dark:text-white; 7 | } 8 | 9 | .form-field > label.arg-label { 10 | @apply font-mono text-xs; 11 | } 12 | 13 | .form-field > label > svg { 14 | @apply ml-1.5 h-4 w-4 cursor-help dark:text-gray-500; 15 | } 16 | 17 | .argument-form .form-field > label { 18 | @apply font-mono text-xs; 19 | } 20 | 21 | .validation { 22 | @apply mt-2 flex items-center text-sm; 23 | } 24 | 25 | .validation svg { 26 | @apply mr-1 h-3 w-3; 27 | } 28 | 29 | .validation.warning { 30 | @apply text-orange-400; 31 | } 32 | 33 | .validation.error { 34 | @apply text-red-400; 35 | } 36 | 37 | .validation.success { 38 | @apply text-green-400; 39 | } 40 | 41 | [type='text']:focus, 42 | [type='email']:focus, 43 | [type='url']:focus, 44 | [type='password']:focus, 45 | [type='number']:focus, 46 | [type='date']:focus, 47 | [type='datetime-local']:focus, 48 | [type='month']:focus, 49 | [type='search']:focus, 50 | [type='tel']:focus, 51 | [type='time']:focus, 52 | [type='week']:focus, 53 | [multiple]:focus, 54 | textarea:focus, 55 | select:focus { 56 | @apply border-gray-300 outline-none ring-gray-300; 57 | } 58 | 59 | .arg-label { 60 | @apply font-mono text-xs font-semibold text-gray-600 dark:text-white; 61 | } 62 | -------------------------------------------------------------------------------- /src/ui/styles/instantiate.css: -------------------------------------------------------------------------------- 1 | .review { 2 | @apply mb-10 flex flex-wrap space-y-7 rounded-md border border-gray-200 bg-white p-8 text-sm text-gray-700 dark:border-gray-700 dark:bg-elevation-1 dark:text-gray-300; 3 | } 4 | 5 | .review .field { 6 | @apply w-1/2; 7 | } 8 | 9 | .review .field.full { 10 | @apply w-full; 11 | } 12 | 13 | .review .field .key { 14 | @apply mb-2 font-semibold text-gray-700 dark:text-gray-300; 15 | } 16 | 17 | .review .field .value { 18 | @apply text-gray-500; 19 | } 20 | 21 | .instantiate-outcome { 22 | @apply mt-6; 23 | } 24 | 25 | .instantiate-outcome .body { 26 | @apply p-4 text-xs; 27 | } 28 | 29 | .instantiate-outcome .row { 30 | @apply flex items-center dark:text-gray-400; 31 | } 32 | 33 | .instantiate-outcome .row:not(:last-child) { 34 | @apply mb-2; 35 | } 36 | 37 | .instantiate-outcome .row div:last-child { 38 | @apply flex-1 text-left; 39 | } 40 | 41 | .instantiate-outcome .row div:last-child { 42 | @apply text-right; 43 | } 44 | -------------------------------------------------------------------------------- /src/ui/styles/link.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | @apply flex items-center rounded-md border border-transparent px-2 py-2 text-base font-medium capitalize text-gray-600 hover:text-gray-400 dark:text-gray-300 dark:hover:text-white md:py-1 md:text-sm; 3 | } 4 | 5 | .nav-link.active { 6 | @apply border-gray-200 dark:border-gray-stroke dark:bg-elevation-2; 7 | } 8 | 9 | .nav-link svg { 10 | @apply mr-1.5 h-5 w-5 dark:text-gray-500; 11 | } 12 | -------------------------------------------------------------------------------- /src/ui/styles/search.css: -------------------------------------------------------------------------------- 1 | .search-results { 2 | @apply absolute mt-2 flex w-80 max-w-xs flex-col rounded shadow dark:bg-gray-800; 3 | } 4 | 5 | .search-results > * { 6 | @apply px-4 py-2; 7 | } 8 | 9 | .search-results .header { 10 | @apply text-xs dark:text-gray-500; 11 | } 12 | 13 | .search-results .item { 14 | @apply flex cursor-pointer items-center text-sm dark:hover:bg-purple-500; 15 | } 16 | 17 | .search-results .item > * { 18 | @apply mr-2 last:mr-0; 19 | } 20 | 21 | .search-results .item svg { 22 | @apply h-5 w-5 dark:text-gray-500; 23 | } 24 | 25 | .search-results .item .identifier { 26 | @apply overflow-hidden overflow-ellipsis text-gray-500; 27 | } 28 | -------------------------------------------------------------------------------- /src/ui/styles/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | @apply flex; 3 | width: 14rem; 4 | min-width: 14rem; 5 | } 6 | 7 | .sidebar .sidebar-inner { 8 | @apply flex h-0 min-h-full flex-1 flex-col border-r border-gray-200 bg-white dark:border-gray-stroke dark:bg-elevation-1; 9 | } 10 | 11 | .sidebar .sidebar-inner .upper { 12 | @apply flex flex-1 flex-col overflow-y-auto pb-4 pt-3; 13 | } 14 | 15 | .sidebar .sidebar-inner .upper nav { 16 | max-width: 14rem; 17 | @apply space-y-5; 18 | } 19 | 20 | .sidebar .sidebar-inner .upper nav > * { 21 | @apply px-3; 22 | } 23 | 24 | .sidebar .network-selection { 25 | @apply space-y-2; 26 | } 27 | 28 | .sidebar .connect-account { 29 | @apply mt-2 w-full justify-center text-center font-semibold; 30 | } 31 | 32 | .sidebar .navigation { 33 | @apply space-y-2; 34 | } 35 | 36 | .sidebar .quick-links { 37 | @apply space-y-2; 38 | } 39 | 40 | .sidebar .section .header { 41 | @apply py-1 text-xs font-medium text-gray-600 dark:text-gray-400; 42 | } 43 | 44 | .sidebar .section .none-yet { 45 | @apply pl-2 text-xs text-gray-400 dark:text-gray-300; 46 | } 47 | 48 | .sidebar .footer { 49 | @apply flex flex-shrink-0 p-6; 50 | } 51 | 52 | .sidebar .footer > div { 53 | @apply flex w-full flex-shrink-0 justify-between; 54 | } 55 | 56 | .mobilemenu { 57 | @apply dark:bg-elevation-1; 58 | } 59 | -------------------------------------------------------------------------------- /src/ui/styles/tabs.css: -------------------------------------------------------------------------------- 1 | .routed-tabs { 2 | @apply mb-6 flex border-b border-gray-200 pb-2 text-xs dark:border-gray-800 dark:text-white; 3 | } 4 | .routed-tabs .tab { 5 | @apply flex items-center rounded-md border-0 border-transparent px-4 py-2 text-xs font-medium text-gray-600 dark:text-gray-300 dark:hover:text-white; 6 | } 7 | .routed-tabs .tab svg { 8 | @apply mr-1 h-4 w-4; 9 | } 10 | 11 | .routed-tabs .tab.active { 12 | @apply border-gray-200 bg-gray-200 dark:border-gray-stroke dark:bg-elevation-2; 13 | } 14 | -------------------------------------------------------------------------------- /src/ui/util/dropdown.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | 4 | import { MessageSignature } from '../components/message/MessageSignature'; 5 | import { 6 | AbiConstructor, 7 | AbiMessage, 8 | DropdownOption, 9 | ContractDocument, 10 | Registry, 11 | Account, 12 | } from 'types'; 13 | 14 | export function createConstructorOptions( 15 | registry: Registry, 16 | data?: AbiConstructor[], 17 | ): DropdownOption[] { 18 | return (data || []).map((constructor, index) => ({ 19 | label: , 20 | value: index, 21 | })); 22 | } 23 | 24 | export function createMessageOptions( 25 | registry: Registry, 26 | data?: AbiMessage[], 27 | ): DropdownOption[] { 28 | return (data || []).map(message => ({ 29 | label: , 30 | value: message, 31 | })); 32 | } 33 | 34 | export function createAccountOptions(data: Account[]): DropdownOption[] { 35 | return data.map(pair => ({ 36 | label: pair.meta?.name as string, 37 | value: pair.address || '', 38 | })); 39 | } 40 | 41 | export function createContractOptions(data: ContractDocument[]): DropdownOption[] { 42 | return data.map(({ name, address }) => ({ 43 | label: name, 44 | value: address, 45 | })); 46 | } 47 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "noUnusedLocals": true, 6 | "noUnusedParameters": true, 7 | "baseUrl": ".", 8 | "target": "esnext", 9 | "useDefineForClassFields": true, 10 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 11 | "allowJs": false, 12 | "skipLibCheck": true, 13 | "esModuleInterop": false, 14 | "allowSyntheticDefaultImports": true, 15 | "strict": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "module": "esnext", 18 | "moduleResolution": "bundler", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx", 23 | "paths": { 24 | "services": ["src/services"], 25 | "services/*": ["src/services/*"], 26 | "lib": ["src/lib"], 27 | "lib/*": ["src/lib/*"], 28 | "db": ["src/services/db"], 29 | "types": ["./src/types"], 30 | "types/*": ["./src/types/*"], 31 | "ui": ["./src/ui"], 32 | "ui/*": ["./src/ui/*"] 33 | } 34 | }, 35 | "include": ["src", "snapshots.js"] 36 | } 37 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | // Copyright 2022-2024 use-ink/contracts-ui authors & contributors 2 | // SPDX-License-Identifier: GPL-3.0-only 3 | /// 4 | import { defineConfig } from 'vite'; 5 | import react from '@vitejs/plugin-react-swc'; 6 | import tsConfigPaths from 'vite-tsconfig-paths'; 7 | import istanbul from 'vite-plugin-istanbul'; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | plugins: [ 12 | react(), 13 | tsConfigPaths(), 14 | istanbul({ 15 | include: 'src/*', 16 | exclude: ['node_modules', 'cypress/'], 17 | cypress: true, 18 | }), 19 | ], 20 | server: { host: '127.0.0.1', port: 8081 }, 21 | build: { 22 | target: 'esnext', 23 | rollupOptions: { 24 | output: { 25 | dir: './dist', 26 | manualChunks(id) { 27 | if (/[\\/]node_modules[\\/](react|react-dom)[\\/]/.test(id)) { 28 | return 'react'; 29 | } 30 | 31 | if (/[\\/]node_modules[\\/](@polkadot)[\\/]/.test(id)) { 32 | return 'polkadot'; 33 | } 34 | }, 35 | }, 36 | }, 37 | }, 38 | }); 39 | --------------------------------------------------------------------------------