├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── lint.yml │ ├── release.yml │ ├── spellcheck.yml │ └── validate_pr_title.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── cspell.json ├── package.json ├── packages └── useink │ ├── package.json │ ├── src │ ├── chains │ │ ├── data │ │ │ ├── chaindata.ts │ │ │ ├── index.ts │ │ │ ├── testnet-chaindata.ts │ │ │ └── types.ts │ │ ├── index.ts │ │ └── types.ts │ ├── core │ │ ├── contracts │ │ │ ├── call.ts │ │ │ ├── decodeCallResult.ts │ │ │ ├── decodeError.ts │ │ │ ├── getRegistryError.ts │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── jsonToAbi.ts │ │ │ ├── toContractAbiMessage.ts │ │ │ ├── toRegistryErrorDecoded.ts │ │ │ ├── txPaymentInfo.ts │ │ │ └── types │ │ │ │ ├── index.ts │ │ │ │ └── jsonToAbi.ts │ │ ├── index.ts │ │ ├── substrate │ │ │ ├── balances │ │ │ │ ├── getBalance.ts │ │ │ │ ├── index.ts │ │ │ │ └── transfer.ts │ │ │ ├── index.ts │ │ │ ├── registry │ │ │ │ ├── index.ts │ │ │ │ └── tokens.ts │ │ │ └── timestamp │ │ │ │ ├── getTimestampDate.ts │ │ │ │ ├── getTimestampNow.ts │ │ │ │ ├── getTimestampQuery.ts │ │ │ │ └── index.ts │ │ └── types │ │ │ ├── api-contract.ts │ │ │ ├── api.ts │ │ │ ├── array.ts │ │ │ ├── contracts.ts │ │ │ ├── index.ts │ │ │ ├── result.ts │ │ │ ├── substrate.ts │ │ │ ├── talisman-connect-wallets.ts │ │ │ └── unsub.ts │ ├── index.ts │ ├── notifications │ │ ├── README.md │ │ ├── context.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── useNotifications.ts │ │ │ └── useTxNotifications.ts │ │ ├── index.ts │ │ ├── model.ts │ │ ├── provider.tsx │ │ ├── reducer.ts │ │ ├── types.ts │ │ └── utils │ │ │ ├── index.ts │ │ │ └── toNotificationLevel.ts │ ├── react │ │ ├── constants.ts │ │ ├── hooks │ │ │ ├── config │ │ │ │ ├── index.ts │ │ │ │ ├── useChain.ts │ │ │ │ ├── useChainRpc.ts │ │ │ │ ├── useChainRpcList.ts │ │ │ │ ├── useChains.ts │ │ │ │ ├── useConfig.ts │ │ │ │ └── useDefaultCaller.ts │ │ │ ├── contracts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── useAbiMessage.ts │ │ │ │ ├── useCall.ts │ │ │ │ ├── useCallSubscription.ts │ │ │ │ ├── useCodeHash.ts │ │ │ │ ├── useContract.ts │ │ │ │ ├── useDeployer │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── types.ts │ │ │ │ │ └── useDeployer.ts │ │ │ │ ├── useDryRun.ts │ │ │ │ ├── useEventSubscription.ts │ │ │ │ ├── useEvents.ts │ │ │ │ ├── useMessageSigner.ts │ │ │ │ ├── useMetadata.ts │ │ │ │ ├── useSalter.ts │ │ │ │ ├── useSignatureVerifier.ts │ │ │ │ ├── useTx.ts │ │ │ │ ├── useTxEvents.ts │ │ │ │ └── useTxPaymentInfo.ts │ │ │ ├── helpers │ │ │ │ ├── index.ts │ │ │ │ └── useUnixMilliToDate.ts │ │ │ ├── index.ts │ │ │ ├── internal │ │ │ │ ├── useInterval.ts │ │ │ │ └── useIsMounted.ts │ │ │ ├── substrate │ │ │ │ ├── balance │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useBalance.ts │ │ │ │ ├── index.ts │ │ │ │ ├── timestamp │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── useTimestampDate.ts │ │ │ │ │ ├── useTimestampNow.ts │ │ │ │ │ └── useTimestampQuery.ts │ │ │ │ ├── useApi.ts │ │ │ │ ├── useBlockHeader.ts │ │ │ │ ├── useChainDecimals.ts │ │ │ │ ├── useTokenSymbol.ts │ │ │ │ └── useTransfer.ts │ │ │ └── wallets │ │ │ │ ├── index.ts │ │ │ │ ├── useAllWallets.ts │ │ │ │ ├── useInstalledWallets.ts │ │ │ │ ├── useUninstalledWallets.ts │ │ │ │ └── useWallet.ts │ │ ├── index.ts │ │ └── providers │ │ │ ├── UseInkProvider.tsx │ │ │ ├── api │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── provider.tsx │ │ │ └── reducer.ts │ │ │ ├── blockHeader │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── provider.tsx │ │ │ └── reducer.ts │ │ │ ├── config │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── provider.tsx │ │ │ ├── events │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ ├── provider.tsx │ │ │ └── reducer.ts │ │ │ ├── index.ts │ │ │ └── wallet │ │ │ ├── context.ts │ │ │ ├── index.ts │ │ │ ├── model.ts │ │ │ └── provider.tsx │ └── utils │ │ ├── contracts │ │ ├── index.ts │ │ ├── toBasicMetadata.ts │ │ ├── toJSType.ts │ │ ├── toMessageParams.ts │ │ ├── txUtils.ts │ │ └── validateMetadata.ts │ │ ├── events │ │ ├── contracts │ │ │ ├── ContractInstantiated.ts │ │ │ └── index.ts │ │ ├── formatEventName.ts │ │ ├── index.ts │ │ ├── system │ │ │ ├── ExtrinsicFailed.ts │ │ │ └── index.ts │ │ └── types.ts │ │ ├── helpers │ │ ├── NOOP.ts │ │ ├── encodeSalt.ts │ │ ├── formatFileName.ts │ │ ├── getExpiredItem.ts │ │ ├── index.ts │ │ ├── isTxCancelledError.ts │ │ ├── isValidHash.ts │ │ ├── parseUnits │ │ │ ├── index.ts │ │ │ └── parseUnits.ts │ │ ├── pseudoRandomHex.ts │ │ ├── pseudoRandomId.ts │ │ ├── pseudoRandomU8a.ts │ │ └── unixMilliToDate.ts │ │ ├── index.ts │ │ ├── pick │ │ ├── index.ts │ │ ├── pickCallInfo.ts │ │ ├── pickDecoded.ts │ │ ├── pickDecodedError.ts │ │ ├── pickError.ts │ │ ├── pickResultErr.ts │ │ ├── pickResultOk.ts │ │ └── pickTxInfo.ts │ │ ├── substrate │ │ ├── bnToBalance.ts │ │ └── index.ts │ │ └── types │ │ ├── common.ts │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.ts ├── playground ├── .gitignore ├── contract │ ├── Cargo.lock │ ├── Cargo.toml │ └── lib.rs ├── next.config.js ├── package.json ├── postcss.config.js ├── public │ └── squink.svg ├── src │ ├── components │ │ ├── ConnectWallet │ │ │ ├── ConnectWallet.tsx │ │ │ └── index.ts │ │ ├── FileDropper │ │ │ ├── FileDropper.tsx │ │ │ └── index.ts │ │ ├── Notifications │ │ │ ├── Notifications.tsx │ │ │ └── index.ts │ │ ├── SelectList │ │ │ ├── SelectList.tsx │ │ │ └── index.ts │ │ ├── Snackbar │ │ │ ├── Snackbar.tsx │ │ │ └── index.ts │ │ ├── ToggleSwitch │ │ │ ├── ToggleSwitch.tsx │ │ │ └── index.ts │ │ ├── pg-deploy │ │ │ ├── AbiParamInput │ │ │ │ ├── AbiParamInput.tsx │ │ │ │ └── index.ts │ │ │ ├── DeployPage.tsx │ │ │ └── index.ts │ │ └── pg-home │ │ │ ├── HomePage.tsx │ │ │ └── index.ts │ ├── metadata │ │ ├── playground-c.json │ │ └── playground.json │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── deploy.tsx │ │ └── index.tsx │ └── styles │ │ └── globals.css ├── tailwind.config.js └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── useink-logo.svg └── words.txt /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @DoubleOTheven -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Identify faulty behavior 4 | title: "" 5 | labels: bug 6 | --- 7 | 8 | ## Bug Report 9 | 10 | 11 | 12 | ### Current Behavior 13 | 14 | 15 | 16 | ### Expected Behavior 17 | 18 | 19 | 20 | ### Steps To Reproduce 21 | 22 | 29 | 30 | ### Environment 31 | 32 | 40 | 41 | ### Additional Information 42 | 43 | 44 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea 4 | title: "" 5 | labels: feature 6 | --- 7 | 8 | ## Feature Request 9 | 10 | 11 | 12 | ### Suggestion 13 | 14 | 15 | 16 | ### Motivation 17 | 18 | 19 | 20 | ### Use Cases 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Resolves # 2 | 3 | - [ ] There is an associated issue (**required**) 4 | - [ ] The change is described in detail 5 | - [ ] There are new or updated tests validating the change (if applicable) 6 | 7 | ## Description 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | star: 9 | name: Lint 10 | runs-on: ubuntu-latest 11 | timeout-minutes: 15 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: pnpm/action-setup@v2 15 | - run: pnpm install --frozen-lockfile 16 | - name: Lint code 17 | run: pnpm lint:ci -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | release: 4 | types: 5 | - created 6 | jobs: 7 | publish: 8 | name: Build & Publish to NPM 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Set up pnpm 14 | uses: pnpm/action-setup@v2 15 | 16 | - name: Set up node 17 | uses: actions/setup-node@v3 18 | with: 19 | cache: pnpm 20 | node-version: 18 21 | 22 | - name: Retrieve Version 23 | if: startsWith(github.ref, 'refs/tags/') 24 | id: get_tag_version 25 | run: echo "tag_version=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_OUTPUT 26 | 27 | - name: Install Dependencies 28 | run: pnpm install --frozen-lockfile 29 | 30 | - name: Build NPM Package 31 | run: pnpm build 32 | 33 | - run: npm pack 34 | working-directory: "./packages/useink" 35 | 36 | - uses: actions/upload-artifact@v3 37 | with: 38 | name: package 39 | path: "./packages/useink/*.tgz" 40 | 41 | - uses: octokit/request-action@v2.x 42 | if: startsWith(github.ref, 'refs/tags/') 43 | with: 44 | route: POST /repos/paritytech/npm_publish_automation/actions/workflows/publish.yml/dispatches 45 | ref: main 46 | inputs: '${{ format(''{{ "repo": "{0}", "run_id": "{1}" }}'', github.repository, github.run_id) }}' 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.NPM_PUBLISH_AUTOMATION_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/spellcheck.yml: -------------------------------------------------------------------------------- 1 | name: Spellcheck 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | jobs: 8 | spellcheck: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: streetsidesoftware/cspell-action@c5eae96241f654d2437c16bdfad146ff33a025cc # v2.7.0 13 | -------------------------------------------------------------------------------- /.github/workflows/validate_pr_title.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR Title 2 | on: 3 | pull_request_target: 4 | types: 5 | - opened 6 | - edited 7 | - synchronize 8 | 9 | jobs: 10 | main: 11 | name: Validate PR Title 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: amannn/action-semantic-pull-request@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .env 3 | .vscode 4 | benches.json 5 | dist 6 | node_modules 7 | target 8 | packages/useink/README.md 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["biomejs.biome"], 3 | "unwantedRecommendations": ["rome.rome"] 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "quickfix.biome": true, 4 | "source.organizeImports.biome": true 5 | }, 6 | "editor.formatOnSave": true, 7 | "editor.defaultFormatter": "biomejs.biome", 8 | "[typescript]": { 9 | "editor.defaultFormatter": "biomejs.biome" 10 | }, 11 | "[typescriptreact]": { 12 | "editor.defaultFormatter": "biomejs.biome" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . All complaints will be reviewed and investigated promptly and 64 | fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please check our open issues and whether 4 | there is already an issue related to your idea. Please first discuss the change 5 | you wish to make in a GitHub issue and wait for a reply from the maintainers of 6 | this repository before making a change. 7 | 8 | We have a [code of conduct](CODE_OF_CONDUCT.md); please follow it in all your 9 | interactions relating to the project. 10 | 11 | ## Environment setup 12 | 13 | ### Local environment setup 14 | 15 | To develop on your machine, install the following (and please submit issues if 16 | errors crop up) 17 | 18 | - [pnpm](https://pnpm.io/installation) 19 | 20 | ## Rules 21 | 22 | There are a few basic ground-rules for contributors: 23 | 24 | 1. **All modifications** must be made in a **pull-request** to solicit feedback 25 | from other contributors 26 | 2. Contributors should attempt to adhere to the prevailing 27 | [code-style](#code-style) 28 | 29 | ## Pull requests 30 | 31 | **For a pull request to be merged it must at least:** 32 | 33 | :white_check_mark:   Pass CI 34 | 35 | :white_check_mark:   Have one approving review 36 | 37 | :white_check_mark:   Have the PR title follow 38 | [conventional commit](https://www.conventionalcommits.org/) 39 | 40 | **Ideally, a good pull request should:** 41 | 42 | :clock3:   Take less than 15 minutes to review 43 | 44 | :open_book:   Have a meaningful description (describes the problem being 45 | solved) 46 | 47 | :one:   Introduce one feature or solve one bug at a time, for which an open 48 | issue already exists. In case of a project wide refactoring, a larger PR is to 49 | be expected, but the reviewer should be more carefully guided through it 50 | 51 | :jigsaw:   Issues that seem too big for a PR that can be reviewed in 15 52 | minutes or PRs that need to touch other issues should be discussed and probably 53 | split differently before starting any development 54 | 55 | :dart:   Handle renaming, moving files, linting and formatting separately 56 | (not alongside features or bug fixes) 57 | 58 | :test_tube:   Add tests for new functionality 59 | 60 | **Draft pull requests for early feedback are welcome and do not need to adhere 61 | to any guidelines.** 62 | 63 | When reviewing a pull request, the end-goal is to suggest useful changes to the 64 | author. Reviews should finish with approval unless there are issues that would 65 | result in: 66 | 67 | :x:   Buggy behavior 68 | 69 | :x:   Undue maintenance burden 70 | 71 | :x:   Measurable performance issues 72 | 73 | :x:   Feature reduction (i.e. it removes some aspect of functionality that 74 | a significant minority of users rely on) 75 | 76 | :x:   Uselessness (i.e. it does not strictly add a feature or fix a known 77 | issue) 78 | 79 | :x:   Disabling a compiler feature to introduce code that wouldn't compile 80 | 81 | ## Code style 82 | 83 | We use the following tools to enforce linting rules, formatting and spell 84 | checking 85 | 86 | - [`pnpm lint`](https://biomejs.dev/) 87 | - [`cspell`](https://cspell.org/) 88 | 89 | We encourage adding the [recommended](.vscode/extensions.json) (or similar) 90 | extensions to your IDE. 91 | 92 | To run a project wide check you can use: 93 | 94 | ```bash 95 | yarn lint 96 | cspell "**/*" 97 | ``` 98 | 99 | ## Releases 100 | 101 | Declaring formal releases remains the prerogative of the project maintainer(s). 102 | 103 | ## License 104 | 105 | By contributing to project, you agree that your contributions will be licensed 106 | under its [Apache license](LICENSE). 107 | 108 | ## Changes to this arrangement 109 | 110 | This is an experiment and feedback is welcome! This document may also be subject 111 | to pull-requests or changes by contributors where you believe you have something 112 | valuable to add or change. 113 | 114 | ## Heritage 115 | 116 | These contributing guidelines are modified from 117 | 118 | - the "Substrate Project" guidelines 119 | https://github.com/paritytech/substrate/blob/master/docs/CONTRIBUTING.adoc 120 | - the "Substrate Contracts UI" guidelines 121 | https://github.com/paritytech/contracts-ui/blob/master/CONTRIBUTING.md 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dear contributors and users, 2 | 3 | This repository and library is archived. We want to refer you to [`scio-labs/use-inkathon`](https://github.com/scio-labs/use-inkathon) for all your frontend smart contract interaction needs. 4 | 5 | `useink` and `use-inkathon` emerged independently to address the same problem, even using similar tools. After discovering each other and engaging in discussions, it became evident that consolidating efforts into a single library, instead of two similar ones, would better benefit the ink! community. 6 | 7 | The decision to transition from useink to `use-inkathon` was carefully considered, weighing factors such as codebase, userbase, and capabilities. We believe that `use-inkathon` provides a solid foundation for future development and want to foster and encourage ecosystem contributions. 8 | 9 | To learn more about `use-inkathon` and it’s fullstack dapp template inkathon make sure to join check out the repository: [`scio-labs/use-inkathon`](https://github.com/scio-labs/use-inkathon), the telegram group: https://t.me/inkathon and the website: https://inkathon.xyz/ . 10 | 11 | And if you want to learn more about creating a frontend for your dapp and available tools check out the ink! frontend overview: https://use.ink/5.x/frontend/overview. 12 | 13 | --- 14 | 15 |
16 | ink! 17 |
18 |
19 | 20 | 21 | 22 |
23 |

A React hook library for ink!

24 |
25 | 26 |
27 | 28 | [![Element][k1]][k2] [![stack-exchange][s1]][s2] 29 | 30 | [k1]: https://img.shields.io/badge/matrix-chat-brightgreen.svg?colorA=BC83FB&colorB=8747CC&style=flat 31 | [k2]: https://matrix.to/#/#useink:parity.io 32 | 33 | [s1]: 34 | https://img.shields.io/badge/click-white.svg?logo=StackExchange&label=useink%20Support%20on%20StackExchange&colorA=BC83FB&colorB=8747CC 35 | [s2]: https://substrate.stackexchange.com/questions/tagged/useink?tab=Votes 36 | 37 | 38 | ## Getting Started 39 | 40 | * [Read the documentation](https://use.ink/frontend/overview) 41 | 42 | * [Look at some examples](https://github.com/paritytech/useink/tree/main/playground) 43 | 44 | ## License 45 | 46 | useInk! is [Apache licensed](LICENSE). 47 | 48 | ## Contributing 49 | 50 | Contributions are welcome and appreciated! Check out the 51 | [contributing guide](CONTRIBUTING.md) before you dive in. 52 | 53 | ## Code of Conduct 54 | 55 | Everyone interacting in this repo is expected to follow the 56 | [code of conduct](CODE_OF_CONDUCT.md). 57 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.0.0/schema.json", 3 | "files": { 4 | "ignore": [ 5 | "**/node_modules", 6 | ".next", 7 | "public", 8 | "dist", 9 | "**/*.json", 10 | "*.yaml", 11 | ".vscode" 12 | ] 13 | }, 14 | "organizeImports": { 15 | "enabled": true 16 | }, 17 | "formatter": { 18 | "enabled": true, 19 | "formatWithErrors": false, 20 | "indentStyle": "space", 21 | "indentWidth": 2, 22 | "lineWidth": 80 23 | }, 24 | "linter": { 25 | "enabled": true, 26 | "rules": { 27 | "recommended": true, 28 | "correctness": { 29 | "noUnusedVariables": "error", 30 | "useExhaustiveDependencies": "off" 31 | }, 32 | "complexity": { 33 | "noForEach": "off" 34 | }, 35 | "performance": { 36 | "noAccumulatingSpread": "off" 37 | } 38 | } 39 | }, 40 | "javascript": { 41 | "formatter": { 42 | "quoteStyle": "single", 43 | "trailingComma": "all", 44 | "semicolons": "always" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /cspell.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json", 3 | "version": "0.2", 4 | "dictionaryDefinitions": [ 5 | { 6 | "name": "project-words", 7 | "path": "words.txt", 8 | "addWords": true 9 | } 10 | ], 11 | "dictionaries": ["project-words"], 12 | "ignorePaths": [ 13 | "dist", 14 | "node_modules", 15 | ".next", 16 | "**/*.contract", 17 | "pnpm-lock.yaml", 18 | ".gitignore" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "workspaces": [ 5 | "packages/*", 6 | "playground" 7 | ], 8 | "scripts": { 9 | "build": "pnpm --filter useink build && cp ./README.md ./packages/useink/", 10 | "dev": "pnpm --filter useink watch & pnpm --filter playground dev", 11 | "format": "biome format . --write", 12 | "lint": "biome check .", 13 | "lint:fix": "pnpm lint --apply-unsafe", 14 | "lint:ci": "biome ci ." 15 | }, 16 | "dependencies": { 17 | "@polkadot/api-contract": "^10.10.1", 18 | "@polkadot/api-derive": "^10.10.1", 19 | "@polkadot/api": "^10.10.1", 20 | "@polkadot/util": "^12.5.1", 21 | "@polkadot/util-crypto": "^12.5.1", 22 | "@talismn/connect-wallets": "^1.2.3" 23 | }, 24 | "devDependencies": { 25 | "@biomejs/biome": "^1.3.3" 26 | }, 27 | "packageManager": "pnpm@8.10.5" 28 | } 29 | -------------------------------------------------------------------------------- /packages/useink/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "useink", 3 | "keywords": [ 4 | "useink", 5 | "ink!", 6 | "Polkadot", 7 | "Kusama", 8 | "Substrate", 9 | "AlephZero", 10 | "Astar", 11 | "Phala", 12 | "React", 13 | "hooks" 14 | ], 15 | "version": "1.14.2", 16 | "main": "dist/index.js", 17 | "module": "dist/index.mjs", 18 | "description": "A React hooks library for ink! contracts", 19 | "license": "Apache-2.0", 20 | "repository": "github:paritytech/useink", 21 | "types": "dist/index.d.ts", 22 | "scripts": { 23 | "build": "tsup", 24 | "watch": "tsup --watch", 25 | "lint": "tsc" 26 | }, 27 | "dependencies": { 28 | "nanoid": "3" 29 | }, 30 | "devDependencies": { 31 | "@polkadot/types": "^10.10.1", 32 | "@polkadot/types-codec": "^10.10.1", 33 | "@types/react": "^18.2.7", 34 | "tsup": "^6.7.0", 35 | "typescript": "^5.0.4" 36 | }, 37 | "peerDependencies": { 38 | "react": "^18", 39 | "ws": "^8.13.0" 40 | }, 41 | "exports": { 42 | ".": { 43 | "import": { 44 | "types": "./dist/index.d.ts", 45 | "default": "./dist/index.mjs" 46 | }, 47 | "require": { 48 | "types": "./dist/index.d.ts", 49 | "default": "./dist/index.js" 50 | } 51 | }, 52 | "./core": { 53 | "import": { 54 | "types": "./dist/core.d.ts", 55 | "default": "./dist/core.mjs" 56 | }, 57 | "require": { 58 | "types": "./dist/core.d.ts", 59 | "default": "./dist/core.js" 60 | } 61 | }, 62 | "./chains": { 63 | "import": { 64 | "types": "./dist/chains.d.ts", 65 | "default": "./dist/chains.mjs" 66 | }, 67 | "require": { 68 | "types": "./dist/chains.d.ts", 69 | "default": "./dist/chains.js" 70 | } 71 | }, 72 | "./notifications": { 73 | "import": { 74 | "types": "./dist/notifications.d.ts", 75 | "default": "./dist/notifications.mjs" 76 | }, 77 | "require": { 78 | "types": "./dist/notifications.d.ts", 79 | "default": "./dist/notifications.js" 80 | } 81 | }, 82 | "./utils": { 83 | "import": { 84 | "types": "./dist/utils.d.ts", 85 | "default": "./dist/utils.mjs" 86 | }, 87 | "require": { 88 | "types": "./dist/utils.d.ts", 89 | "default": "./dist/utils.js" 90 | } 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/useink/src/chains/data/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chaindata.ts'; 2 | export * from './testnet-chaindata.ts'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/chains/data/types.ts: -------------------------------------------------------------------------------- 1 | export type Account = '*25519' | 'secp256k1' | 'Sr25519'; 2 | 3 | export type JsonString = string; 4 | 5 | export interface Token { 6 | symbol: string; 7 | decimals: number; 8 | // existentialDeposit is the minimum amount an account must hold to stay alive. 9 | // Balances held below this amount will be removed from storage 10 | existentialDeposit: string; 11 | // onChainId is the ID for a token in the pallet 12 | onChainId: JsonString | number; 13 | coingeckoId?: string; 14 | } 15 | 16 | export interface TokenAsset { 17 | assetId: string | number; 18 | symbol: string; 19 | coingeckoId?: string; 20 | } 21 | 22 | export type RpcUrl = `ws://${string}` | `wss://${string}`; 23 | 24 | export interface IChain { 25 | id: T; 26 | name: string; 27 | account: Account; 28 | subscanUrl?: string; 29 | overrideNativeTokenId?: string; 30 | chainspecQrUrl?: string; 31 | latestMetadataQrUrl?: string; 32 | rpcs: readonly RpcUrl[]; 33 | coingeckoId?: string | null; 34 | paraId?: number; 35 | relay?: { 36 | id: string; 37 | }; 38 | balanceModuleConfigs?: { 39 | [k: string]: { 40 | disable?: boolean; 41 | tokens?: readonly (Token | TokenAsset)[]; 42 | }; 43 | }; 44 | } 45 | -------------------------------------------------------------------------------- /packages/useink/src/chains/index.ts: -------------------------------------------------------------------------------- 1 | export * from './data/index'; 2 | export * from './types.ts'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/chains/types.ts: -------------------------------------------------------------------------------- 1 | import * as AllChains from './data/chaindata.ts'; 2 | import * as AllTestnets from './data/testnet-chaindata.ts'; 3 | 4 | type TestnetNetworkName = keyof typeof AllTestnets; 5 | export type TestnetChain = typeof AllTestnets[TestnetNetworkName]; 6 | export type TestnetId = TestnetChain['id']; 7 | 8 | type ProductionNetworkName = keyof typeof AllChains; 9 | export type ProductionChain = typeof AllChains[ProductionNetworkName]; 10 | export type ProductionChainId = ProductionChain['id']; 11 | 12 | export type ChainId = ProductionChainId | TestnetId; 13 | export type Chain = ProductionChain | TestnetChain; 14 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/call.ts: -------------------------------------------------------------------------------- 1 | import { BN_ZERO } from '../../utils/index.ts'; 2 | 3 | import { 4 | AbiMessage, 5 | AccountId, 6 | ContractExecResult, 7 | ContractPromise, 8 | DecodedContractResult, 9 | LazyContractOptions, 10 | } from '../types/index'; 11 | 12 | import { decodeCallResult } from './decodeCallResult.ts'; 13 | 14 | export async function call( 15 | contract: ContractPromise, 16 | abiMessage: AbiMessage, 17 | caller: AccountId | string, 18 | args = [] as unknown[], 19 | options?: LazyContractOptions, 20 | ): Promise | undefined> { 21 | const { value, gasLimit, storageDepositLimit } = options || {}; 22 | 23 | const apiCaller = contract.api.call.contractsApi; 24 | if (!apiCaller?.call) return; 25 | 26 | const raw = await apiCaller.call( 27 | caller, 28 | contract.address, 29 | value ?? BN_ZERO, 30 | gasLimit ?? null, 31 | storageDepositLimit ?? null, 32 | abiMessage.toU8a(args), 33 | ); 34 | 35 | // TODO: handle a call with metadata, but wrong address 36 | // TODO: handle a situation with no response 37 | if (!raw) return; 38 | 39 | const decoded = decodeCallResult( 40 | raw.result, 41 | abiMessage, 42 | contract.abi.registry, 43 | ); 44 | if (!decoded.ok) return decoded; 45 | const { gasConsumed, gasRequired, storageDeposit } = raw; 46 | 47 | return { 48 | ok: true, 49 | value: { 50 | decoded: decoded.value, 51 | raw, 52 | gasConsumed, 53 | gasRequired, 54 | storageDeposit, 55 | }, 56 | }; 57 | } 58 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/decodeCallResult.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AbiMessage, 3 | ContractExecResult, 4 | DecodedResult, 5 | Registry, 6 | } from '../types/index'; 7 | 8 | export function decodeCallResult( 9 | result: ContractExecResult['result'], 10 | message: AbiMessage, 11 | registry: Registry, 12 | ): DecodedResult { 13 | if (result.isErr || !message.returnType) { 14 | return { ok: false, error: result.asErr }; 15 | } 16 | 17 | const raw = registry.createTypeUnsafe( 18 | message.returnType.lookupName || message.returnType.type, 19 | [result.asOk.data.toU8a(true)], 20 | { isPedantic: true }, 21 | ); 22 | 23 | return { ok: true, value: (raw?.toHuman() as Record<'Ok', T>)?.Ok }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/decodeError.ts: -------------------------------------------------------------------------------- 1 | import { DispatchError, RegistryError } from '../types/index'; 2 | import { getRegistryError } from './getRegistryError.ts'; 3 | import { Contract, RegistryErrorMethod } from './types/index.ts'; 4 | 5 | const formatErrorMessage = (registryError: RegistryError): string => 6 | `${registryError.section}.${registryError.method}: ${registryError.docs}`; 7 | 8 | export const decodeError = ( 9 | dispatchError: DispatchError | undefined, 10 | chainContract: Contract | undefined, 11 | moduleMessages?: Record, 12 | defaultMessage?: string, 13 | ): string | undefined => { 14 | if (!chainContract) return undefined; 15 | const registryError = getRegistryError(dispatchError, chainContract); 16 | if (!registryError) return undefined; 17 | 18 | return ( 19 | moduleMessages?.[registryError.method] || 20 | defaultMessage || 21 | formatErrorMessage(registryError) 22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/getRegistryError.ts: -------------------------------------------------------------------------------- 1 | import { DispatchError, RegistryError } from '../types/index'; 2 | import { Contract } from './types/index.ts'; 3 | 4 | export const getRegistryError = ( 5 | error: DispatchError | undefined, 6 | { contract: { api } }: Contract, 7 | ): RegistryError | undefined => { 8 | if (!error?.isModule) return; 9 | 10 | return api?.registry.findMetaError(error.asModule); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/helpers.ts: -------------------------------------------------------------------------------- 1 | import { BN_ZERO } from '../../utils'; 2 | import { ContractOptions, DeployOptions, LazyContractOptions } from '../types'; 3 | 4 | export const DEFAULT_CONTRACT_OPTIONS: ContractOptions = { value: BN_ZERO }; 5 | 6 | export const toContractOptions = ( 7 | options?: LazyContractOptions, 8 | ): ContractOptions => ({ 9 | ...DEFAULT_CONTRACT_OPTIONS, 10 | ...(options || {}), 11 | }); 12 | 13 | export const DEFAULT_DEPLOY_OPTIONS: DeployOptions = { value: BN_ZERO }; 14 | 15 | export const toDeployOptions = ( 16 | options?: LazyContractOptions, 17 | ): DeployOptions => ({ 18 | ...DEFAULT_DEPLOY_OPTIONS, 19 | ...(options || {}), 20 | }); 21 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './call.ts'; 2 | export * from './decodeCallResult.ts'; 3 | export * from './decodeError.ts'; 4 | export * from './getRegistryError.ts'; 5 | export * from './helpers.ts'; 6 | export * from './jsonToAbi.ts'; 7 | export * from './toContractAbiMessage.ts'; 8 | export * from './toRegistryErrorDecoded.ts'; 9 | export * from './txPaymentInfo.ts'; 10 | export * from './types/index.ts'; 11 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/jsonToAbi.ts: -------------------------------------------------------------------------------- 1 | import { formatFileName, validateMetadata } from '../../utils'; 2 | import { Abi, ApiBase } from '../types'; 3 | import { DeriveMetadataOptions, MetadataState } from './types'; 4 | 5 | export const DEFAULT: MetadataState = { 6 | name: '', 7 | formattedFileName: '', 8 | }; 9 | 10 | export const jsonToAbi = ( 11 | options: DeriveMetadataOptions, 12 | source?: Record, 13 | api?: ApiBase<'promise'> | null, 14 | ): MetadataState => { 15 | if (!source) return DEFAULT; 16 | let abi: Abi | undefined = undefined; 17 | 18 | try { 19 | abi = new Abi(source, api?.registry.getChainProperties()); 20 | const name = options.name || abi.info.contract.name.toString(); 21 | const { size } = options; 22 | 23 | return { 24 | source, 25 | name, 26 | abi, 27 | formattedFileName: formatFileName({ size, name }), 28 | ...validateMetadata(abi, options), 29 | }; 30 | } catch (e) { 31 | console.error(e); 32 | 33 | return { 34 | source, 35 | name: '', 36 | formattedFileName: '', 37 | abi, 38 | ...validateMetadata(abi, options), 39 | }; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/toContractAbiMessage.ts: -------------------------------------------------------------------------------- 1 | import { AbiMessage, ContractPromise, Result } from '../types/index'; 2 | 3 | export const toContractAbiMessage = ( 4 | contract: ContractPromise, 5 | message: string, 6 | ): Result => { 7 | const value = contract.abi.messages.find((m) => m.method === message); 8 | 9 | if (!value) { 10 | const messages = contract?.abi.messages.map((m) => m.method).join(', '); 11 | 12 | const error = `"${message}" not found in metadata.spec.messages: [${messages}]`; 13 | console.error(error); 14 | 15 | return { ok: false, error }; 16 | } 17 | 18 | return { ok: true, value }; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/toRegistryErrorDecoded.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ContractExecResultResult, 3 | Registry, 4 | RegistryError, 5 | } from '../types/index'; 6 | 7 | export const toRegistryErrorDecoded = ( 8 | registry: Registry, 9 | result: ContractExecResultResult, 10 | ): RegistryError | undefined => { 11 | try { 12 | return result.isErr && result.asErr.isModule 13 | ? registry.findMetaError(result.asErr.asModule) 14 | : undefined; 15 | } catch (e) { 16 | console.error(e); 17 | return undefined; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/txPaymentInfo.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AccountId, 3 | ContractPromise, 4 | LazyContractOptions, 5 | RuntimeDispatchInfo, 6 | SignerOptions, 7 | toContractOptions, 8 | } from '..'; 9 | 10 | export async function txPaymentInfo( 11 | contract: ContractPromise | undefined, 12 | message: string, 13 | caller: AccountId | string, 14 | params?: unknown[], 15 | options?: LazyContractOptions, 16 | signerOptions?: Partial, 17 | ): Promise { 18 | const tx = contract?.tx?.[message]; 19 | if (!tx || !caller) return; 20 | 21 | try { 22 | const requiresNoArguments = tx.meta.args.length === 0; 23 | return await (requiresNoArguments 24 | ? tx(toContractOptions(options)) 25 | : tx(toContractOptions(options), ...(params || [])) 26 | ).paymentInfo(caller, signerOptions); 27 | } catch (e: unknown) { 28 | console.error(e); 29 | return; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './jsonToAbi'; 2 | 3 | import { ApiBase, DispatchError, Result } from '../../types/index'; 4 | 5 | export interface CallResult { 6 | result?: Result; 7 | } 8 | 9 | export interface Contract { 10 | contract: { 11 | api: ApiBase<'promise'>; 12 | }; 13 | } 14 | 15 | export type RegistryErrorMethod = string; 16 | -------------------------------------------------------------------------------- /packages/useink/src/core/contracts/types/jsonToAbi.ts: -------------------------------------------------------------------------------- 1 | import { Validation } from '../../../utils'; 2 | import { Abi } from '../../types'; 3 | 4 | export interface MetadataOptions { 5 | requireWasm?: boolean; 6 | } 7 | 8 | export interface DeriveMetadataOptions extends MetadataOptions { 9 | name?: string; 10 | size?: number; 11 | } 12 | 13 | export interface MetadataState extends Validation { 14 | source?: Record; 15 | name: string; 16 | formattedFileName: string; 17 | abi?: Abi; 18 | } 19 | -------------------------------------------------------------------------------- /packages/useink/src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contracts'; 2 | export * from './substrate'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/balances/getBalance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiPromise, 3 | DeriveBalancesAccount, 4 | WithAddress, 5 | } from '../../types/index'; 6 | 7 | export const getBalance = async ( 8 | api: ApiPromise | undefined, 9 | account: WithAddress | undefined, 10 | ): Promise => { 11 | if (!api || !account?.address) return; 12 | 13 | return await api.derive.balances.account(account.address); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/balances/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getBalance.ts'; 2 | export * from './transfer.ts'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/balances/transfer.ts: -------------------------------------------------------------------------------- 1 | import { AddressOrPair, ApiPromise, Hash, SignerOptions } from '../../types'; 2 | 3 | export const transfer = async ( 4 | api: ApiPromise | undefined, 5 | to: string | undefined, 6 | amount: number, 7 | signer: AddressOrPair, 8 | options?: SignerOptions, 9 | ): Promise => { 10 | if (!api?.tx?.balances?.transfer || !to) return; 11 | const transfer = api.tx.balances.transfer(to, amount); 12 | 13 | return await transfer.signAndSend(signer, options); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './balances/index'; 2 | export * from './timestamp/index'; 3 | export * from './registry/index'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/registry/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokens'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/registry/tokens.ts: -------------------------------------------------------------------------------- 1 | export interface IRegistryInfo { 2 | registry: { 3 | chainDecimals: (number | undefined)[]; 4 | chainTokens: (string | undefined)[]; 5 | }; 6 | } 7 | 8 | export const chainTokenSymbol = (api: IRegistryInfo): string | undefined => 9 | api.registry.chainTokens[0]; 10 | 11 | export const chainDecimals = (api: IRegistryInfo): number | undefined => 12 | api.registry.chainDecimals[0]; 13 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/timestamp/getTimestampDate.ts: -------------------------------------------------------------------------------- 1 | import { unixMilliToDate } from '../../../utils'; 2 | import { ApiBase } from '../../types/index'; 3 | import { getTimestampUnix } from './getTimestampNow.ts'; 4 | 5 | export const getTimestampDate = async ( 6 | api: ApiBase<'promise'> | undefined, 7 | ): Promise => { 8 | const now = await getTimestampUnix(api); 9 | if (!now) return; 10 | 11 | return unixMilliToDate(now); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/timestamp/getTimestampNow.ts: -------------------------------------------------------------------------------- 1 | import { ApiBase } from '../../types/index'; 2 | import { getTimestampQuery } from './getTimestampQuery.ts'; 3 | 4 | export const getTimestampUnix = async ( 5 | api: ApiBase<'promise'> | undefined, 6 | ): Promise => { 7 | const query = getTimestampQuery(api); 8 | if (!query?.now) return; 9 | 10 | const t = await query.now(); 11 | const stringWithoutCommas = t.toHuman()?.toString().split(',').join(''); 12 | 13 | return stringWithoutCommas ? parseInt(stringWithoutCommas) : undefined; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/timestamp/getTimestampQuery.ts: -------------------------------------------------------------------------------- 1 | import { ApiBase, QueryableModuleCalls } from '../../types/index'; 2 | 3 | export const getTimestampQuery = ( 4 | api: ApiBase<'promise'> | undefined, 5 | ): QueryableModuleCalls<'promise'> | undefined => api?.query?.timestamp; 6 | -------------------------------------------------------------------------------- /packages/useink/src/core/substrate/timestamp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './getTimestampDate.ts'; 2 | export * from './getTimestampNow.ts'; 3 | export * from './getTimestampQuery.ts'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/api-contract.ts: -------------------------------------------------------------------------------- 1 | import { SubmittableResult } from '@polkadot/api'; 2 | import { ISubmittableResult } from './substrate.ts'; 3 | 4 | export { 5 | BlueprintPromise, 6 | CodePromise, 7 | } from '@polkadot/api-contract'; 8 | 9 | import { DecodedEvent } from '@polkadot/api-contract/types'; 10 | export { DecodedEvent }; 11 | 12 | export { 13 | BlueprintSubmittableResult, 14 | CodeSubmittableResult, 15 | } from '@polkadot/api-contract/base'; 16 | 17 | export type { 18 | ContractExecResult, 19 | ContractExecResultResult, 20 | ContractInstantiateResult, 21 | ContractProjectSource, 22 | } from '@polkadot/types/interfaces'; 23 | export type { 24 | AbiParam, 25 | AbiMessage, 26 | ContractOptions, 27 | BlueprintOptions, 28 | } from '@polkadot/api-contract/types'; 29 | export { Abi, ContractPromise } from '@polkadot/api-contract'; 30 | 31 | export declare class ContractSubmittableResult extends SubmittableResult { 32 | readonly contractEvents?: DecodedEvent[] | undefined; 33 | constructor(result: ISubmittableResult, contractEvents?: DecodedEvent[]); 34 | } 35 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/api.ts: -------------------------------------------------------------------------------- 1 | export type { Signer } from '@polkadot/api/types'; 2 | 3 | export type SignatureResult = `0x${string}`; 4 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/array.ts: -------------------------------------------------------------------------------- 1 | export type ArrayOneOrMore = { 0: T } & Array; 2 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/contracts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | AbiMessage, 4 | BlueprintOptions, 5 | ContractExecResult, 6 | ContractOptions, 7 | ContractProjectSource, 8 | } from './api-contract'; 9 | import { Result } from './result'; 10 | import { Balance, DispatchError, StorageDeposit, Weight } from './substrate'; 11 | 12 | export type ContractAbi = string | Record | Abi; 13 | 14 | // Lazy contract options allow for developers to rely on default values that will be 15 | // lazily added so that they don't have to pass in `{ value: BN }` every time. 16 | export type LazyContractOptions = Partial; 17 | 18 | export type LazyCallOptions = LazyContractOptions & { 19 | defaultCaller?: boolean; 20 | }; 21 | 22 | export type DeployOptions = BlueprintOptions & 23 | LazyCallOptions & { codeHash?: string }; 24 | 25 | export interface TxInfo { 26 | gasRequired: Weight; 27 | gasConsumed: Weight; 28 | storageDeposit: StorageDeposit; 29 | partialFee: Balance; 30 | } 31 | 32 | export interface ContractExecResultDecoded 33 | extends Omit { 34 | readonly decoded: T; 35 | readonly raw: ContractExecResult; 36 | } 37 | 38 | export interface TxExecResultDecoded extends TxInfo { 39 | readonly decoded: T; 40 | readonly raw: ContractExecResult; 41 | } 42 | 43 | export type DecodedResult = Result; 44 | 45 | export type DecodedContractResult = DecodedResult< 46 | ContractExecResultDecoded 47 | >; 48 | 49 | export type DecodedTxResult = DecodedResult>; 50 | 51 | export type Wasm = Pick['wasm']; 52 | 53 | export interface ContractCallResultRaw { 54 | readonly callResult: ContractExecResult; 55 | readonly abiMessage: AbiMessage; 56 | } 57 | 58 | export interface CallInfo { 59 | gasRequired: Weight; 60 | gasConsumed: Weight; 61 | storageDeposit: StorageDeposit; 62 | } 63 | 64 | export interface TxInfo { 65 | gasRequired: Weight; 66 | gasConsumed: Weight; 67 | storageDeposit: StorageDeposit; 68 | partialFee: Balance; 69 | } 70 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api-contract.ts'; 2 | export * from './api.ts'; 3 | export * from './array.ts'; 4 | export * from './contracts.ts'; 5 | export * from './result.ts'; 6 | export * from './substrate.ts'; 7 | export * from './talisman-connect-wallets.ts'; 8 | export * from './unsub.ts'; 9 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/result.ts: -------------------------------------------------------------------------------- 1 | export type Result = 2 | | { ok: true; value: T } 3 | | { ok: false; error: E }; 4 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/substrate.ts: -------------------------------------------------------------------------------- 1 | export type { Bytes } from '@polkadot/types'; 2 | 3 | export { TypeDefInfo, getTypeDef } from '@polkadot/types'; 4 | 5 | export type { 6 | DeriveBalancesAccount, 7 | DeriveBalancesMap, 8 | } from '@polkadot/api-derive/types'; 9 | 10 | export { ApiPromise, WsProvider } from '@polkadot/api'; 11 | 12 | export type { 13 | AnyJson, 14 | Codec, 15 | ISubmittableResult, 16 | Registry, 17 | RegistryError, 18 | TypeDef, 19 | } from '@polkadot/types/types'; 20 | 21 | import { ExtrinsicStatus } from '@polkadot/types/interfaces'; 22 | export type { 23 | AccountId, 24 | Balance, 25 | DispatchError, 26 | DispatchInfo, 27 | EventRecord, 28 | ExtrinsicStatus, 29 | Hash, 30 | Header, 31 | RuntimeDispatchInfo, 32 | StorageDeposit, 33 | Weight, 34 | WeightV2, 35 | } from '@polkadot/types/interfaces'; 36 | 37 | export type { 38 | AddressOrPair, 39 | ApiBase, 40 | QueryableModuleCalls, 41 | SignerOptions, 42 | SubmittableExtrinsic, 43 | VoidFn, 44 | } from '@polkadot/api/types'; 45 | 46 | export type TransactionStatus = 47 | | ExtrinsicStatus['type'] 48 | | 'None' 49 | | 'DryRun' 50 | | 'PendingSignature' 51 | | 'Errored'; 52 | 53 | export interface WithAddress { 54 | address: string | undefined; 55 | } 56 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/talisman-connect-wallets.ts: -------------------------------------------------------------------------------- 1 | export { getWalletBySource, getWallets } from '@talismn/connect-wallets'; 2 | import type { Wallet } from '@talismn/connect-wallets'; 3 | import { Signer } from './api'; 4 | 5 | export interface WalletAccount { 6 | // Talisman sets the type as unknown so we must manually set it to Signer 7 | signer?: Signer; 8 | address: string; 9 | source: string; 10 | name?: string; 11 | wallet?: Wallet; 12 | } 13 | -------------------------------------------------------------------------------- /packages/useink/src/core/types/unsub.ts: -------------------------------------------------------------------------------- 1 | export type Unsub = () => void; 2 | -------------------------------------------------------------------------------- /packages/useink/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './react/index'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/README.md: -------------------------------------------------------------------------------- 1 | # Notifications 2 | 3 | This module is an opinionated React state system to show notifications for 4 | contract transactions. This is often used in combination with Snacks in the UI. 5 | Notifications will be added to state, then removed after the expiration time. 6 | The default expiration time is 5 seconds, but can be changed in the config. 7 | 8 | ## Example Setup 9 | 10 | ```tsx 11 | import { UseInkProvider } from 'useink'; 12 | import { NotificationsProvider } from 'useink/notifications'; 13 | 14 | export const MyApp = () => ( 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | ``` 22 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { 3 | AddNotificationPayload, 4 | Config, 5 | DEFAULT_NOTIFICATIONS, 6 | Notifications, 7 | } from './model.ts'; 8 | 9 | export const NotificationsContext = createContext<{ 10 | config?: Config; 11 | notifications: Notifications; 12 | addNotification: (payload: AddNotificationPayload) => void; 13 | removeNotification: (notificationId: string) => void; 14 | }>({ 15 | notifications: DEFAULT_NOTIFICATIONS, 16 | config: undefined, 17 | addNotification: () => null, 18 | removeNotification: () => null, 19 | }); 20 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useNotifications.ts'; 2 | export * from './useTxNotifications.ts'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/hooks/useNotifications.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react'; 2 | import { HALF_A_SECOND } from '../../react/constants.ts'; 3 | import { useInterval } from '../../react/hooks/internal/useInterval.ts'; 4 | import { getExpiredItem } from '../../utils/index'; 5 | import { NotificationsContext } from '../context.ts'; 6 | import { 7 | AddNotificationPayload, 8 | Notification, 9 | Notifications, 10 | } from '../model.ts'; 11 | 12 | export interface UseNotifications { 13 | notifications: Notifications; 14 | addNotification: (payload: AddNotificationPayload) => void; 15 | removeNotification: (notificationId: string) => void; 16 | } 17 | 18 | export const useNotifications = (): UseNotifications => { 19 | const { addNotification, notifications, removeNotification, config } = 20 | useContext(NotificationsContext); 21 | 22 | const chainNotifications = useMemo(() => { 23 | return notifications ?? []; 24 | }, [notifications]); 25 | 26 | useInterval(() => { 27 | if (config?.expiration === 0) return; 28 | 29 | const expiredNotifications = getExpiredItem( 30 | chainNotifications, 31 | config?.expiration, 32 | ); 33 | for (const notification of expiredNotifications) { 34 | removeNotification(notification.id); 35 | } 36 | }, config?.checkInterval || HALF_A_SECOND); 37 | 38 | return { 39 | notifications: chainNotifications, 40 | addNotification, 41 | removeNotification, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/hooks/useTxNotifications.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { ChainId } from '../../chains/types.ts'; 3 | import { Tx } from '../../index'; 4 | import { useNotifications } from './useNotifications.ts'; 5 | 6 | type TxInfo = Pick, 'status'> & Pick, 'result'>; 7 | 8 | export function useTxNotifications(tx: TxInfo, chain?: ChainId): void { 9 | const { addNotification } = useNotifications(); 10 | 11 | useEffect(() => { 12 | // TODO: Add a way for user's to easily customize defaults 13 | if (['Ready', 'None'].includes(tx.status)) return; 14 | 15 | addNotification({ 16 | type: tx.status, 17 | message: tx.status, 18 | result: tx.result, 19 | chain, 20 | }); 21 | }, [tx.status]); 22 | } 23 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './hooks/index'; 3 | export * from './model.ts'; 4 | export * from './provider.tsx'; 5 | export * from './types.ts'; 6 | export * from './utils/index'; 7 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/model.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../chains/types.ts'; 2 | import { 3 | Codec, 4 | ISubmittableResult, 5 | TransactionStatus, 6 | } from '../core/types/index'; 7 | 8 | export type NotificationType = 9 | | 'WalletConnected' 10 | | 'WalletDisconnected' 11 | | TransactionStatus; 12 | 13 | export type NotificationPayload = { 14 | createdAt: number; 15 | type: NotificationType; 16 | result?: Codec | ISubmittableResult; 17 | message: string; 18 | chain?: ChainId; 19 | }; 20 | 21 | export type AddNotificationPayload = Omit; 22 | 23 | export type Notification = { id: string } & NotificationPayload; 24 | 25 | export type Notifications = Notification[]; 26 | 27 | export interface Config { 28 | expiration?: number; 29 | checkInterval?: number; 30 | } 31 | 32 | export const DEFAULT_NOTIFICATIONS: Notifications = []; 33 | 34 | export interface AddNotification { 35 | type: 'ADD_NOTIFICATION'; 36 | notification: Notification; 37 | } 38 | 39 | export interface RemoveNotification { 40 | type: 'REMOVE_NOTIFICATION'; 41 | notificationId: string; 42 | } 43 | 44 | export type Action = AddNotification | RemoveNotification; 45 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useReducer } from 'react'; 2 | import { useIsMounted } from '../react/hooks/internal/useIsMounted.ts'; 3 | import { pseudoRandomId } from '../utils/index'; 4 | import { NotificationsContext } from './context.ts'; 5 | import { 6 | AddNotificationPayload, 7 | Config, 8 | DEFAULT_NOTIFICATIONS, 9 | } from './model.ts'; 10 | import { notificationReducer } from './reducer.ts'; 11 | 12 | export const NotificationsProvider: React.FC< 13 | React.PropsWithChildren<{ 14 | config?: Config; 15 | }> 16 | > = ({ children, config }) => { 17 | const [notifications, dispatch] = useReducer( 18 | notificationReducer, 19 | DEFAULT_NOTIFICATIONS, 20 | ); 21 | const isMounted = useIsMounted(); 22 | 23 | const addNotification = useCallback( 24 | (notification: AddNotificationPayload) => { 25 | if (isMounted()) { 26 | dispatch({ 27 | type: 'ADD_NOTIFICATION', 28 | notification: { 29 | ...notification, 30 | id: pseudoRandomId(), 31 | createdAt: Date.now(), 32 | }, 33 | }); 34 | } 35 | }, 36 | [dispatch], 37 | ); 38 | 39 | const removeNotification = useCallback( 40 | (notificationId: string) => { 41 | if (isMounted()) { 42 | dispatch({ 43 | type: 'REMOVE_NOTIFICATION', 44 | notificationId, 45 | }); 46 | } 47 | }, 48 | [dispatch], 49 | ); 50 | 51 | return ( 52 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Action, Notifications } from './model.ts'; 2 | 3 | export function notificationReducer( 4 | state: Notifications, 5 | action: Action, 6 | ): Notifications { 7 | const chainState = state ?? []; 8 | 9 | switch (action.type) { 10 | case 'ADD_NOTIFICATION': 11 | return [...state, action.notification]; 12 | case 'REMOVE_NOTIFICATION': { 13 | return chainState.filter((n) => n.id !== action.notificationId); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/types.ts: -------------------------------------------------------------------------------- 1 | export type NotificationLevel = 'success' | 'error' | 'warning' | 'info'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toNotificationLevel.ts'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/notifications/utils/toNotificationLevel.ts: -------------------------------------------------------------------------------- 1 | import { NotificationType } from '../model.ts'; 2 | import { NotificationLevel } from '../types.ts'; 3 | 4 | export const toNotificationLevel = ( 5 | type: NotificationType, 6 | ): NotificationLevel => { 7 | switch (type) { 8 | case 'None': 9 | return 'info'; 10 | case 'DryRun': 11 | return 'info'; 12 | case 'PendingSignature': 13 | return 'info'; 14 | case 'Future': 15 | return 'info'; 16 | case 'Ready': 17 | return 'info'; 18 | case 'Broadcast': 19 | return 'info'; 20 | case 'InBlock': 21 | return 'success'; 22 | case 'Retracted': 23 | return 'warning'; 24 | case 'FinalityTimeout': 25 | return 'error'; 26 | case 'Finalized': 27 | return 'success'; 28 | case 'Usurped': 29 | return 'error'; 30 | case 'Dropped': 31 | return 'error'; 32 | case 'Invalid': 33 | return 'warning'; 34 | case 'Errored': 35 | return 'error'; 36 | case 'WalletConnected': 37 | return 'info'; 38 | case 'WalletDisconnected': 39 | return 'info'; 40 | default: 41 | return 'info'; 42 | } 43 | }; 44 | -------------------------------------------------------------------------------- /packages/useink/src/react/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_RPC_URL = 'wss://rococo-contracts-rpc.polkadot.io'; 2 | 3 | export const FIVE_SECONDS = 5000; 4 | export const HALF_A_SECOND = 500; 5 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useChain.ts'; 2 | export * from './useChainRpc.ts'; 3 | export * from './useChainRpcList.ts'; 4 | export * from './useChains.ts'; 5 | export * from './useConfig.ts'; 6 | export * from './useDefaultCaller.ts'; 7 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useChain.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Chain, ChainId } from '../../../chains/index'; 3 | import { useConfig } from './useConfig.ts'; 4 | 5 | export const useChain = (chainId?: ChainId): Chain | undefined => { 6 | const { chains } = useConfig(); 7 | 8 | return useMemo(() => { 9 | return chainId ? chains.find((c) => c.id === chainId) : chains[0]; 10 | }, [chains, chainId]); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useChainRpc.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { useChain } from './useChain.ts'; 3 | import { useConfig } from './useConfig.ts'; 4 | 5 | export const useChainRpc = (chainId?: ChainId): string | undefined => { 6 | const { chainRpcs } = useConfig(); 7 | const chain = useChain(chainId); 8 | const chainIdOrDefault = chainId || chain?.id; 9 | 10 | return chainIdOrDefault && chainRpcs[chainIdOrDefault]; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useChainRpcList.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { SetChainRpc } from '../../providers/config/model.ts'; 3 | import { useChain } from './useChain.ts'; 4 | import { useConfig } from './useConfig.ts'; 5 | 6 | export interface RpcList { 7 | rpcs: readonly string[]; 8 | setChainRpc: SetChainRpc; 9 | } 10 | 11 | export const useChainRpcList = (chainId?: ChainId): RpcList => { 12 | const chain = useChain(chainId); 13 | const { setChainRpc } = useConfig(); 14 | 15 | return { rpcs: chain?.rpcs || [], setChainRpc }; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useChains.ts: -------------------------------------------------------------------------------- 1 | import { Chain } from '../../../chains/index'; 2 | import { ArrayOneOrMore } from '../../../core/index'; 3 | import { useConfig } from './useConfig.ts'; 4 | 5 | export const useChains = (): ArrayOneOrMore => useConfig().chains; 6 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useConfig.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ConfigContext } from '../../providers/config/context.ts'; 3 | import { Config } from '../../providers/config/model.ts'; 4 | 5 | export const useConfig = (): Config => useContext(ConfigContext); 6 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/config/useDefaultCaller.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { ChainId } from '../../../chains/index'; 3 | import { useChain } from './useChain.ts'; 4 | import { useConfig } from './useConfig.ts'; 5 | 6 | export const useDefaultCaller = (chainId?: ChainId): string | undefined => { 7 | const { caller } = useConfig(); 8 | const defaultChain = useChain(); 9 | if (!caller) return; 10 | 11 | return useMemo( 12 | () => caller[`${chainId || defaultChain}` as ChainId] || caller.default, 13 | [chainId, caller, defaultChain], 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types.ts'; 2 | export * from './useAbiMessage.ts'; 3 | export * from './useCall.ts'; 4 | export * from './useCallSubscription.ts'; 5 | export * from './useCodeHash.ts'; 6 | export * from './useContract.ts'; 7 | export * from './useDeployer'; 8 | export * from './useDryRun.ts'; 9 | export * from './useEventSubscription.ts'; 10 | export * from './useEvents.ts'; 11 | export * from './useMessageSigner.ts'; 12 | export * from './useMetadata.ts'; 13 | export * from './useSalter.ts'; 14 | export * from './useSignatureVerifier.ts'; 15 | export * from './useTx.ts'; 16 | export * from './useTxEvents.ts'; 17 | export * from './useTxPaymentInfo.ts'; 18 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/types.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '@polkadot/util'; 2 | import { ChainId } from '../../../chains/types.ts'; 3 | import { Abi, ContractOptions, ContractPromise } from '../../../core/index'; 4 | 5 | export type CallOptions = Omit & { 6 | defaultCaller?: boolean; 7 | value?: bigint | BN | string | number | undefined; 8 | }; 9 | 10 | export type ContractAbi = string | Record | Abi; 11 | 12 | export interface ChainContract { 13 | contract: T; 14 | chainId: ChainId; 15 | } 16 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useAbiMessage.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { 3 | AbiMessage, 4 | ContractPromise, 5 | toContractAbiMessage, 6 | } from '../../../core/index'; 7 | 8 | export function useAbiMessage( 9 | contract: ContractPromise | undefined, 10 | message: string, 11 | ): AbiMessage | undefined { 12 | const abiMessage = useMemo(() => { 13 | if (!contract) return; 14 | return toContractAbiMessage(contract, message); 15 | }, [contract, message]); 16 | 17 | if (!abiMessage || !abiMessage.ok) return; 18 | 19 | return abiMessage.value; 20 | } 21 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useCall.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { 3 | DecodedContractResult, 4 | LazyCallOptions, 5 | call, 6 | } from '../../../core/index'; 7 | import { ChainContract, useDefaultCaller } from '../index'; 8 | import { useWallet } from '../wallets/useWallet.ts'; 9 | import { useAbiMessage } from './useAbiMessage.ts'; 10 | 11 | export type CallSend = ( 12 | args?: unknown[], 13 | options?: LazyCallOptions, 14 | ) => Promise | undefined>; 15 | 16 | export interface UseCall { 17 | send: CallSend; 18 | isSubmitting: boolean; 19 | } 20 | 21 | export enum CallError { 22 | ContractUndefined = 'Contract is undefined', 23 | InvalidAbiMessage = 'Invalid ABI Message', 24 | NoResponse = 'No response', 25 | } 26 | 27 | export interface Call extends UseCall { 28 | result?: DecodedContractResult; 29 | } 30 | 31 | export function useCall( 32 | chainContract: ChainContract | undefined, 33 | message: string, 34 | ): Call { 35 | const [result, setResult] = useState>(); 36 | const [isSubmitting, setIsSubmitting] = useState(false); 37 | const abiMessage = useAbiMessage(chainContract?.contract, message); 38 | const { account } = useWallet(); 39 | const defaultCaller = useDefaultCaller(chainContract?.chainId); 40 | 41 | const send = useCallback( 42 | async ( 43 | args: Parameters[3], 44 | options?: LazyCallOptions, 45 | ): Promise | undefined> => { 46 | const caller = account?.address 47 | ? account.address 48 | : options?.defaultCaller 49 | ? defaultCaller 50 | : undefined; 51 | if (!abiMessage || !chainContract?.contract || !caller) return; 52 | 53 | try { 54 | setIsSubmitting(true); 55 | const callResult = await call( 56 | chainContract.contract, 57 | abiMessage, 58 | caller, 59 | args, 60 | options, 61 | ); 62 | setResult(callResult); 63 | setIsSubmitting(false); 64 | 65 | return callResult; 66 | } catch (e: unknown) { 67 | console.error(e); 68 | setIsSubmitting(false); 69 | return; 70 | } 71 | }, 72 | [account, abiMessage], 73 | ); 74 | 75 | return { send, isSubmitting, result }; 76 | } 77 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useCallSubscription.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { LazyCallOptions } from '../../../core/index.ts'; 3 | import { useBlockHeader } from '../substrate/useBlockHeader.ts'; 4 | import { ChainContract } from './types.ts'; 5 | import { Call, useCall } from './useCall.ts'; 6 | 7 | export function useCallSubscription( 8 | chainContract: ChainContract | undefined, 9 | message: string, 10 | args = [] as unknown[], 11 | options?: LazyCallOptions, 12 | ): Omit, 'send'> { 13 | const call = useCall(chainContract, message); 14 | const blockNumber = useBlockHeader(chainContract?.chainId)?.blockNumber; 15 | 16 | useEffect(() => { 17 | call.send(args, options); 18 | }, [blockNumber]); 19 | 20 | return call; 21 | } 22 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useCodeHash.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import { isValidHash } from '../../../utils'; 3 | 4 | export enum CodeHashError { 5 | InvalidHash = 'Invalid code hash value.', 6 | } 7 | 8 | export interface CodeHashState { 9 | codeHash: string; 10 | set: (hash: string) => void; 11 | error?: CodeHashError; 12 | resetState: () => void; 13 | } 14 | 15 | export const useCodeHash = (): CodeHashState => { 16 | const [codeHash, setCodeHash] = useState(''); 17 | const [error, setError] = useState(); 18 | 19 | useEffect(() => { 20 | if (isValidHash(codeHash, 64)) { 21 | error && setError(undefined); 22 | return; 23 | } 24 | 25 | codeHash && setError(CodeHashError.InvalidHash); 26 | }, [codeHash]); 27 | 28 | const set = useCallback((s: string) => { 29 | setCodeHash(s || ''); 30 | }, []); 31 | 32 | const resetState = useCallback(() => { 33 | setCodeHash(''); 34 | setError(undefined); 35 | }, []); 36 | 37 | return { 38 | codeHash, 39 | resetState, 40 | set, 41 | error, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useContract.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useMemo, useState } from 'react'; 2 | import { ChainId } from '../../../chains/index'; 3 | import { Abi, ContractPromise } from '../../../core/index'; 4 | import { useChain } from '../config/useChain.ts'; 5 | import { useApi } from '../substrate/useApi.ts'; 6 | import { ChainContract } from './types.ts'; 7 | 8 | export function useContract( 9 | address: string, 10 | metadata: Record, 11 | chainId?: ChainId, 12 | ): ChainContract | undefined { 13 | const [contract, setContract] = useState(); 14 | const chainConfig = useChain(chainId); 15 | const { api } = useApi(chainConfig?.id) || {}; 16 | 17 | const abi = useMemo( 18 | () => api && new Abi(metadata, api.registry.getChainProperties()), 19 | [api], 20 | ); 21 | 22 | useEffect(() => { 23 | try { 24 | api && abi && setContract(new ContractPromise(api, abi, address) as T); 25 | } catch (err) { 26 | console.error("Couldn't create contract instance: ", err); 27 | } 28 | }, [abi, address, api]); 29 | 30 | return chainConfig && contract 31 | ? { chainId: chainConfig.id, contract } 32 | : undefined; 33 | } 34 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useDeployer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useDeployer'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useDeployer/types.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Abi, 3 | Balance, 4 | BlueprintOptions, 5 | Bytes, 6 | ContractSubmittableResult, 7 | EventRecord, 8 | ISubmittableResult, 9 | StorageDeposit, 10 | SubmittableExtrinsic, 11 | TransactionStatus, 12 | WeightV2, 13 | } from '../../../../core'; 14 | import { BN } from '../../../../utils'; 15 | import { ContractSubmittableResultCallback } from '../useTx'; 16 | 17 | export enum DeployerError { 18 | NoCodeHashOrWasm = 'You must supply either a code hash or metadata containing Wasm.', 19 | ConstructorNotFound = 'The constructor method was not found.', 20 | InstantiateNotSupportedForApi = 'Instantiate method not found for chain API', 21 | CouldNotCreateTx = 'Could not create deploy transaction function', 22 | TransactionFailed = 'Transaction failed.', 23 | TransactionCancelled = 'Transaction cancelled.', 24 | WalletNotConnected = 'Wallet not connected.', 25 | ApiInstanceNotFound = 'Api client not found. Try refreshing the page.', 26 | InvalidCodeHash = 'Invalid code hash.', 27 | } 28 | 29 | export interface DeployData { 30 | argValues?: Record; 31 | value?: Balance; 32 | metadata?: Abi; 33 | name: string; 34 | constructorIndex: number; 35 | salt: string | Uint8Array | Bytes | null; 36 | storageDepositLimit: Balance | null; 37 | gasLimit: WeightV2 | undefined; 38 | codeHash?: string; 39 | } 40 | 41 | export type DeploySignAndSend = ( 42 | metadata: Abi, 43 | constructorName?: string, 44 | constructorArgs?: Record, 45 | options?: BlueprintOptions & { 46 | codeHash?: string; 47 | value?: bigint | BN | string | number; 48 | }, 49 | cb?: ContractSubmittableResultCallback, 50 | ) => T; 51 | 52 | export type DeployTx = SubmittableExtrinsic<'promise', ISubmittableResult>; 53 | 54 | export interface Deploy<_T> { 55 | dryRun: DeploySignAndSend>; 56 | signAndSend: DeploySignAndSend>; 57 | contractAddress: string | undefined; 58 | status: TransactionStatus; 59 | result: ContractSubmittableResult | undefined; 60 | isSubmitting: boolean; 61 | error: string | undefined; 62 | resetState: () => void; 63 | gasConsumed?: WeightV2; 64 | gasRequired?: WeightV2; 65 | storageDeposit?: StorageDeposit; 66 | willBeSuccessful: boolean; 67 | wasDeployed: boolean; 68 | events: EventRecord[]; 69 | } 70 | 71 | export interface AddNotification { 72 | type: 'ADD_NOTIFICATION'; 73 | notification: Notification; 74 | } 75 | 76 | export interface RemoveNotification { 77 | type: 'REMOVE_NOTIFICATION'; 78 | notificationId: string; 79 | } 80 | 81 | export type Action = AddNotification | RemoveNotification; 82 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useDeployer/useDeployer.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useMemo, useState } from 'react'; 2 | import { ChainId } from '../../../../chains/index.ts'; 3 | import { 4 | BlueprintPromise, 5 | CodePromise, 6 | CodeSubmittableResult, 7 | ContractInstantiateResult, 8 | StorageDeposit, 9 | TransactionStatus, 10 | WeightV2, 11 | decodeError, 12 | toDeployOptions, 13 | } from '../../../../core/index.ts'; 14 | import { 15 | BN_ZERO, 16 | NOOP, 17 | encodeSalt, 18 | formatExtrinsicFailed, 19 | isContractInstantiatedEvent, 20 | isTxCancelledError, 21 | isValidHash, 22 | toMessageParams, 23 | } from '../../../../utils/index.ts'; 24 | import { useChain } from '../../config/useChain.ts'; 25 | import { useConfig } from '../../config/useConfig.ts'; 26 | import { useApi } from '../../substrate/useApi.ts'; 27 | import { useWallet } from '../../wallets/useWallet.ts'; 28 | import { useTxEvents } from '../useTxEvents.ts'; 29 | import { Deploy, DeploySignAndSend, DeployTx, DeployerError } from './types.ts'; 30 | 31 | export function useDeployer(chainId?: ChainId): Deploy { 32 | const { account } = useWallet(); 33 | const chainApi = useApi(chainId); 34 | const chain = useChain(chainId); 35 | const C = useConfig(); 36 | 37 | const [isSubmitting, setIsSubmitting] = useState(false); 38 | const [willBeSuccessful, setWillBeSuccessful] = useState(false); 39 | const [status, setStatus] = useState('None'); 40 | const [result, setResult] = useState>(); 41 | const [contractAddress, setContractAddress] = useState(); 42 | const [error, setError] = useState(); 43 | const [gasConsumed, setGasConsumed] = useState(); 44 | const [gasRequired, setGasRequired] = useState(); 45 | const [storageDeposit, setStorageDeposit] = useState(); 46 | const txEvents = useTxEvents({ status, result }); 47 | 48 | useEffect(() => { 49 | txEvents.events.forEach((event) => { 50 | const failure = formatExtrinsicFailed(event, chainApi?.api); 51 | failure && setError(failure); 52 | }); 53 | }, [txEvents]); 54 | 55 | const wasDeployed = useMemo( 56 | () => Boolean(txEvents.events.find(isContractInstantiatedEvent)), 57 | [txEvents.events], 58 | ); 59 | 60 | const resetState = useCallback(() => { 61 | setResult(undefined); 62 | setError(undefined); 63 | setContractAddress(undefined); 64 | setStatus('None'); 65 | setIsSubmitting(false); 66 | setGasConsumed(undefined); 67 | setGasRequired(undefined); 68 | setStorageDeposit(undefined); 69 | txEvents.resetState(); 70 | }, []); 71 | 72 | const dryRun: DeploySignAndSend> = useMemo( 73 | () => 74 | async (abi, constructorName, constructorParams, options, cb = NOOP) => { 75 | if (!chainApi?.api) { 76 | setError(DeployerError.ApiInstanceNotFound); 77 | return; 78 | } 79 | 80 | setError(undefined); 81 | 82 | const constructorMessage = constructorName 83 | ? abi.constructors.find(({ method }) => method === constructorName) 84 | : abi.constructors?.[0]; 85 | 86 | if (!constructorMessage) { 87 | setError(DeployerError.ConstructorNotFound); 88 | return; 89 | } 90 | 91 | const { codeHash } = toDeployOptions(options); 92 | const wasm = abi.info.source.wasm; 93 | const hasCodeHashOrWasm = codeHash !== undefined || !!wasm; 94 | 95 | if (!hasCodeHashOrWasm) { 96 | setError(DeployerError.NoCodeHashOrWasm); 97 | return; 98 | } 99 | 100 | if (codeHash !== undefined && !isValidHash(codeHash, 64)) { 101 | setError(DeployerError.InvalidCodeHash); 102 | return; 103 | } 104 | 105 | const code = codeHash 106 | ? new BlueprintPromise(chainApi.api, abi, codeHash) 107 | : new CodePromise(chainApi.api, abi, wasm.toU8a()); 108 | 109 | const abiParams = constructorMessage.args; 110 | if (!chainApi?.api?.call?.contractsApi?.instantiate) { 111 | setError(DeployerError.InstantiateNotSupportedForApi); 112 | return; 113 | } 114 | 115 | const messageParams = toMessageParams( 116 | chainApi.api, 117 | abiParams, 118 | constructorParams || {}, 119 | ); 120 | 121 | const caller = 122 | account?.address || 123 | (chain?.id ? C?.caller?.[chain?.id] : C.caller?.default); 124 | 125 | const gasLimitMax = null; 126 | const storageDepositMax = null; 127 | 128 | const payableValue = chainApi.api.registry.createType( 129 | 'Balance', 130 | toDeployOptions(options).value, 131 | ); 132 | 133 | const params = [ 134 | caller, 135 | payableValue, 136 | gasLimitMax, 137 | storageDepositMax, 138 | codeHash ? { Existing: codeHash } : { Upload: abi.info.source.wasm }, 139 | constructorMessage.toU8a(messageParams), 140 | options?.salt ? encodeSalt(options?.salt) : '', 141 | ]; 142 | 143 | setIsSubmitting(true); 144 | 145 | let res: ContractInstantiateResult; 146 | try { 147 | res = await chainApi.api.call.contractsApi.instantiate(...params); 148 | setIsSubmitting(false); 149 | } catch (e: unknown) { 150 | setError(e?.toString()); 151 | setIsSubmitting(false); 152 | return; 153 | } 154 | 155 | setWillBeSuccessful(res.result.isOk); 156 | 157 | if (res.result.isOk) { 158 | setContractAddress(res.result.asOk.accountId.toString()); 159 | } else { 160 | setError( 161 | decodeError(res.result.asErr, { contract: { api: chainApi.api } }), 162 | ); 163 | return; 164 | } 165 | 166 | setGasConsumed(res.gasConsumed); 167 | setGasRequired(res.gasRequired); 168 | setStorageDeposit(res.storageDeposit); 169 | 170 | let tx; 171 | try { 172 | const method = code.tx[constructorMessage.method]; 173 | const methodOptions = { 174 | gasLimit: res.gasRequired, 175 | storageDepositLimit: res.storageDeposit.asCharge || null, 176 | ...toDeployOptions(options), 177 | }; 178 | 179 | tx = 180 | constructorMessage.args.length > 0 181 | ? method?.(methodOptions, ...messageParams) 182 | : method?.(methodOptions); 183 | } catch (e: unknown) { 184 | setError(e?.toString()); 185 | return; 186 | } 187 | 188 | if (!tx) { 189 | cb?.(undefined, chainApi.api, DeployerError.CouldNotCreateTx); 190 | setError(DeployerError.CouldNotCreateTx); 191 | return; 192 | } 193 | 194 | return tx; 195 | }, 196 | [account, chainApi?.api], 197 | ); 198 | 199 | const signAndSend: DeploySignAndSend> = useMemo( 200 | () => 201 | async ( 202 | abi, 203 | constructorName, 204 | constructorArgs = {}, 205 | options = { value: BN_ZERO }, 206 | cb = NOOP, 207 | ) => { 208 | if (!account || !account?.wallet?.extension.signer) { 209 | setError(DeployerError.WalletNotConnected); 210 | return; 211 | } 212 | 213 | if (!chainApi?.api) { 214 | setError(DeployerError.ApiInstanceNotFound); 215 | return; 216 | } 217 | 218 | txEvents.resetState(); 219 | 220 | dryRun(abi, constructorName, constructorArgs, options, cb).then( 221 | (tx) => { 222 | tx && setStatus('PendingSignature'); 223 | tx?.signAndSend( 224 | account?.address, 225 | { signer: account?.wallet?.extension.signer }, 226 | (response: CodeSubmittableResult<'promise'>) => { 227 | if (response.status.isInBlock) { 228 | setContractAddress(response?.contract?.address.toString()); 229 | } 230 | 231 | if (response.isError) { 232 | const err = decodeError(response.dispatchError, { 233 | contract: { api: chainApi.api }, 234 | }); 235 | 236 | setError(err); 237 | } 238 | 239 | setStatus(response.status.type); 240 | setResult(response); 241 | cb?.(response, chainApi.api); 242 | }, 243 | ) 244 | .catch((e: unknown) => { 245 | cb?.(undefined, chainApi.api, e); 246 | setStatus('None'); 247 | 248 | if (isTxCancelledError(e)) { 249 | setError(DeployerError.TransactionCancelled); 250 | return; 251 | } 252 | 253 | setError(DeployerError.TransactionFailed); 254 | }) 255 | .finally(() => { 256 | setIsSubmitting(false); 257 | }); 258 | }, 259 | ); 260 | }, 261 | [account, account?.wallet?.extension?.signer, chainApi?.api], 262 | ); 263 | 264 | return { 265 | dryRun, 266 | signAndSend, 267 | contractAddress, 268 | status, 269 | result, 270 | isSubmitting, 271 | error, 272 | resetState, 273 | willBeSuccessful, 274 | wasDeployed, 275 | storageDeposit, 276 | gasConsumed, 277 | gasRequired, 278 | events: txEvents.events, 279 | }; 280 | } 281 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useDryRun.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { 3 | DecodedTxResult, 4 | LazyCallOptions, 5 | call, 6 | toContractOptions, 7 | } from '../../../core/index'; 8 | import { useDefaultCaller } from '../config/index'; 9 | import { useWallet } from '../wallets/useWallet.ts'; 10 | import { ChainContract } from './types.ts'; 11 | import { useAbiMessage } from './useAbiMessage.ts'; 12 | 13 | export type DryRunResult = DecodedTxResult; 14 | 15 | export type Send = ( 16 | args?: unknown[], 17 | o?: LazyCallOptions, 18 | ) => Promise | undefined>; 19 | 20 | export interface DryRun { 21 | send: Send; 22 | isSubmitting: boolean; 23 | result?: DryRunResult; 24 | resolved: boolean; 25 | resetState: () => void; 26 | } 27 | 28 | export function useDryRun( 29 | chainContract: ChainContract | undefined, 30 | message: string, 31 | ): DryRun { 32 | const { account } = useWallet(); 33 | const defaultCaller = useDefaultCaller(chainContract?.chainId); 34 | const [result, setResult] = useState>(); 35 | const [isSubmitting, setIsSubmitting] = useState(false); 36 | const abiMessage = useAbiMessage(chainContract?.contract, message); 37 | 38 | const send: Send = useMemo( 39 | () => async (params, options) => { 40 | const tx = chainContract?.contract?.tx?.[message]; 41 | const caller = account?.address 42 | ? account.address 43 | : options?.defaultCaller 44 | ? defaultCaller 45 | : undefined; 46 | 47 | if (!caller || !chainContract?.contract || !abiMessage || !tx) { 48 | return; 49 | } 50 | 51 | setIsSubmitting(true); 52 | 53 | try { 54 | const resp = await call( 55 | chainContract.contract, 56 | abiMessage, 57 | caller, 58 | params, 59 | options, 60 | ); 61 | 62 | if (!resp || !resp.ok) return; 63 | 64 | const { gasConsumed, gasRequired, storageDeposit } = resp.value.raw; 65 | 66 | const requiresNoArguments = tx.meta.args.length === 0; 67 | const { partialFee } = await (requiresNoArguments 68 | ? tx(toContractOptions(options)) 69 | : tx(toContractOptions(options), ...(params || [])) 70 | ).paymentInfo(caller); 71 | 72 | const r = { 73 | ...resp, 74 | value: { 75 | ...resp.value, 76 | gasRequired, 77 | gasConsumed, 78 | storageDeposit, 79 | partialFee, 80 | }, 81 | }; 82 | 83 | setIsSubmitting(false); 84 | setResult(r); 85 | 86 | return r; 87 | } catch (e: unknown) { 88 | console.error(e); 89 | setIsSubmitting(false); 90 | return; 91 | } 92 | }, 93 | [account, chainContract?.contract, abiMessage], 94 | ); 95 | 96 | return { 97 | send, 98 | isSubmitting, 99 | result, 100 | resolved: Boolean(result && !isSubmitting), 101 | resetState: () => { 102 | setIsSubmitting(false); 103 | setResult(undefined); 104 | }, 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useEventSubscription.ts: -------------------------------------------------------------------------------- 1 | import { IEventLike } from '@polkadot/types/types/events'; 2 | import { useContext, useEffect } from 'react'; 3 | import { Bytes } from '../../../core/index'; 4 | import { getExpiredItem } from '../../../utils/index'; 5 | import { FIVE_SECONDS, HALF_A_SECOND } from '../../constants.ts'; 6 | import { Event, EventsContext } from '../../providers/events/index'; 7 | import { useConfig } from '../config/useConfig.ts'; 8 | import { useInterval } from '../internal/useInterval.ts'; 9 | import { useBlockHeader } from '../substrate/useBlockHeader.ts'; 10 | import { ChainContract } from './types.ts'; 11 | 12 | export const useEventSubscription = ( 13 | chainContract: ChainContract | undefined, 14 | ): void => { 15 | const { events, addEvent, removeEvent } = useContext(EventsContext); 16 | const { blockNumber, header } = useBlockHeader(chainContract?.chainId) || {}; 17 | const C = useConfig(); 18 | 19 | const address = chainContract?.contract?.address?.toString() || ''; 20 | const contractEvents = events?.[address] || []; 21 | 22 | useEffect(() => { 23 | const contract = chainContract?.contract; 24 | if (!header?.hash || !contract) return; 25 | 26 | contract.api.at(header?.hash).then((apiAt) => { 27 | apiAt.query.system?.events?.((encodedEvent: { event: IEventLike }[]) => { 28 | encodedEvent.forEach(({ event }) => { 29 | if (contract.api.events.contracts?.ContractEmitted?.is(event)) { 30 | const [contractAddress, contractEvent] = event.data; 31 | if ( 32 | !address || 33 | !contractAddress || 34 | !contractEvent || 35 | contractAddress.toString().toLowerCase() !== address.toLowerCase() 36 | ) 37 | return; 38 | 39 | try { 40 | const decodedEvent = contract.abi.decodeEvent( 41 | contractEvent as Bytes, 42 | ); 43 | 44 | const eventItem = { 45 | address, 46 | event: { 47 | name: decodedEvent.event.identifier, 48 | args: decodedEvent.args.map((v) => v.toHuman()), 49 | }, 50 | }; 51 | 52 | addEvent(eventItem); 53 | } catch (e) { 54 | console.error(e); 55 | } 56 | } 57 | }); 58 | }); 59 | }); 60 | }, [chainContract?.contract, blockNumber]); 61 | 62 | useInterval(() => { 63 | if (C.events?.expiration === 0) return; 64 | 65 | const expiredEvents = getExpiredItem( 66 | contractEvents, 67 | C.events?.expiration || FIVE_SECONDS, 68 | ); 69 | 70 | for (const event of expiredEvents) { 71 | removeEvent({ eventId: event.id, address }); 72 | } 73 | }, C.events?.checkInterval || HALF_A_SECOND); 74 | }; 75 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useEvents.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useMemo } from 'react'; 2 | import { AccountId } from '../../../core/index'; 3 | import { Event, EventsContext } from '../../providers/events/index'; 4 | import { RemoveEventPayload } from '../../providers/events/model.ts'; 5 | 6 | export interface Events { 7 | events: Event[]; 8 | removeEvent: (p: RemoveEventPayload) => void; 9 | } 10 | 11 | export const useEvents = ( 12 | contractAddress: AccountId | string | undefined, 13 | filters?: string[], 14 | ): Events => { 15 | const { events, removeEvent } = useContext(EventsContext); 16 | 17 | const contractEvents = useMemo(() => { 18 | if (!contractAddress) return []; 19 | 20 | return ( 21 | events[contractAddress.toString()]?.filter(({ name }) => 22 | filters ? filters.includes(name) : true, 23 | ) ?? [] 24 | ); 25 | }, [events, contractAddress]); 26 | 27 | return { events: contractEvents, removeEvent }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useMessageSigner.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { SignatureResult } from '../../../core/index.ts'; 3 | import { useWallet } from '../wallets/useWallet.ts'; 4 | 5 | export type Sign = (data?: string) => void; 6 | 7 | export enum SignerError { 8 | AccountNotConnected = 'No accounts are connected.', 9 | SignatureRejected = 'Signature rejected.', 10 | SignatureFailed = 'Signature failed.', 11 | } 12 | 13 | export interface MessageSigner { 14 | sign: Sign; 15 | signature: string | undefined; 16 | resetState: () => void; 17 | error: SignerError | undefined; 18 | } 19 | 20 | export function useMessageSigner(): MessageSigner { 21 | const { account } = useWallet(); 22 | const [signature, setSignature] = useState(); 23 | const [error, setError] = useState(); 24 | 25 | const sign: Sign = useCallback( 26 | async (data = '') => { 27 | if (!account || !account.signer?.signRaw) { 28 | setError(SignerError.AccountNotConnected); 29 | return; 30 | } 31 | 32 | setError(undefined); 33 | 34 | account.signer 35 | ?.signRaw?.({ 36 | address: account.address, 37 | data, 38 | type: 'bytes', 39 | }) 40 | .then(({ signature }) => setSignature(signature)) 41 | .catch((e) => { 42 | if (e.toString() === 'Error: Cancelled') { 43 | setError(SignerError.SignatureRejected); 44 | return; 45 | } 46 | 47 | setError(SignerError.SignatureFailed); 48 | }); 49 | }, 50 | [account, account?.wallet?.extension?.signer], 51 | ); 52 | 53 | const resetState = useCallback(() => { 54 | setSignature(undefined); 55 | setError(undefined); 56 | }, []); 57 | 58 | return { 59 | sign, 60 | signature, 61 | resetState, 62 | error, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useMetadata.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ChainId } from '../../../chains'; 3 | import { 4 | DEFAULT, 5 | MetadataOptions, 6 | MetadataState, 7 | jsonToAbi, 8 | } from '../../../core'; 9 | import { MetadataError, toBasicMetadata } from '../../../utils'; 10 | import { useApi } from '../substrate/useApi'; 11 | 12 | export interface BasicMetadataFile { 13 | data: Uint8Array; 14 | name: string; 15 | size: number; 16 | } 17 | 18 | export interface UseMetadata extends MetadataState { 19 | set: (_: File) => void; 20 | clear: () => void; 21 | } 22 | 23 | const utf8decoder = new TextDecoder(); 24 | 25 | const formatName = (n: string): string => 26 | n.replace('.contract', '').replace('.json', '').split('_').join(' '); 27 | 28 | export function useMetadata( 29 | options: MetadataOptions = { requireWasm: true }, 30 | initialValue?: Record, 31 | chainId?: ChainId, 32 | ): UseMetadata { 33 | const chainApi = useApi(chainId); 34 | const { requireWasm } = options; 35 | const [state, setState] = useState(() => 36 | jsonToAbi({ ...options }, initialValue, chainApi?.api), 37 | ); 38 | 39 | function set(file: File): void { 40 | toBasicMetadata(file) 41 | .then((basicFile) => { 42 | const json = JSON.parse(utf8decoder.decode(basicFile.data)) as Record< 43 | string, 44 | unknown 45 | >; 46 | 47 | const newState = jsonToAbi( 48 | { 49 | requireWasm: requireWasm, 50 | name: formatName(basicFile.name), 51 | size: basicFile.size, 52 | }, 53 | json, 54 | chainApi?.api, 55 | ); 56 | 57 | if (newState.error) { 58 | setState({ 59 | ...DEFAULT, 60 | error: newState.error, 61 | }); 62 | 63 | return; 64 | } 65 | 66 | setState(newState); 67 | }) 68 | .catch((_) => { 69 | setState({ 70 | ...DEFAULT, 71 | error: MetadataError.InvalidFile, 72 | }); 73 | }); 74 | } 75 | 76 | function clear(): void { 77 | setState(DEFAULT); 78 | } 79 | 80 | useEffect((): void => { 81 | setState( 82 | jsonToAbi({ requireWasm: requireWasm }, initialValue, chainApi?.api), 83 | ); 84 | }, [chainApi?.api, initialValue, requireWasm]); 85 | 86 | return { 87 | ...state, 88 | set, 89 | clear, 90 | }; 91 | } 92 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useSalter.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { isValidHash, pseudoRandomHex } from '../../../utils'; 3 | 4 | export enum SalterError { 5 | InvalidHash = 'Invalid salt hash value.', 6 | } 7 | 8 | export interface SalterOptions { 9 | randomize: boolean; 10 | initialValue: string; 11 | length: number; 12 | } 13 | 14 | export interface SalterState { 15 | salt: string; 16 | regenerate: () => void; 17 | set: (salt: string) => void; 18 | error?: SalterError; 19 | resetState: () => void; 20 | validate: () => void; 21 | } 22 | 23 | export const useSalter = (options?: Partial): SalterState => { 24 | const { randomize = true, initialValue, length = 64 } = options || {}; 25 | 26 | const initial = 27 | initialValue !== undefined 28 | ? initialValue 29 | : randomize 30 | ? pseudoRandomHex(length) 31 | : ''; 32 | 33 | const [salt, setSalt] = useState(initial); 34 | const [error, setError] = useState(); 35 | 36 | const validate = useCallback(() => { 37 | if (isValidHash(salt, length)) { 38 | error && setError(undefined); 39 | return; 40 | } 41 | 42 | setError(SalterError.InvalidHash); 43 | }, [salt]); 44 | 45 | const regenerate = useCallback(() => { 46 | const s = pseudoRandomHex(); 47 | setSalt(s); 48 | }, []); 49 | 50 | const set = useCallback((s: string) => { 51 | setSalt(s); 52 | }, []); 53 | 54 | const resetState = useCallback(() => { 55 | setSalt(''); 56 | setError(undefined); 57 | }, []); 58 | 59 | return { 60 | salt, 61 | resetState, 62 | regenerate, 63 | validate, 64 | set, 65 | error, 66 | }; 67 | }; 68 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useSignatureVerifier.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { signatureVerify } from '../../../utils'; 3 | 4 | type VerificationParams = Parameters; 5 | 6 | export type Verify = ( 7 | data: VerificationParams[0], 8 | signature: VerificationParams[1], 9 | addressOrPublicKey: VerificationParams[2], 10 | ) => void; 11 | 12 | export enum VerificationState { 13 | Unchecked = 'Unchecked', 14 | Valid = 'Valid signature', 15 | Invalid = 'Invalid signature', 16 | } 17 | 18 | export interface SignatureVerifier { 19 | verify: Verify; 20 | result: VerificationState; 21 | resetState: () => void; 22 | } 23 | 24 | export function useSignatureVerifier(): SignatureVerifier { 25 | const [result, setVerificationResult] = useState(VerificationState.Unchecked); 26 | 27 | const verify: Verify = useCallback( 28 | (data, signature, addressOrPublicKey) => { 29 | const { isValid } = signatureVerify(data, signature, addressOrPublicKey); 30 | 31 | if (isValid) { 32 | setVerificationResult(VerificationState.Valid); 33 | return; 34 | } 35 | 36 | setVerificationResult(VerificationState.Invalid); 37 | }, 38 | [], 39 | ); 40 | 41 | const resetState = useCallback(() => { 42 | setVerificationResult(VerificationState.Unchecked); 43 | }, []); 44 | 45 | return { 46 | verify, 47 | result, 48 | resetState, 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useTx.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react'; 2 | import { 3 | ApiBase, 4 | ContractSubmittableResult, 5 | EventRecord, 6 | LazyContractOptions, 7 | TransactionStatus, 8 | toContractOptions, 9 | } from '../../../core/index'; 10 | import { useWallet } from '../wallets/useWallet.ts'; 11 | import { ChainContract } from './types.ts'; 12 | import { useDryRun } from './useDryRun.ts'; 13 | import { useTxEvents } from './useTxEvents.ts'; 14 | 15 | export type ContractSubmittableResultCallback = ( 16 | result?: ContractSubmittableResult, 17 | api?: ApiBase<'promise'>, 18 | error?: unknown, 19 | ) => void; 20 | 21 | export type SignAndSend = ( 22 | args?: unknown[], 23 | o?: LazyContractOptions, 24 | cb?: ContractSubmittableResultCallback, 25 | ) => void; 26 | 27 | export interface Tx<_T> { 28 | signAndSend: SignAndSend; 29 | status: TransactionStatus; 30 | result: ContractSubmittableResult | undefined; 31 | resetState: () => void; 32 | events: EventRecord[]; 33 | } 34 | 35 | export function useTx( 36 | chainContract: ChainContract | undefined, 37 | message: string, 38 | ): Tx { 39 | const { account } = useWallet(); 40 | const [status, setStatus] = useState('None'); 41 | const [result, setResult] = useState(); 42 | const dryRun = useDryRun(chainContract, message); 43 | const txEvents = useTxEvents({ status, result }); 44 | 45 | const signAndSend: SignAndSend = useMemo( 46 | () => (params, options, cb) => { 47 | if (!chainContract?.contract || !account || !account.wallet?.extension) { 48 | return; 49 | } 50 | 51 | dryRun 52 | .send(params, options) 53 | .then((response) => { 54 | if (!response || !response.ok) return; 55 | setStatus('PendingSignature'); 56 | 57 | const { gasRequired } = response.value.raw; 58 | const tx = chainContract?.contract.tx[message]; 59 | 60 | if (!tx) { 61 | cb?.( 62 | undefined, 63 | chainContract.contract.api, 64 | `'${message}' not found on contract instance`, 65 | ); 66 | return; 67 | } 68 | 69 | tx( 70 | { gasLimit: gasRequired, ...toContractOptions(options) }, 71 | ...(params || []), 72 | ) 73 | .signAndSend( 74 | account.address, 75 | { signer: account.wallet?.extension?.signer }, 76 | (response: ContractSubmittableResult) => { 77 | setResult(response); 78 | setStatus(response.status.type); 79 | cb?.(response, chainContract?.contract.api); 80 | }, 81 | ) 82 | .catch((e: unknown) => { 83 | cb?.(undefined, chainContract.contract.api, e); 84 | setStatus('None'); 85 | }); 86 | }) 87 | .catch((e) => { 88 | cb?.(undefined, chainContract.contract.api, e); 89 | setStatus('None'); 90 | }); 91 | }, 92 | [account, account?.wallet?.extension?.signer, chainContract?.contract], 93 | ); 94 | 95 | return { 96 | signAndSend, 97 | status, 98 | result, 99 | resetState: () => { 100 | setResult(undefined); 101 | setStatus('None'); 102 | txEvents.resetState(); 103 | }, 104 | events: txEvents.events, 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useTxEvents.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from 'react'; 2 | import { EventRecord } from '../../../core'; 3 | import { isInBlock } from '../../../utils'; 4 | import { Tx } from './useTx'; 5 | 6 | type Eventable = Pick, 'status'> & Pick, 'result'>; 7 | 8 | interface TxEvents { 9 | resetState: () => void; 10 | events: EventRecord[]; 11 | } 12 | 13 | export const useTxEvents = (tx: Eventable): TxEvents => { 14 | const [events, setEvents] = useState([]); 15 | 16 | const resetState = useCallback(() => setEvents([]), []); 17 | 18 | useEffect(() => { 19 | if (!isInBlock(tx) || !tx.result) return; 20 | 21 | setEvents([...tx.result.events]); 22 | }, [tx.status]); 23 | 24 | return { events, resetState }; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/contracts/useTxPaymentInfo.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useState } from 'react'; 2 | import { ChainContract, useDefaultCaller } from '..'; 3 | import { 4 | LazyCallOptions, 5 | RuntimeDispatchInfo, 6 | SignerOptions, 7 | txPaymentInfo, 8 | } from '../../../core'; 9 | import { useWallet } from '../wallets/useWallet.ts'; 10 | 11 | type Send = ( 12 | params?: unknown[], 13 | options?: LazyCallOptions, 14 | signerOptions?: Partial, 15 | ) => Promise; 16 | 17 | interface TxPaymentInfo { 18 | isSubmitting: boolean; 19 | result?: RuntimeDispatchInfo; 20 | send: Send; 21 | resolved: boolean; 22 | } 23 | 24 | export function useTxPaymentInfo( 25 | chainContract: ChainContract | undefined, 26 | message: string, 27 | ): TxPaymentInfo { 28 | const [isSubmitting, setIsSubmitting] = useState(false); 29 | const [result, setResult] = useState(); 30 | const { account } = useWallet(); 31 | const defaultCaller = useDefaultCaller(); 32 | 33 | const send = useCallback( 34 | async (params, options, signerOptions) => { 35 | const caller = 36 | account?.address || options?.defaultCaller ? defaultCaller : undefined; 37 | 38 | if (!chainContract?.contract || !caller) return; 39 | 40 | try { 41 | setIsSubmitting(true); 42 | 43 | const paymentInfoResult = await txPaymentInfo( 44 | chainContract.contract, 45 | message, 46 | caller, 47 | params, 48 | options, 49 | signerOptions, 50 | ); 51 | 52 | setResult(paymentInfoResult); 53 | setIsSubmitting(false); 54 | 55 | return paymentInfoResult; 56 | } catch (e: unknown) { 57 | console.error(e); 58 | setIsSubmitting(false); 59 | return; 60 | } 61 | }, 62 | [chainContract?.contract, message, account, defaultCaller], 63 | ); 64 | 65 | return { 66 | isSubmitting, 67 | resolved: Boolean(!isSubmitting && result), 68 | result, 69 | send, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useUnixMilliToDate'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/helpers/useUnixMilliToDate.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { unixMilliToDate } from '../../../utils/helpers/unixMilliToDate'; 3 | 4 | export const useUnixMilliToDate = ( 5 | unixInMilliSeconds: number | undefined, 6 | ): Date | undefined => 7 | useMemo(() => unixMilliToDate(unixInMilliSeconds), [unixInMilliSeconds]); 8 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './config/index'; 2 | export * from './contracts/index'; 3 | export * from './helpers/index'; 4 | export * from './substrate/index'; 5 | export * from './wallets/index'; 6 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/internal/useInterval.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export function useInterval(callback: () => void, delay: number | null) { 4 | const savedCallback = useRef(callback); 5 | 6 | useEffect(() => { 7 | savedCallback.current = callback; 8 | }, [callback]); 9 | 10 | useEffect(() => { 11 | if (delay === null) return; 12 | 13 | const id = setInterval(() => savedCallback.current(), delay); 14 | 15 | return () => clearInterval(id); 16 | }, [delay]); 17 | } 18 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/internal/useIsMounted.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export function useIsMounted() { 4 | const isMounted = React.useRef(false); 5 | 6 | React.useEffect(() => { 7 | isMounted.current = true; 8 | return () => { 9 | isMounted.current = false; 10 | }; 11 | }, []); 12 | 13 | return React.useCallback(() => isMounted.current, []); 14 | } 15 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/balance/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useBalance'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/balance/useBalance.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ChainId } from '../../../../chains/index'; 3 | import { DeriveBalancesAccount, WithAddress } from '../../../../core/index'; 4 | import { getBalance } from '../../../../core/index'; 5 | import { useChain } from '../../index'; 6 | import { useApi } from '../useApi.ts'; 7 | import { useBlockHeader } from '../useBlockHeader.ts'; 8 | 9 | export const useBalance = ( 10 | account: WithAddress | undefined, 11 | chainId?: ChainId, 12 | ): DeriveBalancesAccount | undefined => { 13 | const [balance, setBalance] = useState(); 14 | const { blockNumber } = useBlockHeader(chainId) || {}; 15 | const chainConfig = useChain(chainId); 16 | const chain = useApi(chainConfig?.id); 17 | 18 | useEffect(() => { 19 | if (!chain?.api || !account || !account.address) return; 20 | 21 | getBalance(chain.api, account).then(setBalance).catch(console.error); 22 | }, [blockNumber, account]); 23 | 24 | return balance; 25 | }; 26 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './balance'; 2 | export * from './timestamp'; 3 | export * from './useApi.ts'; 4 | export * from './useBlockHeader.ts'; 5 | export * from './useChainDecimals.ts'; 6 | export * from './useTokenSymbol.ts'; 7 | export * from './useTransfer.ts'; 8 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/timestamp/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useTimestampDate'; 2 | export * from './useTimestampNow'; 3 | export * from './useTimestampQuery'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/timestamp/useTimestampDate.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../../chains'; 2 | import { useUnixMilliToDate } from '../../helpers'; 3 | import { useTimestampNow } from './useTimestampNow'; 4 | 5 | export const useTimestampDate = (chainId?: ChainId): Date | undefined => { 6 | const unix = useTimestampNow(chainId); 7 | 8 | return useUnixMilliToDate(unix); 9 | }; 10 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/timestamp/useTimestampNow.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ChainId } from '../../../../chains'; 3 | import { getTimestampUnix } from '../../../../core'; 4 | import { useApi } from '../useApi'; 5 | import { useBlockHeader } from '../useBlockHeader'; 6 | 7 | // Get the current timestamp in milliseconds 8 | export const useTimestampNow = (chainId?: ChainId): number | undefined => { 9 | const [now, setNow] = useState(); 10 | const b = useBlockHeader(chainId); 11 | const chainApi = useApi(chainId); 12 | 13 | useEffect(() => { 14 | getTimestampUnix(chainApi?.api).then(setNow).catch(); 15 | }, [b?.blockNumber]); 16 | 17 | return now; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/timestamp/useTimestampQuery.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { ChainId } from '../../../../chains'; 3 | import { QueryableModuleCalls, getTimestampQuery } from '../../../../core'; 4 | import { useApi } from '../useApi'; 5 | 6 | // Get a queryable function that can then be used to call a chain: `await timestampQuery.now()` 7 | export const useTimestampQuery = ( 8 | chainId?: ChainId, 9 | ): QueryableModuleCalls<'promise'> | undefined => { 10 | const chainApi = useApi(chainId); 11 | const [timestamp, setTimestamp] = useState>(); 12 | 13 | useEffect(() => { 14 | const t = getTimestampQuery(chainApi?.api); 15 | setTimestamp(t); 16 | }, [chainApi?.api]); 17 | 18 | return timestamp; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/useApi.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ChainId } from '../../../chains/index'; 3 | import { IApiProvider, useChain } from '../../index'; 4 | import { API, APIContext } from '../../providers/api/index'; 5 | 6 | export const useApis = (): API => useContext(APIContext); 7 | 8 | export const useApi = (chainId?: ChainId): IApiProvider | undefined => { 9 | const defaultChain = useChain(); 10 | const idOrDefault = chainId || defaultChain?.id; 11 | 12 | return idOrDefault ? useApis()?.apis?.[idOrDefault] : undefined; 13 | }; 14 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/useBlockHeader.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ChainId } from '../../../chains/index'; 3 | import { 4 | BlockHeaderContext, 5 | ChainBlockHeaders, 6 | } from '../../providers/blockHeader/index'; 7 | import { useChain } from '../config/useChain.ts'; 8 | import { BlockHeader } from './index'; 9 | 10 | export type { BlockHeader } from '../../providers/blockHeader/index'; 11 | 12 | export const useBlockHeader = (chainId?: ChainId): BlockHeader | undefined => { 13 | const chain = useChain(chainId); 14 | return chain ? useContext(BlockHeaderContext)[chain.id] : undefined; 15 | }; 16 | 17 | export const useBlockHeaders = (): ChainBlockHeaders => 18 | useContext(BlockHeaderContext); 19 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/useChainDecimals.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains'; 2 | import { chainDecimals } from '../../../core'; 3 | import { useApi } from './useApi'; 4 | 5 | export const useChainDecimals = (chainId?: ChainId): number | undefined => { 6 | const chainApi = useApi(chainId); 7 | if (!chainApi?.api) return undefined; 8 | 9 | return chainDecimals(chainApi?.api); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/useTokenSymbol.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains'; 2 | import { chainTokenSymbol } from '../../../core'; 3 | import { useApi } from './useApi'; 4 | 5 | export const useTokenSymbol = (chainId?: ChainId): string | undefined => { 6 | const chainApi = useApi(chainId); 7 | if (!chainApi?.api) return undefined; 8 | 9 | return chainTokenSymbol(chainApi?.api); 10 | }; 11 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/substrate/useTransfer.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useMemo, useState } from 'react'; 2 | import { useChain, useWallet } from '../'; 3 | import { ChainId } from '../../../chains'; 4 | import { Hash, SignerOptions, transfer } from '../../../core'; 5 | import { useApi } from './useApi.ts'; 6 | 7 | export type SignAndSendTransfer = ( 8 | to: string, 9 | amount: number, 10 | options?: SignerOptions, 11 | ) => void; 12 | 13 | export interface TransferState { 14 | signAndSend: SignAndSendTransfer; 15 | hash: Hash | undefined; 16 | error: unknown | undefined; 17 | resetState: () => void; 18 | isSubmitting: boolean; 19 | } 20 | 21 | export const useTransfer = (chainId?: ChainId): TransferState | undefined => { 22 | const [hash, setHash] = useState(); 23 | const [error, setError] = useState(); 24 | const [isSubmitting, setIsSubmitting] = useState(false); 25 | const { account } = useWallet(); 26 | const chainConfig = useChain(chainId); 27 | const chain = useApi(chainConfig?.id); 28 | 29 | const resetState = useCallback(() => { 30 | setHash(undefined); 31 | setError(undefined); 32 | }, []); 33 | 34 | const signAndSend: SignAndSendTransfer = useMemo( 35 | () => (to, amount, options) => { 36 | if (!chain?.api || !account || !account.address || !account.wallet) { 37 | return; 38 | } 39 | setIsSubmitting(true); 40 | transfer(chain.api, to, amount, account?.wallet.extension.signer, options) 41 | .then(setHash) 42 | .catch(setError) 43 | .finally(() => setIsSubmitting(false)); 44 | }, 45 | [chain?.api, account?.address], 46 | ); 47 | 48 | return { signAndSend, error, hash, resetState, isSubmitting }; 49 | }; 50 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/wallets/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useAllWallets.ts'; 2 | export * from './useInstalledWallets.ts'; 3 | export * from './useUninstalledWallets.ts'; 4 | export * from './useWallet.ts'; 5 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/wallets/useAllWallets.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Wallet } from '../../providers/wallet/index'; 3 | import { useWallet } from '../index'; 4 | 5 | export const useAllWallets = (): Wallet[] => { 6 | const { getWallets } = useWallet(); 7 | return useMemo(() => getWallets(), []); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/wallets/useInstalledWallets.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Wallet } from '../../providers/wallet/index'; 3 | import { useWallet } from '../index'; 4 | 5 | export const useInstalledWallets = (): Wallet[] => { 6 | const { getWallets } = useWallet(); 7 | return useMemo(() => getWallets().filter((w) => w.installed), []); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/wallets/useUninstalledWallets.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Wallet } from '../../providers/wallet/index'; 3 | import { useWallet } from './useWallet.ts'; 4 | 5 | export const useUninstalledWallets = (): Wallet[] => { 6 | const { getWallets } = useWallet(); 7 | return useMemo(() => getWallets().filter((w) => !w.installed), []); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/useink/src/react/hooks/wallets/useWallet.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { WalletContext, WalletState } from '../../providers/wallet/index'; 3 | 4 | export type { WalletState } from '../../providers/wallet/index'; 5 | 6 | export const useWallet: () => WalletState = () => 7 | useContext(WalletContext); 8 | -------------------------------------------------------------------------------- /packages/useink/src/react/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hooks/index'; 2 | export * from './providers/index'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/UseInkProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { APIProvider } from './api/provider.tsx'; 3 | import { BlockHeaderProvider } from './blockHeader/index'; 4 | import { ConfigProps, ConfigProvider } from './config/index'; 5 | import { EventsProvider } from './events/index'; 6 | import { WalletProvider } from './wallet/index'; 7 | 8 | export type InkConfig = { 9 | config: ConfigProps; 10 | }; 11 | 12 | export const UseInkProvider: React.FC> = ({ 13 | children, 14 | config, 15 | }) => ( 16 | 17 | 18 | 19 | 20 | {children} 21 | 22 | 23 | 24 | 25 | ); 26 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/api/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { API } from './model.ts'; 3 | 4 | export const APIContext = createContext({ 5 | apis: {}, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './model.ts'; 3 | export * from './provider.tsx'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/api/model.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { ApiPromise, WsProvider } from '../../../core/index'; 3 | 4 | export interface IApiProvider { 5 | api: ApiPromise; 6 | provider: WsProvider; 7 | } 8 | 9 | export type IApiProviders = Partial>; 10 | 11 | export interface API { 12 | apis?: IApiProviders; 13 | } 14 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/api/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useReducer } from 'react'; 2 | import { ApiPromise, WsProvider } from '../../../core/index'; 3 | import { useChains } from '../../hooks/index'; 4 | import { useConfig } from '../../index'; 5 | import { APIContext } from './context.ts'; 6 | import { apiProvidersReducer } from './reducer.ts'; 7 | 8 | export const APIProvider: React.FC = ({ 9 | children, 10 | }) => { 11 | const chains = useChains(); 12 | const { chainRpcs } = useConfig(); 13 | const [apis, dispatch] = useReducer(apiProvidersReducer, {}); 14 | 15 | useEffect(() => { 16 | chains.forEach((chain) => { 17 | const provider = new WsProvider(chainRpcs[chain.id] || chain.rpcs[0]); 18 | 19 | ApiPromise.create({ provider }).then((api) => { 20 | dispatch({ 21 | type: 'ADD_API_PROVIDER', 22 | chainId: chain.id, 23 | apiProvider: { api, provider }, 24 | }); 25 | }); 26 | }); 27 | }, [chains, chainRpcs]); 28 | 29 | return {children}; 30 | }; 31 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/api/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { IApiProvider, IApiProviders } from './model.ts'; 3 | 4 | interface AddApiProvider { 5 | type: 'ADD_API_PROVIDER'; 6 | chainId: ChainId; 7 | apiProvider: IApiProvider; 8 | } 9 | 10 | type Action = AddApiProvider; 11 | 12 | export function apiProvidersReducer( 13 | state: IApiProviders, 14 | action: Action, 15 | ): IApiProviders { 16 | switch (action.type) { 17 | case 'ADD_API_PROVIDER': 18 | return { 19 | ...state, 20 | [action.chainId]: action.apiProvider, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/blockHeader/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { BLOCK_HEADER_DEFAULTS, ChainBlockHeaders } from './model.ts'; 3 | 4 | export const BlockHeaderContext = createContext({ 5 | ...BLOCK_HEADER_DEFAULTS, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/blockHeader/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './model.ts'; 3 | export * from './provider.tsx'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/blockHeader/model.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { Header } from '../../../core/index'; 3 | 4 | export interface BlockHeader { 5 | blockNumber: number | undefined; 6 | header: Header | undefined; 7 | } 8 | 9 | export type ChainBlockHeaders = Partial>; 10 | 11 | export const BLOCK_HEADER_DEFAULTS: ChainBlockHeaders = {}; 12 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/blockHeader/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo, useReducer } from 'react'; 2 | import { ChainId } from '../../../chains/index'; 3 | import { useApis } from '../../index'; 4 | import { BlockHeaderContext } from './context.ts'; 5 | import { ChainBlockHeaders } from './model.ts'; 6 | import { chainBlockHeaderReducer } from './reducer.ts'; 7 | 8 | const toBlockNumber = (valWithComma: string | undefined): number => 9 | parseInt(`${valWithComma?.split(',').join('')}`); 10 | 11 | export const BlockHeaderProvider: React.FC = ({ 12 | children, 13 | }) => { 14 | const chainApis = useApis(); 15 | 16 | const [chainBlockHeaders, dispatch] = useReducer( 17 | chainBlockHeaderReducer, 18 | {} as ChainBlockHeaders, 19 | ); 20 | 21 | const chainIds: ChainId[] = useMemo( 22 | () => (chainApis.apis ? (Object.keys(chainApis.apis) as ChainId[]) : []), 23 | [chainApis], 24 | ); 25 | 26 | useEffect(() => { 27 | function listenToBlocks() { 28 | return chainIds.map((chainId) => { 29 | return chainApis?.apis?.[chainId]?.api?.rpc.chain.subscribeNewHeads( 30 | (header) => { 31 | try { 32 | const blockNumber = toBlockNumber( 33 | header.number.toHuman()?.toString(), 34 | ); 35 | blockNumber && 36 | dispatch({ 37 | type: 'ADD_CHAIN_BLOCK_HEADER', 38 | chainId, 39 | blockHeader: { blockNumber, header }, 40 | }); 41 | } catch (e) { 42 | console.error(e); 43 | } 44 | }, 45 | ); 46 | }); 47 | } 48 | 49 | let unsubFuncs: (VoidFunction | undefined)[] | undefined; 50 | Promise.all(listenToBlocks()).then((unsubs) => unsubFuncs === unsubs); 51 | 52 | return () => { 53 | unsubFuncs?.forEach((unsub) => unsub?.()); 54 | }; 55 | }, [chainApis]); 56 | 57 | return ( 58 | 59 | {children} 60 | 61 | ); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/blockHeader/reducer.ts: -------------------------------------------------------------------------------- 1 | import { ChainId } from '../../../chains/index'; 2 | import { BlockHeader, ChainBlockHeaders } from './model.ts'; 3 | 4 | interface AddChainBlockHeader { 5 | type: 'ADD_CHAIN_BLOCK_HEADER'; 6 | chainId: ChainId; 7 | blockHeader: BlockHeader; 8 | } 9 | 10 | type Action = AddChainBlockHeader; 11 | 12 | export function chainBlockHeaderReducer( 13 | state: ChainBlockHeaders, 14 | action: Action, 15 | ): ChainBlockHeaders { 16 | switch (action.type) { 17 | case 'ADD_CHAIN_BLOCK_HEADER': 18 | return { 19 | ...state, 20 | [action.chainId]: action.blockHeader, 21 | }; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/config/context.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ChainConfig, ConfigProps, DEFAULT_CONFIG } from './model.ts'; 3 | 4 | export const ConfigContext = React.createContext( 5 | DEFAULT_CONFIG, 6 | ); 7 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './model.ts'; 3 | export * from './provider.tsx'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/config/model.ts: -------------------------------------------------------------------------------- 1 | import { Chain, ChainId, RococoContractsTestnet } from '../../../chains/index'; 2 | import { ArrayOneOrMore } from '../../../core/index'; 3 | import { FIVE_SECONDS, HALF_A_SECOND } from '../../constants.ts'; 4 | 5 | export type ChainRPCs = Partial>; 6 | 7 | export type CallerAddress = string; 8 | 9 | export type ConfigProps = { 10 | dappName?: string; 11 | chains: ArrayOneOrMore; 12 | caller?: { 13 | default?: CallerAddress; 14 | } & Partial>; 15 | events?: { 16 | expiration?: number; 17 | checkInterval?: number; 18 | }; 19 | wallet?: { 20 | skipAutoConnect?: boolean; 21 | }; 22 | }; 23 | 24 | export type SetChainRpc = (rpc: string, chain?: ChainId) => void; 25 | 26 | export interface ChainConfig { 27 | setChainRpc: SetChainRpc; 28 | chainRpcs: ChainRPCs; 29 | } 30 | 31 | export type Config = ChainConfig & ConfigProps; 32 | 33 | export const DEFAULT_CONFIG: Config = { 34 | dappName: 'A dapp built with useInk!', 35 | chains: [RococoContractsTestnet], 36 | events: { 37 | expiration: FIVE_SECONDS, 38 | checkInterval: HALF_A_SECOND, 39 | }, 40 | setChainRpc: () => null, 41 | chainRpcs: {}, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/config/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 2 | import { Chain, ChainId } from '../../../chains/index'; 3 | import { ConfigContext } from './context.ts'; 4 | import { ChainRPCs, ConfigProps, DEFAULT_CONFIG } from './model.ts'; 5 | 6 | export interface Props { 7 | config: ConfigProps; 8 | } 9 | 10 | const toInitialRpcs = (c: Chain[], _rpcs: ChainRPCs): ChainRPCs => 11 | c.reduce( 12 | (acc, ch) => ({ ...acc, [ch.id]: ch.rpcs?.[0] || '' }), 13 | {} as Record, 14 | ); 15 | 16 | export const ConfigProvider: React.FC> = ({ 17 | config, 18 | children, 19 | }) => { 20 | const defaultChainId = useMemo(() => config.chains[0].id, [config.chains[0]]); 21 | const [chainRpcs, setChainRpcs] = useState( 22 | toInitialRpcs(config.chains, {} as ChainRPCs), 23 | ); 24 | 25 | const setChainRpc = useCallback((rpc: string, cid?: ChainId) => { 26 | const chainIdOrDefault = cid || defaultChainId; 27 | chainIdOrDefault && setChainRpcs({ ...chainRpcs, [chainIdOrDefault]: rpc }); 28 | }, []); 29 | 30 | useEffect(() => { 31 | setChainRpcs(toInitialRpcs(config.chains, chainRpcs)); 32 | 33 | if (!config.chains.length) { 34 | const error = 'Chains not configured in Config Provider'; 35 | console.error(error); 36 | throw Error(error); 37 | } 38 | }, [config.chains]); 39 | 40 | return ( 41 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/events/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { 3 | AddEventPayload, 4 | DEFAULT_EVENTS, 5 | Events, 6 | RemoveEventPayload, 7 | } from './model.ts'; 8 | 9 | export const EventsContext = createContext<{ 10 | events: Events; 11 | addEvent: (payload: AddEventPayload) => void; 12 | removeEvent: (payload: RemoveEventPayload) => void; 13 | }>({ 14 | events: DEFAULT_EVENTS, 15 | addEvent: () => null, 16 | removeEvent: () => null, 17 | }); 18 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './model.ts'; 3 | export * from './provider.tsx'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/events/model.ts: -------------------------------------------------------------------------------- 1 | export type EventPayload = { 2 | createdAt: number; 3 | name: string; 4 | args: unknown[]; 5 | }; 6 | 7 | export type AddEventPayload = { 8 | event: Omit; 9 | address: string; 10 | }; 11 | 12 | export type RemoveEventPayload = { 13 | eventId: string; 14 | address: string; 15 | }; 16 | 17 | export type Event = { id: string } & EventPayload; 18 | 19 | export type Events = { 20 | [address: string]: Event[]; 21 | }; 22 | 23 | export const DEFAULT_EVENTS: Events = {}; 24 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/events/provider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { pseudoRandomId } from '../../../utils/index'; 3 | import { EventsContext } from './context.ts'; 4 | import { 5 | AddEventPayload, 6 | DEFAULT_EVENTS, 7 | RemoveEventPayload, 8 | } from './model.ts'; 9 | import { eventsReducer } from './reducer.ts'; 10 | 11 | import { useIsMounted } from '../../hooks/internal/useIsMounted.ts'; 12 | 13 | // @internal 14 | export const EventsProvider: React.FC = ({ 15 | children, 16 | }) => { 17 | const [events, dispatch] = React.useReducer(eventsReducer, DEFAULT_EVENTS); 18 | const isMounted = useIsMounted(); 19 | 20 | const addEvent = React.useCallback( 21 | ({ event, address }: AddEventPayload) => { 22 | if (isMounted()) { 23 | dispatch({ 24 | type: 'ADD_EVENT', 25 | address, 26 | event: { ...event, id: pseudoRandomId(), createdAt: Date.now() }, 27 | }); 28 | } 29 | }, 30 | [dispatch], 31 | ); 32 | 33 | const removeEvent = React.useCallback( 34 | ({ eventId, address }: RemoveEventPayload) => { 35 | if (isMounted()) { 36 | dispatch({ 37 | type: 'REMOVE_EVENT', 38 | address, 39 | eventId, 40 | }); 41 | } 42 | }, 43 | [dispatch], 44 | ); 45 | 46 | return ( 47 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/events/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Event, Events } from './model.ts'; 2 | 3 | interface AddEvent { 4 | type: 'ADD_EVENT'; 5 | event: Event; 6 | address: string; 7 | } 8 | 9 | interface RemoveEvent { 10 | type: 'REMOVE_EVENT'; 11 | eventId: string; 12 | address: string; 13 | } 14 | 15 | type Action = AddEvent | RemoveEvent; 16 | 17 | export function eventsReducer(state: Events, action: Action): Events { 18 | switch (action.type) { 19 | case 'ADD_EVENT': 20 | return { 21 | ...state, 22 | [action.address]: [...(state[action.address] || []), action.event], 23 | }; 24 | case 'REMOVE_EVENT': { 25 | const events = state[action.address]; 26 | if (!events) return state; 27 | 28 | const idx = events.findIndex((e) => e.id === action.eventId); 29 | if (idx < 0) return state; 30 | 31 | const newContractState: Event[] = [ 32 | ...events.slice(0, idx), 33 | ...events.slice(idx + 1, events.length), 34 | ]; 35 | 36 | return { 37 | ...state, 38 | [action.address]: newContractState, 39 | }; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './UseInkProvider.tsx'; 2 | export * from './api/index'; 3 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/wallet/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | import { WALLET_DEFAULTS, WalletState } from './model.ts'; 3 | 4 | export const WalletContext = createContext({ 5 | ...WALLET_DEFAULTS, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/wallet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context.ts'; 2 | export * from './model.ts'; 3 | export * from './provider.tsx'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/wallet/model.ts: -------------------------------------------------------------------------------- 1 | import { 2 | WalletAccount, 3 | getWalletBySource, 4 | getWallets, 5 | } from '../../../core/index'; 6 | 7 | export enum WalletError { 8 | AccountDisabled = 'AccountNotEnabled', 9 | ConnectionError = 'ConnectionError', 10 | EnableFailed = 'EnableFailed', 11 | NoAccountsEnabled = 'NoAccountsEnabled', 12 | WalletNotInstalled = 'WalletNotInstalled', 13 | } 14 | 15 | export interface WalletState { 16 | account?: WalletAccount | undefined; 17 | accounts: WalletAccount[] | undefined; 18 | connect: (walletName: WalletName) => void; 19 | disconnect: () => void; 20 | walletError?: WalletError; 21 | isConnected: boolean; 22 | setAccount: (account: WalletAccount) => void; 23 | getWallets: typeof getWallets; 24 | getWalletBySource: typeof getWalletBySource; 25 | } 26 | 27 | export type Wallet = Exclude, undefined>; 28 | 29 | export const WALLET_DEFAULTS: WalletState = { 30 | connect: () => undefined, 31 | disconnect: () => undefined, 32 | account: undefined, 33 | accounts: undefined, 34 | setAccount: () => undefined, 35 | isConnected: false, 36 | getWallets, 37 | getWalletBySource, 38 | }; 39 | 40 | export type WalletName = string; 41 | 42 | export interface AutoConnect { 43 | address: string; 44 | wallet: WalletName; 45 | } 46 | -------------------------------------------------------------------------------- /packages/useink/src/react/providers/wallet/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useMemo, useState } from 'react'; 2 | import { 3 | Unsub, 4 | WalletAccount, 5 | getWalletBySource, 6 | getWallets, 7 | } from '../../../core'; 8 | import { useConfig } from '../../hooks'; 9 | import { WalletContext } from './context.ts'; 10 | import { AutoConnect, WalletError, WalletName } from './model.ts'; 11 | 12 | function getAutoConnectWalletInfo(key: string): AutoConnect | null { 13 | const item = localStorage.getItem(key); 14 | return item ? (JSON.parse(item) as AutoConnect) : null; 15 | } 16 | 17 | function enableAutoConnect(a: AutoConnect, key: string) { 18 | localStorage.setItem(key, JSON.stringify(a)); 19 | } 20 | 21 | function disableAutoConnect(key: string) { 22 | if (getAutoConnectWalletInfo(key)) localStorage.removeItem(key); 23 | } 24 | 25 | export const WalletProvider: React.FC = ({ 26 | children, 27 | }) => { 28 | const C = useConfig(); 29 | const [account, setWalletAccount] = useState(); 30 | const [accounts, setAccounts] = useState(); 31 | const [walletError, setWalletError] = useState(); 32 | const dappName = useMemo( 33 | () => 34 | C.dappName && C.dappName.trim().length > 0 35 | ? C.dappName 36 | : 'A Dapp built in useink', 37 | [C.dappName], 38 | ); 39 | 40 | const [activeWallet, setActiveWallet] = useState(); 41 | 42 | const disconnect = useCallback(() => { 43 | disableAutoConnect(dappName); 44 | setAccounts(undefined); 45 | setWalletAccount(undefined); 46 | setActiveWallet(undefined); 47 | setWalletError(undefined); 48 | }, [dappName]); 49 | 50 | const setAccount = useCallback( 51 | (newAccount: WalletAccount) => { 52 | const accountDisabled = !accounts?.find( 53 | (a) => a.address === newAccount.address, 54 | ); 55 | 56 | if (accountDisabled) { 57 | setWalletError(WalletError.AccountDisabled); 58 | return; 59 | } 60 | 61 | walletError !== undefined && setWalletError(undefined); 62 | 63 | setWalletAccount(newAccount); 64 | 65 | if (!C.wallet?.skipAutoConnect) { 66 | enableAutoConnect( 67 | { 68 | address: newAccount.address, 69 | wallet: newAccount.source, 70 | }, 71 | dappName, 72 | ); 73 | } 74 | }, 75 | [accounts, C.wallet?.skipAutoConnect], 76 | ); 77 | 78 | const connectWallet = useCallback( 79 | async (walletName: WalletName): Promise => { 80 | walletError && setWalletError(undefined); 81 | const w = getWalletBySource(walletName); 82 | 83 | if (!w) { 84 | setWalletError(WalletError.WalletNotInstalled); 85 | setActiveWallet(undefined); 86 | return; 87 | } 88 | 89 | try { 90 | await w.enable(dappName); 91 | } catch (_e) { 92 | setWalletError(WalletError.EnableFailed); 93 | setActiveWallet(undefined); 94 | return; 95 | } 96 | 97 | const unsub = (await w.subscribeAccounts((accts) => { 98 | setAccounts(accts as WalletAccount[]); 99 | 100 | const firstAccount = accts?.[0]; 101 | 102 | const noAccountsEnabled = !accts || !firstAccount; 103 | if (noAccountsEnabled) { 104 | setWalletError(WalletError.NoAccountsEnabled); 105 | setWalletAccount(undefined); 106 | disableAutoConnect(dappName); 107 | return; 108 | } 109 | 110 | const activeAccountNoLongerConnected = 111 | account && !accts?.find((a) => a.address === account?.address); 112 | 113 | if (activeAccountNoLongerConnected) { 114 | setWalletAccount(firstAccount as WalletAccount); 115 | 116 | if (!C.wallet?.skipAutoConnect) { 117 | enableAutoConnect( 118 | { 119 | address: firstAccount.address, 120 | wallet: firstAccount.source, 121 | }, 122 | dappName, 123 | ); 124 | } 125 | return; 126 | } 127 | 128 | const autoConnect = getAutoConnectWalletInfo(dappName); 129 | 130 | const autoConnectAccount = 131 | autoConnect && accts.find((a) => a.address === autoConnect.address); 132 | 133 | const initialAccount = autoConnectAccount || firstAccount; 134 | 135 | setWalletAccount(initialAccount as WalletAccount); 136 | 137 | if (!C.wallet?.skipAutoConnect) { 138 | enableAutoConnect( 139 | { 140 | address: initialAccount.address, 141 | wallet: initialAccount.source, 142 | }, 143 | dappName, 144 | ); 145 | } 146 | })) as Unsub; 147 | 148 | return unsub; 149 | }, 150 | [], 151 | ); 152 | 153 | const connect = useCallback((walletName: WalletName) => { 154 | setActiveWallet(walletName); 155 | }, []); 156 | 157 | useEffect(() => { 158 | if (!activeWallet) { 159 | const wallet = getAutoConnectWalletInfo(dappName)?.wallet; 160 | if (wallet) setActiveWallet(wallet); 161 | return; 162 | } 163 | 164 | let unsubFunc: (Unsub | undefined) | undefined; 165 | connectWallet(activeWallet).then((unsub) => unsubFunc === unsub); 166 | 167 | return () => unsubFunc?.(); 168 | }, [activeWallet]); 169 | 170 | return ( 171 | 184 | {children} 185 | 186 | ); 187 | }; 188 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toBasicMetadata.ts'; 2 | export * from './toJSType.ts'; 3 | export * from './toMessageParams.ts'; 4 | export * from './txUtils.ts'; 5 | export * from './validateMetadata.ts'; 6 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/toBasicMetadata.ts: -------------------------------------------------------------------------------- 1 | import { BasicMetadataFile } from '../../react/hooks/contracts/useMetadata'; 2 | 3 | export const toBasicMetadata = async ( 4 | file: File, 5 | ): Promise => { 6 | return await new Promise((resolve, reject) => { 7 | const reader = new FileReader(); 8 | reader.onload = ({ target }: ProgressEvent): void => { 9 | if (target?.result) { 10 | const name = file.name; 11 | const data = new Uint8Array(target.result as ArrayBuffer); 12 | const size = data.length; 13 | 14 | resolve({ data, name, size }); 15 | } 16 | 17 | reject('Target result not found in file'); 18 | }; 19 | 20 | reader.readAsArrayBuffer(file); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/toJSType.ts: -------------------------------------------------------------------------------- 1 | import { AbiParam } from '../../core'; 2 | 3 | export type JSType = 'BN' | 'Bytes' | 'boolean' | 'null' | 'number' | 'string'; 4 | 5 | export type TypeDefType = string; 6 | export type RegistryTypesMap = Record; 7 | 8 | export const toJSType = ( 9 | abiParam: AbiParam, 10 | ): [TypeDefType, JSType | undefined] => { 11 | const map: RegistryTypesMap = { 12 | AccountIndex: 'BN', 13 | Balance: 'BN', 14 | BalanceOf: 'BN', 15 | Compact: 'BN', 16 | Gas: 'BN', 17 | Index: 'BN', 18 | Nonce: 'BN', 19 | ParaId: 'BN', 20 | PropIndex: 'BN', 21 | ProposalIndex: 'BN', 22 | ReferendumIndex: 'BN', 23 | i64: 'BN', 24 | i128: 'BN', 25 | u32: 'BN', 26 | u64: 'BN', 27 | u128: 'BN', 28 | VoteIndex: 'BN', 29 | Moment: 'BN', 30 | VoteThreshold: 'BN', 31 | Vote: 'BN', 32 | BlockNumber: 'number', 33 | u8: 'number', 34 | u16: 'number', 35 | i8: 'number', 36 | i16: 'number', 37 | i32: 'number', 38 | bool: 'boolean', 39 | String: 'string', 40 | Text: 'string', 41 | Hash: 'string', 42 | H256: 'string', 43 | H512: 'string', 44 | H160: 'string', 45 | BlockHash: 'string', 46 | CodeHash: 'string', 47 | AccountId: 'string', 48 | AccountIdOf: 'string', 49 | Address: 'string', 50 | Call: 'string', 51 | CandidateReceipt: 'string', 52 | Digest: 'string', 53 | Header: 'string', 54 | KeyValue: 'string', 55 | LookupSource: 'string', 56 | MisbehaviorReport: 'string', 57 | Proposal: 'string', 58 | Signature: 'string', 59 | SessionKey: 'string', 60 | StorageKey: 'string', 61 | ValidatorId: 'string', 62 | Raw: 'string', 63 | Keys: 'string', 64 | Null: 'null', 65 | Bytes: 'Bytes', 66 | Extrinsic: 'Bytes', 67 | }; 68 | 69 | return [abiParam.type.type, map[abiParam.type.type]]; 70 | }; 71 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/toMessageParams.ts: -------------------------------------------------------------------------------- 1 | import { AbiParam, ApiBase } from '../../core'; 2 | 3 | export const toMessageParams = ( 4 | api: ApiBase<'promise'>, 5 | abiParams: AbiParam[], 6 | userParams?: Record, 7 | ): unknown[] => { 8 | return abiParams.map(({ name, type: { type } }) => { 9 | const value = userParams ? userParams[name] : null; 10 | 11 | if (type === 'Balance') return api.registry.createType('Balance', value); 12 | 13 | return value || null; 14 | }); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/txUtils.ts: -------------------------------------------------------------------------------- 1 | import { TransactionStatus } from '../../core/index'; 2 | 3 | export type Response = { 4 | status: TransactionStatus; 5 | }; 6 | 7 | export const isNone = (tx: { status: TransactionStatus }): boolean => 8 | tx.status === 'None'; 9 | 10 | export const isDryRun = (tx: { status: TransactionStatus }): boolean => 11 | tx.status === 'DryRun'; 12 | 13 | export const isPendingSignature = (tx: { 14 | status: TransactionStatus; 15 | }): boolean => tx.status === 'PendingSignature'; 16 | 17 | export const isErrored = (tx: { status: TransactionStatus }): boolean => 18 | tx.status === 'Errored'; 19 | 20 | export const isFuture = (tx: { status: TransactionStatus }): boolean => 21 | tx.status === 'Future'; 22 | 23 | export const isReady = (tx: { status: TransactionStatus }): boolean => 24 | tx.status === 'Ready'; 25 | 26 | export const isBroadcast = (tx: { status: TransactionStatus }): boolean => 27 | tx.status === 'Broadcast'; 28 | 29 | export const isRetracted = (tx: { status: TransactionStatus }): boolean => 30 | tx.status === 'Retracted'; 31 | 32 | export const isFinalityTimeout = (tx: { status: TransactionStatus }): boolean => 33 | tx.status === 'FinalityTimeout'; 34 | 35 | export const isInBlock = (tx: { status: TransactionStatus }): boolean => 36 | tx.status === 'InBlock'; 37 | 38 | export const isFinalized = (tx: { status: TransactionStatus }): boolean => 39 | tx.status === 'Finalized'; 40 | 41 | export const isUsurped = (tx: { status: TransactionStatus }): boolean => 42 | tx.status === 'Usurped'; 43 | 44 | export const isDropped = (tx: { status: TransactionStatus }): boolean => 45 | tx.status === 'Dropped'; 46 | 47 | export const isInvalid = (tx: { status: TransactionStatus }): boolean => 48 | tx.status === 'Invalid'; 49 | 50 | export const hasAny = ( 51 | tx: { status: TransactionStatus }, 52 | ...statuses: TransactionStatus[] 53 | ): boolean => statuses.includes(tx.status); 54 | 55 | export const shouldDisable = (tx: { status: TransactionStatus }): boolean => 56 | hasAny(tx, 'DryRun', 'PendingSignature', 'Broadcast'); 57 | 58 | export const shouldDisableStrict = (tx: { 59 | status: TransactionStatus; 60 | }): boolean => shouldDisable(tx) || isInBlock(tx); 61 | -------------------------------------------------------------------------------- /packages/useink/src/utils/contracts/validateMetadata.ts: -------------------------------------------------------------------------------- 1 | import { Abi, MetadataOptions } from '../../core'; 2 | 3 | export enum MetadataError { 4 | InvalidFile = 'Invalid file.', 5 | EmptyWasm = 'Wasm field not found.', 6 | InvalidWasm = 'Invalid Wasm field.', 7 | } 8 | 9 | export interface Validation { 10 | error?: MetadataError; 11 | } 12 | 13 | export const validateMetadata = ( 14 | metadata: Abi | undefined, 15 | { requireWasm }: MetadataOptions, 16 | ): Validation => { 17 | if (!metadata) return { error: MetadataError.InvalidFile }; 18 | 19 | const wasm = metadata.info.source.wasm; 20 | const isWasmEmpty = wasm.isEmpty; 21 | const isWasmInvalid = !WebAssembly.validate(wasm); 22 | 23 | if (requireWasm && isWasmEmpty) { 24 | return { error: MetadataError.EmptyWasm }; 25 | } 26 | 27 | if (requireWasm && isWasmInvalid) { 28 | return { error: MetadataError.InvalidWasm }; 29 | } 30 | 31 | return {}; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/contracts/ContractInstantiated.ts: -------------------------------------------------------------------------------- 1 | import { EventRecord } from '../../../core'; 2 | import { formatEventName } from '../formatEventName'; 3 | import { IEvent } from '../types'; 4 | 5 | export interface ContractInstantiated extends IEvent { 6 | deployer: string; 7 | contractAddress: string; 8 | } 9 | 10 | export const isContractInstantiatedEvent = ({ event }: EventRecord): boolean => 11 | event.section === 'contracts' && event.method === 'Instantiated'; 12 | 13 | export const asContractInstantiatedEvent = ( 14 | eventRecord: EventRecord, 15 | ): ContractInstantiated | undefined => { 16 | if (!isContractInstantiatedEvent(eventRecord)) return; 17 | 18 | const deployer = eventRecord.event.data[0]?.toString(); 19 | const contractAddress = eventRecord.event.data[1]?.toString(); 20 | if (!deployer || !contractAddress) return; 21 | 22 | return { deployer, contractAddress, name: formatEventName(eventRecord) }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/contracts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ContractInstantiated'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/formatEventName.ts: -------------------------------------------------------------------------------- 1 | import { EventRecord } from '../../core'; 2 | 3 | export const formatEventName = ({ event }: EventRecord): string => 4 | `${event.section}:${event.method}`; 5 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contracts'; 2 | export * from './formatEventName'; 3 | export * from './system'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/system/ExtrinsicFailed.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ApiBase, 3 | DispatchError, 4 | DispatchInfo, 5 | EventRecord, 6 | decodeError, 7 | } from '../../../core'; 8 | import { formatEventName } from '../formatEventName'; 9 | 10 | export type ExtrinsicFailedEvent = EventRecord & { 11 | name: string; 12 | event: { 13 | data: { 14 | dispatchError: DispatchError; 15 | dispatchInfo: DispatchInfo; 16 | }; 17 | }; 18 | }; 19 | 20 | export const isExtrinsicFailedEvent = ({ event }: EventRecord): boolean => 21 | event.section === 'system' && event.method === 'ExtrinsicFailed'; 22 | 23 | export const asExtrinsicFailedEvent = ( 24 | event: EventRecord, 25 | ): ExtrinsicFailedEvent | undefined => { 26 | if (!isExtrinsicFailedEvent(event)) return; 27 | 28 | return { ...event, name: formatEventName(event) } as ExtrinsicFailedEvent; 29 | }; 30 | 31 | export const formatExtrinsicFailed = ( 32 | event: EventRecord, 33 | api: ApiBase<'promise'> | undefined, 34 | ): string | undefined => { 35 | if (!api) return; 36 | 37 | const ev = asExtrinsicFailedEvent(event); 38 | if (!ev) return; 39 | 40 | return decodeError(ev.event?.data?.dispatchError, { contract: { api } }); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/system/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ExtrinsicFailed'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/events/types.ts: -------------------------------------------------------------------------------- 1 | export interface IEvent { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/NOOP.ts: -------------------------------------------------------------------------------- 1 | export const NOOP: () => void = () => undefined; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/encodeSalt.ts: -------------------------------------------------------------------------------- 1 | import { pseudoRandomU8a } from './pseudoRandomU8a'; 2 | 3 | export const encodeSalt = ( 4 | salt: Uint8Array | string = pseudoRandomU8a(32), 5 | ): Uint8Array => { 6 | const encoder = new TextEncoder(); 7 | 8 | return typeof salt === 'string' ? encoder.encode(salt) : salt; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/formatFileName.ts: -------------------------------------------------------------------------------- 1 | interface FileState { 2 | name: string; 3 | size?: number; 4 | } 5 | 6 | export const formatFileName = ({ name, size }: FileState): string => 7 | size !== undefined ? `${name} (${(size / 1000).toFixed(2)}kb)` : name; 8 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/getExpiredItem.ts: -------------------------------------------------------------------------------- 1 | const FIVE_SECONDS = 5_000; 2 | 3 | export type CreatedItem = { createdAt: number }; 4 | 5 | export function getExpiredItem( 6 | items: CreatedItem[], 7 | expirationPeriod?: number, 8 | ): T[] { 9 | if (expirationPeriod === 0) return []; 10 | 11 | const timeFromCreation = (creationTime: number) => Date.now() - creationTime; 12 | 13 | return items.filter( 14 | (item) => 15 | timeFromCreation(item.createdAt) >= (expirationPeriod || FIVE_SECONDS), 16 | ) as T[]; 17 | } 18 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NOOP.ts'; 2 | export * from './encodeSalt.ts'; 3 | export * from './formatFileName.ts'; 4 | export * from './getExpiredItem.ts'; 5 | export * from './isTxCancelledError.ts'; 6 | export * from './isValidHash.ts'; 7 | export * from './parseUnits'; 8 | export * from './pseudoRandomHex.ts'; 9 | export * from './pseudoRandomId.ts'; 10 | export * from './pseudoRandomU8a.ts'; 11 | export * from './unixMilliToDate.ts'; 12 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/isTxCancelledError.ts: -------------------------------------------------------------------------------- 1 | /// This is triggered when a user rejects a transaction in a wallet 2 | export const isTxCancelledError = (e?: unknown): boolean => 3 | e?.toString() === 'Error: Cancelled' || e?.toString() === 'Error: Canceled'; 4 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/isValidHash.ts: -------------------------------------------------------------------------------- 1 | export const isValidHash = ( 2 | value: string | undefined, 3 | length = 64, 4 | ): boolean => { 5 | if (!value) return false; 6 | 7 | return new RegExp(`^0x[0-9a-f]{${length}}$`).test(value); 8 | }; 9 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/parseUnits/index.ts: -------------------------------------------------------------------------------- 1 | export * from './parseUnits'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/parseUnits/parseUnits.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '../..'; 2 | import { IRegistryInfo, chainTokenSymbol } from '../../../core'; 3 | 4 | // convert string with commas to a BN e.g. 1,000,000 to new BN('1_000_000') 5 | export const stringNumberToBN = (valWithCommas: string): BN => { 6 | const v = valWithCommas.split(',').join(''); 7 | return new BN(v); 8 | }; 9 | 10 | interface DecimalOptions { 11 | api?: IRegistryInfo; 12 | decimals?: number; 13 | } 14 | 15 | // convert decimal to planck unit e.g. 1.0000 to 1000000000000 16 | export const planckToDecimal = ( 17 | amount: undefined | string | number | number[] | BN | Uint8Array | Buffer, 18 | options: DecimalOptions, 19 | ): number | undefined => { 20 | const decimals = 21 | options.decimals !== undefined 22 | ? options.decimals 23 | : options.api?.registry.chainDecimals[0]; 24 | 25 | if (!decimals || !amount) return; 26 | if (decimals === undefined || amount === undefined) return; 27 | 28 | const base = new BN(10).pow(new BN(decimals)); 29 | const { div, mod } = new BN(amount).divmod(base); 30 | 31 | return parseFloat( 32 | `${div.toString()}.${mod.toString().padStart(decimals, '0')}`, 33 | ); 34 | }; 35 | 36 | interface PlanckToDecimalOptions { 37 | significantFigures?: number; 38 | symbol?: string; 39 | } 40 | 41 | // convert planck unit to decimal with token name (ROC,DOT,KSM) e.g. 100000000000 to 1.0000 ROC 42 | export const planckToDecimalFormatted = ( 43 | amount: undefined | string | number | number[] | BN | Uint8Array | Buffer, 44 | options: PlanckToDecimalOptions & DecimalOptions, 45 | ): string | undefined => { 46 | const decimalAmount = planckToDecimal(amount, options); 47 | if (decimalAmount === undefined) return; 48 | 49 | const formattedVal = 50 | options?.significantFigures === undefined 51 | ? decimalAmount.toString() 52 | : decimalAmount.toFixed(options?.significantFigures).toString(); 53 | 54 | const symbol = options?.symbol 55 | ? options.symbol 56 | : options.api 57 | ? chainTokenSymbol(options.api) 58 | : ''; 59 | 60 | return `${formattedVal} ${symbol}`; 61 | }; 62 | 63 | // convert decimal to planck unit e.g. 1.0000 to 1000000000000 64 | export const decimalToPlanck = ( 65 | amount: number, 66 | options: DecimalOptions | undefined, 67 | ): bigint | undefined => { 68 | const decimals = options?.decimals || options?.api?.registry.chainDecimals[0]; 69 | if (!decimals) return; 70 | 71 | const convertedValue = BigInt(amount * 10 ** decimals); 72 | 73 | return convertedValue; 74 | }; 75 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/pseudoRandomHex.ts: -------------------------------------------------------------------------------- 1 | export const pseudoRandomHex = (size = 64): string => 2 | `0x${[...Array(size)] 3 | .map(() => Math.floor(Math.random() * 16).toString(16)) 4 | .join('')}`; 5 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/pseudoRandomId.ts: -------------------------------------------------------------------------------- 1 | import { nanoid } from 'nanoid'; 2 | 3 | export const pseudoRandomId = (t = 21) => nanoid(t); 4 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/pseudoRandomU8a.ts: -------------------------------------------------------------------------------- 1 | export const pseudoRandomU8a = (t = 21) => 2 | crypto.getRandomValues(new Uint8Array(t)); 3 | -------------------------------------------------------------------------------- /packages/useink/src/utils/helpers/unixMilliToDate.ts: -------------------------------------------------------------------------------- 1 | export const unixMilliToDate = ( 2 | unixInMilliSeconds: number | undefined, 3 | ): Date | undefined => 4 | unixInMilliSeconds ? new Date(unixInMilliSeconds) : undefined; 5 | -------------------------------------------------------------------------------- /packages/useink/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@polkadot/util'; 2 | export { signatureVerify } from '@polkadot/util-crypto'; 3 | 4 | export * from './contracts'; 5 | export * from './events'; 6 | export * from './helpers'; 7 | export * from './pick'; 8 | export * from './substrate'; 9 | export * from './types'; 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pickCallInfo.ts'; 2 | export * from './pickDecoded.ts'; 3 | export * from './pickDecodedError.ts'; 4 | export * from './pickError.ts'; 5 | export * from './pickResultErr.ts'; 6 | export * from './pickResultOk.ts'; 7 | export * from './pickTxInfo.ts'; 8 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickCallInfo.ts: -------------------------------------------------------------------------------- 1 | import { CallInfo, DecodedContractResult } from '../../core/index'; 2 | 3 | /// pickCallInfo gets gasRequired, gasConsumed, and storageDeposit or undefined from a 4 | /// contract message response 5 | export function pickCallInfo( 6 | decoded: DecodedContractResult | undefined, 7 | ): CallInfo | undefined { 8 | if (!decoded?.ok) return; 9 | 10 | const { gasRequired, gasConsumed, storageDeposit } = decoded.value; 11 | 12 | return { gasRequired, gasConsumed, storageDeposit }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickDecoded.ts: -------------------------------------------------------------------------------- 1 | import { DecodedContractResult } from '../../core/index'; 2 | 3 | /// pickError is a helper function to quickly get the decoded value or undefined from a 4 | /// contract message response 5 | export function pickDecoded( 6 | decoded: DecodedContractResult | undefined, 7 | ): T | undefined { 8 | return decoded?.ok ? decoded.value.decoded : undefined; 9 | } 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickDecodedError.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallResult, 3 | Contract, 4 | RegistryErrorMethod, 5 | decodeError, 6 | } from '../../core/index'; 7 | 8 | /// pickDecodedError is a helper function to quickly get a decoded error from a call. 9 | /// 10 | /// // e.g. 11 | /// const call = useCall(contract, 'foo'); 12 | /// pickDecodedError(call, contract); 13 | export function pickDecodedError( 14 | call: CallResult | undefined, 15 | contract: Contract, 16 | moduleMessages?: Record, 17 | defaultMessage?: string, 18 | ): string | undefined { 19 | const { result } = call || {}; 20 | if (!result || result?.ok) return; 21 | return decodeError(result.error, contract, moduleMessages, defaultMessage); 22 | } 23 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickError.ts: -------------------------------------------------------------------------------- 1 | import { DecodedContractResult, DispatchError } from '../../core/index'; 2 | 3 | /// pickError is a helper function to quickly get a dispatch error or undefined from a 4 | /// contract message response 5 | export function pickError( 6 | decoded: DecodedContractResult | undefined, 7 | ): DispatchError | undefined { 8 | return decoded?.ok ? undefined : decoded?.error; 9 | } 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickResultErr.ts: -------------------------------------------------------------------------------- 1 | import { DecodedContractResult } from '../../core/index'; 2 | 3 | /// pickResultErr is a helper function to quickly get an Err result from a contract message 4 | /// or undefined 5 | export function pickResultErr( 6 | decoded: DecodedContractResult | undefined, 7 | ): E | undefined { 8 | return decoded?.ok ? decoded.value.decoded?.Err : undefined; 9 | } 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickResultOk.ts: -------------------------------------------------------------------------------- 1 | import { DecodedContractResult } from '../../core/index'; 2 | 3 | /// pickResultOk is a helper function to quickly get an Ok result from a contract message 4 | /// or undefined 5 | export function pickResultOk( 6 | decoded: DecodedContractResult | undefined, 7 | ): K | undefined { 8 | return decoded?.ok ? decoded.value.decoded?.Ok : undefined; 9 | } 10 | -------------------------------------------------------------------------------- /packages/useink/src/utils/pick/pickTxInfo.ts: -------------------------------------------------------------------------------- 1 | import { DecodedTxResult, TxInfo } from '../../core/index'; 2 | 3 | /// pickTxInfo gets gasRequired, gasConsumed, and storageDeposit or undefined from a 4 | /// DryRun. 5 | export function pickTxInfo( 6 | result: DecodedTxResult | undefined, 7 | ): TxInfo | undefined { 8 | if (!result?.ok) return; 9 | 10 | const { gasRequired, gasConsumed, storageDeposit, partialFee } = result.value; 11 | 12 | return { gasRequired, gasConsumed, storageDeposit, partialFee }; 13 | } 14 | -------------------------------------------------------------------------------- /packages/useink/src/utils/substrate/bnToBalance.ts: -------------------------------------------------------------------------------- 1 | import { BN } from '..'; 2 | import { ApiBase, Balance } from '../../core'; 3 | 4 | export const bnToBalance = ( 5 | api: Pick, 'createType'> | undefined, 6 | bn: BN | undefined, 7 | ): Balance | undefined => api?.createType('Balance', bn); 8 | -------------------------------------------------------------------------------- /packages/useink/src/utils/substrate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bnToBalance'; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/types/common.ts: -------------------------------------------------------------------------------- 1 | export type RustResult = { Ok?: T; Err?: E }; 2 | -------------------------------------------------------------------------------- /packages/useink/src/utils/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common.ts'; 2 | -------------------------------------------------------------------------------- /packages/useink/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 4 | "module": "commonjs" /* Specify what module code is generated. */, 5 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 6 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 7 | "strict": true /* Enable all strict type-checking options. */, 8 | "skipLibCheck": true /* Skip type checking all .d.ts files. */, 9 | "noUncheckedIndexedAccess": true, 10 | "noEmit": true, 11 | "allowImportingTsExtensions": true, 12 | "jsx": "react" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/useink/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export default defineConfig({ 4 | dts: true, 5 | format: ['cjs', 'esm'], 6 | entry: { 7 | index: 'src/index.ts', 8 | core: 'src/core/index.ts', 9 | chains: 'src/chains/index.ts', 10 | notifications: 'src/notifications/index.ts', 11 | utils: 'src/utils/index.ts', 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /playground/contract/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "playground" 3 | version = "0.1.0" 4 | authors = ["[your_name] <[your_email]>"] 5 | edition = "2021" 6 | 7 | [dependencies] 8 | ink = { version = "4.0.0", default-features = false } 9 | 10 | scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } 11 | scale-info = { version = "2.3", default-features = false, features = ["derive"], optional = true } 12 | 13 | [dev-dependencies] 14 | ink_e2e = "4.0.0" 15 | 16 | [lib] 17 | path = "lib.rs" 18 | 19 | [features] 20 | default = ["std"] 21 | std = [ 22 | "ink/std", 23 | "scale/std", 24 | "scale-info/std", 25 | ] 26 | ink-as-dependency = [] 27 | e2e-tests = [] 28 | -------------------------------------------------------------------------------- /playground/contract/lib.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr(not(feature = "std"), no_std)] 2 | 3 | #[ink::contract] 4 | mod playground { 5 | use ink::prelude::string::String; 6 | 7 | /// Defines the storage of your contract. 8 | /// Add new fields to the below struct in order 9 | /// to add new static storage fields to your contract. 10 | #[ink(storage)] 11 | pub struct Playground { 12 | /// Stores a single `bool` value on the storage. 13 | value: bool, 14 | } 15 | 16 | #[ink(event)] 17 | pub struct Flipped { 18 | /// The user account ID. 19 | flipper: AccountId, 20 | /// The new value. 21 | value: bool, 22 | } 23 | 24 | #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] 25 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 26 | pub struct Unhappy { 27 | mood: ink::prelude::string::String, 28 | } 29 | 30 | #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] 31 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 32 | pub struct Happy { 33 | mood: String, 34 | } 35 | 36 | // The Playground error types. 37 | #[derive(Debug, PartialEq, Eq, scale::Encode, scale::Decode)] 38 | #[cfg_attr(feature = "std", derive(scale_info::TypeInfo))] 39 | pub enum Error { 40 | BadMood(Unhappy), 41 | } 42 | 43 | impl Playground { 44 | /// Constructor that initializes the `bool` value to the given `init_value`. 45 | #[ink(constructor)] 46 | pub fn new(init_value: bool) -> Self { 47 | Self { value: init_value } 48 | } 49 | 50 | /// Constructor that initializes the `bool` value to `false`. 51 | /// 52 | /// Constructors can delegate to other constructors. 53 | #[ink(constructor)] 54 | pub fn default() -> Self { 55 | Self::new(Default::default()) 56 | } 57 | 58 | /// A message that can be called on instantiated contracts. 59 | /// This one flips the value of the stored `bool` from `true` 60 | /// to `false` and vice versa. 61 | #[ink(message)] 62 | pub fn flip(&mut self) -> bool { 63 | self.value = !self.value; 64 | 65 | Self::env().emit_event(Flipped { 66 | flipper: Self::env().caller(), 67 | value: self.value, 68 | }); 69 | 70 | self.value 71 | } 72 | 73 | /// Simply returns the current value of our `bool`. 74 | #[ink(message)] 75 | pub fn get(&self) -> bool { 76 | self.value 77 | } 78 | 79 | /// panic! in Rust will throw a 'Module' Dispatch error in PolkadotJS 80 | #[ink(message)] 81 | pub fn panic(&self) -> bool { 82 | panic!("Panic!!!"); 83 | } 84 | 85 | #[ink(message)] 86 | pub fn assert_boom(&self) -> bool { 87 | assert!(false, "Assertion go boom!"); 88 | true 89 | } 90 | 91 | #[ink(message)] 92 | pub fn result(&self) -> bool { 93 | assert!(false, "Assertion go boom!"); 94 | true 95 | } 96 | 97 | #[ink(message)] 98 | pub fn mood(&self, value: u64) -> Result { 99 | if value % 2 == 0 { 100 | return Ok(Happy { 101 | mood: String::from("Even numbers make me happy :)"), 102 | }); 103 | } 104 | 105 | Err(Error::BadMood(Unhappy { 106 | mood: String::from("Odd values make me sad :("), 107 | })) 108 | } 109 | 110 | #[ink(message)] 111 | pub fn option(&self, value: u64) -> Option { 112 | if value % 2 == 0 { 113 | return Some(Happy { 114 | mood: String::from("Even numbers make me happy :)"), 115 | }); 116 | } 117 | None 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /playground/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | }; 5 | 6 | module.exports = { 7 | ...nextConfig, 8 | }; 9 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "version": "0.1.0", 4 | "license": "Apache-2.0", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.7.14", 14 | "@heroicons/react": "^2.0.18", 15 | "@types/node": "18.15.6", 16 | "@types/react": "18.0.28", 17 | "@types/react-dom": "18.0.11", 18 | "autoprefixer": "^10.4.14", 19 | "classnames": "^2.3.2", 20 | "next": "13.4.6", 21 | "postcss": "^8.4.21", 22 | "react": "18.2.0", 23 | "react-dom": "18.2.0", 24 | "react-dropzone": "^14.2.3", 25 | "react-select": "^5.7.3", 26 | "typescript": "5.0.2", 27 | "useink": "workspace:useink@*", 28 | "ws": "^8.13.0" 29 | }, 30 | "devDependencies": { 31 | "tailwindcss": "^3.2.7" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /playground/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /playground/public/squink.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/src/components/ConnectWallet/ConnectWallet.tsx: -------------------------------------------------------------------------------- 1 | import { useInstalledWallets, useWallet } from 'useink'; 2 | 3 | export const ConnectWallet = () => { 4 | const { account, connect } = useWallet(); 5 | const installedWallets = useInstalledWallets(); 6 | 7 | if (account) return null; 8 | 9 | if (installedWallets.length === 0) { 10 | return ( 11 |

12 | You don't have any wallets installed... 13 |

14 | ); 15 | } 16 | 17 | return ( 18 | <> 19 |

Connect a Wallet

20 |

Installed Wallets

21 |
    22 | {installedWallets.map((w) => ( 23 |
  • 24 | 32 |
  • 33 | ))} 34 |
35 | 36 | ); 37 | }; 38 | -------------------------------------------------------------------------------- /playground/src/components/ConnectWallet/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ConnectWallet'; 2 | -------------------------------------------------------------------------------- /playground/src/components/FileDropper/FileDropper.tsx: -------------------------------------------------------------------------------- 1 | import classNames from 'classnames'; 2 | import { createRef } from 'react'; 3 | import Dropzone, { DropzoneOptions, DropzoneRef } from 'react-dropzone'; 4 | 5 | export interface Props { 6 | onDrop: Pick['onDrop']; 7 | cta: string; 8 | } 9 | 10 | export const FileDropper: React.FC = ({ onDrop, cta }) => { 11 | const ref = createRef(); 12 | 13 | return ( 14 | 15 | {({ getInputProps, getRootProps }) => { 16 | return ( 17 |
18 | 32 | 33 |
34 | ); 35 | }} 36 |
37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /playground/src/components/FileDropper/index.ts: -------------------------------------------------------------------------------- 1 | export * from './FileDropper'; 2 | -------------------------------------------------------------------------------- /playground/src/components/Notifications/Notifications.tsx: -------------------------------------------------------------------------------- 1 | import { toNotificationLevel, useNotifications } from 'useink/notifications'; 2 | import { Snackbar } from '../Snackbar'; 3 | 4 | export const Notifications: React.FC = () => { 5 | const { notifications, removeNotification } = useNotifications(); 6 | 7 | if (!notifications.length) return null; 8 | 9 | return ( 10 |
    11 | {notifications.map((n) => ( 12 |
  • 13 | removeNotification(n.id)} 17 | show 18 | /> 19 |
  • 20 | ))} 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /playground/src/components/Notifications/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Notifications'; 2 | -------------------------------------------------------------------------------- /playground/src/components/SelectList/SelectList.tsx: -------------------------------------------------------------------------------- 1 | import Select from 'react-select'; 2 | 3 | export interface SelectOption { 4 | value: string; 5 | label: string; 6 | } 7 | 8 | interface Props { 9 | className?: string; 10 | onChange: (value: SelectOption | null) => void; 11 | options: SelectOption[]; 12 | value: SelectOption | null; 13 | placeholder?: string; 14 | } 15 | 16 | export const SelectList: React.FC = ({ 17 | className, 18 | onChange, 19 | options, 20 | value, 21 | placeholder, 22 | }) => { 23 | return ( 24 |
25 |