├── README.md ├── packages ├── core │ ├── src │ │ ├── helpers │ │ │ ├── noop.ts │ │ │ ├── index.ts │ │ │ ├── use-is-mounted-ref.ts │ │ │ ├── get-encoded-address.ts │ │ │ ├── string-trim-trailing-zeros.ts │ │ │ ├── use-effect-once.ts │ │ │ ├── fetch-system-properties.ts │ │ │ ├── get-asset-balance.ts │ │ │ ├── format-price.ts │ │ │ └── get-account-balance.ts │ │ ├── types │ │ │ ├── index.ts │ │ │ ├── system-properties.ts │ │ │ └── reducer.ts │ │ ├── providers │ │ │ ├── extension │ │ │ │ ├── index.ts │ │ │ │ ├── context.ts │ │ │ │ ├── reducer.ts │ │ │ │ └── provider.tsx │ │ │ ├── index.ts │ │ │ └── substrahooks-provider │ │ │ │ ├── index.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── context.ts │ │ │ │ └── provider.tsx │ │ ├── index.ts │ │ ├── hooks │ │ │ ├── index.ts │ │ │ ├── use-errors.ts │ │ │ ├── use-api-provider.ts │ │ │ ├── use-system-properties.ts │ │ │ ├── use-encoded-address.ts │ │ │ ├── use-asset-balance.ts │ │ │ ├── use-account-balance.ts │ │ │ └── use-polkadot-extension.ts │ │ └── state │ │ │ ├── errors.ts │ │ │ └── balances.ts │ ├── prettier.config.js │ ├── .eslintrc.json │ ├── test │ │ ├── setup.js │ │ └── helpers │ │ │ ├── fetch-system-properties.test.ts │ │ │ └── format-price.test.ts │ ├── jest.config.js │ ├── tsconfig.json │ ├── babel.config.js │ ├── package.json │ └── README.md ├── example-nextjs │ ├── index.d.ts │ ├── .eslintrc.json │ ├── next.config.js │ ├── public │ │ ├── favicon.ico │ │ └── vercel.svg │ ├── next-env.d.ts │ ├── styles │ │ ├── globals.css │ │ └── Home.module.css │ ├── pages │ │ ├── api │ │ │ └── hello.ts │ │ ├── _app.tsx │ │ ├── page-two.tsx │ │ └── index.tsx │ ├── .gitignore │ ├── tsconfig.json │ ├── components │ │ └── app │ │ │ └── substra-hooks-provider.tsx │ ├── package.json │ └── README.md └── dotsama-wallet-react │ ├── prettier.config.js │ ├── src │ ├── index.ts │ ├── lib │ │ ├── index.ts │ │ ├── utils │ │ │ ├── dashify.ts │ │ │ ├── web3-from-source.ts │ │ │ ├── shorten-account-id.ts │ │ │ ├── use-screen-size.ts │ │ │ └── ua-detect.ts │ │ ├── store │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── use-wallet-store.ts │ │ │ └── use-account-store.ts │ │ ├── errors │ │ │ ├── auth-error.tsx │ │ │ └── base-wallet-error.tsx │ │ ├── wallets │ │ │ ├── mobile │ │ │ │ └── nova-wallet │ │ │ │ │ ├── nova-wallet.ts │ │ │ │ │ └── math-wallet.ts │ │ │ └── desktop │ │ │ │ ├── subwallet │ │ │ │ └── subwallet.ts │ │ │ │ ├── polkadotjs-wallet │ │ │ │ └── polkadot-wallet.ts │ │ │ │ └── talisman-wallet │ │ │ │ └── talisman-wallet.ts │ │ ├── wallets.ts │ │ ├── types.ts │ │ ├── base-dotsama-wallet │ │ │ └── base-dotsama-wallet.ts │ │ └── logos-svg.ts │ ├── components │ │ ├── account-select │ │ │ ├── index.ts │ │ │ ├── not-allowed.tsx │ │ │ ├── connect-account-modal.tsx │ │ │ ├── account-store-sync.tsx │ │ │ ├── account-selection.tsx │ │ │ ├── account-radio.tsx │ │ │ └── account-select.tsx │ │ ├── wallet │ │ │ ├── wallet-switch-mobile.tsx │ │ │ ├── wallet-switch-desktop.tsx │ │ │ ├── wallet-button.tsx │ │ │ └── wallet-select.tsx │ │ └── common │ │ │ ├── search-box.tsx │ │ │ └── identity-avatar.tsx │ ├── hooks │ │ ├── use-is-dark-mode.ts │ │ ├── use-neumorphic-shadow.ts │ │ ├── use-debounce-value.ts │ │ └── use-search-in-objects.ts │ └── public │ │ └── logos │ │ ├── polkadot-js-logo.svg │ │ ├── talisman-logo.svg │ │ └── sub-wallet-logo.svg │ ├── additionals.d.ts │ ├── tsconfig.json │ ├── README.md │ └── package.json ├── .gitignore ├── tsconfig.json ├── .prettierrc.json ├── .changeset ├── config.json └── README.md ├── package.json ├── .eslintrc.json ├── LICENSE └── .github └── workflows └── release.yml /README.md: -------------------------------------------------------------------------------- 1 | ./packages/core/README.md -------------------------------------------------------------------------------- /packages/core/src/helpers/noop.ts: -------------------------------------------------------------------------------- 1 | export const noop = () => null; 2 | -------------------------------------------------------------------------------- /packages/core/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './system-properties'; 2 | -------------------------------------------------------------------------------- /packages/example-nextjs/index.d.ts: -------------------------------------------------------------------------------- 1 | import "@polkadot/api-augment"; 2 | -------------------------------------------------------------------------------- /packages/core/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../.prettierrc.json') 2 | -------------------------------------------------------------------------------- /packages/example-nextjs/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "../../.eslintrc.json" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('../../.prettierrc.json') 2 | -------------------------------------------------------------------------------- /packages/core/test/setup.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | require('regenerator-runtime/runtime'); 3 | -------------------------------------------------------------------------------- /packages/core/src/providers/extension/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './provider'; 3 | -------------------------------------------------------------------------------- /packages/core/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './substrahooks-provider'; 2 | export * from './extension'; 3 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib'; 2 | export * from './components/account-select' 3 | -------------------------------------------------------------------------------- /packages/core/src/providers/substrahooks-provider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './context'; 2 | export * from './provider'; 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .DS_Store 4 | .idea 5 | coverage 6 | .npmrc 7 | .vscode 8 | /.yarn/ 9 | .yarnrc 10 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from './store'; 2 | export * from './wallets'; 3 | export * from './types'; 4 | -------------------------------------------------------------------------------- /packages/example-nextjs/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /packages/example-nextjs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rmrk-team/substra-hooks/HEAD/packages/example-nextjs/public/favicon.ico -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/utils/dashify.ts: -------------------------------------------------------------------------------- 1 | export const dashify = (str: string): string => str.replace(/\s+/g, '-').toLowerCase(); 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "jsx": "react-jsx", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/index.ts: -------------------------------------------------------------------------------- 1 | export * from './account-store-sync'; 2 | export * from './connect-account-modal'; 3 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-account-store'; 2 | export * from './types'; 3 | export * from './use-wallet-store'; 4 | -------------------------------------------------------------------------------- /packages/core/src/types/system-properties.ts: -------------------------------------------------------------------------------- 1 | export interface ISystemProperties { 2 | tokenDecimals: number; 3 | tokenSymbol: string; 4 | ss58Format: number; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import "@polkadot/api-augment"; 2 | export * from './providers'; 3 | export * from './hooks'; 4 | export * from './types'; 5 | export * from './helpers'; 6 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/store/types.ts: -------------------------------------------------------------------------------- 1 | export enum ACCOUNT_MODAL_STEPS { 2 | wallets = 'wallets', 3 | accounts = 'accounts', 4 | notAllowed = 'notAllowed', 5 | } 6 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/errors/auth-error.tsx: -------------------------------------------------------------------------------- 1 | import { BaseWalletError } from './base-wallet-error'; 2 | 3 | export class AuthError extends BaseWalletError { 4 | readonly name = 'AuthError'; 5 | } 6 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/hooks/use-is-dark-mode.ts: -------------------------------------------------------------------------------- 1 | import { useColorMode } from '@chakra-ui/react'; 2 | 3 | export const useIsDarkMode = () => { 4 | const isDark = useColorMode().colorMode === 'dark'; 5 | 6 | return isDark; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/example-nextjs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(t|j)sx?$': 'babel-jest', 4 | }, 5 | transformIgnorePatterns: ['/node_modules/(?!(@polkadot|@babel/runtime/helpers/esm/))'], 6 | setupFilesAfterEnv: ['./test/setup.js'] 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/src/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './get-account-balance'; 2 | export * from './get-asset-balance'; 3 | export * from './fetch-system-properties'; 4 | export * from './format-price'; 5 | export * from './get-encoded-address'; 6 | export * from './noop'; 7 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/additionals.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | declare module '*.svg' { 3 | const content: any; 4 | export const ReactComponent: any; 5 | export default content; 6 | } 7 | 8 | declare let window: any; 9 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true, 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": true, 6 | "parser": "typescript", 7 | "printWidth": 100, 8 | "tabWidth": 2, 9 | "useTabs": false, 10 | "semi": true 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/src/types/reducer.ts: -------------------------------------------------------------------------------- 1 | export type ActionMap = { 2 | [Key in keyof M]: M[Key] extends undefined 3 | ? { 4 | type: Key; 5 | } 6 | : { 7 | type: Key; 8 | payload: M[Key]; 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './use-account-balance'; 2 | export * from './use-system-properties'; 3 | export * from './use-polkadot-extension'; 4 | export * from './use-asset-balance'; 5 | export * from './use-encoded-address'; 6 | export * from './use-api-provider'; 7 | export * from './use-errors'; 8 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.2.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "linked": [], 6 | "access": "public", 7 | "baseBranch": "master", 8 | "updateInternalDependencies": "patch", 9 | "ignore": [] 10 | } 11 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/hooks/use-neumorphic-shadow.ts: -------------------------------------------------------------------------------- 1 | import { useIsDarkMode } from './use-is-dark-mode'; 2 | 3 | export const useNeumorphicShadow = () => { 4 | const isDark = useIsDarkMode(); 5 | 6 | return isDark 7 | ? '5px 5px 10px #141922, -5px -5px 10px #202736' 8 | : '5px 5px 10px #a8b1ba, -5px -5px 10px #eef9ff'; 9 | }; 10 | -------------------------------------------------------------------------------- /packages/example-nextjs/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | -------------------------------------------------------------------------------- /packages/example-nextjs/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/src/helpers/use-is-mounted-ref.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef } from 'react'; 2 | 3 | export const useIsMountedRef = (): { readonly current: boolean } => { 4 | const isMountedRef = useRef(false); 5 | 6 | useEffect(() => { 7 | isMountedRef.current = true; 8 | return () => { 9 | isMountedRef.current = false; 10 | }; 11 | }, []); 12 | 13 | return isMountedRef; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/utils/web3-from-source.ts: -------------------------------------------------------------------------------- 1 | import { useWalletsStore } from '../store/use-wallet-store'; 2 | import { getWalletBySource } from '../wallets'; 3 | import { WALLET_EXTENSIONS } from '../types'; 4 | 5 | export const web3FromSource = () => { 6 | const { selectedWallet } = useWalletsStore.getState(); 7 | const wallet = getWalletBySource(selectedWallet as WALLET_EXTENSIONS); 8 | 9 | return wallet?.extension; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "module": "commonjs", 6 | "composite": true, 7 | "declaration": true, 8 | "sourceMap": true, 9 | "declarationMap": true, 10 | "resolveJsonModule": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "node" 13 | }, 14 | "include": [ 15 | "src", 16 | "src/**/*.json", 17 | "test" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-errors.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { SubstraHooksContext, useSubstraHooksState } from '../providers'; 3 | 4 | export const useBlockSyncError = (apiProviderId?: string): Error | null => { 5 | const defaultId = useContext(SubstraHooksContext).defaultApiProviderId; 6 | const { errorsState } = useSubstraHooksState(); 7 | 8 | const networkId = apiProviderId || defaultId; 9 | 10 | return errorsState.blockSyncErrors[networkId] || null; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/helpers/get-encoded-address.ts: -------------------------------------------------------------------------------- 1 | import { encodeAddress } from '@polkadot/keyring'; 2 | import { ISystemProperties } from '../types/system-properties'; 3 | 4 | export const getEncodedAddress = ( 5 | address: string, 6 | systemProperties: ISystemProperties, 7 | ss58Format?: number, 8 | ) => { 9 | try { 10 | return encodeAddress(address, ss58Format || systemProperties.ss58Format); 11 | } catch (error: any) { 12 | console.log(error); 13 | return ''; 14 | } 15 | 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-api-provider.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { 3 | SubstraHooksContext, 4 | useApiProvidersState, 5 | } from '../providers/substrahooks-provider/context'; 6 | import { ApiPromise } from '@polkadot/api'; 7 | 8 | export const useApiProvider = (apiProviderId?: string): ApiPromise | null => { 9 | const defaultId = useContext(SubstraHooksContext).defaultApiProviderId; 10 | return useApiProvidersState().apiProviders[apiProviderId || defaultId]?.apiProvider; 11 | }; 12 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/utils/shorten-account-id.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Shorten long addresses byt adding ellipsis in between 3 | * @param id 4 | * @param inMiddle 5 | * @param sliceSize 6 | */ 7 | export const shortenAccountId = (id: string, inMiddle?: boolean, sliceSize: number = 4) => { 8 | if (id) { 9 | const beginning = id.slice(0, inMiddle ? sliceSize : 12); 10 | const end = id.slice(-sliceSize); 11 | 12 | return inMiddle ? `${beginning}...${end}` : `${beginning}${id.length > 12 ? '...' : ''}`; 13 | } 14 | 15 | return ''; 16 | }; 17 | -------------------------------------------------------------------------------- /packages/core/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | if (api) api.cache(true); 3 | return { 4 | presets: [ 5 | "@babel/typescript", 6 | [ 7 | "@babel/preset-env", 8 | { 9 | targets: { browsers: "defaults, not ie 11", node: 'current' }, 10 | modules: false, 11 | useBuiltIns: false, 12 | loose: true, 13 | }, 14 | ], 15 | ], 16 | plugins: [ 17 | "@babel/plugin-proposal-optional-chaining", 18 | "@babel/plugin-transform-modules-commonjs", 19 | ], 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "substra-hooks", 3 | "private": true, 4 | "engines": { 5 | "node": ">=18" 6 | }, 7 | "workspaces": { 8 | "packages": [ 9 | "packages/*" 10 | ] 11 | }, 12 | "scripts": { 13 | "build": "wsrun -e -c -s -x example-nextjs build", 14 | "test": "wsrun -e -c -s --exclude-missing test", 15 | "release": "yarn build && yarn changeset publish" 16 | }, 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "dependencies": { 21 | "@changesets/cli": "^2.26.1", 22 | "prettier": "2.4.1", 23 | "wsrun": "^5.2.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/example-nextjs/.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 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-system-properties.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | import { ISystemProperties } from '../types/system-properties'; 3 | import { 4 | SubstraHooksContext, 5 | useApiProvidersState, 6 | } from '../providers/substrahooks-provider/context'; 7 | import { systemPropertiesDefaults } from '../helpers'; 8 | 9 | export const useSystemProperties = (apiProviderId?: string): ISystemProperties => { 10 | const id = apiProviderId || useContext(SubstraHooksContext).defaultApiProviderId; 11 | return useApiProvidersState().apiProviders[id]?.systemProperties || systemPropertiesDefaults; 12 | }; 13 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets/mobile/nova-wallet/nova-wallet.ts: -------------------------------------------------------------------------------- 1 | import { MOBILE_WALLET_APPS, WalletMobile } from '../../../types'; 2 | 3 | export const novaWallet: WalletMobile = { 4 | appName: MOBILE_WALLET_APPS.nova, 5 | title: 'Nova', 6 | installUrl: { 7 | android: 'https://play.google.com/store/apps/details?id=io.novafoundation.nova.market', 8 | ios: 'https://apps.apple.com/app/nova-polkadot-kusama-wallet/id1597119355', 9 | }, 10 | logo: { 11 | src: 'https://crustwebsites.net/ipfs/QmWX5gB7qYikWs1M3NyEMTRmx2gFWDsxvRi6qv5VbLbB1D', 12 | alt: 'Nova Wallet Logo', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/example-nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets/mobile/nova-wallet/math-wallet.ts: -------------------------------------------------------------------------------- 1 | import { MOBILE_WALLET_APPS, WalletMobile } from '../../../types'; 2 | 3 | export const mathWallet: WalletMobile = { 4 | appName: MOBILE_WALLET_APPS.mathWallet, 5 | title: 'Math Wallet', 6 | installUrl: { 7 | android: 'https://play.google.com/store/apps/details?id=com.mathwallet.android', 8 | ios: 'https://apps.apple.com/us/app/math-wallet-blockchain-wallet/id1383637331', 9 | }, 10 | logo: { 11 | src: 'https://crustwebsites.net/ipfs/QmQJDumvuyzMfh9bntXZtgzQp6xeRYr4F2DraL8hvGuzr1', 12 | alt: 'Math Wallet Logo', 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/core/src/helpers/string-trim-trailing-zeros.ts: -------------------------------------------------------------------------------- 1 | export const stringTrimTrailingZeros = (price: string) => { 2 | // if not containing a dot, we do not need to do anything 3 | if (price.indexOf('.') === -1) { 4 | return price; 5 | } 6 | 7 | let cutFrom = price.length - 1; 8 | 9 | // as long as the last character is a 0, remove it 10 | do { 11 | if (price[cutFrom] === '0') { 12 | cutFrom--; 13 | } 14 | } while (price[cutFrom] === '0'); 15 | 16 | // final check to make sure we end correctly 17 | if (price[cutFrom] === '.') { 18 | cutFrom--; 19 | } 20 | 21 | return price.substr(0, cutFrom + 1); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "strict": true, 5 | "lib": ["es2017", "dom", "es2019", "es2020", "esnext"], 6 | "target": "es2020", 7 | "outDir": "dist", 8 | "module": "commonjs", 9 | "composite": true, 10 | "declaration": true, 11 | "sourceMap": true, 12 | "declarationMap": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "moduleResolution": "node" 16 | }, 17 | "include": [ 18 | "src/**/*.ts", 19 | "src/**/*.tsx", 20 | "additionals.d.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules" 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/utils/use-screen-size.ts: -------------------------------------------------------------------------------- 1 | import { useMediaQuery } from '@chakra-ui/react'; 2 | 3 | const defaultBreakpointValues = { 4 | sm: '40em', 5 | md: '52em', 6 | lg: '64em', 7 | xl: '80em', 8 | '2xl': '96em', 9 | }; 10 | 11 | export const useScreenSize = ( 12 | breakpointValues: Record = defaultBreakpointValues, 13 | ) => { 14 | const [isSm, isMd, isLg, isXl, isXxl] = useMediaQuery([ 15 | `(max-width: ${breakpointValues.sm})`, 16 | `(max-width: ${breakpointValues.md})`, 17 | `(max-width: ${breakpointValues.lg})`, 18 | `(max-width: ${breakpointValues.xl})`, 19 | `(min-width: ${breakpointValues.xl})`, 20 | ]); 21 | 22 | return { isSm, isMd, isLg, isXl, isXxl }; 23 | }; 24 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-encoded-address.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSystemProperties } from './use-system-properties'; 3 | import { getEncodedAddress } from '../helpers/get-encoded-address'; 4 | 5 | type Options = { 6 | skip?: boolean 7 | } 8 | 9 | export const useEncodedAddress = (address: string, ss58Format?: number, options?: Options) => { 10 | const { skip = false } = options || {}; 11 | const systemProperties = useSystemProperties(); 12 | 13 | const userAddressEncoded = useMemo(() => { 14 | if (!skip && address) { 15 | return getEncodedAddress(address, systemProperties, ss58Format); 16 | } 17 | return ''; 18 | }, [address, systemProperties.ss58Format, ss58Format, skip]); 19 | 20 | return userAddressEncoded; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/example-nextjs/components/app/substra-hooks-provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from 'react'; 2 | import { ApiProviderConfig, SubstraHooksProvider } from '@substra-hooks/core'; 3 | 4 | interface ISubstraHooksProviderProps { 5 | apiProviderConfig: ApiProviderConfig; 6 | children: ReactNode; 7 | defaultApiProviderId?: string; 8 | } 9 | 10 | const SubstraHooksProviderSSR = ({ 11 | apiProviderConfig, 12 | children, 13 | defaultApiProviderId, 14 | }: ISubstraHooksProviderProps) => { 15 | return ( 16 | 19 | {children} 20 | 21 | ); 22 | }; 23 | 24 | export default SubstraHooksProviderSSR; 25 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/utils/ua-detect.ts: -------------------------------------------------------------------------------- 1 | const isWindow = 2 | typeof window !== 'undefined' && 3 | window.document && 4 | // @ts-ignore 5 | window.document.createElement && 6 | window.navigator; 7 | export const isFirefox = isWindow 8 | ? window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1 9 | : false; 10 | 11 | export const isMobile = isWindow 12 | ? /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( 13 | window.navigator.userAgent, 14 | ) 15 | : false; 16 | export const isAndroid = isWindow ? /Android/i.test(window.navigator.userAgent) : false; 17 | export const isIOs = isWindow ? /iPhone|iPad|iPod/i.test(window.navigator.userAgent) : false; 18 | export const isNovaWallet = isWindow ? /NovaWallet/i.test(window.navigator.userAgent) : false; 19 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets/desktop/subwallet/subwallet.ts: -------------------------------------------------------------------------------- 1 | import { BaseDotsamaWallet } from '../../../base-dotsama-wallet/base-dotsama-wallet'; 2 | import { WALLET_EXTENSIONS } from '../../../types'; 3 | import { icons } from '../../../logos-svg'; 4 | 5 | export class SubWallet extends BaseDotsamaWallet { 6 | extensionName = WALLET_EXTENSIONS.subwallet; 7 | title = 'SubWallet'; 8 | installUrl = { 9 | chrome: 10 | 'https://chrome.google.com/webstore/detail/subwallet/onhogfjeacnfoofkfgppdlbmlmnplgbn?hl=en&authuser=0', 11 | firefox: 'https://addons.mozilla.org/af/firefox/addon/subwallet/', 12 | }; 13 | noExtensionMessage = 'You can use any Polkadot compatible wallet'; 14 | logo = { 15 | src: `data:image/svg+xml;base64,${btoa(icons.subWalletLogo)}`, 16 | alt: 'Subwallet Logo', 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/store/use-wallet-store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | import { WALLET_EXTENSIONS } from '../types'; 4 | 5 | export type TWalletsStore = { 6 | selectedWallet: WALLET_EXTENSIONS | null; 7 | setSelectedWallet: (wallet: WALLET_EXTENSIONS | null) => void; 8 | }; 9 | 10 | export const useWalletsStore = create()( 11 | persist( 12 | (set) => ({ 13 | selectedWallet: null, 14 | setSelectedWallet: (selectedWallet) => { 15 | set({ selectedWallet }); 16 | }, 17 | }), 18 | { 19 | name: 'select-wallet-storage', 20 | }, 21 | ), 22 | ); 23 | 24 | export const selectedWalletSelector = (state: TWalletsStore) => state.selectedWallet; 25 | export const setSelectedWalletSelector = (state: TWalletsStore) => state.setSelectedWallet; 26 | -------------------------------------------------------------------------------- /packages/example-nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-nextjs", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/react": "^2.6.1", 13 | "@emotion/react": "^11.10.5", 14 | "@emotion/styled": "^11.10.5", 15 | "framer-motion": "^7.6.2", 16 | "next": "13.4.2", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0", 19 | "@polkadot/api": "^11.2.1", 20 | "@polkadot/extension-dapp": "^0.47.5", 21 | "@polkadot/react-identicon": "^3.6.6" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "18.16.12", 25 | "@types/react": "18.0.14", 26 | "eslint": "8.40.0", 27 | "eslint-config-next": "13.4.2", 28 | "typescript": "5.4.5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/README.md: -------------------------------------------------------------------------------- 1 | # Dotsama wallet react 2 | 3 | Highly opinionated Dotsama react wallet. 4 | 5 | A lot of the code is taken from [Subwallet](https://github.com/Koniverse/SubWallet-Extension) and [Talisman](https://talisman.xyz/) 6 | 7 | Opinionated because: 8 | 9 | - uses [Chakra UI](https://chakra-ui.com/) for UI and styling. 10 | - uses [zustand](https://github.com/pmndrs/zustand) for state management 11 | - uses [Substra hooks](https://github/rmrk-team/substra-hooks/packages/core) because reasons (provides a nice set of utils to work with polkadot.js) 12 | 13 | ## Example 14 | 15 | [Example use](../example-nextjs/pages/index.tsx) 16 | 17 | ## TODO: 18 | - Provide a set of hooks to be used with any UI library and not just Chakra UI 19 | - Provide a way to pass a custom config with translations and wallet list/config 20 | - Remove @substra-hooks/core dependency 21 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets/desktop/polkadotjs-wallet/polkadot-wallet.ts: -------------------------------------------------------------------------------- 1 | import { BaseDotsamaWallet } from '../../../base-dotsama-wallet/base-dotsama-wallet'; 2 | import { WALLET_EXTENSIONS } from '../../../types'; 3 | import { icons } from '../../../logos-svg'; 4 | 5 | export class PolkadotjsWallet extends BaseDotsamaWallet { 6 | extensionName = WALLET_EXTENSIONS.polkadot; 7 | title = 'Polkadot{.js}'; 8 | noExtensionMessage = 'You can use any Polkadot compatible wallet'; 9 | installUrl = { 10 | chrome: 11 | 'https://chrome.google.com/webstore/detail/polkadot%7Bjs%7D-extension/mopnmbcafieddcagagdcbnhejhlodfdd/related', 12 | firefox: 'https://addons.mozilla.org/en-GB/firefox/addon/polkadot-js-extension/', 13 | }; 14 | logo = { 15 | src: `data:image/svg+xml;base64,${btoa(icons.polkadotJsLogo)}`, 16 | alt: 'Polkadotjs Logo', 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets/desktop/talisman-wallet/talisman-wallet.ts: -------------------------------------------------------------------------------- 1 | import { BaseDotsamaWallet } from '../../../base-dotsama-wallet/base-dotsama-wallet'; 2 | import { WALLET_EXTENSIONS } from '../../../types'; 3 | import { icons } from '../../../logos-svg'; 4 | 5 | export class TalismanWallet extends BaseDotsamaWallet { 6 | extensionName = WALLET_EXTENSIONS.talisman; 7 | title = 'Talisman'; 8 | installUrl = { 9 | chrome: 10 | 'https://chrome.google.com/webstore/detail/talisman-wallet/fijngjgcjhjmmpcmkeiomlglpeiijkld', 11 | firefox: 12 | 'https://chrome.google.com/webstore/detail/talisman-wallet/fijngjgcjhjmmpcmkeiomlglpeiijkld', 13 | }; 14 | noExtensionMessage = 'You can use any Polkadot compatible wallet'; 15 | logo = { 16 | src: `data:image/svg+xml;base64,${btoa(icons.talismanLogo)}`, 17 | alt: 'Talisman Logo', 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/not-allowed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Alert, AlertIcon, AlertDescription } from '@chakra-ui/react'; 3 | import { selectedWalletSelector, useWalletsStore } from '../../lib/store/use-wallet-store'; 4 | import { getWalletBySource } from '../../lib/wallets'; 5 | 6 | const NowAllowed = () => { 7 | const selectedWallet = useWalletsStore(selectedWalletSelector); 8 | const wallet = getWalletBySource(selectedWallet); 9 | const walletName = wallet?.title; 10 | 11 | return ( 12 | 13 | 14 | 15 | 16 | Please allow {walletName} extension to interact with Singular and try again 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default NowAllowed; 24 | -------------------------------------------------------------------------------- /packages/core/src/providers/extension/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, useContext } from 'react'; 2 | import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; 3 | import { ExtensionActions } from './reducer'; 4 | 5 | export interface ExtensionState { 6 | w3Enabled: boolean; 7 | accounts: InjectedAccountWithMeta[] | null; 8 | initialised: boolean; 9 | } 10 | 11 | export const initialState: ExtensionState = { 12 | w3Enabled: false, 13 | accounts: null, 14 | initialised: false, 15 | }; 16 | 17 | export const ExtensionContext = createContext<{ 18 | state: ExtensionState; 19 | dispatch: Dispatch; 20 | extensionName?: string; 21 | }>({ 22 | state: initialState, 23 | dispatch: () => null, 24 | extensionName: 'polkadot-extension', 25 | }); 26 | 27 | export function useExtensionState() { 28 | return useContext(ExtensionContext); 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/state/errors.ts: -------------------------------------------------------------------------------- 1 | import { mergeRight } from 'ramda'; 2 | import { ActionMap } from '../types/reducer'; 3 | 4 | export interface ErrorsState { 5 | blockSyncErrors: { 6 | [network: string]: Error | undefined; 7 | }; 8 | } 9 | 10 | export enum ErrorActionTypes { 11 | BLOCK_SYNC_ERROR = 'BLOCK_SYNC_ERROR', 12 | } 13 | 14 | type ErrorsPayload = { 15 | [ErrorActionTypes.BLOCK_SYNC_ERROR]: { 16 | network: string; 17 | error?: Error; 18 | }; 19 | }; 20 | 21 | export type ErrorsActions = ActionMap[keyof ActionMap]; 22 | 23 | export const errorsReducer = (state: ErrorsState, action: ErrorsActions) => { 24 | switch (action.type) { 25 | case ErrorActionTypes.BLOCK_SYNC_ERROR: 26 | return mergeRight(state, { 27 | blockSyncErrors: mergeRight(state.blockSyncErrors, { 28 | [action.payload.network]: action.payload.error, 29 | }), 30 | }); 31 | 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/errors/base-wallet-error.tsx: -------------------------------------------------------------------------------- 1 | // This file is base on https://github.com/TalismanSociety/talisman-connect/blob/master/libs/wallets/src/lib/errors/AuthError.ts with some modifications 2 | import { Wallet } from '../types'; 3 | 4 | export interface WalletError extends Error { 5 | readonly wallet: Wallet; 6 | } 7 | 8 | export class BaseWalletError extends Error implements WalletError { 9 | name = 'WalletError'; 10 | readonly wallet: Wallet; 11 | 12 | constructor(message: string, wallet: Wallet) { 13 | super(message); 14 | 15 | // https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html 16 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 17 | 18 | // Maintains proper stack trace for where our error was thrown (only available on V8) 19 | if (Error.captureStackTrace) { 20 | Error.captureStackTrace(this, BaseWalletError); 21 | } 22 | 23 | this.wallet = wallet; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/hooks/use-debounce-value.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | 3 | // generic version of lib/hooks/use-debounce-value from singular-rmrk2 4 | export function useDebounceValue(value: V, delay: number): V { 5 | // State and setters for debounced value 6 | const [debouncedValue, setDebouncedValue] = useState(value); 7 | useEffect( 8 | () => { 9 | // Update debounced value after delay 10 | const handler = setTimeout(() => { 11 | setDebouncedValue(value); 12 | }, delay); 13 | // Cancel the timeout if value changes (also on delay change or unmount) 14 | // This is how we prevent debounced value from updating if value is changed ... 15 | // .. within the delay period. Timeout gets cleared and restarted. 16 | return () => { 17 | clearTimeout(handler); 18 | }; 19 | }, 20 | [value, delay], // Only re-call effect if value or delay changes 21 | ); 22 | return debouncedValue; 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es2020": true, 4 | "es6": true, 5 | "node": true, 6 | "mocha": true, 7 | "browser": true 8 | }, 9 | "extends": [ 10 | "plugin:@typescript-eslint/recommended", 11 | "eslint:recommended" 12 | ], 13 | "parser": "@typescript-eslint/parser", 14 | "parserOptions": { 15 | "project": "./tsconfig.json", 16 | "sourceType": "module" 17 | }, 18 | "rules": { 19 | "@typescript-eslint/explicit-module-boundary-types": "off", 20 | "@typescript-eslint/no-explicit-any": "off", 21 | "no-redeclare": "off", 22 | "no-unused-vars": "off", 23 | "prefer-const": ["error", {"destructuring": "all"}], 24 | "@typescript-eslint/no-extra-semi": "off", 25 | "@typescript-eslint/ban-ts-comment": "off" 26 | }, 27 | "overrides": [ 28 | { 29 | "files": [ 30 | "test/**/*.{js,ts,tsx}" 31 | ], 32 | "rules": { 33 | "@typescript-eslint/no-non-null-assertion": "off", 34 | "no-unused-expressions": "off" 35 | } 36 | } 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 RMRK. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /packages/core/src/helpers/use-effect-once.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | export const useEffectOnce = (effect: () => void | (() => void)) => { 4 | const destroyFunc = useRef void)>(); 5 | const effectCalled = useRef(false); 6 | const renderAfterCalled = useRef(false); 7 | const [val, setVal] = useState(0); 8 | 9 | if (effectCalled.current) { 10 | renderAfterCalled.current = true; 11 | } 12 | 13 | useEffect(() => { 14 | // only execute the effect first time around 15 | if (!effectCalled.current) { 16 | destroyFunc.current = effect(); 17 | effectCalled.current = true; 18 | } 19 | 20 | // this forces one render after the effect is run 21 | setVal((val) => val + 1); 22 | 23 | return () => { 24 | // if the comp didn't render since the useEffect was called, 25 | // we know it's the dummy React cycle 26 | if (!renderAfterCalled.current) { 27 | return; 28 | } 29 | if (destroyFunc.current) { 30 | destroyFunc.current(); 31 | } 32 | }; 33 | }, []); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/example-nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@master 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 18.x 20 | uses: actions/setup-node@master 21 | with: 22 | node-version: 18.x 23 | 24 | - name: Install Dependencies 25 | run: yarn install --frozen-lockfile 26 | 27 | - name: Create Release Pull Request or Publish to npm 28 | id: changesets 29 | uses: changesets/action@master 30 | with: 31 | # This expects you to have a script called release which does a build for your packages and calls changeset publish 32 | publish: yarn release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/wallet/wallet-switch-mobile.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { WALLET_EXTENSIONS, WalletMobile } from '../../lib/types'; 3 | import { setStepSelector, useAccountModalStore } from '../../lib/store/use-account-store'; 4 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 5 | import WalletButton from './wallet-button'; 6 | import { isAndroid } from '../../lib/utils/ua-detect'; 7 | 8 | interface IProps { 9 | onClick: (wallet: WALLET_EXTENSIONS) => void; 10 | wallet: WalletMobile; 11 | } 12 | 13 | const WalletSwitchMobile = ({ onClick, wallet: { appName, installUrl, title, logo } }: IProps) => { 14 | const setStep = useAccountModalStore(setStepSelector); 15 | 16 | const onClickButton = () => { 17 | onClick(WALLET_EXTENSIONS.polkadot); 18 | setStep(ACCOUNT_MODAL_STEPS.accounts); 19 | }; 20 | 21 | return ( 22 | 28 | ); 29 | }; 30 | 31 | export default WalletSwitchMobile; 32 | -------------------------------------------------------------------------------- /packages/example-nextjs/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '../styles/globals.css'; 2 | import dynamic from 'next/dynamic'; 3 | import type { AppProps } from 'next/app'; 4 | const SubstraHooksProviderSSR = dynamic(() => import('../components/app/substra-hooks-provider'), { 5 | ssr: false, 6 | }); 7 | import { ChakraProvider } from '@chakra-ui/react'; 8 | 9 | function MyApp({ Component, pageProps }: AppProps) { 10 | return ( 11 | 12 | 28 | 29 | 30 | 31 | ); 32 | } 33 | 34 | export default MyApp; 35 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/common/search-box.tsx: -------------------------------------------------------------------------------- 1 | import React, { ChangeEvent } from 'react'; 2 | import { Input, InputGroup, InputLeftElement, useColorMode } from '@chakra-ui/react'; 3 | import { SearchIcon } from '@chakra-ui/icons'; 4 | 5 | interface IProps { 6 | onSearch: (event: ChangeEvent) => void; 7 | searchValue: string; 8 | placeholder?: string; 9 | } 10 | 11 | const SearchBox = ({ onSearch, searchValue, placeholder = undefined }: IProps) => { 12 | const isDark = useColorMode().colorMode === 'dark'; 13 | 14 | return ( 15 | 16 | 17 | 18 | 19 | 31 | 32 | ); 33 | }; 34 | 35 | export default SearchBox; 36 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/public/logos/polkadot-js-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /packages/core/src/providers/substrahooks-provider/reducer.ts: -------------------------------------------------------------------------------- 1 | import { mergeRight } from 'ramda'; 2 | import { ApiProvider, ProvidersState } from './context'; 3 | import { ActionMap } from '../../types/reducer'; 4 | 5 | export enum Types { 6 | SET_PROVIDER = 'SET_PROVIDER', 7 | SET_PROVIDERS = 'SET_PROVIDERS', 8 | } 9 | 10 | type ProvidersPayload = { 11 | [Types.SET_PROVIDERS]: { 12 | apiProviders: ProvidersState['apiProviders']; 13 | }; 14 | [Types.SET_PROVIDER]: { 15 | provider: ApiProvider; 16 | id: string; 17 | }; 18 | }; 19 | 20 | export type ProvidersActions = ActionMap[keyof ActionMap]; 21 | 22 | export const providersReducer = (state: ProvidersState, action: ProvidersActions) => { 23 | switch (action.type) { 24 | case Types.SET_PROVIDERS: 25 | return mergeRight(state, { apiProviders: action.payload.apiProviders }); 26 | 27 | case Types.SET_PROVIDER: 28 | return mergeRight(state, { 29 | apiProviders: mergeRight(state.apiProviders, { [action.payload.id]: action.payload.provider }), 30 | }); 31 | 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/wallet/wallet-switch-desktop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Wallet, WALLET_EXTENSIONS } from '../../lib/types'; 3 | import { setStepSelector, useAccountModalStore } from '../../lib/store/use-account-store'; 4 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 5 | import { isFirefox } from '../../lib/utils/ua-detect'; 6 | import WalletButton from './wallet-button'; 7 | 8 | 9 | interface IProps { 10 | onClick: (wallet: WALLET_EXTENSIONS) => void; 11 | wallet: Wallet; 12 | } 13 | 14 | const WalletSwitchDesktop = ({ 15 | onClick, 16 | wallet: { extensionName, installed, installUrl, title, logo }, 17 | }: IProps) => { 18 | const setStep = useAccountModalStore(setStepSelector); 19 | 20 | const onClickButton = () => { 21 | if (installed) { 22 | onClick(extensionName as WALLET_EXTENSIONS); 23 | setStep(ACCOUNT_MODAL_STEPS.accounts); 24 | } 25 | }; 26 | 27 | return ( 28 | 35 | ); 36 | }; 37 | 38 | export default WalletSwitchDesktop; 39 | -------------------------------------------------------------------------------- /packages/core/src/providers/extension/reducer.ts: -------------------------------------------------------------------------------- 1 | import { mergeRight } from 'ramda'; 2 | import { ExtensionState } from './context'; 3 | import { ActionMap } from '../../types/reducer'; 4 | 5 | export enum Types { 6 | W3_ENABLE = 'W3_ENABLE', 7 | ACCOUNTS_SET = 'ACCOUNTS_SET', 8 | INITIALIZE = 'INITIALIZE', 9 | } 10 | 11 | type ExtensionPayload = { 12 | [Types.W3_ENABLE]: { 13 | w3Enabled: ExtensionState['w3Enabled']; 14 | }; 15 | [Types.ACCOUNTS_SET]: { 16 | accounts: ExtensionState['accounts']; 17 | }; 18 | [Types.INITIALIZE]: { 19 | initialised: ExtensionState['initialised']; 20 | }; 21 | }; 22 | 23 | export type ExtensionActions = ActionMap[keyof ActionMap]; 24 | 25 | export const extensionReducer = (state: ExtensionState, action: ExtensionActions) => { 26 | switch (action.type) { 27 | case Types.W3_ENABLE: 28 | return mergeRight(state, { w3Enabled: action.payload.w3Enabled }); 29 | 30 | case Types.ACCOUNTS_SET: 31 | return mergeRight(state, { accounts: action.payload.accounts }); 32 | 33 | case Types.INITIALIZE: 34 | return mergeRight(state, { initialised: action.payload.initialised }); 35 | 36 | default: 37 | return state; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/wallets.ts: -------------------------------------------------------------------------------- 1 | import { TalismanWallet } from './wallets/desktop/talisman-wallet/talisman-wallet'; 2 | import { SubWallet } from './wallets/desktop/subwallet/subwallet'; 3 | import { PolkadotjsWallet } from './wallets/desktop/polkadotjs-wallet/polkadot-wallet'; 4 | import { Wallet, WALLET_EXTENSIONS, WalletMobile } from './types'; 5 | import { novaWallet } from './wallets/mobile/nova-wallet/nova-wallet'; 6 | import { mathWallet } from './wallets/mobile/nova-wallet/math-wallet'; 7 | 8 | export const WALLETS: Record = { 9 | talisman: 'Talisman', 10 | 'subwallet-js': 'SubWallet', 11 | 'polkadot-js': 'Polkadot{.js}', 12 | }; 13 | 14 | export const desktopWallets = [ 15 | new TalismanWallet() as Wallet, 16 | new SubWallet() as Wallet, 17 | new PolkadotjsWallet() as Wallet, 18 | ]; 19 | 20 | export const mobileWallets: WalletMobile[] = [novaWallet, mathWallet]; 21 | 22 | export const getWalletBySource = (source: WALLET_EXTENSIONS | null): Wallet | undefined => { 23 | return desktopWallets.find((wallet) => { 24 | return wallet.extensionName === source; 25 | }); 26 | }; 27 | 28 | export const isWalletInstalled = (source: WALLET_EXTENSIONS): boolean => { 29 | const wallet = getWalletBySource(source); 30 | 31 | return Boolean(wallet?.installed); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/src/helpers/fetch-system-properties.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise } from '@polkadot/api'; 2 | 3 | export const systemPropertiesDefaults = { 4 | tokenDecimals: 12, 5 | tokenSymbol: 'KSM', 6 | ss58Format: 2, 7 | }; 8 | 9 | export const fetchSystemProperties = async (polkadotApi: ApiPromise) => { 10 | const systemProperties = await polkadotApi.rpc.system.properties(); 11 | const { tokenDecimals, tokenSymbol, ss58Format } = systemProperties.toHuman(); 12 | let decimals = tokenDecimals; 13 | let symbol = tokenSymbol; 14 | if (Array.isArray(tokenDecimals)) { 15 | decimals = tokenDecimals[0]; 16 | } 17 | if (Array.isArray(tokenSymbol)) { 18 | symbol = tokenSymbol[0]; 19 | } 20 | if (typeof decimals !== 'string') { 21 | decimals = systemPropertiesDefaults.tokenDecimals; 22 | } 23 | if (typeof decimals === 'string') { 24 | decimals = parseInt(decimals); 25 | } 26 | 27 | let ss58FormatFinal = systemPropertiesDefaults.ss58Format; 28 | try { 29 | ss58FormatFinal = !isNaN(parseInt(ss58Format as string)) 30 | ? parseInt(ss58Format as string) 31 | : ss58FormatFinal; 32 | } catch (error: any) { 33 | console.log(error); 34 | } 35 | 36 | return { 37 | tokenDecimals: decimals || systemPropertiesDefaults.tokenDecimals, 38 | tokenSymbol: (symbol as string) || systemPropertiesDefaults.tokenSymbol, 39 | ss58Format: ss58FormatFinal, 40 | }; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/hooks/use-search-in-objects.ts: -------------------------------------------------------------------------------- 1 | import React, { useMemo, useState } from 'react'; 2 | import { useDebounceValue } from './use-debounce-value'; 3 | import Fuse from 'fuse.js'; 4 | 5 | /** 6 | * A simple hook to abstract and debounce search logic using Fuse.js 7 | */ 8 | export function useSearchInObjects( 9 | objects: SearchObjects[], 10 | options?: { 11 | fuseOptions?: Fuse.IFuseOptions; 12 | debounceAmount?: number; 13 | }, 14 | ) { 15 | const { debounceAmount = 200, fuseOptions } = options || {}; 16 | const [searchValue, setSearchValue] = useState(''); 17 | const debouncedSearchValue = useDebounceValue(searchValue, debounceAmount); 18 | 19 | // filter objects based on search query using Fuse.js 20 | const filtered = useMemo(() => { 21 | if (debouncedSearchValue.trim().length === 0) return null; 22 | const fuse = new Fuse(objects, fuseOptions); 23 | return fuse.search(debouncedSearchValue).map(({ item }) => item); 24 | }, [debouncedSearchValue, fuseOptions, objects]); 25 | 26 | // onSearch handler for SearchBox component 27 | const onSearch = (e: React.ChangeEvent) => { 28 | setSearchValue(e.target.value); 29 | }; 30 | 31 | return { 32 | filtered: filtered ?? objects, 33 | searchValue, 34 | onSearch, 35 | searchBoxProps: { searchValue, onSearch }, 36 | }; 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/test/helpers/fetch-system-properties.test.ts: -------------------------------------------------------------------------------- 1 | import { ApiPromise, WsProvider } from '@polkadot/api'; 2 | import { fetchSystemProperties } from '../../src/helpers/fetch-system-properties'; 3 | 4 | const createApi = async(wsUrl: string) => { 5 | jest.setTimeout(30000); 6 | 7 | const provider = new WsProvider(wsUrl); 8 | // const provider = new WsProvider('wss://westend-rpc.polkadot.io/'); 9 | // const provider = new WsProvider('ws://127.0.0.1:9944/'); 10 | 11 | const api = new ApiPromise({ provider }) 12 | 13 | await api.isReady; 14 | return api 15 | } 16 | 17 | // Test fetchSystemProperties 18 | describe('utils: fetchSystemProperties', () => { 19 | it('Should return and parse KUSAMA systemProperties', async () => { 20 | const api = await createApi('wss://kusama-rpc.polkadot.io'); 21 | const systemProperties = await fetchSystemProperties(api); 22 | expect(systemProperties).toStrictEqual({ 23 | ss58Format: 2, 24 | tokenDecimals: 12, 25 | tokenSymbol: 'KSM', 26 | }); 27 | 28 | await api.disconnect(); 29 | }); 30 | 31 | it('Should return and parse POLKADOT systemProperties', async () => { 32 | const api = await createApi('wss://rpc.polkadot.io'); 33 | const systemProperties = await fetchSystemProperties(api); 34 | expect(systemProperties).toStrictEqual({ 35 | ss58Format: 0, 36 | tokenDecimals: 10, 37 | tokenSymbol: 'DOT', 38 | }); 39 | 40 | await api.disconnect(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/core/src/helpers/get-asset-balance.ts: -------------------------------------------------------------------------------- 1 | import { ISystemProperties } from '../types/system-properties'; 2 | import { ApiPromise } from '@polkadot/api'; 3 | import { formatPrice } from './format-price'; 4 | import { fetchSystemProperties } from './fetch-system-properties'; 5 | import { BalanceReturnType } from './get-account-balance'; 6 | import { hexToString } from '@polkadot/util'; 7 | 8 | export const getAssetBalance = async ( 9 | account: string, 10 | assetId: number, 11 | api: ApiPromise, 12 | callback: (balance: BalanceReturnType) => void, 13 | systemProperties: ISystemProperties | null, 14 | ) => { 15 | const _systemProperties = systemProperties || (await fetchSystemProperties(api)); 16 | const getAsset = api.query.assets; 17 | getAsset.account(assetId, account).then((accountData) => { 18 | if (accountData) { 19 | getAsset.metadata(assetId).then((data) => { 20 | const balanceRaw = accountData.value.balance?.toBigInt() || BigInt(0); 21 | const tokenDecimals = data.decimals.toNumber(); 22 | const tokenSymbol = hexToString(data.symbol.toHex()); 23 | 24 | const balanceFormatted = formatPrice( 25 | balanceRaw, 26 | { tokenDecimals, tokenSymbol, ss58Format: _systemProperties.ss58Format }, 27 | true, 28 | ); 29 | 30 | const balance = { 31 | raw: balanceRaw, 32 | formatted: balanceFormatted, 33 | }; 34 | 35 | callback({ balance }); 36 | }); 37 | } 38 | }); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/state/balances.ts: -------------------------------------------------------------------------------- 1 | import { mergeRight } from 'ramda'; 2 | import { BalanceReturnType } from '../helpers'; 3 | import { ActionMap } from '../types/reducer'; 4 | 5 | export interface BalancesState { 6 | balances: Record; 7 | assets: Record>; 8 | } 9 | 10 | export enum BalanceTypes { 11 | SET_BALANCE = 'SET_BALANCE', 12 | SET_ASSET = 'SET_ASSET', 13 | } 14 | 15 | type BalancesPayload = { 16 | [BalanceTypes.SET_BALANCE]: { 17 | balance: BalanceReturnType; 18 | network: string; 19 | }; 20 | [BalanceTypes.SET_ASSET]: { 21 | balance: BalanceReturnType; 22 | network: string; 23 | assetId: number; 24 | }; 25 | }; 26 | 27 | export type BalancesActions = ActionMap[keyof ActionMap]; 28 | 29 | export const balancesReducer = (state: BalancesState, action: BalancesActions) => { 30 | switch (action.type) { 31 | case BalanceTypes.SET_BALANCE: 32 | return mergeRight(state, { 33 | balances: mergeRight(state.balances, { [action.payload.network]: action.payload.balance }), 34 | }); 35 | 36 | case BalanceTypes.SET_ASSET: 37 | return mergeRight(state, { 38 | assets: mergeRight(state.assets, { 39 | [action.payload.network]: mergeRight(state.assets[action.payload.network] || {}, { 40 | [action.payload.assetId]: action.payload.balance, 41 | }), 42 | }), 43 | }); 44 | 45 | default: 46 | return state; 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /packages/core/test/helpers/format-price.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatPrice, 3 | bigIntPriceToNumber, 4 | 5 | } from '../../src/helpers/format-price'; 6 | 7 | // Test formatPrice 8 | describe('utils: formatPrice', () => { 9 | const systemProperties = { 10 | ss58Format: 2, 11 | tokenDecimals: 12, 12 | tokenSymbol: 'KSM', 13 | }; 14 | it('Should return formatted price', () => { 15 | expect(formatPrice(BigInt(1.2e12), systemProperties)).toEqual('1.2000 KSM'); 16 | }); 17 | 18 | it('Should return formatted fixed price', () => { 19 | expect(formatPrice(BigInt(1.2e12), systemProperties, true)).toEqual('1.2 KSM'); 20 | }); 21 | 22 | it('Should return formatted price if string is passed', () => { 23 | expect(formatPrice('1000000000000', systemProperties, true)).toEqual('1 KSM'); 24 | }); 25 | 26 | it('Should return formatted price if number is passed', () => { 27 | expect(formatPrice(2, systemProperties, true)).toEqual('2 KSM'); 28 | }); 29 | }); 30 | 31 | describe('utils: bigIntPriceToNumber', () => { 32 | const systemProperties = { 33 | ss58Format: 2, 34 | tokenDecimals: 12, 35 | tokenSymbol: 'KSM', 36 | }; 37 | it('Should return fixed price to be displayed to user', () => { 38 | expect(bigIntPriceToNumber(BigInt(1.2e12), systemProperties)).toEqual(1.2); 39 | }); 40 | 41 | it('Should return fixed price to be displayed to user', () => { 42 | expect(bigIntPriceToNumber(BigInt(0), systemProperties)).toEqual(0); 43 | }); 44 | 45 | it('Should return fixed price to be displayed to user', () => { 46 | expect(bigIntPriceToNumber(BigInt(4.31e12), systemProperties)).toEqual(4.31); 47 | }); 48 | }); 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /packages/example-nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/common/identity-avatar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, useToast } from '@chakra-ui/react'; 3 | import Identicon from '@polkadot/react-identicon'; 4 | import Image from 'next/image'; 5 | import copy from 'copy-to-clipboard'; 6 | import { shortenAccountId } from '../../lib/utils/shorten-account-id'; 7 | 8 | interface IProps { 9 | size: number; 10 | id: string; 11 | userpicUrl?: string; 12 | } 13 | 14 | const IdentityAvatar = ({ userpicUrl, size, id }: IProps) => { 15 | const toast = useToast(); 16 | 17 | const onClick: React.MouseEventHandler = (event) => { 18 | event.stopPropagation(); 19 | event.preventDefault(); 20 | toast({ 21 | title: `Address ${shortenAccountId(id, true)} copied to clipboard.`, 22 | duration: 3000, 23 | position: 'top', 24 | }); 25 | copy(id); 26 | return false; 27 | }; 28 | 29 | return ( 30 | 39 | {userpicUrl ? ( 40 | 47 | {id} 50 | 51 | ) : ( 52 | 53 | )} 54 | 55 | ); 56 | }; 57 | 58 | export default IdentityAvatar; 59 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/connect-account-modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Box, 4 | Modal, 5 | ModalBody, 6 | ModalCloseButton, 7 | ModalContent, 8 | ModalHeader, 9 | ModalOverlay, 10 | } from '@chakra-ui/react'; 11 | import AccountSelection from './account-selection'; 12 | import { 13 | modalOpenedSelector, 14 | stepSelector, 15 | toggleAccountSelectionModalSelector, 16 | useAccountModalStore, 17 | } from '../../lib/store/use-account-store'; 18 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 19 | import WalletSelect from '../wallet/wallet-select'; 20 | 21 | export const ConnectAccountModal = () => { 22 | const modalOpened = useAccountModalStore(modalOpenedSelector); 23 | const step = useAccountModalStore(stepSelector); 24 | const toggleAccountSelectionModal = useAccountModalStore(toggleAccountSelectionModalSelector); 25 | 26 | const onClose = () => { 27 | toggleAccountSelectionModal(false); 28 | }; 29 | 30 | const modalContent = () => { 31 | switch (step) { 32 | case ACCOUNT_MODAL_STEPS.wallets: 33 | return ; 34 | case ACCOUNT_MODAL_STEPS.accounts: 35 | case ACCOUNT_MODAL_STEPS.notAllowed: 36 | return ; 37 | } 38 | }; 39 | 40 | return ( 41 | 42 | 43 | 44 | Connect your wallet 45 | 46 | 47 | {modalContent()} 48 | 49 | 50 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-asset-balance.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react'; 2 | import { useSystemProperties } from './use-system-properties'; 3 | import { useIsMountedRef } from '../helpers/use-is-mounted-ref'; 4 | import { BalanceReturnType } from '../helpers/get-account-balance'; 5 | import { getAssetBalance } from '../helpers/get-asset-balance'; 6 | import { useApiProvider } from './use-api-provider'; 7 | import { SubstraHooksContext, useSubstraHooksState } from '../providers'; 8 | import { BalanceTypes } from '../state/balances'; 9 | 10 | type Options = { 11 | skip?: boolean 12 | } 13 | 14 | export const useAssetBalance = ( 15 | account: string, 16 | assetId: number, 17 | apiProviderId?: string, 18 | options?: Options 19 | ): BalanceReturnType | null => { 20 | const { skip = false } = options || {}; 21 | const apiProvider = useApiProvider(apiProviderId); 22 | const defaultId = useContext(SubstraHooksContext).defaultApiProviderId; 23 | const isMountedRef = useIsMountedRef(); 24 | const systemProperties = useSystemProperties(apiProviderId); 25 | const { balancesDispatch, balancesState } = useSubstraHooksState(); 26 | 27 | const networkId = apiProviderId || defaultId; 28 | 29 | useEffect(() => { 30 | if (!skip && account && apiProvider && assetId) { 31 | const callback = ({ balance }: BalanceReturnType) => { 32 | if (isMountedRef.current) { 33 | balancesDispatch({ 34 | type: BalanceTypes.SET_ASSET, 35 | payload: { network: networkId, balance: { balance }, assetId }, 36 | }); 37 | } 38 | }; 39 | getAssetBalance(account, assetId, apiProvider, callback, systemProperties); 40 | } 41 | }, [account, assetId, JSON.stringify(apiProvider?.rpc), systemProperties, isMountedRef, skip]); 42 | 43 | return balancesState.assets[networkId]?.[assetId] || null; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/example-nextjs/pages/page-two.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import Image from 'next/image'; 4 | import styles from '../styles/Home.module.css'; 5 | import { 6 | useAccountBalance, 7 | useAssetBalance, 8 | usePolkadotExtension, 9 | useSystemProperties, 10 | } from '@substra-hooks/core'; 11 | import { useEffect } from 'react'; 12 | import Link from 'next/link'; 13 | 14 | const PageTwo: NextPage = () => { 15 | const { accounts, w3enable, w3Enabled } = usePolkadotExtension(); 16 | const balancePayload = useAccountBalance(accounts?.[5]?.address || ''); 17 | const assetPayload = useAssetBalance(accounts?.[5]?.address || '', 8, 'statemine'); 18 | const systemProperties = useSystemProperties(); 19 | 20 | console.log('systemProperties', systemProperties); 21 | 22 | console.log('accounts', accounts); 23 | 24 | console.log('balancePayload', accounts?.[5]?.address || '', balancePayload); 25 | console.log('assetPayload', assetPayload); 26 | 27 | return ( 28 |
29 | 30 | Create Next App 31 | 32 | 33 | 34 | 35 |
36 |
Balance: {balancePayload?.balance.formatted}
37 |
Locked Balance: {balancePayload && balancePayload?.locked?.formatted}
38 |
Reserved Balance: {balancePayload?.reserved?.formatted}
39 |
Total Balance: {balancePayload?.total?.formatted}
40 |
Available Balance: {balancePayload?.available?.formatted}
41 | 42 |
43 |
Asset balance: {assetPayload?.balance.formatted}
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default PageTwo; 50 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rmrk-team/dotsama-wallet-react", 3 | "description": "Highly opinionated dotsama wallet in React", 4 | "version": "0.0.8", 5 | "repository": "rmrk-team/substra-hooks", 6 | "author": "RMRK team", 7 | "homepage": "https://rmrk.app", 8 | "main": "dist/cjs/src/index.js", 9 | "module": "dist/esm/src/index.js", 10 | "types": "dist/esm/src/index.d.ts", 11 | "sideEffects": false, 12 | "scripts": { 13 | "build": "yarn process-svg-icons && rimraf ./dist && yarn run build:esm && yarn run build:cjs", 14 | "build:esm": "tsc --module es2020 --target es2017 --outDir dist/esm", 15 | "build:cjs": "tsc --outDir dist/cjs", 16 | "process-svg-icons": "svg-to-ts-object -s './src/public/**/*.svg' --outputDirectory='./src/lib' --fileName='logos-svg'" 17 | }, 18 | "peerDependencies": { 19 | "@chakra-ui/react": "*", 20 | "@polkadot/api": "*", 21 | "@polkadot/extension-dapp": "*", 22 | "@polkadot/keyring": "*", 23 | "@polkadot/util": "*", 24 | "@polkadot/util-crypto": "*", 25 | "react": "*", 26 | "react-dom": "*" 27 | }, 28 | "dependencies": { 29 | "@chakra-ui/react": "^2.6.1", 30 | "@chakra-ui/system": "^2.5.7", 31 | "@polkadot/react-identicon": "^3.6.6", 32 | "@react-icons/all-files": "^4.1.0", 33 | "@substra-hooks/core": "^0.0.61", 34 | "fuse.js": "^6.6.2", 35 | "react-copy-to-clipboard": "^5.1.0", 36 | "zustand": "^4.1.3", 37 | "typescript": "^5.4.5" 38 | }, 39 | "devDependencies": { 40 | "@chakra-ui/icons": "2.0.19", 41 | "@chakra-ui/react": "^2.6.1", 42 | "@types/react": "18.0.14", 43 | "@types/react-copy-to-clipboard": "^5.0.2", 44 | "@types/react-dom": "18.0.5", 45 | "react": "18.2.0", 46 | "react-dom": "18.2.0", 47 | "rimraf": "^3.0.2", 48 | "svg-to-ts": "^8.8.0" 49 | }, 50 | "keywords": [ 51 | "react", 52 | "hooks", 53 | "polkadot", 54 | "web3" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /packages/core/src/providers/extension/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useReducer } from 'react'; 2 | import { ExtensionContext, initialState } from './context'; 3 | import { extensionReducer, Types } from './reducer'; 4 | import { useSystemProperties } from '../../hooks'; 5 | import { checkEnabled } from '../../hooks/use-polkadot-extension'; 6 | import { useIsMountedRef } from '../../helpers/use-is-mounted-ref'; 7 | 8 | interface ExtensionProviderProps { 9 | children: ReactNode; 10 | extensionName?: string; 11 | autoInitialiseExtension?: boolean; 12 | } 13 | 14 | export const ExtensionProvider = ({ 15 | children, 16 | autoInitialiseExtension, 17 | extensionName, 18 | }: ExtensionProviderProps) => { 19 | const isMountedRef = useIsMountedRef(); 20 | const [state, dispatch] = useReducer(extensionReducer, initialState); 21 | const systemProperties = useSystemProperties(); 22 | 23 | useEffect(() => { 24 | if (autoInitialiseExtension && systemProperties && !state.w3Enabled) { 25 | checkEnabled(extensionName || 'polkadot-extension', systemProperties).then(({ accounts, w3Enabled }) => { 26 | if (isMountedRef.current) { 27 | if (w3Enabled) { 28 | dispatch({ 29 | type: Types.ACCOUNTS_SET, 30 | payload: { 31 | accounts, 32 | }, 33 | }); 34 | } 35 | 36 | dispatch({ 37 | type: Types.W3_ENABLE, 38 | payload: { 39 | w3Enabled, 40 | }, 41 | }); 42 | 43 | dispatch({ 44 | type: Types.INITIALIZE, 45 | payload: { 46 | initialised: true, 47 | }, 48 | }); 49 | } 50 | }); 51 | } 52 | }, [JSON.stringify(systemProperties), isMountedRef, autoInitialiseExtension]); 53 | 54 | return ( 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/core/src/providers/substrahooks-provider/context.ts: -------------------------------------------------------------------------------- 1 | import { createContext, Dispatch, useContext } from 'react'; 2 | import { ApiPromise, WsProvider } from '@polkadot/api'; 3 | import { ISystemProperties } from '../../types/system-properties'; 4 | import { BalancesActions, BalancesState } from '../../state/balances'; 5 | import { ErrorsActions, ErrorsState } from '../../state/errors'; 6 | import { ProvidersActions } from './reducer'; 7 | 8 | 9 | export type ApiProvider = { 10 | apiProvider: ApiPromise | null; 11 | systemProperties: ISystemProperties; 12 | wsProvider?: WsProvider; 13 | rpcEndpoint?: string 14 | }; 15 | export type ApiProviders = Record; 16 | 17 | export interface ProvidersState { 18 | apiProviders: ApiProviders; 19 | } 20 | 21 | export const initialProvidersState: ProvidersState = { 22 | apiProviders: {} 23 | }; 24 | 25 | export const initialErrorsState: ErrorsState = { 26 | blockSyncErrors: {}, 27 | }; 28 | 29 | export const initialBalancesState: BalancesState = { 30 | balances: {}, 31 | assets: {}, 32 | }; 33 | 34 | export const SubstraHooksContext = createContext<{ 35 | apiProvidersState: ProvidersState; 36 | apiProvidersStateDispatch: Dispatch; 37 | defaultApiProviderId: string; 38 | balancesState: BalancesState; 39 | balancesDispatch: Dispatch; 40 | errorsState: ErrorsState; 41 | errorsDispatch: Dispatch; 42 | }>({ 43 | apiProvidersState: initialProvidersState, 44 | apiProvidersStateDispatch: () => null, 45 | defaultApiProviderId: '', 46 | errorsState: initialErrorsState, 47 | errorsDispatch: () => null, 48 | balancesState: initialBalancesState, 49 | balancesDispatch: () => null, 50 | }); 51 | 52 | export const useSubstraHooksState = () => { 53 | return useContext(SubstraHooksContext); 54 | }; 55 | 56 | export const useBalancesState = () => { 57 | return useContext(SubstraHooksContext).balancesState; 58 | }; 59 | 60 | export const useApiProvidersState = () => { 61 | return useContext(SubstraHooksContext).apiProvidersState; 62 | }; 63 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-account-balance.ts: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect } from 'react'; 2 | import { useSystemProperties } from './use-system-properties'; 3 | import { useIsMountedRef } from '../helpers/use-is-mounted-ref'; 4 | import { BalanceReturnType, getAccountBalance } from '../helpers/get-account-balance'; 5 | import { useApiProvider } from './use-api-provider'; 6 | import { SubstraHooksContext, useApiProvidersState, useSubstraHooksState } from '../providers'; 7 | import { BalanceTypes } from '../state/balances'; 8 | 9 | type Options = { 10 | skip?: boolean 11 | } 12 | 13 | export const useAccountBalance = ( 14 | account: string, 15 | apiProviderId?: string, 16 | options?: Options 17 | ): BalanceReturnType | null => { 18 | const { skip = false } = options || {}; 19 | const isMountedRef = useIsMountedRef(); 20 | const defaultId = useContext(SubstraHooksContext).defaultApiProviderId; 21 | const { balancesDispatch, balancesState } = useSubstraHooksState(); 22 | const systemProperties = useSystemProperties(); 23 | const networkId = apiProviderId || defaultId; 24 | const apiProvider = useApiProvider(networkId); 25 | 26 | const rpcEndpoint = useApiProvidersState().apiProviders[networkId]?.rpcEndpoint; 27 | 28 | useEffect(() => { 29 | if (!skip && account && apiProvider && systemProperties) { 30 | const callback = ({ balance, locked, reserved, total, available }: BalanceReturnType) => { 31 | if (isMountedRef.current) { 32 | balancesDispatch({ 33 | type: BalanceTypes.SET_BALANCE, 34 | payload: { 35 | network: networkId, 36 | balance: { 37 | balance, 38 | locked, 39 | reserved, 40 | total, 41 | available, 42 | }, 43 | }, 44 | }); 45 | } 46 | }; 47 | getAccountBalance(account, systemProperties, apiProvider, callback); 48 | } 49 | }, [account, JSON.stringify(apiProvider?.rpc), systemProperties, isMountedRef, rpcEndpoint, skip]); 50 | 51 | return balancesState.balances[networkId]; 52 | }; 53 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/wallet/wallet-button.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, Flex, Button, Link, useColorMode } from '@chakra-ui/react'; 3 | import { HiDownload } from '@react-icons/all-files/hi/HiDownload'; 4 | import { CheckIcon } from '@chakra-ui/icons'; 5 | 6 | interface IProps { 7 | installed?: boolean; 8 | installUrl?: string; 9 | title: string; 10 | logo: { 11 | src: string; 12 | alt: string; 13 | }; 14 | onClick: () => void; 15 | disabled?: boolean; 16 | } 17 | 18 | const WalletButton = ({ 19 | installed, 20 | installUrl, 21 | title, 22 | logo: { src, alt }, 23 | onClick, 24 | disabled, 25 | }: IProps) => { 26 | const isDark = useColorMode().colorMode === 'dark'; 27 | const connected = installed && disabled; 28 | 29 | const content = ( 30 | 59 | ); 60 | 61 | return installed ? ( 62 | content 63 | ) : installUrl ? ( 64 | 71 | {content} 72 | 73 | ) : null; 74 | }; 75 | 76 | export default WalletButton; 77 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@substra-hooks/core", 3 | "version": "0.0.61", 4 | "main": "dist/cjs/src/index.js", 5 | "module": "dist/esm/src/index.js", 6 | "types": "dist/esm/src/index.d.ts", 7 | "repository": "git@github.com:rmrk-team/substra-hooks.git", 8 | "author": "RMRK team", 9 | "license": "MIT", 10 | "sideEffects": false, 11 | "publishConfig": { 12 | "access": "public" 13 | }, 14 | "scripts": { 15 | "build": "yarn run build:esm && yarn run build:cjs", 16 | "build:esm": "tsc --module es2020 --target es2017 --outDir dist/esm", 17 | "build:cjs": "tsc --outDir dist/cjs", 18 | "test": "jest --forceExit --runInBand", 19 | "test:watch": "jest --watch", 20 | "lint": "yarn lint:prettier --check && yarn lint:eslint", 21 | "lint:fix": "yarn lint:prettier --write && yarn lint:eslint --fix", 22 | "lint:eslint": "eslint './{src,test}/**/*.{ts,tsx}'", 23 | "lint:prettier": "yarn prettier './{src,test}/**/*.{ts,tsx}'" 24 | }, 25 | "devDependencies": { 26 | "@babel/core": "^7.13.8", 27 | "@babel/plugin-proposal-optional-chaining": "^7.13.8", 28 | "@babel/preset-env": "^7.13.9", 29 | "@babel/preset-typescript": "^7.13.0", 30 | "@polkadot/api": "^11.2.1", 31 | "@polkadot/extension-dapp": "^0.47.5", 32 | "@polkadot/keyring": "^12.6.2", 33 | "@polkadot/util": "^12.6.2", 34 | "@polkadot/util-crypto": "^12.6.2", 35 | "@types/jest": "^29.5.1", 36 | "@types/ramda": "^0.29.1", 37 | "@typescript-eslint/eslint-plugin": "^5.59.6", 38 | "@typescript-eslint/parser": "^5.59.6", 39 | "babel-jest": "^29.5.0", 40 | "eslint": "8.40.0", 41 | "jest": "^29.5.0", 42 | "prettier": "^2.8.8", 43 | "ts-jest": "^29.1.0", 44 | "ts-node": "^10.9.1", 45 | "typescript": "^5.4.5" 46 | }, 47 | "dependencies": { 48 | "@types/react": "18.0.24", 49 | "nanoid": "3.3.1", 50 | "ramda": "^0.29.0" 51 | }, 52 | "peerDependencies": { 53 | "@polkadot/api": "*", 54 | "@polkadot/extension-dapp": "*", 55 | "@polkadot/keyring": "*", 56 | "@polkadot/util": "*", 57 | "@polkadot/util-crypto": "*", 58 | "react": "*", 59 | "react-dom": "*" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/account-store-sync.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { useSystemProperties } from '@substra-hooks/core'; 3 | import { 4 | setAccountsSelector, 5 | setSelectedAccountSelector, 6 | setStepSelector, 7 | useAccountModalStore, 8 | useAccountsStore, 9 | useSelectedAccountsStore, 10 | } from '../../lib/store/use-account-store'; 11 | import { 12 | selectedWalletSelector, 13 | setSelectedWalletSelector, 14 | useWalletsStore, 15 | } from '../../lib/store/use-wallet-store'; 16 | import { getWalletBySource, isWalletInstalled } from '../../lib/wallets'; 17 | import { isMobile } from '../../lib/utils/ua-detect'; 18 | import { WALLET_EXTENSIONS } from '../../lib/types'; 19 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 20 | 21 | export const AccountStoreSync = () => { 22 | const setSelectedAccount = useSelectedAccountsStore(setSelectedAccountSelector); 23 | const setAccounts = useAccountsStore(setAccountsSelector); 24 | const selectedWallet = useWalletsStore(selectedWalletSelector); 25 | const wallet = getWalletBySource(selectedWallet); 26 | const systemProperties = useSystemProperties(); 27 | const setStep = useAccountModalStore(setStepSelector); 28 | const setSelectedWallet = useWalletsStore(setSelectedWalletSelector); 29 | 30 | useEffect(() => { 31 | if (isMobile) { 32 | const isPolkadot = isWalletInstalled(WALLET_EXTENSIONS.polkadot); 33 | 34 | if (isPolkadot) { 35 | setSelectedWallet(WALLET_EXTENSIONS.polkadot); 36 | setStep(ACCOUNT_MODAL_STEPS.accounts); 37 | } 38 | } 39 | }, [isMobile]); 40 | 41 | useEffect(() => { 42 | if (wallet?.installed) { 43 | const saveAccountsAsync = async () => { 44 | try { 45 | await wallet.enable(); 46 | } catch (e: any) { 47 | if (e.message.includes('not allowed to interact')) { 48 | setStep(ACCOUNT_MODAL_STEPS.notAllowed); 49 | } 50 | } 51 | 52 | const accounts = await wallet.getAccounts(systemProperties.ss58Format); 53 | setAccounts(accounts); 54 | }; 55 | 56 | saveAccountsAsync(); 57 | } else if (!selectedWallet) { 58 | setSelectedAccount(null); 59 | } 60 | }, [wallet?.installed, selectedWallet]); 61 | 62 | return null; 63 | }; 64 | 65 | -------------------------------------------------------------------------------- /packages/example-nextjs/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /packages/core/src/helpers/format-price.ts: -------------------------------------------------------------------------------- 1 | import { formatBalance } from '@polkadot/util'; 2 | import { stringTrimTrailingZeros } from './string-trim-trailing-zeros'; 3 | import { ISystemProperties } from '../types/system-properties'; 4 | 5 | /** 6 | * forsale can come in different types (from dumps it's a string) so convert it back to BigInt 7 | * @param forsale 8 | * @param systemProperties 9 | */ 10 | export const forSaleToBigInt = ( 11 | forsale: string | number | bigint, 12 | systemProperties: ISystemProperties, 13 | ): bigint => { 14 | let priceBigInt = forsale; 15 | if (typeof forsale == 'string') { 16 | return BigInt(forsale); 17 | } 18 | 19 | if (typeof forsale === 'number') { 20 | return BigInt(Number(`${forsale}e${systemProperties.tokenDecimals}`)); 21 | } 22 | 23 | return BigInt(priceBigInt); 24 | }; 25 | 26 | /** 27 | * Format BigInt price based on chain's decimal and symbol 28 | * @param price - string or number inputted price in plancks 29 | * @param systemProperties - chain's systemProperties as returned from polkadot api 30 | * @param toFixed - whether to format price to fixed number (will convert 1.0000 KSM to 1 KSM) 31 | */ 32 | export const formatPrice = ( 33 | price: bigint | string | number, 34 | systemProperties: ISystemProperties, 35 | toFixed: boolean = false, 36 | ) => { 37 | const numberPrice = forSaleToBigInt(price, systemProperties); 38 | 39 | const { tokenDecimals, tokenSymbol } = systemProperties; 40 | if (toFixed) { 41 | let formatted = formatBalance(numberPrice, { 42 | decimals: tokenDecimals, 43 | withUnit: false, 44 | forceUnit: '-', 45 | }); 46 | return `${stringTrimTrailingZeros(formatted)} ${tokenSymbol}`.replace(',', ''); 47 | } else { 48 | return formatBalance(numberPrice, { 49 | decimals: tokenDecimals, 50 | withUnit: tokenSymbol, 51 | forceUnit: '-', 52 | }).replace(',', ''); 53 | } 54 | }; 55 | 56 | /** 57 | * Formant NFT forsale price which is in plancks into whole number unit 58 | * @param price 59 | * @param systemProperties 60 | */ 61 | export const bigIntPriceToNumber = ( 62 | price: bigint | string | number, 63 | systemProperties: ISystemProperties, 64 | ) => { 65 | const base = Math.pow(Number(BigInt(10)), Number(BigInt(systemProperties.tokenDecimals))); 66 | const numberPrice = Number(forSaleToBigInt(price, systemProperties)); 67 | return numberPrice / base; 68 | }; 69 | -------------------------------------------------------------------------------- /packages/core/src/helpers/get-account-balance.ts: -------------------------------------------------------------------------------- 1 | import { ISystemProperties } from '../types/system-properties'; 2 | import { ApiPromise } from '@polkadot/api'; 3 | import { formatPrice } from './format-price'; 4 | 5 | interface BalanceDataType { 6 | raw: bigint | null; 7 | formatted: string | null; 8 | } 9 | 10 | export interface BalanceReturnType { 11 | balance: BalanceDataType; 12 | locked?: BalanceDataType; 13 | reserved?: BalanceDataType; 14 | total?: BalanceDataType; 15 | available?: BalanceDataType; 16 | } 17 | 18 | export const getAccountBalance = async ( 19 | account: string, 20 | systemProperties: ISystemProperties, 21 | api: ApiPromise, 22 | callback: (balance: BalanceReturnType) => void, 23 | ) => { 24 | api.query.system.account( 25 | account, 26 | async ({ data: { free: currentFree, frozen: currentLocked, reserved: currentReserved } }) => { 27 | const { availableBalance } = await api.derive.balances.all(account); 28 | 29 | const balanceRaw = currentFree?.toBigInt() || BigInt(0); 30 | const balanceLockedRaw = currentLocked?.toBigInt() || BigInt(0); 31 | const balanceReservedRaw = currentReserved?.toBigInt() || BigInt(0); 32 | const balanceTotalRaw = balanceRaw + balanceReservedRaw; 33 | 34 | const balanceFormatted = formatPrice(balanceRaw, systemProperties, true); 35 | const balanceLockedFormatted = formatPrice(balanceLockedRaw, systemProperties, true); 36 | const balanceReservedFormatted = formatPrice(balanceReservedRaw, systemProperties, true); 37 | const balanceTotalFormatted = formatPrice(balanceTotalRaw, systemProperties, true); 38 | 39 | const balance = { 40 | raw: balanceRaw, 41 | formatted: balanceFormatted, 42 | }; 43 | 44 | const locked = { 45 | raw: balanceLockedRaw, 46 | formatted: balanceLockedFormatted, 47 | }; 48 | 49 | const reserved = { 50 | raw: balanceReservedRaw, 51 | formatted: balanceReservedFormatted, 52 | }; 53 | 54 | const total = { 55 | raw: balanceTotalRaw, 56 | formatted: balanceTotalFormatted, 57 | }; 58 | 59 | const available = { 60 | raw: availableBalance.toBigInt(), 61 | formatted: formatPrice(availableBalance.toBigInt(), systemProperties, true), 62 | }; 63 | 64 | if (callback) { 65 | callback({ 66 | balance, 67 | locked, 68 | reserved, 69 | total, 70 | available, 71 | }); 72 | } 73 | }, 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/wallet/wallet-select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box, VStack } from '@chakra-ui/react'; 3 | import WalletSwitchDesktop from './wallet-switch-desktop'; 4 | import { useSystemProperties } from '@substra-hooks/core'; 5 | import { dashify } from '../../lib/utils/dashify'; 6 | import { setSelectedWalletSelector, useWalletsStore } from '../../lib/store/use-wallet-store'; 7 | import { WALLET_EXTENSIONS } from '../../lib/types'; 8 | import { desktopWallets, getWalletBySource, mobileWallets } from '../../lib/wallets'; 9 | import { 10 | setAccountsSelector, 11 | setStepSelector, 12 | useAccountModalStore, 13 | useAccountsStore, 14 | } from '../../lib/store/use-account-store'; 15 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 16 | import WalletSwitchMobile from './wallet-switch-mobile'; 17 | import { isMobile } from '../../lib/utils/ua-detect'; 18 | 19 | const WalletSelect = () => { 20 | const systemProperties = useSystemProperties(); 21 | const setSelectedWallet = useWalletsStore(setSelectedWalletSelector); 22 | const setAccounts = useAccountsStore(setAccountsSelector); 23 | const setStep = useAccountModalStore(setStepSelector); 24 | 25 | const onSelect = async (walletKey: WALLET_EXTENSIONS) => { 26 | const wallet = getWalletBySource(walletKey); 27 | 28 | if (wallet?.installed) { 29 | await setSelectedWallet(walletKey); 30 | 31 | try { 32 | await wallet.enable(); 33 | } catch (e: any) { 34 | if (e.message.includes('not allowed to interact')) { 35 | setStep(ACCOUNT_MODAL_STEPS.notAllowed); 36 | } 37 | } 38 | 39 | await wallet.subscribeAccounts((accounts) => { 40 | accounts && setAccounts(accounts); 41 | }, systemProperties.ss58Format); 42 | } 43 | }; 44 | 45 | return ( 46 | 47 | 48 | {isMobile 49 | ? mobileWallets.map((item) => ( 50 | 55 | )) 56 | : desktopWallets.map((item) => ( 57 | 62 | ))} 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default WalletSelect; 69 | -------------------------------------------------------------------------------- /packages/core/src/hooks/use-polkadot-extension.ts: -------------------------------------------------------------------------------- 1 | import { ExtensionState, useExtensionState } from '../providers/extension'; 2 | import { useEffect, useState } from 'react'; 3 | import { useSystemProperties } from './use-system-properties'; 4 | import { ISystemProperties } from '../types/system-properties'; 5 | import { Types } from '../providers/extension/reducer'; 6 | import { useIsMountedRef } from '../helpers/use-is-mounted-ref'; 7 | import { InjectedAccountWithMeta } from '@polkadot/extension-inject/types'; 8 | 9 | interface UsePolkadotExtensionReturnType extends ExtensionState { 10 | w3enable: () => void; 11 | initialised: boolean; 12 | } 13 | 14 | export const checkEnabled: (extensionName: string, systemProperties: ISystemProperties) => Promise<{ accounts: InjectedAccountWithMeta[] | null, w3Enabled: boolean }> = async ( 15 | extensionName: string = 'polkadot-extension', 16 | systemProperties: ISystemProperties, 17 | ) => { 18 | const extensionDapp = await import('@polkadot/extension-dapp'); 19 | const { web3Accounts, web3Enable } = extensionDapp; 20 | const enabledApps = await web3Enable(extensionName); 21 | const w3Enabled = enabledApps.length > 0; 22 | 23 | let accounts = null; 24 | 25 | if (w3Enabled) { 26 | accounts = await web3Accounts({ ss58Format: systemProperties.ss58Format, accountType: ['sr25519'] }); 27 | } 28 | return { accounts, w3Enabled }; 29 | }; 30 | 31 | type Options = { 32 | skip?: boolean 33 | } 34 | 35 | export const usePolkadotExtension = (options?: Options): UsePolkadotExtensionReturnType => { 36 | const { skip = false } = options || {}; 37 | const isMountedRef = useIsMountedRef(); 38 | const { state, dispatch, extensionName } = useExtensionState(); 39 | const { w3Enabled, accounts, initialised } = state; 40 | const [ready, setReady] = useState(false); 41 | const systemProperties = useSystemProperties(); 42 | 43 | useEffect(() => { 44 | if (!skip && ready && systemProperties && !w3Enabled) { 45 | checkEnabled(extensionName || 'polkadot-extension', systemProperties).then(({ accounts, w3Enabled }) => { 46 | if (isMountedRef.current) { 47 | if (w3Enabled) { 48 | dispatch({ 49 | type: Types.ACCOUNTS_SET, 50 | payload: { 51 | accounts, 52 | }, 53 | }); 54 | } 55 | 56 | dispatch({ 57 | type: Types.W3_ENABLE, 58 | payload: { 59 | w3Enabled, 60 | }, 61 | }); 62 | 63 | dispatch({ 64 | type: Types.INITIALIZE, 65 | payload: { 66 | initialised: true, 67 | }, 68 | }); 69 | } 70 | }); 71 | } 72 | }, [ready, w3Enabled, systemProperties, skip]); 73 | 74 | const w3enable = () => { 75 | setReady(true); 76 | }; 77 | 78 | return { accounts, w3enable, w3Enabled, initialised }; 79 | }; 80 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { InjectedMetadata, InjectedProvider, Unsubcall } from '@polkadot/extension-inject/types'; 2 | import { Signer as InjectedSigner } from '@polkadot/api/types'; 3 | import { WalletError } from './errors/base-wallet-error'; 4 | 5 | export type SubscriptionFn = (accounts: WalletAccount[] | undefined) => void | Promise; 6 | 7 | export interface WalletLogoProps { 8 | // Logo url 9 | src: string; 10 | // Alt for the Logo url 11 | alt: string; 12 | } 13 | 14 | export interface WalletAccount { 15 | address: string; 16 | source: string; 17 | name?: string; 18 | wallet?: Wallet; 19 | signer?: unknown; 20 | } 21 | 22 | export enum WALLET_EXTENSIONS { 23 | polkadot = 'polkadot-js', 24 | subwallet = 'subwallet-js', 25 | talisman = 'talisman', 26 | } 27 | 28 | export enum MOBILE_WALLET_APPS { 29 | nova = 'nova', 30 | mathWallet = 'mathWallet', 31 | } 32 | 33 | export interface WalletData { 34 | // The name of the wallet extension. Should match `Account.source` 35 | extensionName: WALLET_EXTENSIONS; 36 | // Display name for the wallet extension 37 | title: string; 38 | // Message to display if wallet extension is not installed 39 | noExtensionMessage?: string; 40 | // The URL to install the wallet extension 41 | installUrl: { 42 | chrome: string; 43 | firefox: string; 44 | }; 45 | // The wallet logo 46 | logo: WalletLogoProps; 47 | } 48 | 49 | export interface WalletExtension { 50 | installed: boolean | undefined; 51 | 52 | // The raw extension object which will have everything a dapp developer needs. 53 | // Refer to a specific wallet's extension documentation 54 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 55 | extension: any; 56 | 57 | // The raw signer object for convenience. Usually the implementer can derive this from the extension object. 58 | // Refer to a specific wallet's extension documentation 59 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 60 | signer: InjectedSigner | undefined; 61 | 62 | metadata: InjectedMetadata | undefined; 63 | 64 | provider: InjectedProvider | undefined; 65 | } 66 | 67 | interface Connector { 68 | enable: () => unknown; 69 | 70 | // The subscribe to accounts function 71 | subscribeAccounts: (callback: SubscriptionFn, ss58Format?: number) => Promise; 72 | 73 | getAccounts: (ss58Format?: number) => Promise; 74 | } 75 | 76 | interface WalletErrors { 77 | transformError: (err: WalletError) => Error; 78 | } 79 | 80 | export interface Wallet extends WalletData, WalletExtension, Connector, WalletErrors {} 81 | 82 | export interface WalletMobile { 83 | appName: MOBILE_WALLET_APPS; 84 | // Display name for the wallet extension 85 | title: string; 86 | // The URL to install the wallet extension 87 | installUrl: { 88 | android: string; 89 | ios: string; 90 | }; 91 | // The wallet logo 92 | logo: WalletLogoProps; 93 | } 94 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/account-selection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { Box, Button } from '@chakra-ui/react'; 3 | import { ArrowBackIcon } from '@chakra-ui/icons'; 4 | import { useSystemProperties } from '@substra-hooks/core'; 5 | import { 6 | accountsSelector, 7 | setAccountsSelector, 8 | setStepSelector, 9 | stepSelector, 10 | useAccountModalStore, 11 | useAccountsStore, 12 | } from '../../lib/store/use-account-store'; 13 | import { selectedWalletSelector, useWalletsStore } from '../../lib/store/use-wallet-store'; 14 | import { getWalletBySource } from '../../lib/wallets'; 15 | import { ACCOUNT_MODAL_STEPS } from '../../lib/store/types'; 16 | import AccountSelect from './account-select'; 17 | import NowAllowed from './not-allowed'; 18 | import { isMobile } from '../../lib/utils/ua-detect'; 19 | 20 | const AccountSelection = () => { 21 | const systemProperties = useSystemProperties(); 22 | const accounts = useAccountsStore(accountsSelector); 23 | const setAccounts = useAccountsStore(setAccountsSelector); 24 | const setStep = useAccountModalStore(setStepSelector); 25 | const step = useAccountModalStore(stepSelector); 26 | const selectedWallet = useWalletsStore(selectedWalletSelector); 27 | const wallet = getWalletBySource(selectedWallet); 28 | 29 | const onBackClick = () => { 30 | setStep(ACCOUNT_MODAL_STEPS.wallets); 31 | }; 32 | 33 | const modalContent = () => { 34 | switch (step) { 35 | case ACCOUNT_MODAL_STEPS.accounts: 36 | return ; 37 | 38 | case ACCOUNT_MODAL_STEPS.notAllowed: 39 | return ; 40 | default: 41 | return; 42 | } 43 | }; 44 | 45 | const subscribeToAccountWallets = async () => { 46 | if (wallet?.installed) { 47 | try { 48 | await wallet.enable(); 49 | } catch (e: any) { 50 | if (e.message.includes('not allowed to interact')) { 51 | setStep(ACCOUNT_MODAL_STEPS.notAllowed); 52 | } 53 | } 54 | await wallet.subscribeAccounts((accounts) => { 55 | accounts && setAccounts(accounts); 56 | }, systemProperties.ss58Format); 57 | } 58 | }; 59 | 60 | useEffect(() => { 61 | subscribeToAccountWallets(); 62 | }, []); 63 | 64 | return ( 65 | 66 | {!isMobile && ( 67 | 68 | 87 | 88 | )} 89 | 90 | {modalContent()} 91 | 92 | ); 93 | }; 94 | 95 | export default AccountSelection; 96 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/store/use-account-store.ts: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { persist } from 'zustand/middleware'; 3 | import { ACCOUNT_MODAL_STEPS } from './types'; 4 | import { WalletAccount } from '../types'; 5 | 6 | export type TAccountModalStore = { 7 | modalOpened: boolean; 8 | toggleAccountSelectionModal: (modalOpened: boolean, step?: ACCOUNT_MODAL_STEPS) => void; 9 | step: ACCOUNT_MODAL_STEPS; 10 | setStep: (step: ACCOUNT_MODAL_STEPS) => void; 11 | }; 12 | 13 | export const useAccountModalStore = create((set) => ({ 14 | modalOpened: false, 15 | toggleAccountSelectionModal: ( 16 | modalOpened: boolean, 17 | step: ACCOUNT_MODAL_STEPS = ACCOUNT_MODAL_STEPS.wallets, 18 | ) => { 19 | set({ modalOpened, step }); 20 | }, 21 | step: ACCOUNT_MODAL_STEPS.wallets, 22 | setStep: (step: ACCOUNT_MODAL_STEPS) => { 23 | set({ step }); 24 | }, 25 | })); 26 | 27 | export const modalOpenedSelector = (state: TAccountModalStore) => state.modalOpened; 28 | export const toggleAccountSelectionModalSelector = (state: TAccountModalStore) => 29 | state.toggleAccountSelectionModal; 30 | export const stepSelector = (state: TAccountModalStore) => state.step; 31 | export const setStepSelector = (state: TAccountModalStore) => state.setStep; 32 | 33 | export type TPurchaseTokensModalStore = { 34 | toggleRampModal: (rampModalOpened: boolean) => void; 35 | rampModalOpened: boolean; 36 | }; 37 | 38 | export const usePurchaseTokensModalStore = create((set) => ({ 39 | rampModalOpened: false, 40 | toggleRampModal: (rampModalOpened: boolean) => { 41 | set({ rampModalOpened }); 42 | }, 43 | })); 44 | 45 | export const rampModalOpenedSelector = (state: TPurchaseTokensModalStore) => state.rampModalOpened; 46 | export const toggleRampModalSelector = (state: TPurchaseTokensModalStore) => state.toggleRampModal; 47 | 48 | /* 49 | * Wallet accounts store 50 | */ 51 | 52 | export type TAccountsStore = { 53 | accounts: WalletAccount[] | null; 54 | setAccounts: (accounts: WalletAccount[] | null) => void; 55 | }; 56 | 57 | export const useAccountsStore = create((set) => ({ 58 | accounts: null, 59 | setAccounts: (accounts: WalletAccount[] | null) => { 60 | set({ accounts }); 61 | }, 62 | })); 63 | 64 | export const accountsSelector = (state: TAccountsStore) => state.accounts; 65 | export const setAccountsSelector = (state: TAccountsStore) => state.setAccounts; 66 | 67 | /* 68 | * Select account store 69 | */ 70 | export type TSelectedAccountsStore = { 71 | selectedAccount: WalletAccount | null; 72 | setSelectedAccount: (selectedAccount: WalletAccount | null) => void; 73 | }; 74 | 75 | export const useSelectedAccountsStore = create()( 76 | persist( 77 | (set) => ({ 78 | setSelectedAccount: (selectedAccount: WalletAccount | null) => { 79 | set({ selectedAccount }); 80 | }, 81 | selectedAccount: null, 82 | }), 83 | { 84 | name: 'selected-account-storage', 85 | }, 86 | ), 87 | ); 88 | 89 | export const setSelectedAccountSelector = (state: TSelectedAccountsStore) => 90 | state.setSelectedAccount; 91 | export const selectedAccountSelector = (state: TSelectedAccountsStore) => state.selectedAccount; 92 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/public/logos/talisman-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/account-radio.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Box, 4 | Flex, 5 | useRadio, 6 | RadioProps, 7 | useColorMode, 8 | IconButton, 9 | useToast, 10 | } from '@chakra-ui/react'; 11 | import { encodeAddress } from '@polkadot/util-crypto'; 12 | import { ISystemProperties } from '@substra-hooks/core'; 13 | import { BiLink } from '@react-icons/all-files/bi/BiLink'; 14 | import { BiCopy } from '@react-icons/all-files/bi/BiCopy'; 15 | import { CopyToClipboard } from 'react-copy-to-clipboard'; 16 | import { WalletAccount } from '../../lib/types'; 17 | import { shortenAccountId } from '../../lib/utils/shorten-account-id'; 18 | import IdentityAvatar from '../common/identity-avatar'; 19 | import { useScreenSize } from '../../lib/utils/use-screen-size'; 20 | 21 | interface IProps extends RadioProps { 22 | account: WalletAccount; 23 | systemProperties: ISystemProperties; 24 | } 25 | 26 | const AccountRadio = ({ account, systemProperties, ...restProps }: IProps) => { 27 | const toast = useToast(); 28 | const isDark = useColorMode().colorMode === 'dark'; 29 | const { getInputProps, getCheckboxProps } = useRadio(restProps); 30 | const input = getInputProps(); 31 | const checkbox = getCheckboxProps(); 32 | const { isSm } = useScreenSize(); 33 | const address = isSm 34 | ? shortenAccountId(encodeAddress(account.address, systemProperties.ss58Format), true, 6) 35 | : account.address; 36 | 37 | const onCopyRef = () => { 38 | toast({ 39 | title: 'Copied your referral link.', 40 | duration: 1000, 41 | }); 42 | }; 43 | 44 | const onCopyAddress = () => { 45 | toast({ 46 | title: 'Copied your address.', 47 | duration: 1000, 48 | }); 49 | }; 50 | 51 | return ( 52 | 53 | 54 | 70 | 71 | 72 | } 77 | aria-label="Copy referal link" 78 | /> 79 | 80 | 81 | } 86 | aria-label="Copy address" 87 | /> 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {account.name} 96 | 97 | 105 | {address} 106 | 107 | 108 | 109 | 110 | ); 111 | }; 112 | 113 | export default AccountRadio; 114 | -------------------------------------------------------------------------------- /packages/example-nextjs/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next'; 2 | import Head from 'next/head'; 3 | import styles from '../styles/Home.module.css'; 4 | import { 5 | useAccountBalance, 6 | useAssetBalance, 7 | usePolkadotExtension, 8 | useSystemProperties, 9 | useBlockSyncError, 10 | } from '@substra-hooks/core'; 11 | import { useEffect } from 'react'; 12 | import Link from 'next/link'; 13 | import { 14 | ConnectAccountModal, 15 | selectedAccountSelector, 16 | toggleAccountSelectionModalSelector, 17 | useAccountModalStore, 18 | useSelectedAccountsStore, 19 | selectedWalletSelector, 20 | useWalletsStore, 21 | ACCOUNT_MODAL_STEPS, 22 | } from '@rmrk-team/dotsama-wallet-react'; 23 | import { Box, Button } from '@chakra-ui/react'; 24 | 25 | const shortenAccountName = (name: string, sliceSize: number = 6) => { 26 | if (name) { 27 | const beginning = name.slice(0, sliceSize); 28 | 29 | return `${beginning}${name.length > sliceSize ? '...' : ''}`; 30 | } 31 | 32 | return ''; 33 | }; 34 | 35 | const Home: NextPage = () => { 36 | const blockSyncError = useBlockSyncError('development'); 37 | 38 | // console.log('systemProperties', systemProperties); 39 | 40 | // useEffect(() => { 41 | // if (!w3Enabled) { 42 | // w3enable(); 43 | // } 44 | // if (initialised && !w3Enabled) { 45 | // console.log('polkadot.js is disabled'); 46 | // } 47 | // console.log('initialised', initialised); 48 | // }, [w3Enabled, initialised]); 49 | // 50 | // console.log('accounts', accounts); 51 | // 52 | // console.log('balancePayload', balancePayload); 53 | 54 | // console.log('assetPayload', assetPayload); 55 | 56 | const selectedAccount = useSelectedAccountsStore(selectedAccountSelector); 57 | const toggleAccountSelectionModal = useAccountModalStore(toggleAccountSelectionModalSelector); 58 | const selectedWallet = useWalletsStore(selectedWalletSelector); 59 | const openSubstrateAccountsModal = () => { 60 | toggleAccountSelectionModal(true, selectedWallet ? ACCOUNT_MODAL_STEPS.accounts : undefined); 61 | }; 62 | const accountCopy = selectedAccount?.name || selectedAccount?.address; 63 | 64 | const balancePayload = useAccountBalance(selectedAccount?.address as string); 65 | const assetPayload = useAssetBalance(selectedAccount?.address as string, 8, 'statemine'); 66 | const systemProperties = useSystemProperties(); 67 | 68 | console.log('assetPayload', assetPayload) 69 | 70 | return ( 71 | 72 | 73 | Create Next App 74 | 75 | 76 | 77 | 78 |
79 | 80 | 81 | 82 | 85 | 86 | {blockSyncError && ( 87 |
88 |
Block sync error:
89 |
{JSON.stringify(blockSyncError, null, 2)}
90 |
91 |
92 | )} 93 |
Balance: {balancePayload?.balance.formatted}
94 |
Locked Balance: {balancePayload && balancePayload?.locked?.formatted}
95 |
Reserved Balance: {balancePayload?.reserved?.formatted}
96 |
Total Balance: {balancePayload?.total?.formatted}
97 |
Available Balance: {balancePayload?.available?.formatted}
98 | 99 |
100 |
Asset balance: {assetPayload?.balance.formatted}
101 | 102 |
103 | Page two 104 |
105 |
106 | ); 107 | }; 108 | 109 | export default Home; 110 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/base-dotsama-wallet/base-dotsama-wallet.ts: -------------------------------------------------------------------------------- 1 | import type { Signer as InjectedSigner } from '@polkadot/api/types'; 2 | import { SubscriptionFn, Wallet, WalletAccount } from '../types'; 3 | import { 4 | InjectedAccount, 5 | InjectedExtension, 6 | InjectedMetadata, 7 | InjectedProvider, 8 | InjectedWindow, 9 | } from '@polkadot/extension-inject/types'; 10 | import { encodeAddress } from '@polkadot/keyring'; 11 | import { WalletError } from '../errors/base-wallet-error'; 12 | import { AuthError } from '../errors/auth-error'; 13 | 14 | const DAPP_NAME = 'Singular'; 15 | 16 | export class BaseDotsamaWallet implements Omit { 17 | extensionName = ''; 18 | title = ''; 19 | installUrl = { 20 | chrome: '', 21 | firefox: '', 22 | }; 23 | logo = { 24 | src: '', 25 | alt: '', 26 | }; 27 | 28 | _extension: InjectedExtension | undefined; 29 | _signer: InjectedSigner | undefined; 30 | _metadata: InjectedMetadata | undefined; 31 | _provider: InjectedProvider | undefined; 32 | 33 | // API docs: https://polkadot.js.org/docs/extension/ 34 | get extension() { 35 | return this._extension; 36 | } 37 | 38 | // API docs: https://polkadot.js.org/docs/extension/ 39 | get signer() { 40 | return this._signer; 41 | } 42 | 43 | get metadata() { 44 | return this._metadata; 45 | } 46 | 47 | get provider() { 48 | return this._provider; 49 | } 50 | 51 | get installed() { 52 | const injectedWindow = window as Window & InjectedWindow; 53 | const injectedExtension = injectedWindow?.injectedWeb3?.[this.extensionName]; 54 | 55 | return !!injectedExtension; 56 | } 57 | 58 | get rawExtension() { 59 | const injectedWindow = window as Window & InjectedWindow; 60 | const injectedExtension = injectedWindow?.injectedWeb3?.[this.extensionName]; 61 | return injectedExtension; 62 | } 63 | 64 | transformError = (err: Error): WalletError | Error => { 65 | if (err.message.includes('pending authorization request')) { 66 | return new AuthError(err.message, this as Wallet); 67 | } 68 | return err; 69 | }; 70 | 71 | enable = async () => { 72 | if (!this.installed) { 73 | return; 74 | } 75 | 76 | try { 77 | const injectedExtension = this.rawExtension; 78 | const rawExtension = await injectedExtension?.enable?.(DAPP_NAME); 79 | if (!rawExtension || !injectedExtension.version) { 80 | return; 81 | } 82 | 83 | const extension: InjectedExtension = { 84 | ...rawExtension, 85 | // Manually add `InjectedExtensionInfo` so as to have a consistent response. 86 | name: this.extensionName, 87 | version: injectedExtension.version, 88 | }; 89 | 90 | this._extension = extension; 91 | this._signer = extension?.signer; 92 | this._metadata = extension?.metadata; 93 | this._provider = extension?.provider; 94 | } catch (err) { 95 | throw this.transformError(err as WalletError); 96 | } 97 | }; 98 | 99 | subscribeAccounts = async (callback: SubscriptionFn, ss58Format?: number) => { 100 | if (!this._extension) { 101 | await this?.enable(); 102 | } 103 | 104 | if (!this._extension) { 105 | callback(undefined); 106 | return null; 107 | } 108 | 109 | const unsubscribe = this._extension.accounts.subscribe((accounts: InjectedAccount[]) => { 110 | const accountsWithWallet = accounts.filter(a => a.type === 'sr25519').map((account) => { 111 | return { 112 | ...account, 113 | address: encodeAddress(account.address, ss58Format), 114 | source: this._extension?.name as string, 115 | // Added extra fields here for convenience 116 | wallet: this, 117 | signer: this._extension?.signer, 118 | }; 119 | }); 120 | callback(accountsWithWallet as WalletAccount[]); 121 | }); 122 | 123 | return unsubscribe; 124 | }; 125 | 126 | get isActive() { 127 | return Boolean(this.signer); 128 | } 129 | 130 | public async getAccounts(ss58Format?: number): Promise { 131 | if (!this._extension) { 132 | await this?.enable(); 133 | } 134 | 135 | if (!this._extension) { 136 | return null; 137 | } 138 | 139 | const accounts = await this._extension.accounts.get(); 140 | 141 | return accounts.map((account) => { 142 | return { 143 | ...account, 144 | address: encodeAddress(account.address, ss58Format), 145 | source: this._extension?.name as string, 146 | // Added extra fields here for convenience 147 | wallet: this, 148 | signer: this._extension?.signer, 149 | }; 150 | }); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/components/account-select/account-select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | Alert, 4 | AlertDescription, 5 | AlertIcon, 6 | Box, 7 | Button, 8 | Flex, 9 | Stack, 10 | useRadioGroup, 11 | } from '@chakra-ui/react'; 12 | import AccountRadio from './account-radio'; 13 | import { useSystemProperties } from '@substra-hooks/core'; 14 | import { AiOutlineDisconnect } from '@react-icons/all-files/ai/AiOutlineDisconnect'; 15 | import Fuse from 'fuse.js'; 16 | import { WalletAccount } from '../../lib/types'; 17 | import { 18 | selectedAccountSelector, 19 | setSelectedAccountSelector, 20 | toggleAccountSelectionModalSelector, 21 | useAccountModalStore, 22 | useSelectedAccountsStore, 23 | } from '../../lib/store/use-account-store'; 24 | import { 25 | selectedWalletSelector, 26 | setSelectedWalletSelector, 27 | useWalletsStore, 28 | } from '../../lib/store/use-wallet-store'; 29 | import { getWalletBySource } from '../../lib/wallets'; 30 | import { useSearchInObjects } from '../../hooks/use-search-in-objects'; 31 | import SearchBox from '../common/search-box'; 32 | 33 | interface IProps { 34 | accounts?: WalletAccount[] | null; 35 | } 36 | 37 | const FUSE_OPTIONS: Fuse.IFuseOptions = { 38 | keys: [ 39 | { 40 | name: 'name', 41 | weight: 0.7, 42 | }, 43 | { 44 | name: 'address', 45 | weight: 0.3, 46 | }, 47 | ], 48 | threshold: 0.45, 49 | }; 50 | 51 | const AccountSelect = ({ accounts }: IProps) => { 52 | const systemProperties = useSystemProperties(); 53 | const toggleAccountSelectionModal = useAccountModalStore(toggleAccountSelectionModalSelector); 54 | const selectedAccount = useSelectedAccountsStore(selectedAccountSelector); 55 | const setSelectedAccount = useSelectedAccountsStore(setSelectedAccountSelector); 56 | const setSelectedWallet = useWalletsStore(setSelectedWalletSelector); 57 | const selectedWallet = useWalletsStore(selectedWalletSelector); 58 | const wallet = getWalletBySource(selectedWallet); 59 | const walletName = wallet?.title; 60 | 61 | const selectAccount = async (accountId: string) => { 62 | if (accounts) { 63 | const newAccount = accounts.find((account) => account.address === accountId); 64 | if (newAccount) { 65 | setSelectedAccount(newAccount); 66 | } 67 | toggleAccountSelectionModal(false); 68 | } else { 69 | setSelectedAccount(null); 70 | } 71 | }; 72 | 73 | const disconnectSubstrateAccount = () => { 74 | setSelectedAccount(null); 75 | setSelectedWallet(null); 76 | toggleAccountSelectionModal(false); 77 | }; 78 | 79 | const { getRootProps, getRadioProps } = useRadioGroup({ 80 | defaultValue: selectedAccount?.address, 81 | name: 'account', 82 | onChange: selectAccount, 83 | }); 84 | 85 | const group = getRootProps(); 86 | 87 | const { filtered: filteredAccounts, searchBoxProps } = useSearchInObjects(accounts || [], { 88 | fuseOptions: FUSE_OPTIONS, 89 | }); 90 | 91 | return ( 92 | 93 | 94 | 95 | {accounts && accounts.length > 0 ? ( 96 | filteredAccounts.length > 0 ? ( 97 | filteredAccounts.map((account) => { 98 | const radio = getRadioProps({ value: account.address }); 99 | return ( 100 | 101 | 102 | 107 | 108 | 109 | ); 110 | }) 111 | ) : ( 112 | No matching accounts found 113 | ) 114 | ) : ( 115 | 116 | Sorry you don't have any accounts on {walletName} 117 | 118 | )} 119 | 120 | {selectedAccount && ( 121 | 124 | )} 125 | 126 | ); 127 | }; 128 | 129 | interface EmptyStateProps { 130 | children?: React.ReactNode 131 | }; 132 | 133 | const AccountSelectEmptyState: React.FC = ({ children }) => ( 134 | 135 | 136 | 137 | 138 | {children} 139 | 140 | 141 | 142 | ); 143 | 144 | export default AccountSelect; 145 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/public/logos/sub-wallet-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /packages/core/src/providers/substrahooks-provider/provider.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode, useEffect, useReducer } from 'react'; 2 | import { ApiPromise, WsProvider } from '@polkadot/api'; 3 | import { 4 | ApiProviders, 5 | initialBalancesState, 6 | initialProvidersState, 7 | ProvidersState, 8 | SubstraHooksContext, 9 | } from './context'; 10 | import { 11 | fetchSystemProperties, 12 | systemPropertiesDefaults, 13 | } from '../../helpers/fetch-system-properties'; 14 | import { useIsMountedRef } from '../../helpers/use-is-mounted-ref'; 15 | import { RegistryTypes } from '@polkadot/types/types'; 16 | import { balancesReducer } from '../../state/balances'; 17 | import { ErrorActionTypes, errorsReducer } from '../../state/errors'; 18 | import { initialErrorsState } from '.'; 19 | import { ProvidersActions, providersReducer, Types } from './reducer'; 20 | 21 | export type ApiProviderConfig = Record< 22 | string, 23 | { id: string; wsProviderUrl: string; types?: RegistryTypes } 24 | >; 25 | 26 | interface ISubstraHooksProviderProps { 27 | apiProviderConfig: ApiProviderConfig | null; 28 | defaultApiProviderId: string; 29 | autoInitialiseExtension?: boolean; 30 | children: ReactNode; 31 | } 32 | 33 | export const initPolkadotPromise = async ( 34 | id: string, 35 | wsProviderUrl: string, 36 | apiProvidersState: ProvidersState, 37 | apiProvidersStateDispatch: React.Dispatch, 38 | types?: RegistryTypes, 39 | ) => { 40 | if ( 41 | apiProvidersState.apiProviders[id] && 42 | apiProvidersState.apiProviders[id]?.rpcEndpoint === wsProviderUrl 43 | ) { 44 | return apiProvidersState.apiProviders[id]; 45 | } 46 | 47 | let polkadotApi = null; 48 | let systemProperties = systemPropertiesDefaults; 49 | try { 50 | const wsProvider = new WsProvider(wsProviderUrl); 51 | polkadotApi = await ApiPromise.create({ 52 | provider: wsProvider, 53 | types: types, 54 | throwOnConnect: true, 55 | }); 56 | await polkadotApi.isReady; 57 | systemProperties = await fetchSystemProperties(polkadotApi); 58 | apiProvidersStateDispatch({ 59 | type: Types.SET_PROVIDER, 60 | payload: { 61 | id, 62 | provider: { 63 | systemProperties, 64 | apiProvider: polkadotApi, 65 | wsProvider, 66 | rpcEndpoint: wsProviderUrl, 67 | }, 68 | }, 69 | }); 70 | } catch (error: any) { 71 | console.warn(`RPC ${wsProviderUrl} has failed with an error`, error); 72 | } 73 | 74 | apiProvidersStateDispatch({ 75 | type: Types.SET_PROVIDER, 76 | payload: { 77 | id, 78 | provider: { 79 | ...apiProvidersState.apiProviders[id], 80 | systemProperties, 81 | apiProvider: polkadotApi, 82 | rpcEndpoint: wsProviderUrl, 83 | }, 84 | }, 85 | }); 86 | 87 | return apiProvidersState.apiProviders[id]; 88 | }; 89 | 90 | const initAllApis = async ( 91 | apiProviderConfig: ApiProviderConfig, 92 | apiProvidersState: ProvidersState, 93 | apiProvidersStateDispatch: React.Dispatch, 94 | ) => { 95 | return await Promise.all( 96 | Object.keys(apiProviderConfig).map(async (configId) => 97 | initPolkadotPromise( 98 | apiProviderConfig[configId].id, 99 | apiProviderConfig[configId].wsProviderUrl, 100 | apiProvidersState, 101 | apiProvidersStateDispatch, 102 | apiProviderConfig[configId].types, 103 | ), 104 | ), 105 | ); 106 | }; 107 | 108 | export const SubstraHooksProvider = ({ 109 | children, 110 | apiProviderConfig, 111 | defaultApiProviderId, 112 | }: ISubstraHooksProviderProps) => { 113 | const [errorsState, errorsDispatch] = useReducer(errorsReducer, initialErrorsState); 114 | const [balancesState, balancesDispatch] = useReducer(balancesReducer, initialBalancesState); 115 | const [apiProvidersState, apiProvidersStateDispatch] = useReducer( 116 | providersReducer, 117 | initialProvidersState, 118 | ); 119 | const isMountedRef = useIsMountedRef(); 120 | 121 | useEffect(() => { 122 | if (apiProviderConfig && isMountedRef.current) { 123 | const initApisPromise = async () => { 124 | await initAllApis(apiProviderConfig, apiProvidersState, apiProvidersStateDispatch); 125 | }; 126 | 127 | initApisPromise(); 128 | } 129 | }, [JSON.stringify(apiProviderConfig), isMountedRef]); 130 | 131 | useEffect(() => { 132 | if (Object.keys(apiProvidersState.apiProviders).length > 0) { 133 | Object.keys(apiProvidersState.apiProviders).map((apiProviderId) => { 134 | const wsProvider = apiProvidersState.apiProviders[apiProviderId].wsProvider; 135 | const errorHandler = (error: Error | string) => { 136 | errorsDispatch({ 137 | type: ErrorActionTypes.BLOCK_SYNC_ERROR, 138 | payload: { 139 | network: apiProviderId, 140 | error: typeof error === 'string' ? new Error(error) : error, 141 | }, 142 | }); 143 | }; 144 | const connectedHandler = () => { 145 | errorsDispatch({ 146 | type: ErrorActionTypes.BLOCK_SYNC_ERROR, 147 | payload: { 148 | network: apiProviderId, 149 | error: undefined, 150 | }, 151 | }); 152 | }; 153 | wsProvider?.on('error', errorHandler); 154 | wsProvider?.on('connected', connectedHandler); 155 | }); 156 | } 157 | }, [ 158 | JSON.stringify(apiProvidersState.apiProviders?.provider?.rpcEndpoint), 159 | JSON.stringify(apiProvidersState.apiProviders?.provider?.systemProperties), 160 | isMountedRef, 161 | ]); 162 | 163 | return ( 164 | 174 | {children} 175 | 176 | ); 177 | }; 178 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # Deprecated 2 | 3 | # SubstraHooks Core 4 | 5 | [![No Maintenance Intended](http://unmaintained.tech/badge.svg)](http://unmaintained.tech/) 6 | 7 | SubstraHooks is a collection of useful react hooks that work with polkadot.js on [Substrate](https://substrate.io/) blockchains 8 | 9 | inspired by [useDApp](https://github.com/EthWorks/useDApp) 10 | 11 | ## Usage 12 | 13 | Add it to your project: 14 | 15 | ```console 16 | yarn add @substra-hooks/core @polkadot/api @polkadot/extension-dapp 17 | ``` 18 | 19 | Use it in your React app: 20 | 21 | ```tsx 22 | import React from 'react' 23 | import { SubstraHooksProvider } from '@substra-hooks/core'; 24 | 25 | export enum NETWORKS { 26 | kusama = 'kusama', 27 | statemine = 'statemine', 28 | } 29 | 30 | const apiProviderConfig = { 31 | [NETWORKS.kusama]: { 32 | id: NETWORKS.kusama, 33 | wsProviderUrl: 'wss://kusama-rpc.polkadot.io', 34 | }, 35 | [NETWORKS.statemine]: { 36 | id: NETWORKS.statemine, 37 | wsProviderUrl: 'wss://statemine-rpc.polkadot.io', 38 | }, 39 | } 40 | 41 | // Wrap everything in 42 | export default () => ( 43 | 44 | 45 | 46 | ) 47 | ``` 48 | 49 | ```tsx 50 | // App.tsx 51 | import React from 'react' 52 | import { useAccountBalance, useSystemProperties, useAssetBalance } from '@substra-hooks/core' 53 | 54 | const App = () => { 55 | const { accounts, w3enable, w3Enabled } = usePolkadotExtension(); 56 | const balancePayload = useAccountBalance(accounts?.[0]?.address || ''); 57 | const assetPayload = useAssetBalance(accounts?.[0]?.address || '', 8, NETWORKS.statemine); 58 | const systemProperties = useSystemProperties() 59 | 60 | console.log('systemProperties', systemProperties) 61 | 62 | useEffect(() => { 63 | if (!w3Enabled) { 64 | w3enable(); 65 | } 66 | }, [w3Enabled]) 67 | 68 | console.log('accounts', accounts) 69 | console.log('balanceFormatted', accounts?.[5]?.address || '', balanceFormatted); 70 | console.log('assetPayload', assetPayload); 71 | 72 | return ( 73 | <> 74 |
Balance: {balancePayload?.balance.formatted}
75 |
Locked Balance: {balancePayload && balancePayload?.locked?.formatted}
76 |
Reserved Balance: {balancePayload?.reserved?.formatted}
77 |
Total Balance: {balancePayload?.total?.formatted}
78 | 79 | ) 80 | } 81 | ``` 82 | 83 | If your app is using SSR (i.e. next.js) then you need to dynamically import Provider with no SSR, create your own local Provider first 84 | 85 | ```tsx 86 | import { ReactNode } from 'react'; 87 | import { SubstraHooksProvider } from '@substra-hooks/core'; 88 | 89 | interface ISubstraHooksProviderProps { 90 | apiProviderConfig: ApiProviderConfig; 91 | children: ReactNode; 92 | } 93 | 94 | 95 | const SubstraHooksProviderSSR = ({ apiProviderConfig, children }: ISubstraHooksProviderProps) => { 96 | return ( 97 | 100 | {children} 101 | 102 | ); 103 | }; 104 | 105 | export default SubstraHooksProviderSSR; 106 | ``` 107 | 108 | ```tsx 109 | const SubstraHooksProviderSSR = dynamic(() => import('./substra-hook-provider'), { 110 | ssr: false, 111 | }); 112 | 113 | const MyApp = ({ Component, pageProps }: AppProps) => { 114 | 115 | return ( 116 | 117 | 118 | 119 | ); 120 | }; 121 | 122 | export default MyApp; 123 | ``` 124 | 125 | ## API 126 | 127 | ### Providers 128 | 129 | #### SubstraHooksProvider 130 | Main Provider that includes `ExtensionProvider` 131 | 132 | #### ExtensionProvider 133 | Provider that mainly deals with `polkadot browser extension` 134 | 135 | ### Hooks 136 | 137 | #### useApiProvider 138 | 139 | Returns polkadot.js `ApiPromise`. Returns default `ApiPromise` as defined by `defaultApiProviderId` on SubstraHooksProvider, additional argument can be passed to return different `ApiPromise` from default one 140 | 141 | `const polkadotStatemineApi = useApiProvider('statemine');` 142 | 143 | #### useSystemProperties 144 | 145 | Returns parsed results of `polkadotApi.rpc.system.properties` API in following format. 146 | ```ts 147 | { 148 | tokenDecimals: number; 149 | tokenSymbol: string; 150 | ss58Format: number; 151 | } 152 | ``` 153 | 154 | Returns system properties fetched from the chain connected by your default api provider, additional argument can be passed to return different system properties from different node 155 | 156 | `const systemProperties = useSystemProperties()` 157 | 158 | #### useAccountBalance 159 | 160 | Returns token balance of given address from the default node. 161 | 162 | `const { balanceFormatted, balanceRaw } = useAccountBalance(userEncodedAddress);` 163 | 164 | #### useAssetBalance 165 | 166 | Returns balance of specified asset id for given address from the default node. 167 | 168 | ```tsx 169 | const { balanceFormatted, balanceRaw } = useAssetBalance( 170 | userEncodedAddress, 171 | ASSET_ID, 172 | 'statemine', 173 | ); 174 | ``` 175 | 176 | #### useEncodedAddress 177 | 178 | Returns substrate address in a format of `ss58Format` of your default chain node 179 | 180 | `const ownerAddressEncoded = useEncodedAddress(owner);` 181 | 182 | #### usePolkadotExtension 183 | 184 | ```tsx 185 | import {useEffect} from "react"; 186 | ... 187 | const { w3Enabled, w3enable, accounts } = usePolkadotExtension(); 188 | 189 | const initialise = () => { 190 | if (!w3Enabled) { 191 | w3enable(); 192 | } 193 | }; 194 | 195 | useEffect(() => { 196 | if (!w3Enabled) { 197 | initialise(); 198 | } 199 | }, [w3Enabled]) 200 | 201 | console.log(accounts); 202 | 203 | ``` 204 | -------------------------------------------------------------------------------- /packages/dotsama-wallet-react/src/lib/logos-svg.ts: -------------------------------------------------------------------------------- 1 | /* 🤖 this file was generated by svg-to-ts */ 2 | export const icons: { [key in MyIconType]: string } = { 3 | polkadotJsLogo: 4 | '', 5 | subWalletLogo: 6 | '', 7 | talismanLogo: 8 | '' 9 | }; 10 | export type MyIconType = 'polkadotJsLogo' | 'subWalletLogo' | 'talismanLogo'; 11 | --------------------------------------------------------------------------------