diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 9f3ecb2a..00000000 --- a/.eslintignore +++ /dev/null @@ -1,8 +0,0 @@ -node_modules/* -dist/* -web-dev-server.config.js -tsup.config.ts -**/test/** -vite.config.ts -scripts/prepare-packages.ts -tests/e2e diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index f2107517..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -const Config = require('@vechain/repo-config'); - -module.exports = { - ...Config.EslintLibrary, - rules: { - ...Config.EslintLibrary.rules, - 'import/no-extraneous-dependencies': 'error', - }, -}; diff --git a/.gitignore b/.gitignore index 9f120d2a..7c8abae2 100644 --- a/.gitignore +++ b/.gitignore @@ -38,14 +38,5 @@ yarn-error.log* .vscode .parcel-cache +veworld-dist .reports - -packages/**/dist -packages/**/.turbo -packages/**/node_modules -apps/**/dist -apps/**/.turbo -apps/**/node_modules -docs/**/dist - -coverageUnit \ No newline at end of file diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 65b912cf..9cd1aa9a 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/LICENSE b/LICENSE index 329efe3f..51f73f5a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 Vechain +Copyright (c) 2023 VeChain Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 53f45718..4e252838 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,20 @@ -# vechain-dapp-kit - -The Vechain DAppKit is a TypeScript library that facilitates seamless interaction between vechain wallets (veworld, sync2) -and dApps, enhancing user experience and developer convenience. Please refer to [Vechain Docs](https://docs.vechain.org/developer-resources/sdks-and-providers/dapp-kit) for full documentation and usage. +
+

vechain-dapp-kit

+

+ A TypeScript library that facilitates seamless interaction between VeChain wallets. +

+

+ Quality Gate Status + Security Rating + Maintainability Rating + License: MIT +

+
+ +## Introduction + +The VeChain DAppKit is a TypeScript library that facilitates seamless interaction between VeChain wallets (VeWorld, sync2) +and dApps, enhancing user experience and developer convenience. Please refer to [VeChain Docs](https://docs.vechain.org/developer-resources/sdks-and-providers/dapp-kit) for full documentation and usage. ## Table of Contents @@ -19,34 +32,24 @@ and dApps, enhancing user experience and developer convenience. Please refer to ## Why ? - Allow easy interaction with all wallets. -- Currently, connex only plays nice with Sync / Sync2 +- Currently, Connex only plays nice with Sync / Sync2 - Enable a better UX for users ## Key features -Key Features a.k.a scope - -1. wallet connectivity - - key components that handle interaction with veworld and sync 2 - -2. customizable UI - - ability to totally customize the UI of components - -3. User Experience +1. **Wallet Connectivity**: Key components that handle interaction with VeWorld and Sync 2. - Consistent experience with Ethereum and other chains +2. **Customizable UI**: Ability to totally customize the UI of components. -4. Developer friendly +3. **User Experience**: Consistent experience with Ethereum and other chains. - Easy to adopt with proper documentation. +4. **Developer friendly**: Easy to adopt with proper documentation. --- ## Contributing -- Please refer to the [Contributing Guide](./CONTRIBUTING.md) for more information on how to contribute to the project. +Please refer to the [Contributing Guide](./CONTRIBUTING.md) for more information on how to contribute to the project. --- @@ -163,7 +166,7 @@ yarn test:e2e:headless ## Further Documentation & Usage -- Please refer to [Vechain Docs](https://docs.vechain.org/developer-resources/sdks-and-providers) for more information +- Please refer to [VeChain Docs](https://docs.vechain.org/developer-resources/sdks-and-providers) for more information on how to use the library. --- @@ -171,10 +174,15 @@ yarn test:e2e:headless ## Publishing ```bash -git clone git@github.com:vechainfoundation/vechain-dapp-kit.git -cd vechain-dapp-kit -git checkout X.Y.Z -yarn install:all -yarn build:release X.Y.Z -yarn changeset publish +# prepare the release, this will check out the release branch, install dependencies, build packages, test and update the package versions +yarn prepare:release X.Y.Z +``` + +Create the PR for the release branch `vX.Y.Z`. + +When the PR is merged, create the release on github called `X.Y.Z`, it will automatically tag the commit with the version `X.Y.Z`. + +```bash +# publish the release +yarn publish:release X.Y.Z ``` diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..c9e3ec07 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,17 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + ignores: ['**/*.config.ts', 'dist/**'], + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-console': ['error', { allow: ['error'] }], + 'eslint-comments/no-unused-disable': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, +}); diff --git a/examples/sample-angular-app/.eslintrc.js b/examples/sample-angular-app/.eslintrc.js deleted file mode 100644 index 4185cb4f..00000000 --- a/examples/sample-angular-app/.eslintrc.js +++ /dev/null @@ -1,9 +0,0 @@ -const Config = require('@vechain/repo-config'); - -module.exports = { - ...Config.EslintLibrary, - rules: { - 'no-constant-binary-expression': 'off', - 'eslint-comments/disable-enable-pair': 'off', - }, -}; diff --git a/examples/sample-angular-app/package.json b/examples/sample-angular-app/package.json index ecb9fb57..f00ca7e8 100644 --- a/examples/sample-angular-app/package.json +++ b/examples/sample-angular-app/package.json @@ -8,7 +8,6 @@ "clean": "rm -rf dist .turbo .angular", "dev": "ng serve --port 5004", "gh-pages-build": "ng build --configuration development --base-href '/vechain-dapp-kit/angular/'", - "lint": "eslint src --ext .js,.jsx,.ts,.tsx", "preview": "ng serve --port 5004", "purge": "yarn clean; rm -rf node_modules", "watch": "ng build --watch --configuration development" @@ -50,14 +49,13 @@ "@angular/cli": "^18.2.2", "@angular/compiler-cli": "^18.2.2", "@types/jasmine": "~3.10.0", - "@types/node": "^22.9.0", - "@vechain/repo-config": "https://github.com/vechain/repo-config#v0.0.1", + "@types/node": "^12.11.1", "jasmine-core": "~4.0.0", "karma": "~6.3.0", "karma-chrome-launcher": "~3.1.0", "karma-coverage": "~2.1.0", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "~1.7.0", - "typescript": "5.5.4" + "typescript": "5.4.2" } } diff --git a/examples/sample-angular-app/src/environments/environment.prod.ts b/examples/sample-angular-app/src/environments/environment.prod.ts index b931095f..3938526c 100644 --- a/examples/sample-angular-app/src/environments/environment.prod.ts +++ b/examples/sample-angular-app/src/environments/environment.prod.ts @@ -1,5 +1,4 @@ // eslint-disable @typescript-eslint/no-unsafe-assignment -// eslint-disable @typescript-eslint/no-unsafe-member-access // Packages import packageInfo from '../../package.json'; @@ -12,7 +11,6 @@ const baseUrl = scheme + host + port + path; export const environment = { production: true, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access version: packageInfo.version, appName: 'EasyAngular', envName: 'prod', diff --git a/examples/sample-angular-app/src/environments/environment.ts b/examples/sample-angular-app/src/environments/environment.ts index 5244976c..1158e730 100644 --- a/examples/sample-angular-app/src/environments/environment.ts +++ b/examples/sample-angular-app/src/environments/environment.ts @@ -1,5 +1,4 @@ // eslint-disable @typescript-eslint/no-unsafe-assignment -// eslint-disable @typescript-eslint/no-unsafe-member-access // This file can be replaced during build by using the `fileReplacements` array. // `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. @@ -18,7 +17,6 @@ const baseUrl = scheme + host + port + path; export const environment = { production: false, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access version: packageInfo.version, appName: 'EasyAngular', envName: 'local', diff --git a/examples/sample-angular-app/src/main.ts b/examples/sample-angular-app/src/main.ts index 5d15c9f8..ae94c856 100644 --- a/examples/sample-angular-app/src/main.ts +++ b/examples/sample-angular-app/src/main.ts @@ -3,5 +3,4 @@ import { AppComponent } from './app/app.component'; bootstrapApplication(AppComponent, { providers: [], - // eslint-disable-next-line no-console }).catch((err) => console.error(err)); diff --git a/examples/sample-angular-app/src/polyfills.ts b/examples/sample-angular-app/src/polyfills.ts index 3db6b9bd..730b959b 100644 --- a/examples/sample-angular-app/src/polyfills.ts +++ b/examples/sample-angular-app/src/polyfills.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ /** * This file includes polyfills needed by Angular and is loaded before the app. * You can add your own extra polyfills to this file. @@ -55,6 +53,7 @@ import 'zone.js'; // Included with Angular CLI. import '@angular/localize/init'; (window as any).global = window; +// eslint-disable-next-line @typescript-eslint/no-require-imports global.Buffer = global.Buffer || require('buffer').Buffer; (window as any).process = { env: { DEBUG: undefined }, diff --git a/examples/sample-angular-app/tsconfig.spec.json b/examples/sample-angular-app/tsconfig.spec.json deleted file mode 100644 index e6a51278..00000000 --- a/examples/sample-angular-app/tsconfig.spec.json +++ /dev/null @@ -1,10 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "outDir": "./out-tsc/spec", - "types": ["jasmine"] - }, - "files": ["src/test.ts", "src/polyfills.ts"], - "include": ["src/**/*.spec.ts", "src/**/*.d.ts"] -} diff --git a/examples/sample-next-app/.eslintignore b/examples/sample-next-app/.eslintignore deleted file mode 100644 index 35e54460..00000000 --- a/examples/sample-next-app/.eslintignore +++ /dev/null @@ -1,2 +0,0 @@ -.eslintrc.js -tsconfig.json \ No newline at end of file diff --git a/examples/sample-next-app/package.json b/examples/sample-next-app/package.json index fe2018e1..9a6baecf 100644 --- a/examples/sample-next-app/package.json +++ b/examples/sample-next-app/package.json @@ -11,10 +11,10 @@ "purge": "yarn clean; rm -rf node_modules" }, "dependencies": { - "@vechain/dapp-kit": "workspace:^", - "@vechain/dapp-kit-react": "workspace:^", - "@vechain/dapp-kit-ui": "workspace:^", - "next": "15.0.3", + "@vechain/dapp-kit": "*", + "@vechain/dapp-kit-react": "*", + "@vechain/dapp-kit-ui": "*", + "next": "14.2.10", "react": "^18", "react-dom": "^18" }, @@ -23,7 +23,7 @@ "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", - "eslint": "^8", + "eslint": "^9.12.0", "eslint-config-next": "14.1.4", "typescript": "5.3.3" } diff --git a/examples/sample-next-app/tsconfig.json b/examples/sample-next-app/tsconfig.json index 70cb05a2..b5e2b899 100644 --- a/examples/sample-next-app/tsconfig.json +++ b/examples/sample-next-app/tsconfig.json @@ -24,6 +24,12 @@ }, "typeRoots": ["node_modules/@types"] }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "dist/types/**/*.ts" + ], "exclude": ["node_modules"] } diff --git a/examples/sample-react-app/.eslintrc.cjs b/examples/sample-react-app/.eslintrc.cjs deleted file mode 100644 index 74080493..00000000 --- a/examples/sample-react-app/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -}; diff --git a/examples/sample-react-app/eslint.config.mjs b/examples/sample-react-app/eslint.config.mjs new file mode 100644 index 00000000..c9e3ec07 --- /dev/null +++ b/examples/sample-react-app/eslint.config.mjs @@ -0,0 +1,17 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + ignores: ['**/*.config.ts', 'dist/**'], + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-console': ['error', { allow: ['error'] }], + 'eslint-comments/no-unused-disable': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, +}); diff --git a/examples/sample-react-app/package.json b/examples/sample-react-app/package.json index 60b5070c..22721a43 100644 --- a/examples/sample-react-app/package.json +++ b/examples/sample-react-app/package.json @@ -8,7 +8,7 @@ "clean": "rm -rf dist .turbo", "dev": "vite", "gh-pages-build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "lint": "eslint", "preview": "vite preview --mode=development", "purge": "yarn clean; rm -rf node_modules", "test": "vitest" @@ -23,7 +23,7 @@ "ethers": "6.13.2", "react": "^18.2.0", "react-dom": "^18.2.0", - "vite": "^5.0.12" + "vite": "^5.3.6" }, "devDependencies": { "@originjs/vite-plugin-commonjs": "^1.0.3", @@ -32,7 +32,7 @@ "@typescript-eslint/eslint-plugin": "^6.10.0", "@typescript-eslint/parser": "^6.10.0", "@vitejs/plugin-react": "^4.2.0", - "eslint": "^8.53.0", + "eslint": "^9.12.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", "react-test-renderer": "^18.2.0", diff --git a/examples/sample-react-app/src/App.tsx b/examples/sample-react-app/src/App.tsx index 8f21d7bf..76359ffb 100644 --- a/examples/sample-react-app/src/App.tsx +++ b/examples/sample-react-app/src/App.tsx @@ -1,81 +1,28 @@ import { - useThor, + WalletButton, useWallet, useWalletModal, - WalletButton, } from '@vechain/dapp-kit-react'; -import { useEffect, useMemo, useState } from 'react'; -import { Counter } from './counter.ts'; +import { friendlyAddress } from '@vechain/dapp-kit-ui'; +import { useEffect, useState } from 'react'; function App() { - const { account, signer } = useWallet(); - const thor = useThor(); - const [count, setCount] = useState(BigInt(0)); - const [error, setError] = useState(); - const [txId, setTxId] = useState(''); - const [loading, setLoading] = useState(false); - const { open, onConnectionStatusChange } = useWalletModal(); - const [buttonText, setButtonText] = useState('Connect Custom Button'); - - const counterContract = useMemo(() => { - return Counter.load(thor, signer); - }, [thor, signer]); + const { account, accountDomain, isAccountDomainLoading } = useWallet(); - useEffect(() => { - const loadCounter = async () => { - const counter = await counterContract.read.counter({ - revision: {}, - }); - setCount(counter[0]); - }; - - loadCounter(); - }, [counterContract, loading, error, txId]); - - useEffect(() => { - const handleConnected = (address: string | null) => { - if (address) { - const formattedAddress = `${address.slice( - 0, - 6, - )}...${address.slice(-4)}`; - setButtonText(`Disconnect from ${formattedAddress}`); - } else { - setButtonText('Connect Custom Button'); - } - }; - - handleConnected(account); - - onConnectionStatusChange(handleConnected); - }, [account, onConnectionStatusChange]); + const { open } = useWalletModal(); + const [buttonText, setButtonText] = useState('Connect Custom Button'); useEffect(() => { - console.log('signer', signer); - }, [signer]); - - const testTx = async () => { - setTxId(''); - setError(undefined); - try { - setLoading(true); - - // TODO: Set the delegation URL so that transactions are free - const tx = await counterContract.transact.increment(); - - const receipt = await tx.wait(); - if (receipt == null || receipt.reverted) { - setError(new Error('Transaction failed')); - return; - } - - setTxId(receipt.meta.txID!); - } catch (e) { - setError(e as Error); - } finally { - setLoading(false); + if (account) { + const addressOrDomain = + accountDomain && !isAccountDomainLoading + ? accountDomain + : friendlyAddress(account || ''); + setButtonText(`Disconnect from ${addressOrDomain}`); + } else { + setButtonText('Connect Custom Button'); } - }; + }, [account, accountDomain, isAccountDomainLoading]); return (
@@ -84,15 +31,8 @@ function App() {
custom button:
-

-
Counter
- {account && !loading && } - {loading &&
Loading...
} - {error &&
Error: {error.message}
} - {account && txId &&
Transaction ID: {txId}
} -
Counter: {count.toString()}
); } -export default App; +export default App; \ No newline at end of file diff --git a/examples/sample-react-app/src/main.tsx b/examples/sample-react-app/src/main.tsx index f593cb6b..105360e7 100644 --- a/examples/sample-react-app/src/main.tsx +++ b/examples/sample-react-app/src/main.tsx @@ -19,7 +19,7 @@ const walletConnectOptions: WalletConnectOptions = { ReactDOM.createRoot(document.getElementById('root')!).render( { + return { + plugins: [nodePolyfills(), react()], + build: { + commonjsOptions: { + transformMixedEsModules: true + } + }, + preview: { + port: 5001, + strictPort: true + }, + server: { + port: 5001, + strictPort: true, + host: true, + origin: "http://0.0.0.0:5001" + }, + //vitest + test: { + globals: true, + environment: "happy-dom", + setupFiles: [ + resolve(__vite_injected_original_dirname, "test/setup/setup.ts"), + resolve(__vite_injected_original_dirname, "test/setup/resizeObserverMock.ts") + ] + }, + base: mode === "production" ? "/vechain-dapp-kit/react/" : "/" + }; +}); +export { + vite_config_default as default +}; +//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvVXNlcnMvZGF2aWRlY2FycGluaS9hcHBzL3ZlY2hhaW4tZGFwcC1raXQvZXhhbXBsZXMvc2FtcGxlLXJlYWN0LWFwcFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL1VzZXJzL2RhdmlkZWNhcnBpbmkvYXBwcy92ZWNoYWluLWRhcHAta2l0L2V4YW1wbGVzL3NhbXBsZS1yZWFjdC1hcHAvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL1VzZXJzL2RhdmlkZWNhcnBpbmkvYXBwcy92ZWNoYWluLWRhcHAta2l0L2V4YW1wbGVzL3NhbXBsZS1yZWFjdC1hcHAvdml0ZS5jb25maWcudHNcIjsvLy8gPHJlZmVyZW5jZSB0eXBlcz1cInZpdGVzdFwiIC8+XG5cbmltcG9ydCB7IGRlZmluZUNvbmZpZyB9IGZyb20gJ3ZpdGUnO1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0JztcbmltcG9ydCB7IG5vZGVQb2x5ZmlsbHMgfSBmcm9tICd2aXRlLXBsdWdpbi1ub2RlLXBvbHlmaWxscyc7XG5pbXBvcnQgeyByZXNvbHZlIH0gZnJvbSAncGF0aCc7XG5cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZygoeyBtb2RlIH0pID0+IHtcbiAgICByZXR1cm4ge1xuICAgICAgICBwbHVnaW5zOiBbbm9kZVBvbHlmaWxscygpLCByZWFjdCgpXSxcbiAgICAgICAgYnVpbGQ6IHtcbiAgICAgICAgICAgIGNvbW1vbmpzT3B0aW9uczoge1xuICAgICAgICAgICAgICAgIHRyYW5zZm9ybU1peGVkRXNNb2R1bGVzOiB0cnVlLFxuICAgICAgICAgICAgfSxcbiAgICAgICAgfSxcbiAgICAgICAgcHJldmlldzoge1xuICAgICAgICAgICAgcG9ydDogNTAwMSxcbiAgICAgICAgICAgIHN0cmljdFBvcnQ6IHRydWUsXG4gICAgICAgIH0sXG4gICAgICAgIHNlcnZlcjoge1xuICAgICAgICAgICAgcG9ydDogNTAwMSxcbiAgICAgICAgICAgIHN0cmljdFBvcnQ6IHRydWUsXG4gICAgICAgICAgICBob3N0OiB0cnVlLFxuICAgICAgICAgICAgb3JpZ2luOiAnaHR0cDovLzAuMC4wLjA6NTAwMScsXG4gICAgICAgIH0sXG4gICAgICAgIC8vdml0ZXN0XG4gICAgICAgIHRlc3Q6IHtcbiAgICAgICAgICAgIGdsb2JhbHM6IHRydWUsXG4gICAgICAgICAgICBlbnZpcm9ubWVudDogJ2hhcHB5LWRvbScsXG4gICAgICAgICAgICBzZXR1cEZpbGVzOiBbXG4gICAgICAgICAgICAgICAgcmVzb2x2ZShfX2Rpcm5hbWUsICd0ZXN0L3NldHVwL3NldHVwLnRzJyksXG4gICAgICAgICAgICAgICAgcmVzb2x2ZShfX2Rpcm5hbWUsICd0ZXN0L3NldHVwL3Jlc2l6ZU9ic2VydmVyTW9jay50cycpLFxuICAgICAgICAgICAgXSxcbiAgICAgICAgfSxcbiAgICAgICAgYmFzZTogbW9kZSA9PT0gJ3Byb2R1Y3Rpb24nID8gJy92ZWNoYWluLWRhcHAta2l0L3JlYWN0LycgOiAnLycsXG4gICAgfTtcbn0pO1xuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUVBLFNBQVMsb0JBQW9CO0FBQzdCLE9BQU8sV0FBVztBQUNsQixTQUFTLHFCQUFxQjtBQUM5QixTQUFTLGVBQWU7QUFMeEIsSUFBTSxtQ0FBbUM7QUFPekMsSUFBTyxzQkFBUSxhQUFhLENBQUMsRUFBRSxLQUFLLE1BQU07QUFDdEMsU0FBTztBQUFBLElBQ0gsU0FBUyxDQUFDLGNBQWMsR0FBRyxNQUFNLENBQUM7QUFBQSxJQUNsQyxPQUFPO0FBQUEsTUFDSCxpQkFBaUI7QUFBQSxRQUNiLHlCQUF5QjtBQUFBLE1BQzdCO0FBQUEsSUFDSjtBQUFBLElBQ0EsU0FBUztBQUFBLE1BQ0wsTUFBTTtBQUFBLE1BQ04sWUFBWTtBQUFBLElBQ2hCO0FBQUEsSUFDQSxRQUFRO0FBQUEsTUFDSixNQUFNO0FBQUEsTUFDTixZQUFZO0FBQUEsTUFDWixNQUFNO0FBQUEsTUFDTixRQUFRO0FBQUEsSUFDWjtBQUFBO0FBQUEsSUFFQSxNQUFNO0FBQUEsTUFDRixTQUFTO0FBQUEsTUFDVCxhQUFhO0FBQUEsTUFDYixZQUFZO0FBQUEsUUFDUixRQUFRLGtDQUFXLHFCQUFxQjtBQUFBLFFBQ3hDLFFBQVEsa0NBQVcsa0NBQWtDO0FBQUEsTUFDekQ7QUFBQSxJQUNKO0FBQUEsSUFDQSxNQUFNLFNBQVMsZUFBZSw2QkFBNkI7QUFBQSxFQUMvRDtBQUNKLENBQUM7IiwKICAibmFtZXMiOiBbXQp9Cg== diff --git a/examples/sample-remix-app/.eslintrc.cjs b/examples/sample-remix-app/.eslintrc.cjs deleted file mode 100644 index 4f6f59ee..00000000 --- a/examples/sample-remix-app/.eslintrc.cjs +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This is intended to be a basic starting point for linting in your app. - * It relies on recommended configs out of the box for simplicity, but you can - * and should modify this configuration to best suit your team's needs. - */ - -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - parserOptions: { - ecmaVersion: "latest", - sourceType: "module", - ecmaFeatures: { - jsx: true, - }, - }, - env: { - browser: true, - commonjs: true, - es6: true, - }, - ignorePatterns: ["!**/.server", "!**/.client"], - - // Base config - extends: ["eslint:recommended"], - - overrides: [ - // React - { - files: ["**/*.{js,jsx,ts,tsx}"], - plugins: ["react", "jsx-a11y"], - extends: [ - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended", - "plugin:jsx-a11y/recommended", - ], - settings: { - react: { - version: "detect", - }, - formComponents: ["Form"], - linkComponents: [ - { name: "Link", linkAttribute: "to" }, - { name: "NavLink", linkAttribute: "to" }, - ], - "import/resolver": { - typescript: {}, - }, - }, - }, - - // Typescript - { - files: ["**/*.{ts,tsx}"], - plugins: ["@typescript-eslint", "import"], - parser: "@typescript-eslint/parser", - settings: { - "import/internal-regex": "^~/", - "import/resolver": { - node: { - extensions: [".ts", ".tsx"], - }, - typescript: { - alwaysTryTypes: true, - }, - }, - }, - extends: [ - "plugin:@typescript-eslint/recommended", - "plugin:import/recommended", - "plugin:import/typescript", - ], - }, - - // Node - { - files: [".eslintrc.cjs"], - env: { - node: true, - }, - }, - ], -}; diff --git a/examples/sample-remix-app/package.json b/examples/sample-remix-app/package.json index c1d776af..07c2f25a 100644 --- a/examples/sample-remix-app/package.json +++ b/examples/sample-remix-app/package.json @@ -8,7 +8,6 @@ "build": "remix vite:build --mode=development", "dev": "remix vite:dev", "gh-pages-build": "remix vite:build", - "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .", "preview": "PORT=5007 remix-serve ./build/server/index.js", "typecheck": "tsc" }, @@ -16,7 +15,7 @@ "@remix-run/node": "^2.8.1", "@remix-run/react": "^2.8.1", "@remix-run/serve": "^2.8.1", - "@vechain/dapp-kit-react": "^1.0.12", + "@vechain/dapp-kit-react": "*", "buffer": "^6.0.3", "isbot": "^4.1.0", "react": "^18.2.0", @@ -29,14 +28,14 @@ "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.7.4", "@typescript-eslint/parser": "^6.7.4", - "eslint": "^8.38.0", + "eslint": "^9.12.0", "eslint-import-resolver-typescript": "^3.6.1", "eslint-plugin-import": "^2.28.1", "eslint-plugin-jsx-a11y": "^6.7.1", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "typescript": "5.3.3", - "vite": "^5.1.0", + "typescript": "^5.1.6", + "vite": "^5.3.6", "vite-plugin-node-polyfills": "^0.21.0", "vite-tsconfig-paths": "^4.2.1" }, diff --git a/examples/sample-svelte-app/.eslintignore b/examples/sample-svelte-app/.eslintignore deleted file mode 100644 index ca7e2daa..00000000 --- a/examples/sample-svelte-app/.eslintignore +++ /dev/null @@ -1,14 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock -.eslintrc.cjs diff --git a/examples/sample-svelte-app/.eslintrc.cjs b/examples/sample-svelte-app/.eslintrc.cjs deleted file mode 100644 index abfdd0be..00000000 --- a/examples/sample-svelte-app/.eslintrc.cjs +++ /dev/null @@ -1,8 +0,0 @@ -const Config = require('@vechain/repo-config'); - -module.exports = { - ...Config.EslintLibrary, - rules: { - 'no-constant-binary-expression': 'off', - }, -}; diff --git a/examples/sample-svelte-app/eslint.config.mjs b/examples/sample-svelte-app/eslint.config.mjs new file mode 100644 index 00000000..ef17617a --- /dev/null +++ b/examples/sample-svelte-app/eslint.config.mjs @@ -0,0 +1,5 @@ +export default { + rules: { + 'no-constant-binary-expression': 'off', + }, +}; diff --git a/examples/sample-svelte-app/package.json b/examples/sample-svelte-app/package.json index d4592306..0c46b905 100644 --- a/examples/sample-svelte-app/package.json +++ b/examples/sample-svelte-app/package.json @@ -8,7 +8,7 @@ "clean": "rm -rf .svelte-kit dist .turbo", "dev": "vite dev", "gh-pages-build": "BASE_PATH='/vechain-dapp-kit/svelte' vite build", - "lint": "eslint src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint", "preview": "vite preview", "purge": "yarn clean; rm -rf node_modules" }, @@ -23,8 +23,7 @@ "@sveltejs/kit": "^1.27.4", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", - "@vechain/repo-config": "https://github.com/vechain/repo-config#v0.0.1", - "eslint": "^8.28.0", + "eslint": "^9.12.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-svelte": "^2.30.0", "prettier": "^3.0.0", @@ -32,8 +31,8 @@ "svelte": "^4.0.5", "svelte-check": "^3.6.0", "tslib": "^2.4.1", - "typescript": "5.3.3", - "vite": "^4.5.2", + "typescript": "^5.0.0", + "vite": "^4.5.5", "vite-plugin-node-polyfills": "^0.16.0" } } diff --git a/examples/sample-svelte-app/svelte.config.js b/examples/sample-svelte-app/svelte.config.js index dcee8126..30257126 100644 --- a/examples/sample-svelte-app/svelte.config.js +++ b/examples/sample-svelte-app/svelte.config.js @@ -25,5 +25,4 @@ const config = { }, }; -// eslint-disable-next-line import/no-default-export export default config; diff --git a/examples/sample-vanilla-app/package.json b/examples/sample-vanilla-app/package.json index d808ad9b..4ab7c027 100644 --- a/examples/sample-vanilla-app/package.json +++ b/examples/sample-vanilla-app/package.json @@ -15,7 +15,7 @@ "@vechain/dapp-kit-ui": "workspace:^" }, "devDependencies": { - "typescript": "5.3.3", - "vite": "^5.2.0" + "typescript": "^5.2.2", + "vite": "^5.3.6" } } diff --git a/examples/sample-vanilla-app/src/main.ts b/examples/sample-vanilla-app/src/main.ts index 9f23b4df..1ed815ad 100644 --- a/examples/sample-vanilla-app/src/main.ts +++ b/examples/sample-vanilla-app/src/main.ts @@ -1,13 +1,13 @@ -import { DAppKitUI } from '@vechain/dapp-kit-ui'; +import { DAppKitUI, friendlyAddress } from '@vechain/dapp-kit-ui'; document.querySelector('#app')!.innerHTML = ` -
-

Vanilla JS

-
kit button:
- -
custom button:
- -
+
+

Vanilla JS

+
kit button:
+ +
custom button:
+ +
`; const walletConnectOptions = { @@ -21,7 +21,7 @@ const walletConnectOptions = { }; DAppKitUI.configure({ - nodeUrl: 'https://testnet.vechain.org/', + nodeUrl: 'https://mainnet.vechain.org/', walletConnectOptions, usePersistence: true, }); @@ -35,18 +35,28 @@ if (customButton) { DAppKitUI.modal.open(); }); - const handleConnected = (address: string | null) => { + const render = () => { + const address = DAppKitUI.wallet.state.address; + const accountDomain = DAppKitUI.wallet.state.accountDomain; + const isAccountDomainLoading = + DAppKitUI.wallet.state.isAccountDomainLoading; + + const addressOrDomain = + accountDomain && !isAccountDomainLoading + ? accountDomain + : friendlyAddress(address || ''); + if (address) { - const formattedAddress = `${address.slice(0, 6)}...${address.slice( - -4, - )}`; - customButton.innerText = `Disconnect from ${formattedAddress}`; + customButton.innerText = `Disconnect from ${addressOrDomain}`; } else { customButton.innerText = 'Connect Custom Button'; } }; - handleConnected(DAppKitUI.wallet.state.address); + render(); - DAppKitUI.modal.onConnectionStatusChange(handleConnected); + DAppKitUI.modal.onConnectionStatusChange(render); + DAppKitUI.wallet.subscribeToKey('address', render); + DAppKitUI.wallet.subscribeToKey('accountDomain', render); + DAppKitUI.wallet.subscribeToKey('isAccountDomainLoading', render); } diff --git a/examples/sample-vue-app/package.json b/examples/sample-vue-app/package.json index c0ea3a64..f1ab3273 100644 --- a/examples/sample-vue-app/package.json +++ b/examples/sample-vue-app/package.json @@ -16,8 +16,8 @@ }, "devDependencies": { "@vitejs/plugin-vue": "^5.0.4", - "typescript": "5.3.3", - "vite": "^5.0.12", + "typescript": "^5.2.2", + "vite": "^5.3.6", "vite-plugin-node-polyfills": "^0.17.0", "vue-tsc": "^2.0.6" } diff --git a/package.json b/package.json index 74112219..a268b2f7 100755 --- a/package.json +++ b/package.json @@ -9,15 +9,15 @@ "tests/*" ], "scripts": { + "install:all": "yarn && yarn run build:deps", "build": "turbo run build", "build-react-kit": "turbo run build --filter='@vechain/dapp-kit-react'", "build:deps": "turbo build --no-daemon --filter='@vechain/*'", - "build:release": "ts-node scripts/prepare-packages.ts", + "prepare:release": "ts-node scripts/prepare-packages.ts", + "publish:release": "ts-node scripts/publish-packages.ts", "clean": "rm -rf .turbo .parcel-cache .reports build && npx turbo@latest run clean", "dev": "turbo run dev --filter='@vechain/*'", - "format": "prettier --write \"**/*.{ts,tsx,md,json,js,jsx}\"", "gh-pages-build": "turbo run gh-pages-build", - "install:all": "yarn && yarn run build:deps", "lint": "turbo run lint", "prepare": "husky install", "preview": "turbo run preview", @@ -47,9 +47,8 @@ "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@changesets/cli": "^2.27.1", "@commitlint/config-conventional": "^18.0.0", - "@vechain/repo-config": "https://github.com/vechain/repo-config#v0.0.1", "commitlint": "^18.0.0", - "eslint": "^8.4.1", + "eslint": "^9.12.0", "eslint-plugin-prefer-arrow": "1.2.3", "husky": "^8.0.0", "lint-staged": "^15.0.2", @@ -57,7 +56,8 @@ "punycode": "^1.4.1", "ts-node": "^10.9.2", "turbo": "latest", - "typescript": "5.3.3" + "typescript": "5.3.3", + "typescript-eslint": "^8.11.0" }, "packageManager": "yarn@4.5.1" } diff --git a/packages/dapp-kit-react/.eslintrc.cjs b/packages/dapp-kit-react/.eslintrc.cjs deleted file mode 100644 index 94a56c6f..00000000 --- a/packages/dapp-kit-react/.eslintrc.cjs +++ /dev/null @@ -1,14 +0,0 @@ -const Config = require('@vechain/repo-config'); - -module.exports = { - ...Config.EslintReact, - ignorePatterns: [ - ...Config.EslintReact.ignorePatterns, - '*.test.ts', - 'test/**', - ], - rules: { - ...Config.EslintReact.rules, - 'import/no-extraneous-dependencies': 'error', - }, -}; diff --git a/packages/dapp-kit-react/eslint.config.mjs b/packages/dapp-kit-react/eslint.config.mjs new file mode 100644 index 00000000..c9e3ec07 --- /dev/null +++ b/packages/dapp-kit-react/eslint.config.mjs @@ -0,0 +1,17 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + ignores: ['**/*.config.ts', 'dist/**'], + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-console': ['error', { allow: ['error'] }], + 'eslint-comments/no-unused-disable': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, +}); diff --git a/packages/dapp-kit-react/package.json b/packages/dapp-kit-react/package.json index 0cf10d0f..e12c8830 100644 --- a/packages/dapp-kit-react/package.json +++ b/packages/dapp-kit-react/package.json @@ -1,6 +1,6 @@ { "name": "@vechain/dapp-kit-react", - "version": "1.0.11", + "version": "1.1.1", "homepage": "https://github.com/vechain/vechain-dapp-kit", "repository": "github:vechain/vechain-dapp-kit", "license": "MIT", @@ -18,26 +18,24 @@ "scripts": { "build": "tsup", "clean": "rm -rf dist .turbo", - "lint": "tsc --noEmit && eslint src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint", "purge": "yarn clean && rm -rf node_modules", "test": "vitest run --coverage", "watch": "yarn build --watch" }, "dependencies": { "@lit/react": "^1.0.1", - "@vechain/dapp-kit": "workspace:^", - "@vechain/dapp-kit-ui": "workspace:^", - "@vechain/sdk-core": "1.0.0-rc.1", - "@vechain/sdk-network": "1.0.0-rc.1", - "react": "^18.2.0", + "@vechain/dapp-kit": "*", + "@vechain/dapp-kit-ui": "*", + "@vechain/sdk-core": "1.0.0-beta.32", "valtio": "1.11.2" }, "devDependencies": { "@testing-library/react": "^14.1.2", "@types/react": "^18.2.28", "@types/react-dom": "^18.2.13", - "@vechain/repo-config": "https://github.com/vechain/repo-config#v0.0.1", - "eslint": "^8.15.0", + "eslint": "^9.12.0", + "react": "^18.2.0", "tsup": "*", "typescript": "*", "vite": "^4.5.5", diff --git a/packages/dapp-kit-react/src/DAppKitProvider.tsx b/packages/dapp-kit-react/src/DAppKitProvider/DAppKitProvider.tsx similarity index 56% rename from packages/dapp-kit-react/src/DAppKitProvider.tsx rename to packages/dapp-kit-react/src/DAppKitProvider/DAppKitProvider.tsx index 3c318675..d3c66cbf 100644 --- a/packages/dapp-kit-react/src/DAppKitProvider.tsx +++ b/packages/dapp-kit-react/src/DAppKitProvider/DAppKitProvider.tsx @@ -1,72 +1,27 @@ -import React, { - createContext, - useCallback, - useContext, - useEffect, - useMemo, - useState, -} from 'react'; -import type { WalletSource } from '@vechain/dapp-kit'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import type { DAppKit, WalletSource } from '@vechain/dapp-kit'; import { DAppKitUI } from '@vechain/dapp-kit-ui'; import type { CertificateData } from '@vechain/sdk-core'; import { subscribeKey } from 'valtio/vanilla/utils'; -import type { DAppKitContext, DAppKitProviderOptions } from './types'; +import type { DAppKitContext, DAppKitProviderOptions } from '../types'; +import { Context } from './context'; -/** - * Context - */ -const Context = createContext(undefined); - -export const DAppKitProvider: React.FC = ({ +export const DAppKitProviderData = ({ children, - nodeUrl, - walletConnectOptions, - usePersistence = false, - logLevel, - requireCertificate, - themeMode, - themeVariables, - i18n, - language, - modalParent, - onSourceClick, - connectionCertificate: connectionCertificateData, + dAppKit +}: { + children: React.ReactNode; + dAppKit: DAppKit; }): React.ReactElement => { - const dAppKit = useMemo( - () => - DAppKitUI.configure({ - nodeUrl, - walletConnectOptions, - usePersistence, - logLevel, - requireCertificate, - themeVariables, - themeMode, - i18n, - language, - modalParent, - onSourceClick, - connectionCertificate: connectionCertificateData, - }), - [ - nodeUrl, - walletConnectOptions, - usePersistence, - logLevel, - requireCertificate, - themeVariables, - themeMode, - i18n, - language, - modalParent, - onSourceClick, - connectionCertificateData, - ], - ); - const [account, setAccount] = useState( dAppKit.wallet.state.address, ); + const [accountDomain, setAccountDomain] = useState( + dAppKit.wallet.state.accountDomain, + ); + const [isAccountDomainLoading, setIsAccountDomainLoading] = useState( + dAppKit.wallet.state.isAccountDomainLoading, + ); const [source, setSource] = useState( dAppKit.wallet.state.source, ); @@ -83,6 +38,20 @@ export const DAppKitProvider: React.FC = ({ setAccount(v); }, ); + const domainSub = subscribeKey( + dAppKit.wallet.state, + 'accountDomain', + (v) => { + setAccountDomain(v); + }, + ); + const isAccountDomainLoadingSub = subscribeKey( + dAppKit.wallet.state, + 'isAccountDomainLoading', + (v) => { + setIsAccountDomainLoading(v); + }, + ); const sourceSub = subscribeKey(dAppKit.wallet.state, 'source', (v) => { setSource(v); }); @@ -96,6 +65,8 @@ export const DAppKitProvider: React.FC = ({ return () => { addressSub(); + domainSub(); + isAccountDomainLoadingSub(); sourceSub(); certificateSub(); }; @@ -108,6 +79,7 @@ export const DAppKitProvider: React.FC = ({ const closeModal = useCallback(() => { DAppKitUI.modal.close(); }, []); + const onModalConnected = useCallback( (callback: (address: string | null) => void) => DAppKitUI.modal.onConnectionStatusChange(callback), @@ -124,6 +96,8 @@ export const DAppKitProvider: React.FC = ({ signer: dAppKit.signer, availableWallets: dAppKit.wallet.state.availableSources, account, + accountDomain, + isAccountDomainLoading, source, connectionCertificate, }, @@ -136,43 +110,75 @@ export const DAppKitProvider: React.FC = ({ }, [ dAppKit, account, + accountDomain, + isAccountDomainLoading, source, - closeModal, + connectionCertificate, openModal, + closeModal, onModalConnected, - connectionCertificate, ]); return {children}; }; -export const useThor = (): DAppKitContext['thor'] => { - const context = useContext(Context); - - if (!context) { - throw new Error('"useThor" must be used within a DAppKitProvider'); - } - - return context.thor; -}; - -export const useWallet = (): DAppKitContext['wallet'] => { - const context = useContext(Context); - - if (!context) { - throw new Error('"useWallet" must be used within a DAppKitProvider'); - } - - return context.wallet; -}; - -export const useWalletModal = (): DAppKitContext['modal'] => { - const context = useContext(Context); - - if (!context) { - throw new Error( - '"useWalletModal" must be used within a DAppKitProvider', +export const DAppKitProvider = ({ + children, + nodeUrl, + genesis, + walletConnectOptions, + usePersistence = false, + logLevel, + requireCertificate, + themeVariables, + themeMode, + i18n, + language, + modalParent, + onSourceClick, + connectionCertificate: connectionCertificateData, + allowedWallets, +}: DAppKitProviderOptions): React.ReactElement | null => { + const [dAppKit, setDAppKit] = useState(null); + useEffect(() => { + setDAppKit( + DAppKitUI.configure({ + nodeUrl, + genesis, + walletConnectOptions, + usePersistence, + logLevel, + requireCertificate, + themeVariables, + themeMode, + i18n, + language, + modalParent, + onSourceClick, + connectionCertificate: connectionCertificateData, + allowedWallets, + }), ); + }, [ + nodeUrl, + genesis, + walletConnectOptions, + usePersistence, + logLevel, + requireCertificate, + themeVariables, + themeMode, + i18n, + language, + modalParent, + onSourceClick, + connectionCertificateData, + allowedWallets, + ]); + if (!dAppKit) { + return null; } - return context.modal; -}; + return ( + {children} + ); +}; \ No newline at end of file diff --git a/packages/dapp-kit-react/src/DAppKitProvider/context.ts b/packages/dapp-kit-react/src/DAppKitProvider/context.ts new file mode 100644 index 00000000..07719cd5 --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/context.ts @@ -0,0 +1,4 @@ +import { createContext } from 'react'; +import type { DAppKitContext } from '../types'; + +export const Context = createContext(undefined); diff --git a/packages/dapp-kit-react/src/DAppKitProvider/hooks/index.ts b/packages/dapp-kit-react/src/DAppKitProvider/hooks/index.ts new file mode 100644 index 00000000..a3490637 --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/index.ts @@ -0,0 +1,3 @@ +export * from './useThor'; +export * from './useWallet'; +export * from './useWalletModal'; diff --git a/packages/dapp-kit-react/src/DAppKitProvider/hooks/useThor.ts b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useThor.ts new file mode 100644 index 00000000..77c71d69 --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useThor.ts @@ -0,0 +1,16 @@ +import { useContext } from 'react'; +import { type DAppKitContext } from '../../types'; +import { Context } from '../context'; + +/** + * Hook to get the thor object from the DAppKitProvider + */ +export const useThor = (): DAppKitContext['thor'] => { + const context = useContext(Context); + + if (!context) { + throw new Error('"useThor" must be used within a DAppKitProvider'); + } + + return context.thor; +}; diff --git a/packages/dapp-kit-react/test/useWallet.test.tsx b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.test.tsx similarity index 80% rename from packages/dapp-kit-react/test/useWallet.test.tsx rename to packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.test.tsx index c39b9675..72516203 100644 --- a/packages/dapp-kit-react/test/useWallet.test.tsx +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.test.tsx @@ -1,8 +1,7 @@ import { describe, expect, it } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; -import { useWallet } from '../src'; -import { wrapper } from './helpers/react-test-helpers'; -import { mockedConnexSigner } from './helpers/mocked-signer'; +import { mockedConnexSigner, wrapper } from '../../../test'; +import { useWallet } from './useWallet'; window.vechain = {} as any; window.vechain = { @@ -46,4 +45,10 @@ describe('useWallet', () => { expect(result.current.account).toBeNull(); }); }); + + it('should throw an error when used outside of DAppKitProvider', () => { + expect(() => renderHook(() => useWallet())).toThrow( + '"useWallet" must be used within a DAppKitProvider', + ); + }); }); diff --git a/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.ts b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.ts new file mode 100644 index 00000000..da18a9bd --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWallet.ts @@ -0,0 +1,16 @@ +import { useContext } from 'react'; +import { type DAppKitContext } from '../../types'; +import { Context } from '../context'; + +/** + * Hook to get the wallet object from the DAppKitProvider + */ +export const useWallet = (): DAppKitContext['wallet'] => { + const context = useContext(Context); + + if (!context) { + throw new Error('"useWallet" must be used within a DAppKitProvider'); + } + + return context.wallet; +}; diff --git a/packages/dapp-kit-react/test/useWalletModal.test.tsx b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.test.tsx similarity index 71% rename from packages/dapp-kit-react/test/useWalletModal.test.tsx rename to packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.test.tsx index e2178b39..2ef3e623 100644 --- a/packages/dapp-kit-react/test/useWalletModal.test.tsx +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.test.tsx @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { renderHook, waitFor } from '@testing-library/react'; -import { useWalletModal } from '../src'; -import { wrapper } from './helpers/react-test-helpers'; +import { useWalletModal } from '../..'; +import { wrapper } from '../../../test/helpers/react-test-helpers'; describe('useWalletModal', () => { it('should be able to open the modal', async () => { @@ -27,4 +27,9 @@ describe('useWalletModal', () => { result.current.close(); }); + it('should throw an error when used outside of DAppKitProvider', () => { + expect(() => renderHook(() => useWalletModal())).toThrow( + '"useWalletModal" must be used within a ConnexProvider', + ); + }); }); diff --git a/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.ts b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.ts new file mode 100644 index 00000000..ce7fa479 --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/hooks/useWalletModal.ts @@ -0,0 +1,17 @@ +import { useContext } from 'react'; +import { type DAppKitContext } from '../../types'; +import { Context } from '../context'; + +/** + * Hook to get the wallet modal object from the DAppKitProvider + */ +export const useWalletModal = (): DAppKitContext['modal'] => { + const context = useContext(Context); + + if (!context) { + throw new Error( + '"useWalletModal" must be used within a ConnexProvider', + ); + } + return context.modal; +}; diff --git a/packages/dapp-kit-react/src/DAppKitProvider/index.ts b/packages/dapp-kit-react/src/DAppKitProvider/index.ts new file mode 100644 index 00000000..22c0080f --- /dev/null +++ b/packages/dapp-kit-react/src/DAppKitProvider/index.ts @@ -0,0 +1,2 @@ +export * from './DAppKitProvider'; +export * from './hooks'; diff --git a/packages/dapp-kit-react/src/hooks/index.ts b/packages/dapp-kit-react/src/hooks/index.ts new file mode 100644 index 00000000..98a863f9 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './useVechainDomain'; diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/api/fetchVechainDomain.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/fetchVechainDomain.ts new file mode 100644 index 00000000..f5f0a4f5 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/fetchVechainDomain.ts @@ -0,0 +1,78 @@ +import { addressUtils } from '@vechain/sdk-core'; +import type { DAppKitContext } from '../../../types'; +import { getDomain } from './getDomain'; +import { getAddress } from './getAddress'; + +export interface VechainDomainResult { + address: string | undefined; + domain: string | undefined; + isValidAddressOrDomain: boolean; +} + +/** + * Function to fetch the vechain domain of an account and vice versa by passing the connex object + */ +export const fetchVechainDomain = async ({ + addressOrDomain, + thor, +}: { + addressOrDomain?: string | null; + thor: DAppKitContext['thor']; +}): Promise => { + if (!addressOrDomain) { + return { + address: undefined, + domain: undefined, + isValidAddressOrDomain: false, + }; + } + + const isValidAddress = addressUtils.isAddress(addressOrDomain); + + if (isValidAddress) { + try { + const domainName = await getDomain({ + address: addressOrDomain, + thor, + }); + return { + address: addressOrDomain, + domain: domainName, + isValidAddressOrDomain: true, + }; + } catch (err) { + console.error('Error getting domain: ', err); + return { + address: addressOrDomain, + domain: undefined, + isValidAddressOrDomain: true, + }; + } + } + + try { + const domainAddress = await getAddress({ + domain: addressOrDomain, + thor, + }); + if (domainAddress === '0x0000000000000000000000000000000000000000') { + return { + address: undefined, + domain: undefined, + isValidAddressOrDomain: false, + }; + } + return { + address: domainAddress, + domain: addressOrDomain, + isValidAddressOrDomain: true, + }; + } catch (err) { + console.error('Error getting address: ', err); + return { + address: undefined, + domain: undefined, + isValidAddressOrDomain: false, + }; + } +}; diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.test.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.test.ts new file mode 100644 index 00000000..0a102ab9 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect, vi } from 'vitest'; +import { getAddress } from './getAddress'; +import { genesisBlocks, VNS_RESOLVER } from '@vechain/dapp-kit'; +import { ABIContract } from '@vechain/sdk-core'; + +describe('getAddress', () => { + const mockThorClient = { + thor: { + blocks: { + getGenesisBlock: vi.fn(), + }, + contracts: { + executeCall: vi.fn(), + }, + }, + } as any; + + it('should return null if domain is null', async () => { + const result = await getAddress({ domain: null, thor: mockThorClient }); + expect(result).toBeUndefined(); + }); + + it('should use main resolver for mainnet', async () => { + mockThorClient.thor.blocks.getGenesisBlock.mockResolvedValue({ id: genesisBlocks.main.id }); + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: ['0x1234567890123456789012345678901234567890'] + }, + }); + + const result = await getAddress({ domain: 'example.vet', thor: mockThorClient.thor }); + + expect(mockThorClient.thor.contracts.executeCall).toHaveBeenCalledWith(VNS_RESOLVER.main, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getAddresses'), ['example.vet']); + + expect(result).toBe('0x1234567890123456789012345678901234567890'); + }); + + it('should use test resolver for testnet', async () => { + mockThorClient.thor.blocks.getGenesisBlock.mockResolvedValue({ id: genesisBlocks.test.id }); + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: ['0x1234567890123456789012345678901234567890'] + }, + }); + + const result = await getAddress({ domain: 'example.vet', thor: mockThorClient.thor }); + + expect(mockThorClient.thor.contracts.executeCall).toHaveBeenCalledWith(VNS_RESOLVER.test, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getAddresses'), ['example.vet']); + + expect(result).toBe('0x1234567890123456789012345678901234567890'); + }); + + it('should return the first address from the resolved addresses', async () => { + const expectedAddress = '0x1234567890123456789012345678901234567890'; + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: [expectedAddress] + }, + }); + + const result = await getAddress({ + domain: 'example.vet', + thor: mockThorClient.thor, + }); + + expect(result).toBe(expectedAddress); + }); + + it('should return null if no addresses are resolved', async () => { + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: [] + }, + }); + + const result = await getAddress({ + domain: 'example.vet', + thor: mockThorClient.thor, + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.ts new file mode 100644 index 00000000..9a3989d8 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getAddress.ts @@ -0,0 +1,28 @@ +import { genesisBlocks, VNS_RESOLVER } from '@vechain/dapp-kit'; +import type { DAppKitContext } from '../../../types'; +import { ABIContract } from '@vechain/sdk-core'; + +/** + * Get the address of the domain + */ +export const getAddress = async ({ + domain, + thor, +}: { + domain: string | null; + thor: DAppKitContext['thor']; +}): Promise => { + if (!domain) return undefined; + + const genesisId = await thor.blocks.getGenesisBlock(); + + const resolver = + genesisId?.id === genesisBlocks.test.id + ? VNS_RESOLVER.test + : VNS_RESOLVER.main; + + const res = await thor.contracts.executeCall(resolver, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getAddresses'), [domain]); + const resArray = res.result.array as string[]; + + return (resArray[0] as string) || undefined; +}; diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.test.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.test.ts new file mode 100644 index 00000000..27049fa0 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.test.ts @@ -0,0 +1,89 @@ +import { describe, it, expect, vi } from 'vitest'; +import { getDomain } from './getDomain'; +import { genesisBlocks, VNS_RESOLVER } from '@vechain/dapp-kit'; +import { ABIContract } from '@vechain/sdk-core'; + +describe('getDomain', () => { + const mockThorClient = { + thor: { + blocks: { + getGenesisBlock: vi.fn(), + }, + contracts: { + executeCall: vi.fn(), + }, + }, + } as any; + + it('should return null if address is null', async () => { + const result = await getDomain({ address: null, thor: mockThorClient }); + expect(result).toBeUndefined(); + }); + + it('should use main resolver for mainnet', async () => { + mockThorClient.thor.blocks.getGenesisBlock.mockResolvedValue({ id: genesisBlocks.main.id }); + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: ['example.vet'] + }, + }); + + const result = await getDomain({ + address: '0x1234567890123456789012345678901234567890', + thor: mockThorClient.thor, + }); + + expect(mockThorClient.thor.contracts.executeCall).toHaveBeenCalledWith(VNS_RESOLVER.main, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getNames'), ["0x1234567890123456789012345678901234567890"]); + + expect(result).toBe('example.vet'); + }); + + it('should use test resolver for testnet', async () => { + mockThorClient.thor.blocks.getGenesisBlock.mockResolvedValue({ id: genesisBlocks.test.id }); + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: ['example.vet'] + }, + }); + + const result = await getDomain({ + address: '0x1234567890123456789012345678901234567890', + thor: mockThorClient.thor, + }); + + expect(mockThorClient.thor.contracts.executeCall).toHaveBeenCalledWith(VNS_RESOLVER.test, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getNames'), ["0x1234567890123456789012345678901234567890"]); + + expect(result).toBe('example.vet'); + }); + + it('should return the first name from the resolved names', async () => { + const expectedDomain = 'example.vet'; + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: [expectedDomain] + }, + }); + + const result = await getDomain({ + address: '0x1234567890123456789012345678901234567890', + thor: mockThorClient.thor, + }); + + expect(result).toBe(expectedDomain); + }); + + it('should return null if no names are resolved', async () => { + mockThorClient.thor.contracts.executeCall.mockResolvedValue({ + result: { + array: [] + }, + }); + + const result = await getDomain({ + address: '0x1234567890123456789012345678901234567890', + thor: mockThorClient.thor, + }); + + expect(result).toBeUndefined(); + }); +}); diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.ts new file mode 100644 index 00000000..c0318ed3 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/api/getDomain.ts @@ -0,0 +1,28 @@ +import { genesisBlocks, VNS_RESOLVER } from '@vechain/dapp-kit'; +import type { DAppKitContext } from '../../../types'; +import { ABIContract } from '@vechain/sdk-core'; + +/** + * Get the domain of an account + */ +export const getDomain = async ({ + address, + thor, +}: { + address?: string | null; + thor: DAppKitContext['thor']; +}): Promise => { + if (!address) return undefined; + + const genesisId = await thor.blocks.getGenesisBlock(); + + const resolver = + genesisId?.id === genesisBlocks.test.id + ? VNS_RESOLVER.test + : VNS_RESOLVER.main; + + const res = await thor.contracts.executeCall(resolver, ABIContract.ofAbi(VNS_RESOLVER.abi).getFunction('getNames'), [address]); + const resArray = res.result.array as string[]; + + return (resArray[0] as string) || undefined; +}; diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/index.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/index.ts new file mode 100644 index 00000000..98a863f9 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/index.ts @@ -0,0 +1 @@ +export * from './useVechainDomain'; diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.catch.test.tsx b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.catch.test.tsx new file mode 100644 index 00000000..c763a391 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.catch.test.tsx @@ -0,0 +1,31 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { useVechainDomain } from './useVechainDomain'; +import { wrapper } from '../../../test'; + +vi.mock('./api/fetchVechainDomain', () => ({ + ...vi.importActual('./api/fetchVechainDomain'), + fetchVechainDomain: vi.fn().mockImplementation(async () => { + throw new Error('Network error'); + }), +})); + +describe('useVechainDomain error handling', () => { + it('should handle error when fetching domain', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: 'test.vet' }), + { wrapper }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: undefined, + domain: undefined, + isLoading: false, + isValidAddressOrDomain: false, + }); + }); + }); +}); diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.test.tsx b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.test.tsx new file mode 100644 index 00000000..8073a2ae --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.test.tsx @@ -0,0 +1,182 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { describe, it, expect, vi } from 'vitest'; +import { useVechainDomain } from './useVechainDomain'; +import { wrapper } from '../../../test'; + +vi.mock('./api/getDomain', () => ({ + getDomain: vi.fn().mockImplementation(({ address }) => { + if (address === '0x1234567890123456789012345678901234567890') { + return Promise.resolve('test.vet'); + } + if (address === '0xERROR') { + return Promise.reject(new Error('Network error')); + } + if (address === '0x0000000000000000000000000000000000000000') { + return Promise.reject(new Error('Network error')); + } + return Promise.resolve(null); + }), +})); + +vi.mock('./api/getAddress', () => ({ + getAddress: vi.fn().mockImplementation(({ domain }) => { + if (domain === 'test.vet') { + return Promise.resolve( + '0x1234567890123456789012345678901234567890', + ); + } + if (domain === 'invalid.vet') { + return Promise.resolve( + '0x0000000000000000000000000000000000000000', + ); + } + if (domain === 'error.vet' || domain === '0xERROR') { + return Promise.reject(new Error('Network error')); + } + return Promise.resolve(null); + }), +})); + +describe('useVechainDomain', () => { + it('should return initial state', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: null }), + { + wrapper, + }, + ); + await waitFor(() => { + expect(result.current).toEqual({ + address: undefined, + domain: undefined, + isLoading: false, + isValidAddressOrDomain: false, + }); + }); + }); + + it('should handle valid address input', async () => { + const { result } = renderHook( + () => + useVechainDomain({ + addressOrDomain: + '0x1234567890123456789012345678901234567890', + }), + { wrapper }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: '0x1234567890123456789012345678901234567890', + domain: 'test.vet', + isLoading: false, + isValidAddressOrDomain: true, + }); + }); + }); + + it('should handle valid domain input', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: 'test.vet' }), + { + wrapper, + }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: '0x1234567890123456789012345678901234567890', + domain: 'test.vet', + isLoading: false, + isValidAddressOrDomain: true, + }); + }); + }); + + it('should handle invalid domain input', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: 'invalid.vet' }), + { + wrapper, + }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: undefined, + domain: undefined, + isLoading: false, + isValidAddressOrDomain: false, + }); + }); + }); + + it('should handle error when getting domain', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: '0xERROR' }), + { + wrapper, + }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: undefined, + domain: undefined, + isLoading: false, + isValidAddressOrDomain: false, + }); + }); + }); + + it('should handle error when getting address', async () => { + const { result } = renderHook( + () => useVechainDomain({ addressOrDomain: 'error.vet' }), + { + wrapper, + }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: undefined, + domain: undefined, + isLoading: false, + isValidAddressOrDomain: false, + }); + }); + }); + it('should handle error when getting zero address', async () => { + const { result } = renderHook( + () => + useVechainDomain({ + addressOrDomain: + '0x0000000000000000000000000000000000000000', + }), + { + wrapper, + }, + ); + + expect(result.current.isLoading).toBe(true); + + await waitFor(() => { + expect(result.current).toEqual({ + address: '0x0000000000000000000000000000000000000000', + domain: undefined, + isLoading: false, + isValidAddressOrDomain: true, + }); + }); + }); +}); diff --git a/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.ts b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.ts new file mode 100644 index 00000000..7489bfa1 --- /dev/null +++ b/packages/dapp-kit-react/src/hooks/useVechainDomain/useVechainDomain.ts @@ -0,0 +1,44 @@ +import { useEffect, useState } from 'react'; +import { useThor } from '../../DAppKitProvider/hooks/useThor'; +import { + fetchVechainDomain, + type VechainDomainResult, +} from './api/fetchVechainDomain'; + +interface UseVechainDomainReturnType extends VechainDomainResult { + isLoading: boolean; +} + +/** + * Hook to get the domain of an account and vice versa by passing the connex object + */ +export const useVechainDomain = ({ + addressOrDomain, +}: { + addressOrDomain?: string | null; +}): UseVechainDomainReturnType => { + const thor = useThor(); + const [result, setResult] = useState({ + address: undefined, + domain: undefined, + isValidAddressOrDomain: false, + }); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + setIsLoading(true); + fetchVechainDomain({ addressOrDomain, thor }) + .then(setResult) + .catch((err) => { + console.error('Error fetching vechain domain: ', err); + setResult({ + address: undefined, + domain: undefined, + isValidAddressOrDomain: false, + }); + }) + .finally(() => setIsLoading(false)); + }, [addressOrDomain, thor]); + + return { ...result, isLoading }; +}; diff --git a/packages/dapp-kit-react/src/index.ts b/packages/dapp-kit-react/src/index.ts index 92010e34..b431e5f5 100644 --- a/packages/dapp-kit-react/src/index.ts +++ b/packages/dapp-kit-react/src/index.ts @@ -1,3 +1,4 @@ export * from './DAppKitProvider'; export * from './types'; export * from './WalletButton'; +export * from './hooks'; diff --git a/packages/dapp-kit-react/src/types.ts b/packages/dapp-kit-react/src/types.ts index 65e5f060..cc337590 100644 --- a/packages/dapp-kit-react/src/types.ts +++ b/packages/dapp-kit-react/src/types.ts @@ -2,7 +2,7 @@ import type React from 'react'; import type { ConnectResponse, VeChainSignerDAppKit, - WalletSource, + WalletSource } from '@vechain/dapp-kit'; import { type DAppKitUIOptions } from '@vechain/dapp-kit-ui'; import type { CertificateData } from '@vechain/sdk-core'; @@ -38,6 +38,8 @@ export interface DAppKitContext { disconnect: () => void; connect: () => Promise; account: string | null; + accountDomain: string | null; + isAccountDomainLoading: boolean; signer: VeChainSignerDAppKit | undefined; source: WalletSource | null; connectionCertificate: CertificateData | null; diff --git a/packages/dapp-kit-react/test/helpers/index.ts b/packages/dapp-kit-react/test/helpers/index.ts new file mode 100644 index 00000000..a3d2ef77 --- /dev/null +++ b/packages/dapp-kit-react/test/helpers/index.ts @@ -0,0 +1,2 @@ +export * from './react-test-helpers'; +export * from './mocked-signer'; diff --git a/packages/dapp-kit-react/test/index.ts b/packages/dapp-kit-react/test/index.ts new file mode 100644 index 00000000..c5f595cf --- /dev/null +++ b/packages/dapp-kit-react/test/index.ts @@ -0,0 +1 @@ +export * from './helpers'; diff --git a/packages/dapp-kit-react/tsconfig.json b/packages/dapp-kit-react/tsconfig.json index ddad55aa..2bc962ed 100644 --- a/packages/dapp-kit-react/tsconfig.json +++ b/packages/dapp-kit-react/tsconfig.json @@ -17,8 +17,7 @@ "skipLibCheck": true, "strict": true, // React - "jsx": "react-jsx", - "typeRoots": ["node_modules/@types"] + "jsx": "react-jsx" }, - "include": ["src/**/*.ts*", "test/**/*.test.ts*"] + "include": ["src/**/*.ts", "test/**/*.test.ts", "eslint.config.mjs"] } diff --git a/packages/dapp-kit-react/vite.config.ts b/packages/dapp-kit-react/vite.config.ts index 9f955e09..d9cb36fb 100644 --- a/packages/dapp-kit-react/vite.config.ts +++ b/packages/dapp-kit-react/vite.config.ts @@ -4,7 +4,12 @@ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { - include: ['test/**/*.test.ts', 'test/**/*.test.tsx'], + include: [ + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'test/**/*.test.ts', + 'test/**/*.test.tsx', + ], environment: 'happy-dom', reporters: 'dot', setupFiles: [resolve(__dirname, 'test/setup/setup.ts')], diff --git a/packages/dapp-kit-ui/.eslintignore b/packages/dapp-kit-ui/.eslintignore deleted file mode 100644 index 59b446e0..00000000 --- a/packages/dapp-kit-ui/.eslintignore +++ /dev/null @@ -1,6 +0,0 @@ -node_modules/* -dist/* -web-dev-server.config.js -tsup.config.ts -test -vite.config.ts diff --git a/packages/dapp-kit-ui/.eslintrc.mjs b/packages/dapp-kit-ui/.eslintrc.mjs deleted file mode 100644 index b53b0fd4..00000000 --- a/packages/dapp-kit-ui/.eslintrc.mjs +++ /dev/null @@ -1,9 +0,0 @@ -const Config = require('@vechain/repo-config'); - -module.exports = { - ...Config.EslintReact, - rules: { - ...Config.EslintReact.rules, - 'import/no-extraneous-dependencies': 'error', - }, -}; diff --git a/packages/dapp-kit-ui/eslint.config.mjs b/packages/dapp-kit-ui/eslint.config.mjs new file mode 100644 index 00000000..c9e3ec07 --- /dev/null +++ b/packages/dapp-kit-ui/eslint.config.mjs @@ -0,0 +1,17 @@ +import tseslint from 'typescript-eslint'; + +export default tseslint.config({ + ignores: ['**/*.config.ts', 'dist/**'], + extends: [...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/ban-ts-comment': 'off', + 'no-console': ['error', { allow: ['error'] }], + 'eslint-comments/no-unused-disable': 'off', + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_' }, + ], + }, +}); diff --git a/packages/dapp-kit-ui/index.js b/packages/dapp-kit-ui/index.js index b862eb26..f1e46894 100644 --- a/packages/dapp-kit-ui/index.js +++ b/packages/dapp-kit-ui/index.js @@ -1,6 +1,4 @@ -// eslint-disable-next-line eslint-comments/disable-enable-pair -/* eslint-disable no-undef */ -import { DAppKitUI } from './dist'; +import { DAppKitUI, friendlyAddress } from './dist'; const walletConnectOptions = { projectId: 'a0b855ceaf109dbc8426479a4c3d38d8', @@ -13,7 +11,7 @@ const walletConnectOptions = { }; const vechainDAppKitOptions = { - nodeUrl: 'https://testnet.vechain.org/', + nodeUrl: 'https://mainnet.vechain.org', walletConnectOptions, usePersistence: true, }; @@ -24,21 +22,33 @@ DAppKitUI.configure(vechainDAppKitOptions); const customButton = document.getElementById('custom-button'); -customButton.addEventListener('click', async () => { - DAppKitUI.modal.open(); -}); - -const handleConnected = (address) => { - if (address) { - const formattedAddress = `${address.slice(0, 6)}...${address.slice( - -4, - )}`; - customButton.innerText = `Disconnect from ${formattedAddress}`; - } else { - customButton.innerText = 'Connect Custom Button'; - } -}; - -handleConnected(DAppKitUI.wallet.state.address); - -DAppKitUI.modal.onConnectionStatusChange(handleConnected); +if (customButton) { + customButton.addEventListener('click', () => { + DAppKitUI.modal.open(); + }); + + const render = () => { + const address = DAppKitUI.wallet.state.address; + const accountDomain = DAppKitUI.wallet.state.accountDomain; + const isAccountDomainLoading = + DAppKitUI.wallet.state.isAccountDomainLoading; + + const addressOrDomain = + accountDomain && !isAccountDomainLoading + ? accountDomain + : friendlyAddress(address || ''); + + if (address) { + customButton.innerText = `Disconnect from ${addressOrDomain}`; + } else { + customButton.innerText = 'Connect Custom Button'; + } + }; + + render(); + + DAppKitUI.modal.onConnectionStatusChange(render); + DAppKitUI.wallet.subscribeToKey('address', render); + DAppKitUI.wallet.subscribeToKey('accountDomain', render); + DAppKitUI.wallet.subscribeToKey('isAccountDomainLoading', render); +} diff --git a/packages/dapp-kit-ui/package.json b/packages/dapp-kit-ui/package.json index e135da42..2f4b7bfb 100644 --- a/packages/dapp-kit-ui/package.json +++ b/packages/dapp-kit-ui/package.json @@ -1,6 +1,6 @@ { "name": "@vechain/dapp-kit-ui", - "version": "1.0.11", + "version": "1.1.1", "description": "Vanilla JS DAppKit", "keywords": [ "web-components", @@ -26,8 +26,7 @@ "build": "tsup", "clean": "rm -rf dist .turbo", "dev": "rm -rf ../../.parcel-cache; parcel --no-cache index.html", - "format": "prettier \"**/*.{cjs,html,js,json,md,ts}\" --ignore-path ./.eslintignore --write", - "lint": "tsc --noEmit && eslint src --ext .js,.jsx,.ts,.tsx", + "lint": "eslint", "purge": "yarn clean && rm -rf node_modules", "test": "vitest run --coverage", "test:dev": "vitest run ", @@ -50,15 +49,15 @@ "@types/qrcode": "^1.5.5", "@typescript-eslint/eslint-plugin": "^5.25.0", "@typescript-eslint/parser": "^5.25.0", - "@vechain/repo-config": "https://github.com/vechain/repo-config#v0.0.1", "@vitest/coverage-v8": "^0.34.6", - "eslint": "^8.15.0", + "eslint": "^9.12.0", "parcel": "^2.10.2", "prettier": "^2.6.2", "punycode": "^1.4.1", "tsup": "^7.2.0", "typechain": "^8.3.2", "typescript": "~5.2.0", + "typescript-eslint": "^8.11.0", "vite": "^4.5.5", "vitest": "^0.34.6" } diff --git a/packages/dapp-kit-ui/src/client.ts b/packages/dapp-kit-ui/src/client.ts index aa0d42ad..a875d61f 100644 --- a/packages/dapp-kit-ui/src/client.ts +++ b/packages/dapp-kit-ui/src/client.ts @@ -74,7 +74,6 @@ export const DAppKitUI = { get(): DAppKit { if (!dappKit) { - // eslint-disable-next-line no-console console.error('🚨🚨🚨 DAppKitUI not configured 🚨🚨🚨'); throw new Error('DAppKitUI not configured'); } diff --git a/packages/dapp-kit-ui/src/components/buttons/address-button.ts b/packages/dapp-kit-ui/src/components/buttons/address-button.ts index ef9d6f6e..322d981d 100644 --- a/packages/dapp-kit-ui/src/components/buttons/address-button.ts +++ b/packages/dapp-kit-ui/src/components/buttons/address-button.ts @@ -4,6 +4,7 @@ import { Font, type ThemeMode } from '../../constants'; import { friendlyAddress, getPicassoImage } from '../../utils/account'; import { buttonStyle } from '../../assets/styles'; import { DAppKitUI } from '../../client'; +import { shortenedDomain } from '@vechain/dapp-kit'; @customElement('vdk-address-button') export class AddressButton extends LitElement { @@ -49,6 +50,12 @@ export class AddressButton extends LitElement { @property() address?: string; + @property() + accountDomain = ''; + + @property() + isAccountDomainLoading = false; + @property() mode: ThemeMode = 'LIGHT'; @@ -77,6 +84,11 @@ export class AddressButton extends LitElement { /> `; } + + const addressOrDomain = + this.accountDomain && !this.isAccountDomainLoading + ? shortenedDomain(this.accountDomain, 7) + : friendlyAddress(this.address ?? ''); return html` `; } } diff --git a/packages/dapp-kit-ui/src/components/buttons/button.ts b/packages/dapp-kit-ui/src/components/buttons/button.ts index f9457e55..d31eb86e 100644 --- a/packages/dapp-kit-ui/src/components/buttons/button.ts +++ b/packages/dapp-kit-ui/src/components/buttons/button.ts @@ -36,6 +36,10 @@ export class Button extends LitElement { private setAddressFromState(): void { this.address = DAppKitUI.wallet.state.address ?? ''; + this.accountDomain = DAppKitUI.wallet.state.accountDomain ?? ''; + this.isAccountDomainLoading = Boolean( + DAppKitUI.wallet.state.isAccountDomainLoading, + ); this.requestUpdate(); } @@ -44,6 +48,11 @@ export class Button extends LitElement { this.i18n = DAppKitUI.configuration?.i18n ?? defaultI18n; this.language = DAppKitUI.configuration?.language ?? 'en'; this.address = DAppKitUI.wallet.state.address ?? ''; + this.accountDomain = DAppKitUI.wallet.state.accountDomain ?? ''; + this.isAccountDomainLoading = Boolean( + DAppKitUI.wallet.state.isAccountDomainLoading, + ); + this.requestUpdate(); } @@ -55,6 +64,20 @@ export class Button extends LitElement { this.requestUpdate(); }, ); + DAppKitUI.wallet.subscribeToKey( + 'accountDomain', + (_accountDomain: string | null) => { + this.accountDomain = _accountDomain ?? ''; + this.requestUpdate(); + }, + ); + DAppKitUI.wallet.subscribeToKey( + 'isAccountDomainLoading', + (_isAccountDomainLoading: boolean) => { + this.isAccountDomainLoading = _isAccountDomainLoading; + this.requestUpdate(); + }, + ); } @property() @@ -69,6 +92,12 @@ export class Button extends LitElement { @property() address = ''; + @property() + accountDomain = ''; + + @property() + isAccountDomainLoading = false; + @property() disabled = false; @@ -80,6 +109,8 @@ export class Button extends LitElement { ? html`` diff --git a/packages/dapp-kit-ui/src/components/modals/address-modal.ts b/packages/dapp-kit-ui/src/components/modals/address-modal.ts index 641c4b90..229f7552 100644 --- a/packages/dapp-kit-ui/src/components/modals/address-modal.ts +++ b/packages/dapp-kit-ui/src/components/modals/address-modal.ts @@ -21,6 +21,7 @@ import { LightDisconnectSvg, } from '../../assets/icons'; import { DAppKitUI } from '../../client'; +import { shortenedDomain } from '@vechain/dapp-kit'; let openWalletModalListener: () => void; let closeWalletModalListener: () => void; @@ -87,7 +88,7 @@ export class AddressModal extends LitElement { ); } - .address { + .address-domain { font-size: var(--vdk-font-size-large, ${Font.Size.Large}); font-family: var(--vdk-font-family, ${Font.Family}); font-weight: var( @@ -99,12 +100,39 @@ export class AddressModal extends LitElement { justify-content: center; } + .secondary-address { + font-size: var(--vdk-font-size-small, ${Font.Size.Small}); + font-family: var(--vdk-font-family, ${Font.Family}); + font-weight: var( + --vdk-font-weight-regular, + ${Font.Weight.Regular} + ); + display: flex; + flex-direction: row; + justify-content: center; + } + + .address-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + gap: 10px; + } + .copy-icon { cursor: pointer; width: 20px; height: 20px; margin-left: 10px; } + + .copy-icon-secondary { + cursor: pointer; + width: 15px; + height: 15px; + margin-left: 8px; + } `, ]; @@ -114,6 +142,12 @@ export class AddressModal extends LitElement { @property({ type: String }) address = ''; + @property() + accountDomain = ''; + + @property() + isAccountDomainLoading = false; + @property({ type: Function }) onDisconnectClick?: () => void = undefined; @@ -130,7 +164,10 @@ export class AddressModal extends LitElement { walletConnectQRcode?: string = undefined; @property() - showCopiedIcon = false; + showCopiedMainIcon = false; + + @property() + showCopiedSecondaryIcon = false; constructor() { super(); @@ -161,10 +198,19 @@ export class AddressModal extends LitElement { override render(): TemplateResult { const translate = useTranslate(this.i18n, this.language); - let copyIcon = this.mode === 'LIGHT' ? LightCopySvg : DarkCopySvg; - if (this.showCopiedIcon) { - copyIcon = CheckSvg; + let copyMainIcon = this.mode === 'LIGHT' ? LightCopySvg : DarkCopySvg; + if (this.showCopiedMainIcon) { + copyMainIcon = CheckSvg; } + let copySecondaryIcon = + this.mode === 'LIGHT' ? LightCopySvg : DarkCopySvg; + if (this.showCopiedSecondaryIcon) { + copySecondaryIcon = CheckSvg; + } + const addressOrDomain = + this.accountDomain && !this.isAccountDomainLoading + ? shortenedDomain(this.accountDomain, 18) + : friendlyAddress(this.address || ''); return html` - - ${friendlyAddress(this.address)} +
+ + ${addressOrDomain}
${copyIcon}
-
- + this.onCopyMainLabel + }>${copyMainIcon}
+
+ ${ + this.accountDomain + ? html` + (${friendlyAddress(this.address, 8, 7)}) +
+ ${copySecondaryIcon} +
+
` + : nothing + } +