diff --git a/examples/taco/react-viem/README.md b/examples/taco/react-viem/README.md new file mode 100644 index 000000000..575fbc572 --- /dev/null +++ b/examples/taco/react-viem/README.md @@ -0,0 +1,17 @@ +# `react-viem-taco` integration example + +Shows how to integrate `@nucypher/taco` into a React application using viem. +## Usage + +```bash +pnpm install +pnpm start +``` + +Next, go to [http://127.0.0.1:3000/](http://127.0.0.1:8080/) in your browser and +inspect the UI and the JS console. + +## Learn more + +Please find developer documentation for +TACo [here](https://docs.taco.build/). diff --git a/examples/taco/react-viem/package.json b/examples/taco/react-viem/package.json new file mode 100644 index 000000000..c0e4b13a7 --- /dev/null +++ b/examples/taco/react-viem/package.json @@ -0,0 +1,42 @@ +{ + "name": "taco-react-example", + "version": "0.0.0", + "private": true, + "scripts": { + "build": "react-scripts build", + "check": "pnpm type-check && pnpm build", + "eject": "react-scripts eject", + "start": "react-scripts start", + "test": "react-scripts test", + "type-check": "tsc -p tsconfig.build.json" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "dependencies": { + "@nucypher/shared": "workspace:*", + "@nucypher/taco": "workspace:*", + "@nucypher/taco-auth": "workspace:*", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/node": "^20.17.28", + "@types/react": "^18.3.20", + "@types/react-dom": "^18.3.5", + "react-scripts": "^5.0.1" + }, + "peerDependencies": { + "ethers": "^5.7.2", + "viem": "^2.0.0" + } +} diff --git a/examples/taco/react-viem/public/favicon.ico b/examples/taco/react-viem/public/favicon.ico new file mode 100644 index 000000000..a11777cc4 Binary files /dev/null and b/examples/taco/react-viem/public/favicon.ico differ diff --git a/examples/taco/react-viem/public/index.html b/examples/taco/react-viem/public/index.html new file mode 100644 index 000000000..e65acb3de --- /dev/null +++ b/examples/taco/react-viem/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + React App + + + +
+ + + diff --git a/examples/taco/react-viem/public/logo192.png b/examples/taco/react-viem/public/logo192.png new file mode 100644 index 000000000..fc44b0a37 Binary files /dev/null and b/examples/taco/react-viem/public/logo192.png differ diff --git a/examples/taco/react-viem/public/logo512.png b/examples/taco/react-viem/public/logo512.png new file mode 100644 index 000000000..a4e47a654 Binary files /dev/null and b/examples/taco/react-viem/public/logo512.png differ diff --git a/examples/taco/react-viem/public/manifest.json b/examples/taco/react-viem/public/manifest.json new file mode 100644 index 000000000..080d6c77a --- /dev/null +++ b/examples/taco/react-viem/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/examples/taco/react-viem/public/robots.txt b/examples/taco/react-viem/public/robots.txt new file mode 100644 index 000000000..e9e57dc4d --- /dev/null +++ b/examples/taco/react-viem/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/examples/taco/react-viem/src/App.tsx b/examples/taco/react-viem/src/App.tsx new file mode 100644 index 000000000..06f2254fd --- /dev/null +++ b/examples/taco/react-viem/src/App.tsx @@ -0,0 +1,164 @@ +import { fromHexString } from '@nucypher/shared'; +import { conditions, domains, fromBytes, toHexString } from '@nucypher/taco'; +import { hexlify } from 'ethers/lib/utils'; +import { useEffect, useState } from 'react'; +import { + createPublicClient, + createWalletClient, + custom, + PublicClient, + WalletClient +} from 'viem'; +import { polygonAmoy } from 'viem/chains'; + +import useTaco from './hooks/useTaco'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +declare const window: any; + +const ritualId = 6; // Replace with your own ritual ID +const domain = domains.TESTNET; + +function App() { + const [publicClient, setPublicClient] = useState(); + const [walletClient, setWalletClient] = useState(); + const [message, setMessage] = useState('this is a secret'); + const [encrypting, setEncrypting] = useState(false); + const [encryptedText, setEncryptedText] = useState(''); + const [decrypting, setDecrypting] = useState(false); + const [decryptedMessage, setDecryptedMessage] = useState( + '', + ); + + const loadWeb3Provider = async () => { + if (!window.ethereum) { + console.error('You need to connect to your wallet first'); + return; + } + + // Request account access + await window.ethereum.request({ method: 'eth_requestAccounts' }); + + // Create public client for reading data + const publicClient = createPublicClient({ + chain: polygonAmoy, + transport: custom(window.ethereum), + }); + + // Get the accounts from the provider + const [account] = await window.ethereum.request({ + method: 'eth_accounts' + }); + // Create wallet client for signing the message + const walletClient = createWalletClient({ + account, + chain: polygonAmoy, + transport: custom(window.ethereum), + }); + + const chainId = await publicClient.getChainId(); + const amoyChainId = 80002; + if (chainId !== amoyChainId) { + // Switch to Polygon Amoy testnet + await window.ethereum.request({ + method: 'wallet_switchEthereumChain', + params: [{ chainId: hexlify(amoyChainId) }], + }); + } + + setPublicClient(publicClient); + setWalletClient(walletClient); + }; + + useEffect(() => { + loadWeb3Provider(); + }, []); + + const { isInit, encryptDataToBytes, decryptDataFromBytes } = useTaco({ + domain, + publicClient, + walletClient, + ritualId, + }); + + if (!isInit || !publicClient || !walletClient) { + return
Loading...
; + } + + const encryptMessage = async () => { + if (!walletClient) { + return; + } + setEncrypting(true); + try { + const hasPositiveBalance = new conditions.base.rpc.RpcCondition({ + chain: 80002, + method: 'eth_getBalance', + parameters: [':userAddress', 'latest'], + returnValueTest: { + comparator: '>=', + value: 0, + }, + }); + + console.log('Encrypting message...'); + const encryptedBytes = await encryptDataToBytes( + message, + hasPositiveBalance, + ); + if (encryptedBytes) { + setEncryptedText(toHexString(encryptedBytes)); + } + } catch (e) { + console.log(e); + } + setEncrypting(false); + }; + + const decryptMessage = async () => { + if (!encryptedText || !walletClient) { + return; + } + try { + setDecrypting(true); + + console.log('Decrypting message...'); + const decryptedMessage = await decryptDataFromBytes( + fromHexString(encryptedText), + ); + if (decryptedMessage) { + setDecryptedMessage(fromBytes(decryptedMessage)); + } + } catch (e) { + console.log(e); + } + setDecrypting(false); + }; + + return ( +
+

+ Secret message:{' '} + setMessage(e.target.value)} + onClick={encryptMessage} + />{' '} + {' '} + {encrypting && 'Encrypting...'} +

+

+ Encrypted message:{' '} + setEncryptedText(e.target.value)} + />{' '} + {' '} + {decrypting && 'Decrypting...'} +

+ {decryptedMessage &&

Decrypted message: {decryptedMessage}

} +
+ ); +} + +export default App; diff --git a/examples/taco/react-viem/src/hooks/useTaco.ts b/examples/taco/react-viem/src/hooks/useTaco.ts new file mode 100644 index 000000000..1ca25d786 --- /dev/null +++ b/examples/taco/react-viem/src/hooks/useTaco.ts @@ -0,0 +1,104 @@ +import { + conditions, + decrypt, + Domain, + encrypt, + initialize, + ThresholdMessageKit, +} from '@nucypher/taco'; +import { + EIP4361AuthProvider, + USER_ADDRESS_PARAM_DEFAULT, +} from '@nucypher/taco-auth'; +import { useCallback, useEffect, useState } from 'react'; +import { createPublicClient, http, PublicClient, WalletClient } from 'viem'; +import { polygonAmoy } from 'viem/chains'; + +export default function useTaco({ + ritualId, + domain, + publicClient, + walletClient, +}: { + ritualId: number; + domain: Domain; + publicClient: PublicClient | undefined; + walletClient: WalletClient | undefined; +}) { + const [isInit, setIsInit] = useState(false); + + useEffect(() => { + initialize().then(() => setIsInit(true)); + }, []); + + const decryptDataFromBytes = useCallback( + async (encryptedBytes: Uint8Array) => { + if (!isInit || !publicClient || !walletClient) { + return; + } + + const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes); + const authProvider = new EIP4361AuthProvider(publicClient, walletClient); + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProvider, + ); + let message; + try { + message = await decrypt( + publicClient, + domain, + messageKit, + conditionContext, + ); + } catch (error) { + if ( + error instanceof Error && + JSON.stringify(error).includes('missing trie node') + ) { + // Using MetaMask Provider may cause "missing trie node". + // Which typically occurs when attempting to query a blockchain state for a specific block that has been pruned from the node's storage. + // To avoid that, a custom public client is created. + const remotePublicClient = createPublicClient({ + chain: polygonAmoy, // Using Polygon Amoy for testnet/devnet + transport: http('https://rpc-amoy.polygon.technology'), + }); + message = await decrypt( + remotePublicClient, + domain, + messageKit, + conditionContext, + ); + } else { + throw error; + } + } + return message; + }, + [isInit, publicClient, walletClient, domain], + ); + + const encryptDataToBytes = useCallback( + async (message: string, condition: conditions.condition.Condition) => { + if (!isInit || !publicClient || !walletClient) return; + const messageKit = await encrypt( + publicClient, + domain, + message, + condition, + ritualId, + walletClient, + ); + return messageKit.toBytes(); + }, + [isInit, publicClient, walletClient, domain, ritualId], + ); + + return { + isInit, + decryptDataFromBytes, + encryptDataToBytes, + }; +} diff --git a/examples/taco/react-viem/src/index.css b/examples/taco/react-viem/src/index.css new file mode 100644 index 000000000..8439a5936 --- /dev/null +++ b/examples/taco/react-viem/src/index.css @@ -0,0 +1,7 @@ +input { + font-size: 20px; +} + +button { + font-size: 15px; +} diff --git a/examples/taco/react-viem/src/index.tsx b/examples/taco/react-viem/src/index.tsx new file mode 100644 index 000000000..3f83eeaea --- /dev/null +++ b/examples/taco/react-viem/src/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +import App from './App'; +import './index.css'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement, +); +root.render( + + + , +); diff --git a/examples/taco/react-viem/src/react-app-env.d.ts b/examples/taco/react-viem/src/react-app-env.d.ts new file mode 100644 index 000000000..8f8826530 --- /dev/null +++ b/examples/taco/react-viem/src/react-app-env.d.ts @@ -0,0 +1,8 @@ +/// +import { ExternalProvider } from '@ethersproject/providers'; + +declare global { + interface Window { + ethereum?: ExternalProvider; + } +} diff --git a/examples/taco/react-viem/tsconfig.build.json b/examples/taco/react-viem/tsconfig.build.json new file mode 100644 index 000000000..7213342cb --- /dev/null +++ b/examples/taco/react-viem/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "include": ["src"], + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "noEmit": true + }, + "references": [ + { + "path": "../../../packages/taco/tsconfig.es.json" + }, + ] +} diff --git a/examples/taco/react-viem/tsconfig.json b/examples/taco/react-viem/tsconfig.json new file mode 100644 index 000000000..aca306ec8 --- /dev/null +++ b/examples/taco/react-viem/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + }, + "include": ["src"], +} diff --git a/package.json b/package.json index e5c742053..7ec9ce79f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "lint": "pnpm run --parallel --aggregate-output --reporter append-only lint", "lint:fix": "pnpm --parallel --aggregate-output --reporter append-only lint:fix", "type-check": "pnpm --parallel --aggregate-output --reporter append-only type-check", - "build": "tsc --build --verbose ./tsconfig.prod.json", + "build": "pnpm -r prebuild && tsc --build --verbose ./tsconfig.prod.json && pnpm -r postbuild", "watch": "tsc --build --verbose --watch ./tsconfig.prod.json", "test": "pnpm build && vitest run", "package:check": "pnpm run --parallel --aggregate-output --reporter append-only --filter './packages/**' package-check", @@ -50,6 +50,7 @@ "sort-package-json": "^2.15.1", "ts-node": "^10.9.2", "ts-unused-exports": "^10.1.0", + "tsx": "^4.20.5", "typedoc": "^0.25.13", "typedoc-plugin-coverage": "^2.2.0", "typedoc-plugin-missing-exports": "^2.3.0", diff --git a/packages/pre/package.json b/packages/pre/package.json index 6aa3ccf7f..c2aef11d6 100644 --- a/packages/pre/package.json +++ b/packages/pre/package.json @@ -14,6 +14,7 @@ "author": "Piotr Roslaniec ", "exports": { ".": { + "types": "./dist/cjs/index.d.ts", "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" } @@ -26,9 +27,9 @@ ], "scripts": { "prebuild": "pnpm clean", - "build": "pnpm build:module && pnpm build:cjs", + "build": "pnpm build:es && pnpm build:cjs", "build:cjs": "tsc --build ./tsconfig.cjs.json --verbose", - "build:module": "tsc --build ./tsconfig.es.json --verbose", + "build:es": "tsc --build ./tsconfig.es.json --verbose", "clean": "rm -rf dist", "exports:lint": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "lint": "eslint --ext .ts src test", diff --git a/packages/shared/package.json b/packages/shared/package.json index 79d0d3ea1..fcaf0c736 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -15,6 +15,7 @@ "author": "NuCypher ", "exports": { ".": { + "types": "./dist/cjs/index.d.ts", "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" } @@ -26,11 +27,14 @@ "dist" ], "scripts": { - "prebuild": "pnpm typechain && pnpm clean", - "build": "pnpm build:module && pnpm build:cjs", + "prebuild": "pnpm clean && pnpm typechain && pnpm ensure-es-compatible-imports", + "build": "pnpm build:es && pnpm build:cjs", + "postbuild": "pnpm ensure-es-package-type", "build:cjs": "tsc --build ./tsconfig.cjs.json --verbose", - "build:module": "tsc --build ./tsconfig.es.json --verbose", + "build:es": "tsc --build ./tsconfig.es.json --verbose", "clean": "rm -rf dist", + "ensure-es-compatible-imports": "pnpm tsx ../../scripts/ensure-es-compatible-imports.ts", + "ensure-es-package-type": "pnpm tsx ../../scripts/ensure-es-package-type.ts", "exports:lint": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "lint": "eslint --ext .ts src", "lint:fix": "pnpm lint --fix", @@ -58,7 +62,16 @@ "@types/tmp": "^0.2.6", "cz-conventional-changelog": "^3.3.0", "standard-version": "^9.5.0", - "typechain": "^8.3.2" + "typechain": "^8.3.2", + "viem": "^2.0.0" + }, + "peerDependencies": { + "viem": "^2.0.0" + }, + "peerDependenciesMeta": { + "viem": { + "optional": true + } }, "engines": { "node": ">=18", diff --git a/packages/shared/src/adapters.ts b/packages/shared/src/adapters.ts new file mode 100644 index 000000000..e6b1a8d54 --- /dev/null +++ b/packages/shared/src/adapters.ts @@ -0,0 +1,45 @@ +import { ethers } from 'ethers'; + +import type { TacoSigner } from './taco-signer.js'; +import { ProviderLike, SignerLike } from './types.js'; +import { viemClientToProvider } from './viem/ethers-adapter.js'; +import { ViemSignerAdapter } from './viem/signer-adapter.js'; +import { isViemClient, isViemSignerAccount } from './viem/type-guards.js'; + +/** + * Convert ethers Signer or viem SignerAccount (LocalAccount or WalletClient) to TacoSigner. + * + * This is the main entry point for creating signers for internal TACo use. + * Unlike toEthersProvider which creates actual ethers objects, + * this creates minimal adapters implementing only what TACo needs. + * + * @param signerLike - Either an ethers Signer or a viem SignerAccount (LocalAccount or WalletClient) + * @returns A TacoSigner interface implementation + */ +export function toTacoSigner(signerLike: SignerLike): TacoSigner { + if (isViemSignerAccount(signerLike)) { + return new ViemSignerAdapter(signerLike); + } else { + return signerLike; + } +} + +/** + * Convert viem client to ethers provider or return existing ethers provider. + * + * This is the main entry point for converting providers. + * It handles both viem clients (converting them to ethers providers) + * and existing ethers providers (returning them unchanged). + * + * @param providerLike - Either a viem PublicClient or an ethers.providers.Provider + * @returns An actual ethers.providers.Provider instance + */ +export function toEthersProvider( + providerLike: ProviderLike, +): ethers.providers.Provider { + if (isViemClient(providerLike)) { + return viemClientToProvider(providerLike); + } else { + return providerLike; + } +} diff --git a/packages/shared/src/contracts/agents/coordinator.ts b/packages/shared/src/contracts/agents/coordinator.ts index ab50500c3..2f2391b0d 100644 --- a/packages/shared/src/contracts/agents/coordinator.ts +++ b/packages/shared/src/contracts/agents/coordinator.ts @@ -6,12 +6,12 @@ import { } from '@nucypher/nucypher-core'; import { BigNumberish, ethers } from 'ethers'; -import { Domain } from '../../porter'; -import { ChecksumAddress } from '../../types'; -import { fromHexString } from '../../utils'; -import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const'; -import { Coordinator__factory } from '../ethers-typechain'; -import { BLS12381, Coordinator } from '../ethers-typechain/Coordinator'; +import { Domain } from '../../porter.js'; +import { ChecksumAddress } from '../../types.js'; +import { fromHexString } from '../../utils.js'; +import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const.js'; +import { BLS12381, Coordinator } from '../ethers-typechain/Coordinator.js'; +import { Coordinator__factory } from '../ethers-typechain/index.js'; export interface CoordinatorRitual { initiator: string; diff --git a/packages/shared/src/contracts/agents/global-allow-list.ts b/packages/shared/src/contracts/agents/global-allow-list.ts index fc09d5946..2cc3407c2 100644 --- a/packages/shared/src/contracts/agents/global-allow-list.ts +++ b/packages/shared/src/contracts/agents/global-allow-list.ts @@ -1,10 +1,10 @@ import { getContract } from '@nucypher/nucypher-contracts'; import { ethers } from 'ethers'; -import { Domain } from '../../porter'; -import { ChecksumAddress } from '../../types'; -import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const'; -import { GlobalAllowList, GlobalAllowList__factory } from '../ethers-typechain'; +import { Domain } from '../../porter.js'; +import { ChecksumAddress } from '../../types.js'; +import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const.js'; +import { GlobalAllowList, GlobalAllowList__factory } from '../ethers-typechain/index.js'; export class GlobalAllowListAgent { public static async registerEncrypters( diff --git a/packages/shared/src/contracts/agents/index.ts b/packages/shared/src/contracts/agents/index.ts index 61e29c0e6..2dea7b365 100644 --- a/packages/shared/src/contracts/agents/index.ts +++ b/packages/shared/src/contracts/agents/index.ts @@ -1,3 +1,3 @@ -export * from './coordinator'; -export * from './global-allow-list'; -export * from './subscription-manager'; +export * from './coordinator.js'; +export * from './global-allow-list.js'; +export * from './subscription-manager.js'; diff --git a/packages/shared/src/contracts/agents/subscription-manager.ts b/packages/shared/src/contracts/agents/subscription-manager.ts index 20f787300..2820f1c4a 100644 --- a/packages/shared/src/contracts/agents/subscription-manager.ts +++ b/packages/shared/src/contracts/agents/subscription-manager.ts @@ -6,13 +6,13 @@ import { utils as ethersUtils, } from 'ethers'; -import { Domain } from '../../porter'; -import { ChecksumAddress } from '../../types'; -import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const'; +import { Domain } from '../../porter.js'; +import { ChecksumAddress } from '../../types.js'; +import { DEFAULT_WAIT_N_CONFIRMATIONS } from '../const.js'; import { SubscriptionManager, SubscriptionManager__factory, -} from '../ethers-typechain'; +} from '../ethers-typechain/index.js'; export class PreSubscriptionManagerAgent { public static async createPolicy( diff --git a/packages/shared/src/contracts/ethers-typechain/Coordinator.ts b/packages/shared/src/contracts/ethers-typechain/Coordinator.ts index 21619add1..5f4d4dc82 100644 --- a/packages/shared/src/contracts/ethers-typechain/Coordinator.ts +++ b/packages/shared/src/contracts/ethers-typechain/Coordinator.ts @@ -24,7 +24,7 @@ import type { TypedEvent, TypedEventFilter, TypedListener, -} from './common'; +} from './common.js'; export declare namespace BLS12381 { export type G2PointStruct = { diff --git a/packages/shared/src/contracts/ethers-typechain/GlobalAllowList.ts b/packages/shared/src/contracts/ethers-typechain/GlobalAllowList.ts index 3d03a022c..11685e19c 100644 --- a/packages/shared/src/contracts/ethers-typechain/GlobalAllowList.ts +++ b/packages/shared/src/contracts/ethers-typechain/GlobalAllowList.ts @@ -24,7 +24,7 @@ import type { TypedEvent, TypedEventFilter, TypedListener, -} from './common'; +} from './common.js'; export interface GlobalAllowListInterface extends utils.Interface { functions: { diff --git a/packages/shared/src/contracts/ethers-typechain/SubscriptionManager.ts b/packages/shared/src/contracts/ethers-typechain/SubscriptionManager.ts index b91b79a13..51fb58f02 100644 --- a/packages/shared/src/contracts/ethers-typechain/SubscriptionManager.ts +++ b/packages/shared/src/contracts/ethers-typechain/SubscriptionManager.ts @@ -25,7 +25,7 @@ import type { TypedEvent, TypedEventFilter, TypedListener, -} from './common'; +} from './common.js'; export declare namespace SubscriptionManager { export type PolicyStruct = { diff --git a/packages/shared/src/contracts/ethers-typechain/factories/Coordinator__factory.ts b/packages/shared/src/contracts/ethers-typechain/factories/Coordinator__factory.ts index e3800e887..1fd17973a 100644 --- a/packages/shared/src/contracts/ethers-typechain/factories/Coordinator__factory.ts +++ b/packages/shared/src/contracts/ethers-typechain/factories/Coordinator__factory.ts @@ -4,7 +4,7 @@ import type { Provider } from '@ethersproject/providers'; import { Contract, Signer, utils } from 'ethers'; -import type { Coordinator, CoordinatorInterface } from '../Coordinator'; +import type { Coordinator, CoordinatorInterface } from '../Coordinator.js'; const _abi = [ { diff --git a/packages/shared/src/contracts/ethers-typechain/factories/GlobalAllowList__factory.ts b/packages/shared/src/contracts/ethers-typechain/factories/GlobalAllowList__factory.ts index 59e19af4f..520fcf2e4 100644 --- a/packages/shared/src/contracts/ethers-typechain/factories/GlobalAllowList__factory.ts +++ b/packages/shared/src/contracts/ethers-typechain/factories/GlobalAllowList__factory.ts @@ -7,7 +7,7 @@ import { Contract, Signer, utils } from 'ethers'; import type { GlobalAllowList, GlobalAllowListInterface, -} from '../GlobalAllowList'; +} from '../GlobalAllowList.js'; const _abi = [ { diff --git a/packages/shared/src/contracts/ethers-typechain/factories/SubscriptionManager__factory.ts b/packages/shared/src/contracts/ethers-typechain/factories/SubscriptionManager__factory.ts index 74413c5fc..c161ac932 100644 --- a/packages/shared/src/contracts/ethers-typechain/factories/SubscriptionManager__factory.ts +++ b/packages/shared/src/contracts/ethers-typechain/factories/SubscriptionManager__factory.ts @@ -7,7 +7,7 @@ import { Contract, Signer, utils } from 'ethers'; import type { SubscriptionManager, SubscriptionManagerInterface, -} from '../SubscriptionManager'; +} from '../SubscriptionManager.js'; const _abi = [ { diff --git a/packages/shared/src/contracts/ethers-typechain/factories/index.ts b/packages/shared/src/contracts/ethers-typechain/factories/index.ts index 8187c7c16..a6d99ea71 100644 --- a/packages/shared/src/contracts/ethers-typechain/factories/index.ts +++ b/packages/shared/src/contracts/ethers-typechain/factories/index.ts @@ -1,6 +1,6 @@ /* Autogenerated file. Do not edit manually. */ /* tslint:disable */ /* eslint-disable */ -export { Coordinator__factory } from './Coordinator__factory'; -export { GlobalAllowList__factory } from './GlobalAllowList__factory'; -export { SubscriptionManager__factory } from './SubscriptionManager__factory'; +export { Coordinator__factory } from './Coordinator__factory.js'; +export { GlobalAllowList__factory } from './GlobalAllowList__factory.js'; +export { SubscriptionManager__factory } from './SubscriptionManager__factory.js'; diff --git a/packages/shared/src/contracts/ethers-typechain/index.ts b/packages/shared/src/contracts/ethers-typechain/index.ts index e0b86d8c9..4d32ba0f6 100644 --- a/packages/shared/src/contracts/ethers-typechain/index.ts +++ b/packages/shared/src/contracts/ethers-typechain/index.ts @@ -1,10 +1,10 @@ /* Autogenerated file. Do not edit manually. */ /* tslint:disable */ /* eslint-disable */ -export type { Coordinator } from './Coordinator'; -export * as factories from './factories'; -export { Coordinator__factory } from './factories/Coordinator__factory'; -export { GlobalAllowList__factory } from './factories/GlobalAllowList__factory'; -export { SubscriptionManager__factory } from './factories/SubscriptionManager__factory'; -export type { GlobalAllowList } from './GlobalAllowList'; -export type { SubscriptionManager } from './SubscriptionManager'; +export type { Coordinator } from './Coordinator.js'; +export * as factories from './factories/index.js'; +export { Coordinator__factory } from './factories/Coordinator__factory.js'; +export { GlobalAllowList__factory } from './factories/GlobalAllowList__factory.js'; +export { SubscriptionManager__factory } from './factories/SubscriptionManager__factory.js'; +export type { GlobalAllowList } from './GlobalAllowList.js'; +export type { SubscriptionManager } from './SubscriptionManager.js'; diff --git a/packages/shared/src/contracts/index.ts b/packages/shared/src/contracts/index.ts index 6fb2308f0..9f2a45d20 100644 --- a/packages/shared/src/contracts/index.ts +++ b/packages/shared/src/contracts/index.ts @@ -1,2 +1,2 @@ -export * from './agents'; -export * from './ethers-typechain'; +export * from './agents/index.js'; +export * from './ethers-typechain/index.js'; diff --git a/packages/shared/src/domain.ts b/packages/shared/src/domain.ts new file mode 100644 index 000000000..8789ff3f7 --- /dev/null +++ b/packages/shared/src/domain.ts @@ -0,0 +1,48 @@ +export type DomainName = 'lynx' | 'tapir' | 'mainnet'; + +/** + * TACo domain configuration with essential network data + * + * Contains domain names, chain IDs, and core infrastructure information + * needed for TACo operations across different networks. + * + * @example + * ```typescript + * // Get domain information + * const lynxInfo = DOMAINS.DEVNET; + * console.log(lynxInfo.domain); // 'lynx' + * console.log(lynxInfo.chainId); // 80002 + * ``` + */ +export const DOMAINS = { + // lynx - DEVNET: Bleeding-edge developer network + DEVNET: { + domain: 'lynx', + chainId: 80002, + }, + // tapir - TESTNET: Stable testnet for current TACo release + TESTNET: { + domain: 'tapir', + chainId: 80002, + }, + // mainnet - MAINNET: Production network + MAINNET: { + domain: 'mainnet', + chainId: 137, + }, +} as const; + +/** + * TACo Domain Name Constants + * + * Convenient constants for referencing TACo domain names in a type-safe manner. + * Use these constants instead of hardcoded strings for better maintainability. + */ +export const DOMAIN_NAMES = { + /** DEVNET domain ('lynx') - Bleeding-edge developer network */ + DEVNET: 'lynx', + /** TESTNET domain ('tapir') - Stable testnet for current TACo release */ + TESTNET: 'tapir', + /** MAINNET domain ('mainnet') - Production network */ + MAINNET: 'mainnet', +} as const; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 526d9cc5b..8f0b026c4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -1,9 +1,13 @@ -export * from './contracts'; -export * from './porter'; -export * from './schemas'; -export type * from './types'; -export * from './utils'; -export * from './web3'; +export * from './adapters.js'; +export * from './contracts/index.js'; +export * from './domain.js'; +export * from './porter.js'; +export * from './schemas.js'; +export * from './taco-signer.js'; +export type * from './types.js'; +export * from './utils.js'; +export * from './viem/index.js'; +export * from './web3.js'; // Re-exports export { diff --git a/packages/shared/src/porter.ts b/packages/shared/src/porter.ts index 0a13050d3..1cfce4aad 100644 --- a/packages/shared/src/porter.ts +++ b/packages/shared/src/porter.ts @@ -13,8 +13,8 @@ import axios, { } from 'axios'; import qs from 'qs'; -import { Base64EncodedBytes, ChecksumAddress, HexEncodedBytes } from './types'; -import { fromBase64, fromHexString, toBase64, toHexString } from './utils'; +import { Base64EncodedBytes, ChecksumAddress, HexEncodedBytes } from './types.js'; +import { fromBase64, fromHexString, toBase64, toHexString } from './utils.js'; const defaultPorterUri: Record = { mainnet: 'https://porter.nucypher.io', diff --git a/packages/shared/src/taco-signer.ts b/packages/shared/src/taco-signer.ts new file mode 100644 index 000000000..24b0fe6f3 --- /dev/null +++ b/packages/shared/src/taco-signer.ts @@ -0,0 +1,20 @@ +/** + * Minimal TACo signer interface. + * + * This interface defines only the essential methods that TACo operations require: + * - `getAddress()`: Get the signer's address + * - `signMessage()`: Sign a message (string or bytes) + * + * Future signer adapters can implement this same minimal interface + */ +export interface TacoSigner { + /** + * Get the address of this signer + */ + getAddress(): Promise; + + /** + * Sign a message + */ + signMessage(message: string | Uint8Array): Promise; +} diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 02603b3c2..8100d70b2 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,3 +1,16 @@ +import { ethers } from 'ethers'; + +import { PublicClient, SignerAccount } from './viem/types.js'; + export type ChecksumAddress = `0x${string}`; export type HexEncodedBytes = string; export type Base64EncodedBytes = string; + +export type ProviderLike = ethers.providers.Provider | PublicClient; + +/** + * Signer-like union for TACo operations. + * + * Accepts either ethers Signer or viem SignerAccount (LocalAccount or WalletClient) + */ +export type SignerLike = ethers.Signer | SignerAccount; diff --git a/packages/shared/src/viem/ethers-adapter.ts b/packages/shared/src/viem/ethers-adapter.ts new file mode 100644 index 000000000..25b22910d --- /dev/null +++ b/packages/shared/src/viem/ethers-adapter.ts @@ -0,0 +1,128 @@ +import { Networkish } from '@ethersproject/providers'; +import { ethers } from 'ethers'; + +import type { Chain, PublicClient } from './types.js'; + +/** + * Create ethers network object from viem chain + */ +function createNetworkish(chain: Chain): ethers.providers.Networkish { + const networkish: Networkish = { + chainId: chain.id, + name: chain.name, + }; + + // Add ENS registry address if available + if (chain.contracts?.ensRegistry?.address) { + networkish.ensAddress = chain.contracts.ensRegistry.address; + } else { + console.warn( + `No ENS registry found on chain ${chain.name} (chainId=${chain.id}).\n` + + `With the current configuration, ENS resolution will not work on the created ethers Provider.\n` + + `To fix this either:\n` + + ` - Set chain.contracts.ensRegistry.address to the correct ENS registry address, or\n` + + ` - Or omit the \`chain\` data to allow automatic ENS registry detection from the provider.`, + ); + } + + return networkish; +} + +/** + * Viem to Ethers.js Adapter for Providers + * + * This adapter converts viem PublicClients to actual ethers.js Provider instances. + * Unlike the signer adapter which implements a minimal internal interface, + * this creates real ethers.providers.Provider objects that can be passed to + * third-party libraries expecting ethers.js providers. + * + * Key differences from signer adapter: + * - Creates ethers.providers.Provider instances + * - Currently handles only transport type: http + * - Required for external library compatibility + */ +/** + * Static method to directly convert client to provider (convenience method) + */ +export function viemClientToProvider( + client: PublicClient, +): ethers.providers.Provider { + const { chain, transport }: PublicClient = client; + + let networkish: ethers.providers.Networkish | undefined; + if (chain) { + networkish = createNetworkish(chain); + } + + // Note: We read minimal, commonly-present properties from transport. + // viem's transport internals are not a public API and may change. + // Also the we are taking only the first transport available. + // This adapter focuses on best-effort extraction of an RPC URL or EIP-1193 provider. + + // fallback transport (multiple RPC endpoints) + if (transport?.type === 'fallback') { + throw new Error( + 'Fallback transport not supported. Please use a single HTTP transport instead.', + ); + // TODO: implement with a code like the following: + // Note: The following takes only the first url of the transports urls. + // const items = transport.transports as ReturnType[]; + // const providers = items.map((t, i) => { + // const url = t?.value?.url; + // if (typeof url !== 'string' || url.length === 0) { + // throw new Error( + // `Fallback transport missing URL at index ${i} (chainId=${chain?.id ?? 'unknown'} name=${chain?.name ?? 'unknown'})`, + // ); + // } + // return new ethers.providers.JsonRpcProvider(url, networkish); + // }); + + // return new ethers.providers.FallbackProvider(providers); + } + + // websocket transport + if (transport?.type === 'webSocket') { + throw new Error('WebSocket transport not supported'); + // TODO: implement with a code like the following: + // const url = transport?.url as string | undefined; + // if (!url) { + // throw new Error( + // `Transport must have a URL (type=webSocket, chainId=${chain?.id ?? 'unknown'} name=${chain?.name ?? 'unknown'})`, + // ); + // } + // return new ethers.providers.WebSocketProvider(url, networkish); + } + + // TODO: this needs to be a lot better tested + // custom (EIP-1193) transport + if (transport?.type === 'custom') { + const value = transport?.value; + const provider = value?.provider ?? value ?? transport; + + // Check if it's an EIP-1193 provider (e.g., MetaMask, WalletConnect) + if (provider && typeof provider.request === 'function') { + return new ethers.providers.Web3Provider(provider, networkish); + } + + // If custom but no EIP-1193 provider found, try URL if present + + throw new Error('Custom non-EIP-1193 provider transport not supported'); + // TODO: implement with a code like the following: + // const url = value?.url ?? transport?.url; + // if (typeof url === 'string' && url.length > 0) { + // return new ethers.providers.JsonRpcProvider(url, networkish); + // } + // throw new Error( + // `Custom transport missing EIP-1193 provider or URL (chainId=${chain?.id ?? 'unknown'} name=${chain?.name ?? 'unknown'})`, + // ); + } + + // Default: assume HTTP-like with a URL + const url = transport?.url as string | undefined; + if (!url) { + throw new Error( + `Transport must have a URL (type=${transport?.type ?? 'unknown'}, chainId=${chain?.id ?? 'unknown'}, name=${chain?.name ?? 'unknown'})`, + ); + } + return new ethers.providers.JsonRpcProvider(url, networkish); +} diff --git a/packages/shared/src/viem/index.ts b/packages/shared/src/viem/index.ts new file mode 100644 index 000000000..45c45f3ed --- /dev/null +++ b/packages/shared/src/viem/index.ts @@ -0,0 +1,2 @@ +export * from './type-guards.js'; +export type * from './types.js'; diff --git a/packages/shared/src/viem/signer-adapter.ts b/packages/shared/src/viem/signer-adapter.ts new file mode 100644 index 000000000..00aac3444 --- /dev/null +++ b/packages/shared/src/viem/signer-adapter.ts @@ -0,0 +1,61 @@ +import { ethers } from 'ethers'; + +import { type TacoSigner } from '../taco-signer.js'; + +import { type Address, type SignerAccount } from './types.js'; + +/** + * Viem Signer Adapter + * + * This adapter implements the minimal TacoSigner interface for internal library use. + */ +export class ViemSignerAdapter implements TacoSigner { + protected viemAccount: SignerAccount; + + constructor(viemAccount: SignerAccount) { + this.viemAccount = viemAccount; + } + + async getAddress(): Promise
{ + let address: Address | undefined; + if ('address' in this.viemAccount) { + // viemAccount is a LocalAccount + address = this.viemAccount.address; + } else if ( + 'account' in this.viemAccount && + this.viemAccount.account && + 'address' in this.viemAccount.account + ) { + // viemAccount is a WalletClient + address = this.viemAccount.account.address; + } + if (address) { + // Get the checksummed address to avoid getting + // "invalid EIP-55 address - 0x31663c14545df87044d2c5407ad0c2696b6d1402" + // that might be thrown at package siwe-parser while perform decryption + return ethers.utils.getAddress(address) as Address; + } + throw new Error( + 'Unable to retrieve address from viem account. Expected a LocalAccount with "address" property or WalletClient with "account.address" property.', + ); + } + + async signMessage(message: string | Uint8Array): Promise { + if (!this.viemAccount.signMessage) { + throw new Error( + 'Account does not support message signing. Expected a LocalAccount or a WalletClient with signing capability.', + ); + } + if (typeof message === 'string') { + return await this.viemAccount.signMessage({ + account: await this.getAddress(), + message, + }); + } else { + return await this.viemAccount.signMessage({ + account: await this.getAddress(), + message: { raw: message }, + }); + } + } +} diff --git a/packages/shared/src/viem/type-guards.ts b/packages/shared/src/viem/type-guards.ts new file mode 100644 index 000000000..796258e8f --- /dev/null +++ b/packages/shared/src/viem/type-guards.ts @@ -0,0 +1,59 @@ +import { ethers } from 'ethers'; + +import { ProviderLike, SignerLike } from '../types.js'; + +import { PublicClient, SignerAccount } from './types.js'; + +/** + * Type guard to determine if the provider-like is a viem PublicClient + * + * Checks for: + * - Ensures it's not an ethers provider instance + * - Presence of viem-specific properties (chain) + * - Presence of viem-specific methods (getChainId) + */ +export function isViemClient( + providerLike: ProviderLike, +): providerLike is PublicClient { + // Early return if it's an ethers provider + if (providerLike instanceof ethers.providers.BaseProvider) { + return false; + } + + // Check for viem-specific properties and methods + const hasChainProperty = 'chain' in providerLike; + const hasGetChainId = + typeof (providerLike as { getChainId: () => Promise }) + .getChainId === 'function'; + + return hasChainProperty || hasGetChainId; +} + +/** + * Type guard to determine if the signer is a viem account that can sign: LocalAccount or WalletClient + * Note: might need modification when supporting viem SmartAccount + * Checks for: + * - Ensures it's not an ethers Signer instance + * - Absence of ethers-specific properties (provider) + * - Presence of viem Local Account properties (address as string) or viem Wallet Client properties (account.address as string) + */ +export function isViemSignerAccount( + signer: SignerLike, +): signer is SignerAccount { + if (signer instanceof ethers.Signer || 'provider' in signer) { + return false; + } + + // Check for viem SignerAccount properties + const hasLocalAccountProperties = + // Local Account: + 'address' in signer && + typeof (signer as { address: string }).address === 'string'; + const hasWalletClientProperties = + // Wallet Client: + 'account' in signer && + typeof (signer as { account: { address: string } }).account.address === + 'string'; + + return hasLocalAccountProperties || hasWalletClientProperties; +} diff --git a/packages/shared/src/viem/types.ts b/packages/shared/src/viem/types.ts new file mode 100644 index 000000000..5a378b62e --- /dev/null +++ b/packages/shared/src/viem/types.ts @@ -0,0 +1,79 @@ +/** + * Shared viem utilities for TACo packages + * + * Features: + * - Optional viem dependency handling with helpful error messages + * - Dynamic import pattern that's webpack-compatible + * - Centralized type definitions for viem objects + * - Runtime availability checking with caching + * - Common wrapper implementations for providers and signers + * + * Usage: + * ```typescript + * import { type PublicClient } from '@nucypher/shared'; + * + * // Use viem clients directly with TACo adapters + * const tacoProvider = await toEthersProvider(publicClient); + * ``` + */ + +// Type helper: Use the real type from 'viem' if available, otherwise fallback to 'any'. +// This is because viem is an optional dependency, so the viem types may or may not be present. +// This pattern preserves type safety for consumers who have 'viem' installed, but does not break for others. +// See: https://github.com/microsoft/TypeScript/issues/47663#issuecomment-1367016530 +// Dynamic imports resolve to 'unknown' when module is not available, no compile-time errors occur +type _Address = import('viem').Address; +type _ViemPublicClient = import('viem').PublicClient; +type _LocalAccount = import('viem').LocalAccount; +type _ViemChain = import('viem').Chain; +type _ViemTransport = import('viem').Transport; +type _WalletClient = import('viem').WalletClient; + +/** + * Viem Address type (`0x${string}`) + */ +// Fallback to hex string +export type Address = [unknown] extends [_Address] ? `0x${string}` : _Address; + +/** + * Viem PublicClient type for read operations + * @see https://viem.sh/docs/clients/public + */ +export type PublicClient = [unknown] extends [_ViemPublicClient] + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + : _ViemPublicClient; + +export type LocalAccount = [unknown] extends [_LocalAccount] + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + : _LocalAccount; + +export type WalletClient = [unknown] extends [_WalletClient] + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + : _WalletClient; + +/** + * Viem signer account type for signing operations (LocalAccount or WalletClient) + * Note: SmartAccount is not supported yet + * @see https://viem.sh/docs/accounts/local + * @see https://viem.sh/docs/clients/wallet + */ +export type SignerAccount = LocalAccount | WalletClient; + +/** + * Viem Chain type for network metadata + * @see https://viem.sh/docs/glossary/types#chain + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type Chain = [unknown] extends [_ViemChain] ? any : _ViemChain; + +/** + * Viem Transport type for network metadata + * @see https://viem.sh/docs/glossary/types#transport + */ +export type Transport = [unknown] extends [_ViemTransport] + ? // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + : _ViemTransport; diff --git a/packages/shared/src/web3.ts b/packages/shared/src/web3.ts index 97655c765..0179262bf 100644 --- a/packages/shared/src/web3.ts +++ b/packages/shared/src/web3.ts @@ -1,4 +1,4 @@ -import { fromHexString } from './utils'; +import { fromHexString } from './utils.js'; export enum ChainId { POLYGON = 137, diff --git a/packages/shared/test/viem-ethers-adapter.test.ts b/packages/shared/test/viem-ethers-adapter.test.ts new file mode 100644 index 000000000..776432a1d --- /dev/null +++ b/packages/shared/test/viem-ethers-adapter.test.ts @@ -0,0 +1,308 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// @ts-nocheck +import { ethers } from 'ethers'; +import { privateKeyToAccount } from 'viem/accounts'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +import { createPublicClient, fallback, http, webSocket } from 'viem'; +import { fromHexString } from '../src'; +import { toEthersProvider, toTacoSigner } from '../src/adapters'; +import { viemClientToProvider } from '../src/viem/ethers-adapter'; +import { ViemSignerAdapter } from '../src/viem/signer-adapter'; +import { isViemClient, isViemSignerAccount } from '../src/viem/type-guards'; + +describe('viem ethers adapter', () => { + const PRIVATE_KEY = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; // 32-byte hex + + describe('viemClientToProvider', () => { + let viemClientConfig: any; + + beforeEach(() => { + viemClientConfig = { + chain: { + id: 80002, + name: 'Polygon Amoy', + contracts: { + ensRegistry: { address: '0x123' }, + }, + }, + transport: http('https://rpc.ankr.com/polygon_amoy'), + }; + }); + + it('should convert to ethers provider with single transport', () => { + const viemClient = createPublicClient(viemClientConfig); + const provider = new viemClientToProvider(viemClient); + expect(provider).toBeInstanceOf(ethers.providers.JsonRpcProvider); + expect(provider.connection.url).toBe('https://rpc.ankr.com/polygon_amoy'); + expect(provider.network.chainId).toBe(80002); + expect(provider.network.name).toBe('Polygon Amoy'); + expect(provider.network.ensAddress).toBe('0x123'); + }); + + it('should throw error when converting to ethers provider with fallback transport', () => { + const fallbackClientConfig = { + ...viemClientConfig, + transport: fallback([ + http('https://rpc1.example.com'), + http('https://rpc2.example.com'), + ]), + }; + const fallbackClient = createPublicClient(fallbackClientConfig); + expect(() => viemClientToProvider(fallbackClient)).toThrow( + 'Fallback transport not supported', + ); + }); + + it('should throw error when converting to ethers provider with webSocket transport', () => { + const webSocketClientConfig = { + ...viemClientConfig, + transport: webSocket('wss://example.com'), + }; + const webSocketClient = createPublicClient(webSocketClientConfig); + expect(() => viemClientToProvider(webSocketClient)).toThrow( + 'WebSocket transport not supported', + ); + }); + + // TODO: this needs to be better tested i.e. tested with an actual custom transport from viem that uses EIP1193 + it('should convert to ethers provider with custom transport (browser injected)', () => { + const mockEIP1193Provider = { + request: vi.fn(), + }; + + const mockCustomClient = { + ...viemClientConfig, + transport: { + type: 'custom', + value: { + provider: mockEIP1193Provider, + }, + }, + }; + + const provider = viemClientToProvider(mockCustomClient); + expect(provider).toBeDefined(); + expect(provider.constructor.name).toBe('Web3Provider'); + }); + + // TODO: this needs to be better tested i.e. tested with an actual custom transport from viem + it('should throw error for custom transport without provider or URL', () => { + const mockCustomClient = { + ...viemClientConfig, + transport: { + type: 'custom', + value: {}, + }, + }; + + expect(() => viemClientToProvider(mockCustomClient)).toThrow( + 'Custom non-EIP-1193 provider transport not supported', + ); + }); + + it('should handle missing chain', () => { + const clientWithoutChainConfig = { + ...viemClientConfig, + chain: undefined, + }; + + const clientWithoutChain = createPublicClient(clientWithoutChainConfig); + expect(() => viemClientToProvider(clientWithoutChain)).not.toThrow(); + }); + + it('should handle missing transport URL', () => { + const clientWithoutUrlConfig = { + ...viemClientConfig, + transport: undefined, // empty string URL + }; + expect(() => viemClientToProvider(clientWithoutUrlConfig)).toThrow( + 'Transport must have a URL', + ); + }); + }); + + describe('ViemSignerAdapter', () => { + const viemAccount = privateKeyToAccount(PRIVATE_KEY); + const ethersSigner = new ethers.Wallet(PRIVATE_KEY); + + it('should create signer without provider', () => { + const viemAdaptedSigner = new ViemSignerAdapter(viemAccount); + expect(viemAdaptedSigner).toBeInstanceOf(ViemSignerAdapter); + }); + + it('should get address from viem account', async () => { + const viemAdaptedSigner = new ViemSignerAdapter(viemAccount); + + const address = await viemAdaptedSigner.getAddress(); + expect(address).toBe(ethersSigner.address); + }); + + it('should sign string message', async () => { + const message = 'test message'; + const viemAdaptedSigner = new ViemSignerAdapter(viemAccount); + const viemSignature = await viemAdaptedSigner.signMessage(message); + + const ethersSignature = await ethersSigner.signMessage(message); + expect(viemSignature).toBe(ethersSignature); + }); + + it('should sign Uint8Array message', async () => { + const viemAdaptedSigner = new ViemSignerAdapter(viemAccount); + const messageBytes = fromHexString('0xdeadbeef'); + + const viemSignature = await viemAdaptedSigner.signMessage(messageBytes); + + const ethersSignature = await ethersSigner.signMessage(messageBytes); + expect(viemSignature).toBe(ethersSignature); + }); + + it('should throw error if account does not support signing', async () => { + const accountWithoutSigning = { + address: '0x742d35Cc6632C0532c718F63b1a8D7d8a7fAd3b2', + // no signMessage method + }; + + const signer = new ViemSignerAdapter(accountWithoutSigning); + + await expect(signer.signMessage('test')).rejects.toThrow( + 'Account does not support message signing', + ); + }); + }); + + describe('toEthersProvider', () => { + it('should create provider from viem client', async () => { + const viemClient = createPublicClient({ + chain: { + id: 80002, + name: 'Polygon Amoy', + }, + transport: http('https://test.com'), + }); + const provider = toEthersProvider(viemClient); + expect(provider).toBeInstanceOf(ethers.providers.JsonRpcProvider); + }); + + it('should return ethers provider unchanged', () => { + const ethersProvider = new ethers.providers.JsonRpcProvider( + 'https://test.com', + ); + const result = toEthersProvider(ethersProvider); + expect(result).toBe(ethersProvider); + }); + + it('should handle non-viem provider correctly', () => { + const nonViemProvider = { + send: vi.fn(), + // This will make it fail the isViemClient check + } as any; + + const result = toEthersProvider(nonViemProvider); + expect(result).toBe(nonViemProvider); + }); + }); + + describe('toTacoSigner', () => { + const PRIVATE_KEY = + '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; // 32-byte hex + + it('should create signer from viem account', async () => { + const viemAccount = privateKeyToAccount(PRIVATE_KEY); + const signer = toTacoSigner(viemAccount); + + expect(signer).toBeInstanceOf(ViemSignerAdapter); + + const address = await signer.getAddress(); + expect(address).toBe(viemAccount.address); + }); + + it('should return ethers signer unchanged', () => { + const ethersSigner = new ethers.Wallet(PRIVATE_KEY); + const result = toTacoSigner(ethersSigner); + + expect(result).toBe(ethersSigner); + }); + + it('should handle non-viem signer correctly', () => { + const nonViemSigner = { + getAddress: vi.fn(), + provider: {}, // This will make it fail the isViemSignerAccount check + } as any; + + const result = toTacoSigner(nonViemSigner); + expect(result).toBe(nonViemSigner); + }); + }); + + describe('type guards', () => { + describe('isViemClient', () => { + it('should identify actual viem client', () => { + const viemClient = createPublicClient({ + chain: { + id: 80002, + name: 'Polygon Amoy', + }, + transport: http('https://test.com'), + }); + expect(isViemClient(viemClient)).toBe(true); + }); + it('should identify viem client by chain property', () => { + const viemClientByChain = { + chain: { id: 1, name: 'mainnet' }, + getChainId: vi.fn(), + }; + expect(isViemClient(viemClientByChain)).toBe(true); + }); + + it('should identify viem client by getChainId method', () => { + const viemClientByGetChainId = { + getChainId: vi.fn(), + }; + + expect(isViemClient(viemClientByGetChainId)).toBe(true); + }); + + it('should reject ethers provider', () => { + const ethersProvider = new ethers.providers.JsonRpcProvider(); + expect(isViemClient(ethersProvider)).toBe(false); + }); + + it('should reject object without viem properties', () => { + const notViemClient = { + send: vi.fn(), + }; + expect(isViemClient(notViemClient)).toBe(false); + }); + }); + + describe('isViemSignerAccount', () => { + it('should identify actual viem account', () => { + const viemAccount = privateKeyToAccount(PRIVATE_KEY); + expect(isViemSignerAccount(viemAccount)).toBe(true); + }); + it('should identify viem account by address property', () => { + const viemAccountByAddress = { + address: '0x742d35Cc6632C0532c718F63b1a8D7d8a7fAd3b2', + }; + + expect(isViemSignerAccount(viemAccountByAddress)).toBe(true); + }); + + it('should reject ethers signer', () => { + const ethersSigner = new ethers.Wallet(PRIVATE_KEY); + expect(isViemSignerAccount(ethersSigner)).toBe(false); + }); + + it('should reject object with provider property', () => { + const notViemAccount = { + address: '0x742d35Cc6632C0532c718F63b1a8D7d8a7fAd3b2', + provider: {}, // This makes it look like an ethers signer + }; + + expect(isViemSignerAccount(notViemAccount)).toBe(false); + }); + }); + }); +}); diff --git a/packages/shared/test/viem-types.test.ts b/packages/shared/test/viem-types.test.ts new file mode 100644 index 000000000..a066d8cd4 --- /dev/null +++ b/packages/shared/test/viem-types.test.ts @@ -0,0 +1,27 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, expect, it } from 'vitest'; + +import type { PublicClient, SignerAccount } from '@nucypher/shared'; + +describe('viem types', () => { + it('should support viem-like client objects', () => { + const viemLikeClient: PublicClient = { + getChainId: () => Promise.resolve(80002), + call: (params: any) => Promise.resolve('0x1234'), + chain: { name: 'Polygon Amoy', id: 80002 }, + } as any; + + expect(viemLikeClient.getChainId).toBeDefined(); + expect(viemLikeClient.call).toBeDefined(); + }); + + it('should support viem-like account objects', () => { + const viemLikeAccount: SignerAccount = { + address: '0x742d35Cc6632C0532c718F63b1a8D7d8a7fAd3b2', + signMessage: () => Promise.resolve('0xsignature'), + } as any; + + expect(viemLikeAccount.address).toBeDefined(); + expect(viemLikeAccount.signMessage).toBeDefined(); + }); +}); diff --git a/packages/taco-auth/package.json b/packages/taco-auth/package.json index 2e68475af..ebd406dc1 100644 --- a/packages/taco-auth/package.json +++ b/packages/taco-auth/package.json @@ -15,6 +15,7 @@ "author": "NuCypher ", "exports": { ".": { + "types": "./dist/cjs/index.d.ts", "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" } @@ -26,11 +27,14 @@ "dist" ], "scripts": { - "prebuild": "pnpm clean", - "build": "pnpm build:module && pnpm build:cjs", + "prebuild": "pnpm clean && pnpm ensure-es-compatible-imports", + "build": "pnpm build:es && pnpm build:cjs", + "postbuild": "pnpm ensure-es-package-type", "build:cjs": "tsc --build ./tsconfig.cjs.json --verbose", - "build:module": "tsc --build ./tsconfig.es.json --verbose", + "build:es": "tsc --build ./tsconfig.es.json --verbose", "clean": "rm -rf dist", + "ensure-es-compatible-imports": "pnpm tsx ../../scripts/ensure-es-compatible-imports.ts", + "ensure-es-package-type": "pnpm tsx ../../scripts/ensure-es-package-type.ts", "exports:lint": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "lint": "eslint --ext .ts src test", "lint:fix": "pnpm lint --fix", diff --git a/packages/taco-auth/src/auth-provider.ts b/packages/taco-auth/src/auth-provider.ts index cc91f1765..a2f8bfabf 100644 --- a/packages/taco-auth/src/auth-provider.ts +++ b/packages/taco-auth/src/auth-provider.ts @@ -1,4 +1,4 @@ -import { AuthSignature } from './auth-sig'; +import { AuthSignature } from './auth-sig.js'; export interface AuthProvider { getOrCreateAuthSignature(): Promise; diff --git a/packages/taco-auth/src/auth-sig.ts b/packages/taco-auth/src/auth-sig.ts index 92720c750..8fe74e907 100644 --- a/packages/taco-auth/src/auth-sig.ts +++ b/packages/taco-auth/src/auth-sig.ts @@ -1,8 +1,8 @@ import { EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { EIP1271AuthSignature } from './providers/eip1271/auth'; -import { EIP4361AuthSignature } from './providers/eip4361/auth'; +import { EIP1271AuthSignature } from './providers/eip1271/auth.js'; +import { EIP4361AuthSignature } from './providers/eip4361/auth.js'; export const baseAuthSignatureSchema = z.object({ signature: z.string(), diff --git a/packages/taco-auth/src/index.ts b/packages/taco-auth/src/index.ts index 2791ece39..056db656e 100644 --- a/packages/taco-auth/src/index.ts +++ b/packages/taco-auth/src/index.ts @@ -1,3 +1,3 @@ -export * from './auth-provider'; -export * from './auth-sig'; -export * from './providers'; +export * from './auth-provider.js'; +export * from './auth-sig.js'; +export * from './providers/index.js'; diff --git a/packages/taco-auth/src/providers/eip1271/auth.ts b/packages/taco-auth/src/providers/eip1271/auth.ts index 4d8c7593c..1ebf0ad91 100644 --- a/packages/taco-auth/src/providers/eip1271/auth.ts +++ b/packages/taco-auth/src/providers/eip1271/auth.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { baseAuthSignatureSchema } from '../../auth-sig'; +import { baseAuthSignatureSchema } from '../../auth-sig.js'; export const EIP1271_AUTH_METHOD = 'EIP1271'; diff --git a/packages/taco-auth/src/providers/eip1271/eip1271.ts b/packages/taco-auth/src/providers/eip1271/eip1271.ts index 43d0c1561..5a9c3433d 100644 --- a/packages/taco-auth/src/providers/eip1271/eip1271.ts +++ b/packages/taco-auth/src/providers/eip1271/eip1271.ts @@ -1,6 +1,6 @@ -import { AuthProvider } from '../../auth-provider'; +import { AuthProvider } from '../../auth-provider.js'; -import { EIP1271_AUTH_METHOD, EIP1271AuthSignature } from './auth'; +import { EIP1271_AUTH_METHOD, EIP1271AuthSignature } from './auth.js'; /** * EIP1271AuthProvider handles EIP-1271 contract-based authentication. diff --git a/packages/taco-auth/src/providers/eip4361/auth.ts b/packages/taco-auth/src/providers/eip4361/auth.ts index 2c67d04cc..4541ec98d 100644 --- a/packages/taco-auth/src/providers/eip4361/auth.ts +++ b/packages/taco-auth/src/providers/eip4361/auth.ts @@ -1,7 +1,7 @@ import { SiweMessage } from 'siwe'; import { z } from 'zod'; -import { baseAuthSignatureSchema } from '../../auth-sig'; +import { baseAuthSignatureSchema } from '../../auth-sig.js'; export const EIP4361_AUTH_METHOD = 'EIP4361'; diff --git a/packages/taco-auth/src/providers/eip4361/eip4361.ts b/packages/taco-auth/src/providers/eip4361/eip4361.ts index 0a7481ecb..571e7aebb 100644 --- a/packages/taco-auth/src/providers/eip4361/eip4361.ts +++ b/packages/taco-auth/src/providers/eip4361/eip4361.ts @@ -1,14 +1,23 @@ +import { + ProviderLike, + PublicClient, + SignerAccount, + SignerLike, + TacoSigner, + toEthersProvider, + toTacoSigner, +} from '@nucypher/shared'; import { ethers } from 'ethers'; import { SiweMessage } from 'siwe'; -import { AuthProvider } from '../../auth-provider'; -import { LocalStorage } from '../../storage'; +import { AuthProvider } from '../../auth-provider.js'; +import { LocalStorage } from '../../storage.js'; import { EIP4361_AUTH_METHOD, EIP4361AuthSignature, eip4361AuthSignatureSchema, -} from './auth'; +} from './auth.js'; export type EIP4361AuthProviderParams = { domain: string; @@ -31,14 +40,32 @@ const TACO_DEFAULT_URI = 'https://taco.build'; * * Messages are valid for 2 hours from creation and stored locally keyed by the signer's address. * + * Supports both ethers.js and viem. + * * @implements {AuthProvider} + * + * @example Ethers.js usage + * ```typescript + * const provider = new ethers.providers.JsonRpcProvider(); + * const signer = new ethers.Wallet(privateKey, provider); + * const authProvider = new EIP4361AuthProvider(provider, signer); + * ``` + * + * @example Viem usage + * ```typescript + * const publicClient = createPublicClient({ chain: polygon, transport: http() }); + * const account = privateKeyToAccount('0x...'); + * const authProvider = new EIP4361AuthProvider(publicClient, account); + * ``` */ export class EIP4361AuthProvider implements AuthProvider { private readonly storage: LocalStorage; private readonly providerParams: EIP4361AuthProviderParams; + private readonly provider: ethers.providers.Provider; + private readonly signer: TacoSigner; /** - * Creates a new EIP4361AuthProvider instance. + * Creates a new EIP4361AuthProvider instance with ethers.js objects. * * @param provider - Ethers provider used to fetch the current chainId * @param signer - Ethers signer used to sign SIWE messages @@ -56,11 +83,43 @@ export class EIP4361AuthProvider implements AuthProvider { * - Nonce: Auto-generated */ constructor( - private readonly provider: ethers.providers.Provider, - private readonly signer: ethers.Signer, + provider: ethers.providers.Provider, + signer: ethers.Signer, + providerParams?: EIP4361AuthProviderParams, + ); + + /** + * Creates a new EIP4361AuthProvider instance with viem objects. + * + * @param publicClient - Viem public client used to fetch the current chainId + * @param account - Viem account used to sign SIWE messages + * @param providerParams - Optional SIWE message configuration + * @param providerParams.domain - Domain name for the signing request (e.g. 'app.example.com'). + * Defaults to current website domain or 'taco.build' + * @param providerParams.uri - Full URI of signing request origin (e.g. 'https://app.example.com'). + * Defaults to current website URL or 'https://taco.build' + * + * The SIWE message will include: + * - A human-readable statement: "{domain} wants you to sign in with your Ethereum account: {address}" + * - Version: "1" + * - 2 hour expiration from creation time + * - Chain ID from the provided provider + * - Nonce: Auto-generated + */ + constructor( + publicClient: PublicClient, + account: SignerAccount, + providerParams?: EIP4361AuthProviderParams, + ); + + constructor( + providerLike: ProviderLike, + signerLike: SignerLike, providerParams?: EIP4361AuthProviderParams, ) { this.storage = new LocalStorage(eip4361AuthSignatureSchema); + this.provider = toEthersProvider(providerLike); + this.signer = toTacoSigner(signerLike); if (providerParams) { this.providerParams = providerParams; } else { diff --git a/packages/taco-auth/src/providers/eip4361/external-eip4361.ts b/packages/taco-auth/src/providers/eip4361/external-eip4361.ts index 8f1eab7f9..2abf0cdfd 100644 --- a/packages/taco-auth/src/providers/eip4361/external-eip4361.ts +++ b/packages/taco-auth/src/providers/eip4361/external-eip4361.ts @@ -1,9 +1,9 @@ import { SiweMessage } from 'siwe'; -import { AuthProvider } from '../../auth-provider'; +import { AuthProvider } from '../../auth-provider.js'; -import { EIP4361_AUTH_METHOD, EIP4361AuthSignature } from './auth'; -import { FRESHNESS_IN_MILLISECONDS } from './eip4361'; +import { EIP4361_AUTH_METHOD, EIP4361AuthSignature } from './auth.js'; +import { FRESHNESS_IN_MILLISECONDS } from './eip4361.js'; async function generateAndVerifySiweMessage( message: string, diff --git a/packages/taco-auth/src/providers/index.ts b/packages/taco-auth/src/providers/index.ts index 76b739f00..6607e2b31 100644 --- a/packages/taco-auth/src/providers/index.ts +++ b/packages/taco-auth/src/providers/index.ts @@ -1,9 +1,9 @@ // TODO: should we export with package names? -export { EIP1271AuthSignature } from './eip1271/auth'; -export * from './eip1271/eip1271'; +export { EIP1271AuthSignature } from './eip1271/auth.js'; +export * from './eip1271/eip1271.js'; export { EIP4361AuthSignature, USER_ADDRESS_PARAM_DEFAULT, -} from './eip4361/auth'; -export * from './eip4361/eip4361'; -export * from './eip4361/external-eip4361'; +} from './eip4361/auth.js'; +export * from './eip4361/eip4361.js'; +export * from './eip4361/external-eip4361.js'; diff --git a/packages/taco-auth/src/storage.ts b/packages/taco-auth/src/storage.ts index fc3ef4165..24e48af1f 100644 --- a/packages/taco-auth/src/storage.ts +++ b/packages/taco-auth/src/storage.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { AuthSignature } from './index'; +import { AuthSignature } from './index.js'; interface IStorage { getItem(key: string): string | null; diff --git a/packages/taco/README.md b/packages/taco/README.md index 0fd7afc6f..5eeecb82f 100644 --- a/packages/taco/README.md +++ b/packages/taco/README.md @@ -64,6 +64,156 @@ const decryptedMessage = await decrypt( ); ``` +## Viem Support + +The TACo SDK supports both [ethers.js](https://docs.ethers.org/) natively, and [viem](https://viem.sh). The same `encrypt` and `decrypt` functions work with both libraries. Here is how to use them with viem: + +```bash +$ yarn add @nucypher/taco viem +``` + +```typescript +import { encrypt, decrypt, conditions, domains, initialize } from '@nucypher/taco'; +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; + +// Initialize TACo +await initialize(); + +const viemClient = createPublicClient({ + chain: polygonAmoy, + transport: http(), +}); +const viemAccount = privateKeyToAccount('0x...'); + +const ownsNFT = new conditions.predefined.ERC721Ownership({ + contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', + parameters: [3591], + chain: 5, +}); + +// Same function names work with viem - TypeScript automatically selects the right overload +const messageKit = await encrypt( + viemClient, // viem PublicClient + domains.TESTNET, + 'my secret message', + ownsNFT, + ritualId, + viemAccount, // viem Signer Account (`LocalAccount` or `WalletClient`) +); + +// Decrypt with viem +const decryptedMessage = await decrypt( + viemClient, + domains.TESTNET, + messageKit, +); +``` + +### Automatic Library Detection + +TypeScript automatically detects which library objects you're passing and works seamlessly: + +```typescript +// Using ethers.js - automatically uses ethers implementation +const ethersEncrypted = await encrypt( + ethersProvider, // ethers.providers.Provider + domains.TESTNET, + message, + condition, + ritualId, + ethersSigner // ethers.Signer +); + +// Using viem - automatically uses viem implementation +const viemEncrypted = await encrypt( + publicClient, // viem PublicClient + domains.TESTNET, + message, + condition, + ritualId, + viemAccount // viem Signer Account (`LocalAccount` or `WalletClient`) +); +``` + +For detailed viem documentation, see [VIEM_SUPPORT.md](./VIEM_SUPPORT.md). + +## AccessClient - Object-Oriented Interface + +For applications requiring multiple TACo cryptographic operations or complex configuration management, the TACo SDK provides an optional object-oriented interface through the `AccessClient` class. This provides a stateful, higher-level abstraction over the functional API. + +The Object-Oriented API is fully backward compatible - you can use both APIs in +the same application as needed. Except that the AccessClient has additional validations +and hence throws some errors earlier with different error messages. + +NOTE: Using `AccessClient` is equivalent to using the functional API. +There are no specific recommendations on which approach to use. +Choose the one that best suits your development preferences. + +### Basic Usage + +```typescript +import { AccessClient, ConditionContext, DOMAIN_NAMES } from '@nucypher/taco'; +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; + +// Initialize TACo +await initialize(); + +// Set up viem client and account +const viemClient = createPublicClient({ + chain: polygonAmoy, + transport: http(), +}); +const viemAccount = privateKeyToAccount('0x...'); + +// Create AccessClient instance with domain constants +const accessClient = new AccessClient({ + domain: DOMAIN_NAMES.TESTNET, // TESTNET -> 'tapir' + ritualId: 6, + viemClient, + viemAccount +}); + +// Encrypt data +const messageKit = await accessClient.encrypt('Hello, secret!', condition); + +// Decrypt +const conditionContext = ConditionContext.fromMessageKit(messageKit); + +// if needed Add authentication for ":userAddress" in condition... + +const decryptedMessage = await accessClient.decrypt(messageKit, conditionContext); +// OR with encrypted bytes: +// const decryptedMessage = await accessClient.decrypt(messageKit.toBytes(), conditionContext); +``` + +### Dual Configuration Support + +AccessClient supports both viem and ethers.js configurations: + +```typescript +import { AccessClient, DOMAIN_NAMES } from '@nucypher/taco'; + +// With viem +const accessClientViem = new AccessClient({ + domain: DOMAIN_NAMES.TESTNET, + ritualId: 6, + viemClient, + viemAccount +}); + +// With ethers.js +const accessClientEthers = new AccessClient({ + domain: DOMAIN_NAMES.TESTNET, + ritualId: 6, + ethersProvider, + ethersSigner +}); +``` + ## Learn more Please find developer documentation for diff --git a/packages/taco/VIEM_SUPPORT.md b/packages/taco/VIEM_SUPPORT.md new file mode 100644 index 000000000..4fbcf4533 --- /dev/null +++ b/packages/taco/VIEM_SUPPORT.md @@ -0,0 +1,134 @@ +# Viem Support + +The TACo SDK provides unified `encrypt` and `decrypt` functions that work +seamlessly with both [ethers.js](https://docs.ethers.org/) and +[viem](https://viem.sh) through TypeScript function overloads. The same function +names automatically detect which library you're using based on parameter types. + +## Installation + +Viem is optional. Install it only if you want to use viem instead of ethers.js: + +```bash +npm install viem +``` + +### For Authentication Providers + +If you need authentication providers that work with viem, install the taco-auth +package: + +```bash +npm install @nucypher/taco-auth viem +``` + +## Supported Libraries + +The same `encrypt` and `decrypt` functions work with both ethers.js and viem. + +Here is how to use them with viem: + +```typescript +import { + encrypt, + decrypt, + conditions, + domains, + initialize, +} from '@nucypher/taco'; +import { createPublicClient, http } from 'viem'; +import { polygonAmoy } from 'viem/chains'; +import { privateKeyToAccount } from 'viem/accounts'; + +// Initialize TACo +await initialize(); + +// Create viem public client +const publicClient = createPublicClient({ + chain: polygonAmoy, + transport: http(), +}); +// Create account +const viemAccount = privateKeyToAccount('0x...'); + +// Create access condition +const condition = new conditions.predefined.erc20.ERC20Balance({ + contractAddress: '0x...', + chain: 80002, + parameters: [':userAddress'], + returnValueTest: { + comparator: '>', + value: 0, + }, +}); + +// Same function names work with viem - TypeScript automatically detects the right overload +const encryptedKit = await encrypt( + publicClient, // viem PublicClient + domains.DEVNET, // or 'lynx' + 'Hello, secret!', + condition, + 27, // ritual ID + viemAccount, // viem Signer Account (`LocalAccount` or `WalletClient`) +); + +// Same decrypt function works with viem +const decryptedMessage = await decrypt( + publicClient, + domains.DEVNET, + encryptedKit, +); + +console.log(new TextDecoder().decode(decryptedMessage)); // "Hello, secret!" +``` + +## Authentication Providers + +For applications that need authentication providers compatible with viem, use +the `@nucypher/taco-auth` package: + +### EIP4361AuthProvider + +`EIP4361AuthProvider` also supports both ethers.js and viem: + +```typescript +import { createPublicClient, http } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; +import { EIP4361AuthProvider } from '@nucypher/taco-auth'; + +const publicClient = createPublicClient({ + chain: polygonAmoy, + transport: http(), +}); +const account = privateKeyToAccount('0x...'); + +// Viem usage +const authProvider = new EIP4361AuthProvider(publicClient, account, { + domain: 'my-app.com', + uri: 'https://my-app.com', +}); + +const authSignature = await authProvider.getOrCreateAuthSignature(); +``` + +**Ethers.js Usage (for comparison):** + +```typescript +import { ethers } from 'ethers'; +import { EIP4361AuthProvider } from '@nucypher/taco-auth'; + +const provider = new ethers.providers.JsonRpcProvider(); +const signer = new ethers.Wallet('0x...', provider); + +// Ethers usage +const authProvider = new EIP4361AuthProvider(provider, signer); +``` + +## Installation + +Use the appropriate package based on your needs: + +- **Encryption only**: Install `@nucypher/taco` + `viem` +- **Authentication required**: Install both `@nucypher/taco` + + `@nucypher/taco-auth` + `viem` diff --git a/packages/taco/integration-test/access-client.test.ts b/packages/taco/integration-test/access-client.test.ts new file mode 100644 index 000000000..dc9721f5a --- /dev/null +++ b/packages/taco/integration-test/access-client.test.ts @@ -0,0 +1,252 @@ +import { beforeAll, describe, expect, test } from 'vitest'; + +import { fromBytes, toBytes } from '@nucypher/shared'; +import { + EIP4361AuthProvider, + USER_ADDRESS_PARAM_DEFAULT, +} from '@nucypher/taco-auth'; +import { ethers } from 'ethers'; +import { createPublicClient, http, LocalAccount } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; +import { + AccessClient, + AccessClientEthersConfig, + AccessClientViemConfig, + conditions, + initialize, + ThresholdMessageKit, +} from '../src'; +import { DkgClient } from '../src/dkg'; + +const RPC_PROVIDER_URL = 'https://rpc-amoy.polygon.technology'; +const ENCRYPTOR_PRIVATE_KEY = + '0x900edb9e8214b2353f82aa195e915128f419a92cfb8bbc0f4784f10ef4112b86'; +const CONSUMER_PRIVATE_KEY = + '0xf307e165339cb5deb2b8ec59c31a5c0a957b8e8453ce7fe8a19d9a4c8acf36d4'; +const DOMAIN = 'tapir'; +const RITUAL_ID = 6; +const CHAIN_ID = 80002; // Polygon Amoy + +// temp type-safe configuration interfaces just for this testing file +type ViemTestConfig = Omit; +type EthersTestConfig = Omit; + +describe.skipIf(!process.env.RUNNING_IN_CI)( + 'AccessClient Integration Test', + () => { + // Create viem accounts from private keys + const encryptorAccount = privateKeyToAccount( + ENCRYPTOR_PRIVATE_KEY as `0x${string}`, + ); + const consumerAccount = privateKeyToAccount( + CONSUMER_PRIVATE_KEY as `0x${string}`, + ); + + // Create viem clients for correct network (Polygon Amoy) + const viemPublicClient = createPublicClient({ + chain: polygonAmoy, + transport: http(RPC_PROVIDER_URL), + }); + const viemTestConfig: ViemTestConfig = { + viemClient: viemPublicClient, + viemSignerAccount: encryptorAccount, + }; + + // Create ethers clients for correct network (Polygon Amoy) + const ethersProvider = new ethers.providers.JsonRpcProvider( + RPC_PROVIDER_URL, + ); + const encryptorSigner = new ethers.Wallet( + ENCRYPTOR_PRIVATE_KEY, + ethersProvider, + ); + const consumerSigner = new ethers.Wallet( + CONSUMER_PRIVATE_KEY, + ethersProvider, + ); + + const ethersTestConfig: EthersTestConfig = { + ethersProvider, + ethersSigner: encryptorSigner, + }; + + beforeAll(async () => { + // Initialize the TACo library + await initialize(); + + // Verify both clients are connected to the correct network + const [viemChainId, ethersNetwork] = await Promise.all([ + viemPublicClient.getChainId(), + ethersProvider.getNetwork(), + ]); + + if (viemChainId !== CHAIN_ID) { + throw new Error( + `Viem client connected to wrong network. Expected ${CHAIN_ID}, got ${viemChainId}`, + ); + } + + if (ethersNetwork.chainId !== CHAIN_ID) { + throw new Error( + `Ethers provider connected to wrong network. Expected ${CHAIN_ID}, got ${ethersNetwork.chainId}`, + ); + } + }, 10000); + + const createTestCondition = () => { + return new conditions.base.rpc.RpcCondition({ + chain: CHAIN_ID, + method: 'eth_getBalance', + parameters: [':userAddress', 'latest'], + returnValueTest: { + comparator: '>=', + value: 0, + }, + }); + }; + + const createAuthProvider = ( + config: ViemTestConfig | EthersTestConfig, + customSigner?: ethers.Wallet | LocalAccount, + ) => { + const provider = + (config as EthersTestConfig).ethersProvider ?? + (config as ViemTestConfig).viemClient; + const signerToUse = + customSigner ?? + (config as EthersTestConfig).ethersSigner ?? + (config as ViemTestConfig).viemSignerAccount; + + return new EIP4361AuthProvider(provider, signerToUse as any); + }; + + const setupConditionContext = async ( + messageKit: ThresholdMessageKit, + config: ViemTestConfig | EthersTestConfig, + customSigner?: ethers.Wallet | LocalAccount, + ) => { + const conditionContext = + conditions.context.ConditionContext.fromMessageKit(messageKit); + + const authProvider = createAuthProvider(config, customSigner); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProvider, + ); + + return conditionContext; + }; + + describe + .skipIf(!process.env.RUNNING_IN_CI) + .each< + | ['ethers', EthersTestConfig, ethers.Wallet] + | ['viem', ViemTestConfig, LocalAccount] + >([ + ['ethers', ethersTestConfig, consumerSigner], + ['viem', viemTestConfig, consumerAccount], + ])('TacoClient with %s', (label, objects, consumerSigner) => { + test('should encrypt and decrypt a message using standard encrypt method', async () => { + // Setup + const accessClient = new AccessClient({ + domain: DOMAIN, + ritualId: RITUAL_ID, + ...objects, + }); + + // Create test message and condition + const messageString = `This is a secret message from TacoClient with ${label} 🤐`; + const message = toBytes(messageString); + const condition = createTestCondition(); + + // Encrypt the message + const messageKit = await accessClient.encrypt(message, condition); + expect(messageKit).toBeInstanceOf(ThresholdMessageKit); + expect(messageKit.toBytes()).toBeInstanceOf(Uint8Array); + + // Setup condition context for decryption + const conditionContext = await setupConditionContext( + messageKit, + objects, + consumerSigner, + ); + + // Test decryption with Uint8Array input + const decryptedBytes = await accessClient.decrypt( + messageKit.toBytes(), + conditionContext, + ); + const decryptedMessageString = fromBytes(decryptedBytes); + expect(decryptedMessageString).toEqual(messageString); + + // Test decryption with MessageKit object input + const decryptedBytes2 = await accessClient.decrypt( + messageKit, + conditionContext, + ); + const decryptedMessageString2 = fromBytes(decryptedBytes2); + expect(decryptedMessageString2).toEqual(messageString); + }, 30000); + + test('should encrypt and decrypt using offline encryptWithPublicKey method', async () => { + // Create AccessClient using the test configuration + const accessClient = new AccessClient({ + domain: DOMAIN, + ritualId: RITUAL_ID, + ...objects, + }); + + const messageString = 'This is an offline encrypted message 🔐'; + const message = toBytes(messageString); + const condition = createTestCondition(); + + // Get DKG public key from ritual for offline encryption + const dkgRitual = await DkgClient.getActiveRitual( + ethersProvider, + DOMAIN, + RITUAL_ID, + ); + const dkgPublicKey = dkgRitual.dkgPublicKey; + expect(dkgPublicKey).toBeDefined(); + + // Perform offline encryption with DKG public key + const messageKit = await accessClient.encryptWithPublicKey( + message, + condition, + dkgPublicKey, + ); + expect(messageKit).toBeInstanceOf(ThresholdMessageKit); + + // Setup condition context with consumer signer for decryption + const conditionContext = await setupConditionContext( + messageKit, + objects, + consumerSigner, + ); + + // Decrypt and verify + const decryptedBytes = await accessClient.decrypt( + messageKit, + conditionContext, + ); + const decryptedMessageString = fromBytes(decryptedBytes); + expect(decryptedMessageString).toEqual(messageString); + }, 15000); + + test('should successfully validate network configuration', async () => { + // Setup + const accessClient = new AccessClient({ + domain: DOMAIN, + ritualId: RITUAL_ID, + ...objects, + }); + + // Validate configuration with network calls + const validation = accessClient.validateConfig(); + + expect(validation).resolves.not.toThrow(); + }, 10000); + }); + }, +); diff --git a/packages/taco/integration-test/encrypt-decrypt.test.ts b/packages/taco/integration-test/encrypt-decrypt.test.ts index db1482940..eb1e02cd7 100644 --- a/packages/taco/integration-test/encrypt-decrypt.test.ts +++ b/packages/taco/integration-test/encrypt-decrypt.test.ts @@ -6,6 +6,9 @@ import { USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco-auth'; import { ethers } from 'ethers'; +import { createPublicClient, http, LocalAccount, PublicClient } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; import { conditions, decrypt, @@ -25,31 +28,49 @@ const DOMAIN = 'lynx'; const RITUAL_ID = 27; const CHAIN_ID = 80002; -describe.skipIf(!process.env.RUNNING_IN_CI)( +describe + .skipIf(!process.env.RUNNING_IN_CI) + .each< + | ['ethers', ethers.providers.Provider, ethers.Wallet, ethers.Wallet] + | ['viem', PublicClient, LocalAccount, LocalAccount] + >([ + [ + 'ethers', + new ethers.providers.JsonRpcProvider(RPC_PROVIDER_URL), + new ethers.Wallet(ENCRYPTOR_PRIVATE_KEY), + new ethers.Wallet(CONSUMER_PRIVATE_KEY), + ], + [ + 'viem', + createPublicClient({ + chain: polygonAmoy, + transport: http(RPC_PROVIDER_URL), + }), + privateKeyToAccount(ENCRYPTOR_PRIVATE_KEY), + privateKeyToAccount(CONSUMER_PRIVATE_KEY), + ], + ])( 'Taco Encrypt/Decrypt Integration Test', - () => { - let provider: ethers.providers.JsonRpcProvider; - let encryptorSigner: ethers.Wallet; - let consumerSigner: ethers.Wallet; - + (label, provider, encryptorSigner, consumerSigner) => { beforeAll(async () => { - provider = new ethers.providers.JsonRpcProvider(RPC_PROVIDER_URL); - encryptorSigner = new ethers.Wallet(ENCRYPTOR_PRIVATE_KEY, provider); - consumerSigner = new ethers.Wallet(CONSUMER_PRIVATE_KEY, provider); - // Initialize the library await initialize(); // Verify network connection - const network = await provider.getNetwork(); - if (network.chainId !== CHAIN_ID) { + let chainId: number; + if (provider instanceof ethers.providers.Provider) { + chainId = (await provider.getNetwork()).chainId; + } else { + chainId = provider.chain!.id as number; + } + if (chainId !== CHAIN_ID) { throw new Error( - `Provider connected to wrong network. Expected ${CHAIN_ID}, got ${network.chainId}`, + `Provider connected to wrong network. Expected ${CHAIN_ID}, got ${chainId}`, ); } }); - test('should encrypt and decrypt a message with large condition values', async (value) => { + test(`should encrypt and decrypt a message with large condition values using ${label}`, async () => { // Create test message const messageString = 'This is a secret 🤐'; const message = toBytes(messageString); @@ -106,10 +127,12 @@ describe.skipIf(!process.env.RUNNING_IN_CI)( USER_ADDRESS_PARAM_DEFAULT, ) ) { - const authProvider = new EIP4361AuthProvider(provider, consumerSigner, { - domain: 'localhost', - uri: 'http://localhost:3000', - }); + const authProvider = new EIP4361AuthProvider( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + consumerSigner as any, + ); conditionContext.addAuthProvider( USER_ADDRESS_PARAM_DEFAULT, authProvider, diff --git a/packages/taco/integration-test/viem-to-ethers-ens.test.ts b/packages/taco/integration-test/viem-to-ethers-ens.test.ts new file mode 100644 index 000000000..204af4faf --- /dev/null +++ b/packages/taco/integration-test/viem-to-ethers-ens.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, test, vi } from 'vitest'; + +import { toEthersProvider } from '@nucypher/shared'; +import { ethers } from 'ethers'; +import { createPublicClient, http } from 'viem'; +import { mainnet } from 'viem/chains'; + +describe.skipIf(!process.env.RUNNING_IN_CI)( + 'Viem-Ethers Adapter ENS Integration Tests', + () => { + test('should properly resolve ENS name to address when no viem chain object is provided', async () => { + // Test with chain that has ENS registry (mainnet) + // Note: mainnet from viem/chains includes ENS registry configuration + const mainnetViemClient = createPublicClient({ + transport: http('https://eth.llamarpc.com'), + }); + + // Convert to ethers provider to verify ENS address mapping + const ethersProvider = toEthersProvider(mainnetViemClient); + + // Test actual ENS name resolution to verify functionality + const resolvedAddress = await ethersProvider.resolveName('vitalik.eth'); + expect(resolvedAddress).toBeTruthy(); + // Currently vitalik.eth is "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045". + // But it may change in the future. So we only check if it's a valid Ethereum address. + expect(ethers.utils.isAddress(resolvedAddress)).toBe(true); // Valid Ethereum address format + + // Currently ethersProvider.network.ensAddress on mainnet is "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e". + expect(ethersProvider.network.ensAddress).toBeTruthy(); + }, 15000); + + test('should properly handle viem chain with explicit ENS registry configuration', async () => { + const chainWithEns = { + ...mainnet, + contracts: { + ...mainnet.contracts, + ensRegistry: { + // Manually providing ENS registry since viem/chains doesn't include it + address: + '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e' as `0x${string}`, + }, + }, + }; + + const mainnetViemClient = createPublicClient({ + chain: chainWithEns, + transport: http(), + }); + + // Convert to ethers provider to verify ENS address mapping + const ethersProvider = toEthersProvider(mainnetViemClient); + + // Don't check for console.warn since the behavior might vary + // The important part is that ENS registry is properly set when provided + + // Verify ENS registry address is properly set + expect(ethersProvider.network.ensAddress).toBe( + '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e', + ); + + // Test actual ENS name resolution to verify functionality + const resolvedAddress = await ethersProvider.resolveName('vitalik.eth'); + expect(resolvedAddress).toBeTruthy(); + expect(ethers.utils.isAddress(resolvedAddress)).toBe(true); // Valid Ethereum address format + }, 15000); + + test('should warn when viem chain lacks ENS registry configuration', async () => { + // mainnet from viem/chains does NOT include ensRegistry, only ensUniversalResolver + const mainnetViemClient = createPublicClient({ + chain: mainnet, + transport: http(), + }); + + const consoleSpy = vi.spyOn(console, 'warn'); + + // Convert to ethers provider + const ethersProvider = toEthersProvider(mainnetViemClient); + + // Should trigger warning since mainnet doesn't have ensRegistry in viem/chains + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('No ENS registry found on chain'), + ); + // Clean up the spy + consoleSpy.mockRestore(); + + // ENS address is expected to not be set + expect(ethersProvider.network.ensAddress).toBeUndefined(); + + // ENS operation is expected to fail + expect(ethersProvider.resolveName('vitalik.eth')).rejects.toThrow( + 'network does not support ENS', + ); + }); + }, +); diff --git a/packages/taco/package.json b/packages/taco/package.json index 39fcf9f7e..6e35115d0 100644 --- a/packages/taco/package.json +++ b/packages/taco/package.json @@ -14,6 +14,7 @@ "author": "NuCypher ", "exports": { ".": { + "types": "./dist/cjs/index.d.ts", "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" } @@ -25,14 +26,17 @@ "dist" ], "scripts": { - "prebuild": "pnpm clean", - "build": "pnpm build:module && pnpm build:cjs", + "prebuild": "pnpm clean && pnpm ensure-es-compatible-imports", + "build": "pnpm build:es && pnpm build:cjs", + "postbuild": "pnpm ensure-es-package-type", "build:cjs": "tsc --build ./tsconfig.cjs.json --verbose", - "build:module": "tsc --build ./tsconfig.es.json --verbose", + "build:es": "tsc --build ./tsconfig.es.json --verbose", "clean": "rm -rf dist", + "ensure-es-compatible-imports": "pnpm tsx ../../scripts/ensure-es-compatible-imports.ts", + "ensure-es-package-type": "pnpm tsx ../../scripts/ensure-es-package-type.ts", "exports:lint": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "generate-zod-docs": "pnpm dlx tsx scripts/schema-docs-generation.ts", - "integration-test": "vitest run --config integration-test/vitest.config.ts", + "integration-test": "export RUNNING_IN_CI=true && vitest run --config integration-test/vitest.config.ts", "lint": "eslint --ext .ts src test", "lint:fix": "pnpm lint --fix", "package-check": "package-check", @@ -54,7 +58,8 @@ "@types/semver": "^7.7.0", "dotenv": "^16.5.0", "glob": "^11.0.1", - "modified-zod2md": "0.1.5-modified.4" + "modified-zod2md": "0.1.5-modified.4", + "viem": "^2.0.0" }, "engines": { "node": ">=18", diff --git a/packages/taco/src/access-client/client.ts b/packages/taco/src/access-client/client.ts new file mode 100644 index 000000000..2743a74b2 --- /dev/null +++ b/packages/taco/src/access-client/client.ts @@ -0,0 +1,283 @@ +import { + DkgPublicKey, + initialize, + ThresholdMessageKit, +} from '@nucypher/nucypher-core'; + +import { Condition } from '../conditions/condition.js'; +import { ConditionContext } from '../conditions/context/index.js'; +import { decrypt, encrypt, encryptWithPublicKey } from '../taco.js'; + +import { AccessConfigValidator } from './config-validator.js'; +import { + type AccessClientConfig, + type AccessClientEthersConfig, + type AccessClientViemConfig, +} from './config.js'; + +/** + * AccessClient provides an object-oriented interface for TACo cryptographic operations + * + * This class encapsulates TACo access-control configuration and provides simplified methods + * for encryption and decryption operations. It handles configuration validation, + * automatic WASM initialization, and provides enhanced error messages. + * + * @example Using with viem: + * ```typescript + * import { AccessClient, DOMAIN_NAMES } from '@nucypher/taco'; + * import { createPublicClient, http } from 'viem'; + * import { polygonAmoy } from 'viem/chains'; + * import { privateKeyToAccount } from 'viem/accounts'; + * + * // Create viem client and account + * const viemClient = createPublicClient({ + * chain: polygonAmoy, + * transport: http() + * }); + * const viemAccount = privateKeyToAccount('0x...'); + * + * // Create AccessClient - WASM initializes automatically + * const accessClient = new AccessClient({ + * domain: DOMAIN_NAMES.TESTNET, // 'tapir' + * ritualId: 6, + * viemClient, + * viemAccount + * }); + * + * // Operations wait for initialization automatically + * const messageKit = await accessClient.encrypt('Hello, secret!', condition); + * const decrypted = await accessClient.decrypt(messageKit, conditionContext); + * ``` + * + * @example Using with ethers.js: + * ```typescript + * import { AccessClient, DOMAIN_NAMES } from '@nucypher/taco'; + * import { ethers } from 'ethers'; + * + * // Create ethers provider and signer + * const ethersProvider = new ethers.providers.JsonRpcProvider('https://rpc-amoy.polygon.technology'); + * const ethersSigner = new ethers.Wallet('0x...', ethersProvider); + * + * // Create AccessClient - WASM initializes automatically + * const accessClient = new AccessClient({ + * domain: DOMAIN_NAMES.TESTNET, + * ritualId: 6, + * ethersProvider, + * ethersSigner + * }); + * + * // Operations are safe and wait for readiness + * const messageKit = await accessClient.encrypt('Hello, secret!', condition); + * const decrypted = await accessClient.decrypt(messageKit, conditionContext); + * ``` + */ +export class AccessClient { + private config: AccessClientConfig; + private static initializationPromise: Promise; + + /** + * Initialize TACo WASM globally (singleton pattern) + * + * This method ensures TACo WASM is initialized exactly once across all AccessClient instances. + * Initialization happens automatically when creating clients or calling operations, but you can + * call this explicitly for performance optimization or error handling. + * + * @returns {Promise} Promise that resolves when TACo WASM is initialized + * + * @example + * ```typescript + * // Optional: Pre-initialize for better performance + * await AccessClient.initialize(); + * + * // All AccessClient instances share the same initialization + * const client1 = new AccessClient(config1); + * const client2 = new AccessClient(config2); + * + * // Operations automatically wait for initialization + * const encrypted = await client1.encrypt(data, condition); + * ``` + */ + static async initialize(): Promise { + if (!AccessClient.initializationPromise) { + AccessClient.initializationPromise = (async () => { + try { + await initialize(); + } catch (error) { + console.error(`TACo initialization failed: ${error}`); + throw error; // Re-throw to maintain error propagation + } + })(); + } + return AccessClient.initializationPromise; + } + + /** + * Create a new AccessClient instance + * + * @param {AccessClientConfig} config - Configuration for the AccessClient + * @throws {Error} If configuration is invalid + */ + constructor(config: AccessClientConfig) { + // Validate configuration using AccessConfig + const result = AccessConfigValidator.validateFast(config); + if (!result.isValid) { + throw new Error(`Invalid configuration: ${result.errors.join(', ')}`); + } + + this.config = config; + + AccessClient.initialize(); + } + + /** + * Fully validate the configuration including network provider checks + * + * This method performs comprehensive validation including: + * - Domain and ritual ID validation + * - Provider/signer configuration validation + * - Network compatibility check (calls provider to verify chain ID matches domain) + * + * @returns {Promise} Promise resolving to validation result with isValid boolean and errors array + * @throws {Error} If configuration validation fails + * + * @example + * ```typescript + * try { + * await accessClient.validateConfig(); + * console.log('Configuration is valid.'); + * } catch (error) { + * console.error('Configuration validation failed:', error.message); + * } + * ``` + */ + async validateConfig(): Promise { + const validationResult = await AccessConfigValidator.validate(this.config); + if (!validationResult.isValid) { + throw new Error( + `Invalid configuration: ${validationResult.errors.join(', ')}`, + ); + } + } + + /** + * Encrypt data with the given access condition + * + * @param {string | Uint8Array} data - String or Uint8Array to encrypt + * @param {Condition} accessCondition - Access condition for decryption + * @returns {Promise} Encrypted message kit + * @throws {Error} If encryption fails + * + * @example + * ```typescript + * const messageKit = await accessClient.encrypt('Hello, secret!', condition); + * ``` + */ + async encrypt( + data: string | Uint8Array, + accessCondition: Condition, + ): Promise { + await AccessClient.initialize(); + + const messageKit = await encrypt( + (this.config as AccessClientEthersConfig).ethersProvider || + (this.config as AccessClientViemConfig).viemClient, + this.config.domain, + data, + accessCondition, + this.config.ritualId, + (this.config as AccessClientEthersConfig).ethersSigner || + (this.config as AccessClientViemConfig).viemSignerAccount, + ); + + return messageKit; + } + + /** + * Encrypt data with a provided DKG public key under a specified condition + * + * This method can be used offline since it doesn't require network access to fetch + * the DKG public key (unlike the `encrypt` method which fetches it from the ritual). + * + * @param {string | Uint8Array} data - String or Uint8Array to encrypt + * @param {Condition} accessCondition - Access condition for decryption + * @param {DkgPublicKey} dkgPublicKey - The DKG public key to use for encryption + * @returns {Promise} Encrypted message kit + * @throws {Error} If encryption fails + * + * @example + * ```typescript + * // Get DKG public key from ritual or cache + * const dkgPublicKey = await getDkgPublicKey(domain, ritualId); + * + * // Encrypt offline using the public key + * const messageKit = await accessClient.encryptWithPublicKey('Hello, secret!', condition, dkgPublicKey); + * ``` + */ + async encryptWithPublicKey( + data: string | Uint8Array, + accessCondition: Condition, + dkgPublicKey: DkgPublicKey, + ): Promise { + await AccessClient.initialize(); + + const messageKit = await encryptWithPublicKey( + data, + accessCondition, + dkgPublicKey, + (this.config as AccessClientEthersConfig).ethersSigner || + (this.config as AccessClientViemConfig).viemSignerAccount, + ); + + return messageKit; + } + + /** + * Decrypt data using TACo + * + * @param {ThresholdMessageKit | Uint8Array} encryptedData - Either a ThresholdMessageKit or encrypted bytes (Uint8Array) + * @param {ConditionContext} [conditionContext] - Optional condition context for time-based conditions + * @returns {Promise} Decrypted data + * @throws {Error} If decryption fails + * + * @example + * ```typescript + * // With messageKit + * const decrypted = await accessClient.decrypt(messageKit, conditionContext); + * + * // With encrypted bytes + * const decrypted = await accessClient.decrypt(encryptedBytes, conditionContext); + * ``` + */ + async decrypt( + encryptedData: ThresholdMessageKit | Uint8Array, + conditionContext?: ConditionContext, + ): Promise { + await AccessClient.initialize(); + + // Handle both messageKit and encrypted bytes + const messageKit = + encryptedData instanceof ThresholdMessageKit + ? encryptedData + : ThresholdMessageKit.fromBytes(encryptedData); + + const decrypted = await decrypt( + (this.config as AccessClientEthersConfig).ethersProvider || + (this.config as AccessClientViemConfig).viemClient, + this.config.domain, + messageKit, + conditionContext, + this.config.porterUris, + ); + + return decrypted; + } + + /** + * Get current client configuration + * + * @returns {Readonly} Client configuration + */ + getConfig(): Readonly { + return Object.freeze({ ...this.config }); + } +} diff --git a/packages/taco/src/access-client/config-validator.ts b/packages/taco/src/access-client/config-validator.ts new file mode 100644 index 000000000..d929643d5 --- /dev/null +++ b/packages/taco/src/access-client/config-validator.ts @@ -0,0 +1,272 @@ +/** + * TACo Domain Configuration and Validation + * + * This module provides domain configuration management, validation utilities, + * and configuration processing for TACo operations across different networks. + */ + +import { + DomainName, + DOMAINS, + isViemClient, + ProviderLike, +} from '@nucypher/shared'; +import { ethers } from 'ethers'; +import type { PublicClient } from 'viem'; + +import { + type AccessClientConfig, + isEthersAccessClientConfig, + isViemAccessClientConfig, +} from './index.js'; + +/** + * Generic validation result interface + */ +export interface ValidationResult { + isValid: boolean; + errors: string[]; +} + +/** + * Access Configuration Validator + * + * Validates Access client configurations, domains, and provider compatibility. + * Provides both fast and full validation methods for TACo operations. + */ +export class AccessConfigValidator { + /** + * Get all supported TACo domain names + * @returns {DomainName[]} Array of supported TACo domain names ('lynx', 'tapir', 'mainnet') + */ + static getSupportedDomains(): DomainName[] { + return Object.values(DOMAINS).map((domain) => domain.domain); + } + + /** + * Check if domain is valid + * @param {DomainName} domain - TACo domain name to check ('lynx', 'tapir', 'mainnet') + * @returns {boolean} True if domain exists + */ + static isValidDomain(domain: DomainName): boolean { + return !!domain && this.getSupportedDomains().includes(domain); + } + + /** + * Get expected chain ID for domain from DOMAINS configuration + * @param {DomainName} domain - Domain name to look up + * @returns {number | undefined} Chain ID for the domain, undefined if not found + * @private + */ + private static getExpectedChainId(domain: DomainName): number | undefined { + const domainEntry = Object.values(DOMAINS).find( + (domainConfig) => domainConfig.domain === domain, + ); + return domainEntry?.chainId; + } + + /** + * Validate ritual ID (basic validation - positive integer or 0) + * @param {number} ritualId - Ritual ID to validate + * @returns {boolean} True if valid (positive integer or 0) + */ + static isValidRitualId(ritualId: number): boolean { + return ( + typeof ritualId === 'number' && + Number.isInteger(ritualId) && + ritualId >= 0 + ); + } + + /** + * Validate provider compatibility with domain + * @param {DomainName} domain - Domain name + * @param {ProviderLike} provider - Provider to validate (ethers Provider or viem PublicClient) + * @returns {Promise} True if provider is valid for domain + */ + static async isValidProvider( + domain: DomainName, + provider: ProviderLike, + ): Promise { + let chainId: number; + + if (!provider || typeof provider !== 'object') { + // Invalid provider + return false; + } + + // Try to detect provider type and get chain ID safely + try { + if (isViemClient(provider)) { + chainId = await (provider as PublicClient).getChainId(); + } else { + const network = await ( + provider as ethers.providers.Provider + ).getNetwork(); + chainId = network.chainId; + } + } catch (error) { + // Error getting chain ID + return false; + } + + // Check if the provider's chain ID matches the domain's expected chain ID + return ( + Object.values(DOMAINS).find( + (domainInfo) => + domainInfo.domain === domain && domainInfo.chainId === chainId, + ) !== undefined + ); + } + + /** + * Fast validation (everything except provider network checks) + * + * Performs synchronous validation of configuration including: + * - Domain name validation + * - Ritual ID validation to ensure it is a positive integer + * - Provider/signer presence validation + * - Chain compatibility check (if chain info is available synchronously) + * + * @param {TacoClientConfig} config - Configuration to validate + * @returns {ValidationResult} Validation result with isValid boolean and errors array + */ + static validateFast(config: AccessClientConfig): ValidationResult { + const errors: string[] = []; + + // Validate domain + if (!config.domain) { + errors.push('The property `domain` is required'); + } else if (!this.isValidDomain(config.domain)) { + errors.push( + `Invalid domain name: ${config.domain}. Supported domains: ${this.getSupportedDomains().join(', ')}`, + ); + } + + // Validate ritual ID + if (!config.ritualId) { + errors.push('The property `ritualId` is required'); + } else if (!this.isValidRitualId(config.ritualId)) { + errors.push( + `Invalid ritual ID: ${config.ritualId} for domain ${config.domain}`, + ); + } + + // Validate blockchain client configuration + if (isViemAccessClientConfig(config)) { + // Viem configuration + if (!config.viemClient) { + errors.push('viemClient is required for viem configuration'); + } + if (!config.viemSignerAccount) { + errors.push('viemSignerAccount is required for viem configuration'); + } + } else if (isEthersAccessClientConfig(config)) { + // Ethers configuration + if (!config.ethersProvider) { + errors.push('ethersProvider is required for ethers configuration'); + } + if (!config.ethersSigner) { + errors.push('ethersSigner is required for ethers configuration'); + } + } else { + errors.push( + 'Configuration must include either viem objects (viemClient + viemSignerAccount) or ethers objects (ethersProvider + ethersSigner)', + ); + } + + // Validate chain compatibility (synchronous check) + const chainValidation = this.validateChainCompatibility(config); + if (!chainValidation.isValid) { + errors.push(...chainValidation.errors); + } + + return { isValid: errors.length === 0, errors }; + } + + /** + * Synchronous chain compatibility validation + * + * Validates provider chain compatibility with domain requirements using + * synchronously available chain information. + * + * @param {TacoClientConfig} config - Configuration to validate + * @returns {ValidationResult} Validation result + * @private + */ + private static validateChainCompatibility( + config: AccessClientConfig, + ): ValidationResult { + const errors: string[] = []; + + // Get expected chain ID for domain + const expectedChainId = this.getExpectedChainId(config.domain); + if (!expectedChainId) { + errors.push(`Unsupported domain: ${config.domain}`); + return { isValid: false, errors }; + } + + if (isViemAccessClientConfig(config) && config.viemClient) { + // Note: If viemClient.chain is undefined, we skip synchronous validation + // Full validation with validateFull() will perform the network check + const viemClient = config.viemClient as PublicClient; + if (viemClient.chain && viemClient.chain.id !== expectedChainId) { + errors.push( + `Provider chain mismatch: viem client chain ID ${viemClient.chain.id} does not match domain '${config.domain}' (expected ${expectedChainId})`, + ); + } + } // No need to count for the other cases. The caller methods already handle them. + + return { isValid: errors.length === 0, errors }; + } + + /** + * Full validation including async provider network checks + * + * Performs comprehensive validation including: + * - All fast validation checks + * - Async network calls to verify provider chain ID matches domain requirements + * + * Use this method when you need complete validation including network connectivity checks. + * For faster validation without network calls, use validateFast(). + * + * @param {TacoClientConfig} config - Configuration to validate + * @returns {Promise} Promise resolving to validation result with isValid boolean and errors array + */ + static async validate(config: AccessClientConfig): Promise { + // First run fast validation + const fastResult = this.validateFast(config); + if (!fastResult.isValid) { + return fastResult; + } + + const errors: string[] = []; + + // Additional async provider validation + let provider: PublicClient | ethers.providers.Provider | undefined; + + if (isViemAccessClientConfig(config)) { + provider = config.viemClient; + } else if (isEthersAccessClientConfig(config)) { + provider = config.ethersProvider; + } + + // Validate provider compatibility with domain (if both exist) + if (provider && config.domain) { + const isValidProvider = await this.isValidProvider( + config.domain, + provider, + ); + if (!isValidProvider) { + errors.push( + `Invalid provider for domain: ${config.domain}. Provider chain ID does not match domain requirements.`, + ); + } + } + + return { + isValid: errors.length === 0, + errors, + }; + } +} diff --git a/packages/taco/src/access-client/config.ts b/packages/taco/src/access-client/config.ts new file mode 100644 index 000000000..7ce37cd4c --- /dev/null +++ b/packages/taco/src/access-client/config.ts @@ -0,0 +1,74 @@ +/** + * AccessClient configuration types and utilities + * + * This module contains all configuration interfaces, type definitions, and utility functions + * for configuring AccessClient instances with different blockchain client libraries (viem, ethers.js). + */ + +import { + DomainName, + type PublicClient, + type SignerAccount, +} from '@nucypher/shared'; +import type { ethers } from 'ethers'; + +/** + * Base configuration for AccessClient + */ +interface AccessClientBaseConfig { + /** TACo domain name (e.g., 'lynx', 'tapir', 'mainnet') */ + domain: DomainName; + /** Ritual ID for the TACo operations */ + ritualId: number; + /** Optional Porter URIs */ + porterUris?: string[]; +} + +/** + * Viem configuration for AccessClient + */ +export interface AccessClientViemConfig extends AccessClientBaseConfig { + /** Viem PublicClient for blockchain operations */ + viemClient: PublicClient; + /** Viem SignerAccount for signing operations */ + viemSignerAccount: SignerAccount; +} + +/** + * Ethers configuration for AccessClient + */ +export interface AccessClientEthersConfig extends AccessClientBaseConfig { + /** Ethers Provider for blockchain operations */ + ethersProvider: ethers.providers.Provider; + /** Ethers Signer for signing operations */ + ethersSigner: ethers.Signer; +} + +/** + * Union type for AccessClient configuration - supports both viem and ethers.js + */ +export type AccessClientConfig = + | AccessClientViemConfig + | AccessClientEthersConfig; + +/** + * Type guard to check if config is viem-based + * @param config - AccessClient configuration to check + * @returns true if the configuration is for viem client + */ +export function isViemAccessClientConfig( + config: AccessClientConfig, +): config is AccessClientViemConfig { + return 'viemClient' in config && 'viemSignerAccount' in config; +} + +/** + * Type guard to check if config is ethers-based + * @param config - AccessClient configuration to check + * @returns true if the configuration is for ethers client + */ +export function isEthersAccessClientConfig( + config: AccessClientConfig, +): config is AccessClientEthersConfig { + return 'ethersProvider' in config && 'ethersSigner' in config; +} diff --git a/packages/taco/src/access-client/index.ts b/packages/taco/src/access-client/index.ts new file mode 100644 index 000000000..652b3f1c6 --- /dev/null +++ b/packages/taco/src/access-client/index.ts @@ -0,0 +1,2 @@ +export * from './client.js'; +export * from './config.js'; diff --git a/packages/taco/src/conditions/base/contract.ts b/packages/taco/src/conditions/base/contract.ts index 54df510dd..bfc236234 100644 --- a/packages/taco/src/conditions/base/contract.ts +++ b/packages/taco/src/conditions/base/contract.ts @@ -1,17 +1,17 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { ContractConditionProps, contractConditionSchema, ContractConditionType, -} from '../schemas/contract'; -import { OmitConditionType } from '../shared'; +} from '../schemas/contract.js'; +import { OmitConditionType } from '../shared.js'; export { ContractConditionProps, contractConditionSchema, ContractConditionType, FunctionAbiProps, -} from '../schemas/contract'; +} from '../schemas/contract.js'; export class ContractCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/index.ts b/packages/taco/src/conditions/base/index.ts index 6de843d86..65fd0836b 100644 --- a/packages/taco/src/conditions/base/index.ts +++ b/packages/taco/src/conditions/base/index.ts @@ -1,9 +1,9 @@ // Exporting classes here instead of their respective schema files to // avoid circular dependency on Condition class. -export * as contract from './contract'; -export * as jsonApi from './json-api'; -export * as jsonRpc from './json-rpc'; -export * as jwt from './jwt'; -export * as rpc from './rpc'; -export * as time from './time'; +export * as contract from './contract.js'; +export * as jsonApi from './json-api.js'; +export * as jsonRpc from './json-rpc.js'; +export * as jwt from './jwt.js'; +export * as rpc from './rpc.js'; +export * as time from './time.js'; diff --git a/packages/taco/src/conditions/base/json-api.ts b/packages/taco/src/conditions/base/json-api.ts index c972b5868..c44088b04 100644 --- a/packages/taco/src/conditions/base/json-api.ts +++ b/packages/taco/src/conditions/base/json-api.ts @@ -1,16 +1,16 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { JsonApiConditionProps, jsonApiConditionSchema, JsonApiConditionType, -} from '../schemas/json-api'; -import { OmitConditionType } from '../shared'; +} from '../schemas/json-api.js'; +import { OmitConditionType } from '../shared.js'; export { JsonApiConditionProps, jsonApiConditionSchema, JsonApiConditionType, -} from '../schemas/json-api'; +} from '../schemas/json-api.js'; export class JsonApiCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/json-rpc.ts b/packages/taco/src/conditions/base/json-rpc.ts index 756e651c0..cbe681aa1 100644 --- a/packages/taco/src/conditions/base/json-rpc.ts +++ b/packages/taco/src/conditions/base/json-rpc.ts @@ -1,16 +1,16 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { JsonRpcConditionProps, jsonRpcConditionSchema, JsonRpcConditionType, -} from '../schemas/json-rpc'; -import { OmitConditionType } from '../shared'; +} from '../schemas/json-rpc.js'; +import { OmitConditionType } from '../shared.js'; export { JsonRpcConditionProps, jsonRpcConditionSchema, JsonRpcConditionType, -} from '../schemas/json-rpc'; +} from '../schemas/json-rpc.js'; export class JsonRpcCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/jwt.ts b/packages/taco/src/conditions/base/jwt.ts index 92469a4cb..4eaa428e0 100644 --- a/packages/taco/src/conditions/base/jwt.ts +++ b/packages/taco/src/conditions/base/jwt.ts @@ -1,17 +1,17 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { JWTConditionProps, jwtConditionSchema, JWTConditionType, -} from '../schemas/jwt'; -import { OmitConditionType } from '../shared'; +} from '../schemas/jwt.js'; +import { OmitConditionType } from '../shared.js'; export { JWT_PARAM_DEFAULT, JWTConditionProps, jwtConditionSchema, JWTConditionType, -} from '../schemas/jwt'; +} from '../schemas/jwt.js'; export class JWTCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/rpc.ts b/packages/taco/src/conditions/base/rpc.ts index 8f73c79c1..8376e4a21 100644 --- a/packages/taco/src/conditions/base/rpc.ts +++ b/packages/taco/src/conditions/base/rpc.ts @@ -1,16 +1,16 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { RpcConditionProps, rpcConditionSchema, RpcConditionType, -} from '../schemas/rpc'; -import { OmitConditionType } from '../shared'; +} from '../schemas/rpc.js'; +import { OmitConditionType } from '../shared.js'; export { RpcConditionProps, rpcConditionSchema, RpcConditionType, -} from '../schemas/rpc'; +} from '../schemas/rpc.js'; export class RpcCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/base/time.ts b/packages/taco/src/conditions/base/time.ts index 2d8be8b63..96f49a105 100644 --- a/packages/taco/src/conditions/base/time.ts +++ b/packages/taco/src/conditions/base/time.ts @@ -1,17 +1,17 @@ -import { Condition } from '../condition'; +import { Condition } from '../condition.js'; import { TimeConditionProps, timeConditionSchema, TimeConditionType, -} from '../schemas/time'; -import { OmitConditionType } from '../shared'; +} from '../schemas/time.js'; +import { OmitConditionType } from '../shared.js'; export { TimeConditionMethod, TimeConditionProps, timeConditionSchema, TimeConditionType, -} from '../schemas/time'; +} from '../schemas/time.js'; export class TimeCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/compound-condition.ts b/packages/taco/src/conditions/compound-condition.ts index d7de784e0..87a2af387 100644 --- a/packages/taco/src/conditions/compound-condition.ts +++ b/packages/taco/src/conditions/compound-condition.ts @@ -1,16 +1,16 @@ -import { Condition, ConditionProps } from './condition'; +import { Condition, ConditionProps } from './condition.js'; import { CompoundConditionProps, compoundConditionSchema, CompoundConditionType, -} from './schemas/compound'; -import { OmitConditionType } from './shared'; +} from './schemas/compound.js'; +import { OmitConditionType } from './shared.js'; export { CompoundConditionProps, compoundConditionSchema, CompoundConditionType, -} from './schemas/compound'; +} from './schemas/compound.js'; export type ConditionOrProps = Condition | ConditionProps; diff --git a/packages/taco/src/conditions/condition-expr.ts b/packages/taco/src/conditions/condition-expr.ts index db1c0bed2..104a0b6c0 100644 --- a/packages/taco/src/conditions/condition-expr.ts +++ b/packages/taco/src/conditions/condition-expr.ts @@ -1,10 +1,10 @@ import { Conditions as CoreConditions } from '@nucypher/nucypher-core'; import { SemVer } from 'semver'; -import { fromJSON, toJSON } from '../utils'; +import { fromJSON, toJSON } from '../utils.js'; -import { Condition } from './condition'; -import { ConditionFactory } from './condition-factory'; +import { ConditionFactory } from './condition-factory.js'; +import { Condition } from './condition.js'; const ERR_VERSION = (provided: string, current: string) => `Version provided, ${provided}, is incompatible with current version, ${current}`; diff --git a/packages/taco/src/conditions/condition-factory.ts b/packages/taco/src/conditions/condition-factory.ts index ab84ae909..895ccae15 100644 --- a/packages/taco/src/conditions/condition-factory.ts +++ b/packages/taco/src/conditions/condition-factory.ts @@ -2,40 +2,40 @@ import { ContractCondition, ContractConditionProps, ContractConditionType, -} from './base/contract'; +} from './base/contract.js'; import { JsonApiCondition, JsonApiConditionProps, JsonApiConditionType, -} from './base/json-api'; +} from './base/json-api.js'; import { JsonRpcCondition, JsonRpcConditionProps, JsonRpcConditionType, -} from './base/json-rpc'; -import { JWTCondition, JWTConditionProps, JWTConditionType } from './base/jwt'; -import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc'; +} from './base/json-rpc.js'; +import { JWTCondition, JWTConditionProps, JWTConditionType } from './base/jwt.js'; +import { RpcCondition, RpcConditionProps, RpcConditionType } from './base/rpc.js'; import { TimeCondition, TimeConditionProps, TimeConditionType, -} from './base/time'; +} from './base/time.js'; import { CompoundCondition, CompoundConditionProps, CompoundConditionType, -} from './compound-condition'; -import { Condition, ConditionProps } from './condition'; +} from './compound-condition.js'; +import { Condition, ConditionProps } from './condition.js'; import { IfThenElseCondition, IfThenElseConditionProps, IfThenElseConditionType, -} from './if-then-else-condition'; +} from './if-then-else-condition.js'; import { SequentialCondition, SequentialConditionProps, SequentialConditionType, -} from './sequential'; +} from './sequential.js'; const ERR_INVALID_CONDITION_TYPE = (type: string) => `Invalid condition type: ${type}`; diff --git a/packages/taco/src/conditions/condition.ts b/packages/taco/src/conditions/condition.ts index 1db41e3b6..30f8648df 100644 --- a/packages/taco/src/conditions/condition.ts +++ b/packages/taco/src/conditions/condition.ts @@ -1,11 +1,11 @@ import { objectEquals } from '@nucypher/shared'; import { z } from 'zod'; -import { toJSON } from '../utils'; +import { toJSON } from '../utils.js'; -import { USER_ADDRESS_PARAMS } from './const'; +import { USER_ADDRESS_PARAMS } from './const.js'; -export { baseConditionSchema } from './schemas/common'; +export { baseConditionSchema } from './schemas/common.js'; type ConditionSchema = z.ZodSchema; export type ConditionProps = z.infer; diff --git a/packages/taco/src/conditions/context/context.ts b/packages/taco/src/conditions/context/context.ts index c22993ac8..c66a28014 100644 --- a/packages/taco/src/conditions/context/context.ts +++ b/packages/taco/src/conditions/context/context.ts @@ -8,16 +8,16 @@ import { USER_ADDRESS_PARAM_DEFAULT, } from '@nucypher/taco-auth'; -import { CoreConditions, CoreContext } from '../../types'; -import { toJSON } from '../../utils'; -import { Condition, ConditionProps } from '../condition'; -import { ConditionExpression } from '../condition-expr'; +import { CoreConditions, CoreContext } from '../../types.js'; +import { toJSON } from '../../utils.js'; +import { ConditionExpression } from '../condition-expr.js'; +import { Condition, ConditionProps } from '../condition.js'; import { CONTEXT_PARAM_FULL_MATCH_REGEXP, CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP, USER_ADDRESS_PARAMS, -} from '../const'; +} from '../const.js'; export type CustomContextParam = | string diff --git a/packages/taco/src/conditions/context/index.ts b/packages/taco/src/conditions/context/index.ts index e18afda2a..5a1db3dfb 100644 --- a/packages/taco/src/conditions/context/index.ts +++ b/packages/taco/src/conditions/context/index.ts @@ -1 +1 @@ -export { ConditionContext, type CustomContextParam } from './context'; +export { ConditionContext, type CustomContextParam } from './context.js'; diff --git a/packages/taco/src/conditions/if-then-else-condition.ts b/packages/taco/src/conditions/if-then-else-condition.ts index 79d99b49c..03635363e 100644 --- a/packages/taco/src/conditions/if-then-else-condition.ts +++ b/packages/taco/src/conditions/if-then-else-condition.ts @@ -1,16 +1,16 @@ -import { Condition } from './condition'; +import { Condition } from './condition.js'; import { IfThenElseConditionProps, ifThenElseConditionSchema, IfThenElseConditionType, -} from './schemas/if-then-else'; -import { OmitConditionType } from './shared'; +} from './schemas/if-then-else.js'; +import { OmitConditionType } from './shared.js'; export { IfThenElseConditionProps, ifThenElseConditionSchema, IfThenElseConditionType, -} from './schemas/if-then-else'; +} from './schemas/if-then-else.js'; export class IfThenElseCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/index.ts b/packages/taco/src/conditions/index.ts index 0c8c3d5c8..be40bebc9 100644 --- a/packages/taco/src/conditions/index.ts +++ b/packages/taco/src/conditions/index.ts @@ -1,11 +1,11 @@ -import * as base from './base'; -import * as predefined from './predefined'; +import * as base from './base/index.js'; +import * as predefined from './predefined/index.js'; -export * as compound from './compound-condition'; -export * as condition from './condition'; -export * as conditionExpr from './condition-expr'; -export { ConditionFactory } from './condition-factory'; -export * as context from './context'; -export * as ifThenElse from './if-then-else-condition'; -export * as sequential from './sequential'; +export * as compound from './compound-condition.js'; +export * as condition from './condition.js'; +export * as conditionExpr from './condition-expr.js'; +export { ConditionFactory } from './condition-factory.js'; +export * as context from './context/index.js'; +export * as ifThenElse from './if-then-else-condition.js'; +export * as sequential from './sequential.js'; export { base, predefined }; diff --git a/packages/taco/src/conditions/multi-condition.ts b/packages/taco/src/conditions/multi-condition.ts index 434ab812a..f2e3fb9fa 100644 --- a/packages/taco/src/conditions/multi-condition.ts +++ b/packages/taco/src/conditions/multi-condition.ts @@ -1,7 +1,7 @@ -import { CompoundConditionType } from './compound-condition'; -import { ConditionProps } from './condition'; -import { IfThenElseConditionType } from './if-then-else-condition'; -import { ConditionVariableProps, SequentialConditionType } from './sequential'; +import { CompoundConditionType } from './compound-condition.js'; +import { ConditionProps } from './condition.js'; +import { IfThenElseConditionType } from './if-then-else-condition.js'; +import { ConditionVariableProps, SequentialConditionType } from './sequential.js'; export const maxNestedDepth = (maxDepth: number) => diff --git a/packages/taco/src/conditions/predefined/erc20.ts b/packages/taco/src/conditions/predefined/erc20.ts index cd3087e7f..eeaa3ab70 100644 --- a/packages/taco/src/conditions/predefined/erc20.ts +++ b/packages/taco/src/conditions/predefined/erc20.ts @@ -4,7 +4,7 @@ import { ContractCondition, ContractConditionProps, ContractConditionType, -} from '../base/contract'; +} from '../base/contract.js'; type ERC20BalanceFields = 'contractAddress' | 'chain' | 'returnValueTest'; diff --git a/packages/taco/src/conditions/predefined/erc721.ts b/packages/taco/src/conditions/predefined/erc721.ts index 90b2a33de..15ec2e20c 100644 --- a/packages/taco/src/conditions/predefined/erc721.ts +++ b/packages/taco/src/conditions/predefined/erc721.ts @@ -4,7 +4,7 @@ import { ContractCondition, ContractConditionProps, ContractConditionType, -} from '../base/contract'; +} from '../base/contract.js'; type ERC721OwnershipFields = 'contractAddress' | 'chain' | 'parameters'; diff --git a/packages/taco/src/conditions/predefined/index.ts b/packages/taco/src/conditions/predefined/index.ts index 7d29ceb02..7309abda4 100644 --- a/packages/taco/src/conditions/predefined/index.ts +++ b/packages/taco/src/conditions/predefined/index.ts @@ -1,2 +1,2 @@ -export * as erc20 from './erc20'; -export * as erc721 from './erc721'; +export * as erc20 from './erc20.js'; +export * as erc721 from './erc721.js'; diff --git a/packages/taco/src/conditions/schemas/common.ts b/packages/taco/src/conditions/schemas/common.ts index 6669e4d30..f4050d7f0 100644 --- a/packages/taco/src/conditions/schemas/common.ts +++ b/packages/taco/src/conditions/schemas/common.ts @@ -2,7 +2,7 @@ import { JSONPath } from '@astronautlabs/jsonpath'; import { USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'; import { Primitive, z, ZodLiteral } from 'zod'; -import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const'; +import { CONTEXT_PARAM_PREFIX, CONTEXT_PARAM_REGEXP } from '../const.js'; // We want to discriminate between ContextParams and plain strings // If a string starts with `:`, it's a ContextParam diff --git a/packages/taco/src/conditions/schemas/compound.ts b/packages/taco/src/conditions/schemas/compound.ts index df8dd40ce..3c750aee6 100644 --- a/packages/taco/src/conditions/schemas/compound.ts +++ b/packages/taco/src/conditions/schemas/compound.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { maxNestedDepth } from '../multi-condition'; +import { maxNestedDepth } from '../multi-condition.js'; -import { baseConditionSchema } from './common'; -import { anyConditionSchema } from './utils'; +import { baseConditionSchema } from './common.js'; +import { anyConditionSchema } from './utils.js'; export const CompoundConditionType = 'compound'; diff --git a/packages/taco/src/conditions/schemas/context.ts b/packages/taco/src/conditions/schemas/context.ts index d4925e8bb..9a423880f 100644 --- a/packages/taco/src/conditions/schemas/context.ts +++ b/packages/taco/src/conditions/schemas/context.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { CONTEXT_PARAM_FULL_MATCH_REGEXP } from '../const'; +import { CONTEXT_PARAM_FULL_MATCH_REGEXP } from '../const.js'; -import { plainStringSchema } from './common'; +import { plainStringSchema } from './common.js'; const UINT256_MAX = BigInt( '115792089237316195423570985008687907853269984665640564039457584007913129639935', diff --git a/packages/taco/src/conditions/schemas/contract.ts b/packages/taco/src/conditions/schemas/contract.ts index 7ff99168a..5fcfd6160 100644 --- a/packages/taco/src/conditions/schemas/contract.ts +++ b/packages/taco/src/conditions/schemas/contract.ts @@ -2,8 +2,8 @@ import { EthAddressSchema } from '@nucypher/shared'; import { ethers } from 'ethers'; import { z } from 'zod'; -import { blockchainParamOrContextParamSchema } from './context'; -import { rpcConditionSchema } from './rpc'; +import { blockchainParamOrContextParamSchema } from './context.js'; +import { rpcConditionSchema } from './rpc.js'; // TODO: Consider replacing with `z.unknown`: // Since Solidity types are tied to Solidity version, we may not be able to accurately represent them in Zod. diff --git a/packages/taco/src/conditions/schemas/export-for-zod-doc-gen.ts b/packages/taco/src/conditions/schemas/export-for-zod-doc-gen.ts index 36e6cfa24..b6441652a 100755 --- a/packages/taco/src/conditions/schemas/export-for-zod-doc-gen.ts +++ b/packages/taco/src/conditions/schemas/export-for-zod-doc-gen.ts @@ -4,18 +4,18 @@ * NOTE: The order of the exported Zod objects in this file dictates the order of the generated markdown. */ -export * from './utils'; +export * from './utils.js'; // ts-unused-exports:disable-next-line - this comment line is added to prevent lint from changing or objecting the export order. -export * from './common'; -export * from './context'; +export * from './common.js'; +export * from './context.js'; // ts-unused-exports:disable-next-line - this comment line is added to prevent lint from changing or objecting the export order. -export * from './compound'; -export * from './contract'; -export * from './if-then-else'; -export * from './json-api'; -export * from './json-rpc'; -export * from './jwt'; -export * from './return-value-test'; -export * from './rpc'; -export * from './sequential'; -export * from './time'; +export * from './compound.js'; +export * from './contract.js'; +export * from './if-then-else.js'; +export * from './json-api.js'; +export * from './json-rpc.js'; +export * from './jwt.js'; +export * from './return-value-test.js'; +export * from './rpc.js'; +export * from './sequential.js'; +export * from './time.js'; diff --git a/packages/taco/src/conditions/schemas/if-then-else.ts b/packages/taco/src/conditions/schemas/if-then-else.ts index dc90ee992..39769fd47 100644 --- a/packages/taco/src/conditions/schemas/if-then-else.ts +++ b/packages/taco/src/conditions/schemas/if-then-else.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { maxNestedDepth } from '../multi-condition'; +import { maxNestedDepth } from '../multi-condition.js'; -import { baseConditionSchema } from './common'; -import { anyConditionSchema } from './utils'; +import { baseConditionSchema } from './common.js'; +import { anyConditionSchema } from './utils.js'; export const IfThenElseConditionType = 'if-then-else'; diff --git a/packages/taco/src/conditions/schemas/json-api.ts b/packages/taco/src/conditions/schemas/json-api.ts index 8fa6e7928..a4b9a9f74 100644 --- a/packages/taco/src/conditions/schemas/json-api.ts +++ b/packages/taco/src/conditions/schemas/json-api.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common'; -import { contextParamSchema } from './context'; -import { returnValueTestSchema } from './return-value-test'; +import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common.js'; +import { contextParamSchema } from './context.js'; +import { returnValueTestSchema } from './return-value-test.js'; export const JsonApiConditionType = 'json-api'; diff --git a/packages/taco/src/conditions/schemas/json-rpc.ts b/packages/taco/src/conditions/schemas/json-rpc.ts index d945f2f5b..c0edc8a8c 100644 --- a/packages/taco/src/conditions/schemas/json-rpc.ts +++ b/packages/taco/src/conditions/schemas/json-rpc.ts @@ -1,8 +1,8 @@ import { z } from 'zod'; -import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common'; -import { contextParamSchema } from './context'; -import { returnValueTestSchema } from './return-value-test'; +import { baseConditionSchema, httpsURLSchema, jsonPathSchema } from './common.js'; +import { contextParamSchema } from './context.js'; +import { returnValueTestSchema } from './return-value-test.js'; export const JsonRpcConditionType = 'json-rpc'; diff --git a/packages/taco/src/conditions/schemas/jwt.ts b/packages/taco/src/conditions/schemas/jwt.ts index 34ef9457e..ab6d47534 100644 --- a/packages/taco/src/conditions/schemas/jwt.ts +++ b/packages/taco/src/conditions/schemas/jwt.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; -import { baseConditionSchema } from './common'; -import { contextParamSchema } from './context'; +import { baseConditionSchema } from './common.js'; +import { contextParamSchema } from './context.js'; export const JWT_PARAM_DEFAULT = ':jwtToken'; diff --git a/packages/taco/src/conditions/schemas/return-value-test.ts b/packages/taco/src/conditions/schemas/return-value-test.ts index 17f168f6c..c48e15626 100644 --- a/packages/taco/src/conditions/schemas/return-value-test.ts +++ b/packages/taco/src/conditions/schemas/return-value-test.ts @@ -3,7 +3,7 @@ import { z } from 'zod'; import { blockchainParamOrContextParamSchema, paramOrContextParamSchema, -} from './context'; +} from './context.js'; const returnValueTestBaseSchema = z.object({ index: z.number().int().nonnegative().optional(), diff --git a/packages/taco/src/conditions/schemas/rpc.ts b/packages/taco/src/conditions/schemas/rpc.ts index faf4c9da7..867c791a8 100644 --- a/packages/taco/src/conditions/schemas/rpc.ts +++ b/packages/taco/src/conditions/schemas/rpc.ts @@ -1,9 +1,9 @@ import { BlockIdentifierSchema, EthAddressSchema } from '@nucypher/shared'; import { z } from 'zod'; -import { baseConditionSchema, UserAddressSchema } from './common'; -import { contextParamSchema } from './context'; -import { blockchainReturnValueTestSchema } from './return-value-test'; +import { baseConditionSchema, UserAddressSchema } from './common.js'; +import { contextParamSchema } from './context.js'; +import { blockchainReturnValueTestSchema } from './return-value-test.js'; export const RpcConditionType = 'rpc'; diff --git a/packages/taco/src/conditions/schemas/sequential.ts b/packages/taco/src/conditions/schemas/sequential.ts index 60942d047..e0cef964c 100644 --- a/packages/taco/src/conditions/schemas/sequential.ts +++ b/packages/taco/src/conditions/schemas/sequential.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { maxNestedDepth } from '../multi-condition'; +import { maxNestedDepth } from '../multi-condition.js'; -import { baseConditionSchema, plainStringSchema } from './common'; -import { anyConditionSchema } from './utils'; +import { baseConditionSchema, plainStringSchema } from './common.js'; +import { anyConditionSchema } from './utils.js'; export const SequentialConditionType = 'sequential'; diff --git a/packages/taco/src/conditions/schemas/time.ts b/packages/taco/src/conditions/schemas/time.ts index 65e57ac95..012ddbfb9 100644 --- a/packages/taco/src/conditions/schemas/time.ts +++ b/packages/taco/src/conditions/schemas/time.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; -import { rpcConditionSchema } from './rpc'; +import { rpcConditionSchema } from './rpc.js'; // TimeCondition is an RpcCondition with the method set to 'blocktime' and no parameters diff --git a/packages/taco/src/conditions/schemas/utils.ts b/packages/taco/src/conditions/schemas/utils.ts index abd14f36b..b08ed27b6 100644 --- a/packages/taco/src/conditions/schemas/utils.ts +++ b/packages/taco/src/conditions/schemas/utils.ts @@ -1,15 +1,15 @@ import { z } from 'zod'; -import { compoundConditionSchema } from '../compound-condition'; +import { compoundConditionSchema } from '../compound-condition.js'; -import { contractConditionSchema } from './contract'; -import { ifThenElseConditionSchema } from './if-then-else'; -import { jsonApiConditionSchema } from './json-api'; -import { jsonRpcConditionSchema } from './json-rpc'; -import { jwtConditionSchema } from './jwt'; -import { rpcConditionSchema } from './rpc'; -import { sequentialConditionSchema } from './sequential'; -import { timeConditionSchema } from './time'; +import { contractConditionSchema } from './contract.js'; +import { ifThenElseConditionSchema } from './if-then-else.js'; +import { jsonApiConditionSchema } from './json-api.js'; +import { jsonRpcConditionSchema } from './json-rpc.js'; +import { jwtConditionSchema } from './jwt.js'; +import { rpcConditionSchema } from './rpc.js'; +import { sequentialConditionSchema } from './sequential.js'; +import { timeConditionSchema } from './time.js'; export const anyConditionSchema: z.ZodSchema = z.lazy(() => z.union([ diff --git a/packages/taco/src/conditions/sequential.ts b/packages/taco/src/conditions/sequential.ts index 65c111112..110aff2a7 100644 --- a/packages/taco/src/conditions/sequential.ts +++ b/packages/taco/src/conditions/sequential.ts @@ -1,17 +1,17 @@ -import { Condition } from './condition'; +import { Condition } from './condition.js'; import { SequentialConditionProps, sequentialConditionSchema, SequentialConditionType, -} from './schemas/sequential'; -import { OmitConditionType } from './shared'; +} from './schemas/sequential.js'; +import { OmitConditionType } from './shared.js'; export { ConditionVariableProps, SequentialConditionProps, sequentialConditionSchema, SequentialConditionType, -} from './schemas/sequential'; +} from './schemas/sequential.js'; export class SequentialCondition extends Condition { constructor(value: OmitConditionType) { diff --git a/packages/taco/src/conditions/shared.ts b/packages/taco/src/conditions/shared.ts index 1df855fed..8a054b581 100644 --- a/packages/taco/src/conditions/shared.ts +++ b/packages/taco/src/conditions/shared.ts @@ -3,11 +3,11 @@ export type OmitConditionType = Omit; export { contextParamSchema, paramOrContextParamSchema, -} from './schemas/context'; +} from './schemas/context.js'; export { BlockchainReturnValueTestProps, ReturnValueTestProps, blockchainReturnValueTestSchema, returnValueTestSchema, -} from './schemas/return-value-test'; +} from './schemas/return-value-test.js'; diff --git a/packages/taco/src/index.ts b/packages/taco/src/index.ts index 698685a1b..09b908491 100644 --- a/packages/taco/src/index.ts +++ b/packages/taco/src/index.ts @@ -9,6 +9,13 @@ export { toHexString, } from '@nucypher/shared'; -export * as conditions from './conditions'; +export * as conditions from './conditions/index.js'; -export { decrypt, encrypt, encryptWithPublicKey } from './taco'; +export { decrypt, encrypt, encryptWithPublicKey } from './taco.js'; + +export { + AccessClient, + type AccessClientConfig, + type AccessClientEthersConfig, + type AccessClientViemConfig, +} from './access-client/index.js'; diff --git a/packages/taco/src/taco.ts b/packages/taco/src/taco.ts index da1fc9ce9..dfc1f2328 100644 --- a/packages/taco/src/taco.ts +++ b/packages/taco/src/taco.ts @@ -10,92 +10,156 @@ import { fromHexString, getPorterUris, PorterClient, + ProviderLike, + PublicClient, + SignerAccount, + SignerLike, toBytes, + toEthersProvider, + toTacoSigner, } from '@nucypher/shared'; import { ethers } from 'ethers'; -import { keccak256 } from 'ethers/lib/utils'; -import { Condition } from './conditions/condition'; -import { ConditionExpression } from './conditions/condition-expr'; -import { ConditionContext } from './conditions/context'; -import { DkgClient } from './dkg'; -import { retrieveAndDecrypt } from './tdec'; +import { ConditionExpression } from './conditions/condition-expr.js'; +import { Condition } from './conditions/condition.js'; +import { ConditionContext } from './conditions/context/index.js'; +import { DkgClient } from './dkg.js'; +import { retrieveAndDecrypt } from './tdec.js'; /** - * Encrypts a message under given conditions using a public key from an active DKG ritual. + * Encrypts a message gated by TACo Conditions using an ethers.js `Provider` and `Signer`. + * + * Use this overload when your application uses ethers.js. * * @export - * @param {ethers.providers.Provider} provider - Instance of ethers provider which is used to interact with - * your selected network. - * @param {Domain} domain - Represents the logical network in which the encryption will be performed. - * Must match the `ritualId`. - * @param {Uint8Array | string} message - The message to be encrypted. - * @param {Condition} condition - Condition under which the message will be encrypted. Those conditions must be - * satisfied in order to decrypt the message. - * @param {number} ritualId - The ID of the DKG Ritual to be used for encryption. The message will be encrypted - * under the public key of this ritual. - * @param {ethers.Signer} authSigner - The signer that will be used to sign the encrypter authorization. - * - * @returns {Promise} Returns Promise that resolves with an instance of ThresholdMessageKit. - * It represents the encrypted message. - * - * @throws {Error} If the active DKG Ritual cannot be retrieved an error is thrown. + * @param {ethers.providers.Provider} provider - Ethers provider for network operations. + * @param {Domain} domain - Logical TACo domain in which encryption will be performed (must match the ritual's domain). + * @param {Uint8Array | string} message - The message to be encrypted. + * @param {Condition} condition - Access condition (single or composite) that must be satisfied at decryption time. + * @param {number} ritualId - ID of the DKG ritual whose public key will be used for encryption. + * @param {ethers.Signer} authSigner - Signer used to identify encryptor and verify authorization. + * + * @returns {Promise} Encrypted message kit representing the ciphertext and associated metadata. + * + * @throws {Error} If the ritual cannot be retrieved or encryption fails. */ -export const encrypt = async ( +// Function overloads for encrypt +export async function encrypt( provider: ethers.providers.Provider, domain: Domain, message: Uint8Array | string, condition: Condition, ritualId: number, authSigner: ethers.Signer, -): Promise => { - // TODO(#264): Enable ritual initialization - // if (ritualId === undefined) { - // ritualId = await DkgClient.initializeRitual( - // provider, - // this.cohort.ursulaAddresses, - // true - // ); - // } - // if (ritualId === undefined) { - // // Given that we just initialized the ritual, this should never happen - // throw new Error('Ritual ID is undefined'); - // } - const dkgRitual = await DkgClient.getActiveRitual(provider, domain, ritualId); +): Promise; + +/** + * Encrypts a message gated by TACo Conditions using a viem `PublicClient` and a Signer Account (`LocalAccount` or `WalletClient`). + * + * Use this overload when your application uses viem. + * + * @export + * @param {PublicClient} publicClient - Viem `PublicClient` for network operations. + * @param {Domain} domain - Logical TACo domain in which encryption will be performed (must match the ritual's domain). + * @param {Uint8Array | string} message - The message to be encrypted. + * @param {Condition} condition - Access condition (single or composite) that must be satisfied at decryption time. + * @param {number} ritualId - ID of the DKG ritual whose public key will be used for encryption. + * @param {SignerAccount} authAccount - Viem account used to identify encryptor and verify authorization. + * + * @returns {Promise} Encrypted message kit representing the ciphertext and associated metadata. + * + * + * @throws {Error} If the ritual cannot be retrieved or encryption fails. + */ +export async function encrypt( + publicClient: PublicClient, + domain: Domain, + message: Uint8Array | string, + condition: Condition, + ritualId: number, + authAccount: SignerAccount, +): Promise; + +export async function encrypt( + providerLike: ProviderLike, + domain: Domain, + message: Uint8Array | string, + condition: Condition, + ritualId: number, + signerLike: SignerLike, +): Promise { + // Create TACo provider and signer adapters from viem objects + const providerAdapter = toEthersProvider(providerLike); + + const dkgRitual = await DkgClient.getActiveRitual( + providerAdapter, + domain, + ritualId, + ); return await encryptWithPublicKey( message, condition, dkgRitual.dkgPublicKey, - authSigner, + // Casting is needed because with the function definition of encryptWithPublicKey, + // this param can be either a Signer or a viem signer account. But not a type that is the union of both. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signerLike as any, ); -}; +} /** - * Encrypts a message with the given DKG public key under a specified condition. + * Encrypts a message with the given DKG public key gated by TACo Conditions. * * @export - * @param {Uint8Array | string} message - The message to be encrypted. - * @param {Condition} condition - Condition under which the message will be encrypted. Those conditions must be - * satisfied in order to decrypt the message. + * @param {Uint8Array | string} message - The message to be encrypted. + * @param {Condition} condition - Access condition (single or composite) that must be satisfied at decryption time. * @param {DkgPublicKey} dkgPublicKey - The public key of an active DKG Ritual to be used for encryption - * @param {ethers.Signer} authSigner - The signer that will be used to sign the encrypter authorization. + * @param {Signer} authSigner - Signer used to identify encryptor and verify authorization. Accepts an ethers `Signer` or a viem Signer Account (`LocalAccount` or `WalletClient`). * - * @returns {Promise} Returns Promise that resolves with an instance of ThresholdMessageKit. - * It represents the encrypted message. + * @returns {Promise} Encrypted message kit representing the ciphertext and associated metadata. * * @throws {Error} If the encryption process throws an error, an error is thrown. */ -export const encryptWithPublicKey = async ( +export async function encryptWithPublicKey( message: Uint8Array | string, condition: Condition, dkgPublicKey: DkgPublicKey, authSigner: ethers.Signer, -): Promise => { +): Promise; + +/** + * Encrypts a message with the given DKG public key gated by TACo Conditions. + * + * @export + * @param {Uint8Array | string} message - The message to be encrypted. + * @param {Condition} condition - Access condition (single or composite) that must be satisfied at decryption time. + * @param {DkgPublicKey} dkgPublicKey - The public key of an active DKG Ritual to be used for encryption + * @param {SignerAccount} authAccount - Viem account used to identify encryptor and verify authorization. + * + * @returns {Promise} Encrypted message kit representing the ciphertext and associated metadata. + * + * @throws {Error} If the encryption process throws an error, an error is thrown. + */ +export async function encryptWithPublicKey( + message: Uint8Array | string, + condition: Condition, + dkgPublicKey: DkgPublicKey, + authAccount: SignerAccount, +): Promise; + +export async function encryptWithPublicKey( + message: Uint8Array | string, + condition: Condition, + dkgPublicKey: DkgPublicKey, + signerLike: SignerLike, +): Promise { if (typeof message === 'string') { message = toBytes(message); } + const signer = toTacoSigner(signerLike); + const conditionExpr = new ConditionExpression(condition); const [ciphertext, authenticatedData] = encryptForDkg( @@ -104,57 +168,85 @@ export const encryptWithPublicKey = async ( conditionExpr.toCoreCondition(), ); - const headerHash = keccak256(ciphertext.header.toBytes()); - const authorization = await authSigner.signMessage(fromHexString(headerHash)); + const headerHash = ethers.utils.keccak256(ciphertext.header.toBytes()); + const authorization = await signer.signMessage(fromHexString(headerHash)); const acp = new AccessControlPolicy( authenticatedData, fromHexString(authorization), ); return new ThresholdMessageKit(ciphertext, acp); -}; +} /** - * Decrypts an encrypted message. + * Decrypts an encrypted message (ethers overload). * * @export - * @param {ethers.providers.Provider} provider - Instance of ethers provider which is used to interact with - * your selected network. - * @param {Domain} domain - Represents the logical network in which the decryption will be performed. - * Must match the `ritualId`. - * @param {ThresholdMessageKit} messageKit - The kit containing the message to be decrypted - * @param {ConditionContext} context - Optional context data used for decryption time values for the condition(s) within the `messageKit`. - * @param {string[]} [porterUris] - Optional URI(s) for the Porter service. If not provided, a value will be obtained - * from the Domain - * - * @returns {Promise} Returns Promise that resolves with a decrypted message - * - * @throws {Error} If the active DKG Ritual cannot be retrieved or decryption process throws an error, - * an error is thrown. + * @param {ethers.providers.Provider} provider - Ethers provider for network operations. + * @param {Domain} domain - Logical TACo domain used for decryption. + * @param {ThresholdMessageKit} messageKit - The representation of the ciphertext and associated metadata. + * @param {ConditionContext} [context] - Optional context data required by conditions. + * @param {string[]} [porterUris] - Optional Porter service URI(s). If omitted, they are resolved via `getPorterUris(domain)`. + * + * @returns {Promise} The decrypted message bytes. + * + * @throws {Error} If the ritual cannot be resolved, Porter retrieval fails, or decryption fails. */ -export const decrypt = async ( +export function decrypt( provider: ethers.providers.Provider, domain: Domain, messageKit: ThresholdMessageKit, context?: ConditionContext, porterUris?: string[], -): Promise => { +): Promise; + +/** + * Decrypts an encrypted message (viem overload). + * + * @export + * @param {PublicClient} publicClient - Viem `PublicClient` for network operations. + * @param {Domain} domain - Logical TACo domain used for decryption. + * @param {ThresholdMessageKit} messageKit - The kit containing the ciphertext and access policy. + * @param {ConditionContext} [context] - Optional context data required by conditions. + * @param {string[]} [porterUris] - Optional Porter service URI(s). If omitted, they are resolved via `getPorterUris(domain)`. + * + * @returns {Promise} The decrypted message bytes. + * + * @throws {Error} If the ritual cannot be resolved, Porter retrieval fails, or decryption fails. + */ +export function decrypt( + publicClient: PublicClient, + domain: Domain, + messageKit: ThresholdMessageKit, + context?: ConditionContext, + porterUris?: string[], +): Promise; + +export async function decrypt( + providerLike: ProviderLike, + domain: Domain, + messageKit: ThresholdMessageKit, + context?: ConditionContext, + porterUris?: string[], +): Promise { const porterUrisFull: string[] = porterUris ? porterUris : await getPorterUris(domain); const porter = new PorterClient(porterUrisFull); + const providerAdapter = toEthersProvider(providerLike); + const ritualId = await DkgCoordinatorAgent.getRitualIdFromPublicKey( - provider, + providerAdapter, domain, messageKit.acp.publicKey, ); return retrieveAndDecrypt( - provider, + providerAdapter, domain, porter, messageKit, ritualId, context, ); -}; +} diff --git a/packages/taco/src/tdec.ts b/packages/taco/src/tdec.ts index 97132437d..649eda8bd 100644 --- a/packages/taco/src/tdec.ts +++ b/packages/taco/src/tdec.ts @@ -20,11 +20,10 @@ import { toBytes, } from '@nucypher/shared'; import { ethers } from 'ethers'; -import { arrayify, keccak256 } from 'ethers/lib/utils'; -import { ConditionExpression } from './conditions/condition-expr'; -import { ConditionContext } from './conditions/context'; -import { DkgClient } from './dkg'; +import { ConditionExpression } from './conditions/condition-expr.js'; +import { ConditionContext } from './conditions/context/index.js'; +import { DkgClient } from './dkg.js'; const ERR_DECRYPTION_FAILED = (errors: unknown) => `Threshold of responses not met; TACo decryption failed with errors: ${JSON.stringify( @@ -47,8 +46,10 @@ export const encryptMessage = async ( conditions.toCoreCondition(), ); - const headerHash = keccak256(ciphertext.header.toBytes()); - const authorization = await authSigner.signMessage(arrayify(headerHash)); + const headerHash = ethers.utils.keccak256(ciphertext.header.toBytes()); + const authorization = await authSigner.signMessage( + ethers.utils.arrayify(headerHash), + ); const acp = new AccessControlPolicy( authenticatedData, toBytes(authorization), diff --git a/packages/taco/test/access-client.test.ts b/packages/taco/test/access-client.test.ts new file mode 100644 index 000000000..a44e650f1 --- /dev/null +++ b/packages/taco/test/access-client.test.ts @@ -0,0 +1,387 @@ +import { DOMAIN_NAMES, DomainName } from '@nucypher/shared'; +import { + fakeProvider, + fakeViemAccount, + fakeViemPublicClient, +} from '@nucypher/test-utils'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +import { + AccessClient, + type AccessClientConfig, + type AccessClientEthersConfig, + type AccessClientViemConfig, +} from '../src'; +import { AccessConfigValidator } from '../src/access-client/config-validator'; + +describe('AccessConfigValidator', () => { + describe('Domain Management', () => { + it('should return all supported domain names', () => { + const domains = AccessConfigValidator.getSupportedDomains(); + expect(domains).toEqual(['lynx', 'tapir', 'mainnet']); + }); + + it.each([ + [DOMAIN_NAMES.TESTNET, 'valid testnet domain'], + [DOMAIN_NAMES.DEVNET, 'valid devnet domain'], + [DOMAIN_NAMES.MAINNET, 'valid production domain'], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ])('should validate domain "%s" as %s', (domain: DomainName, _: string) => { + expect(AccessConfigValidator.isValidDomain(domain)).toBe(true); + }); + + it.each([ + ['INVALID', 'invalid domain name'], + ['', 'empty domain name'], + ['testnet', 'legacy domain key (not domain name)'], + ])('should validate domain "%s" as %s', (domain: string) => { + expect(AccessConfigValidator.isValidDomain(domain as DomainName)).toBe( + false, + ); + }); + + it.each([ + [0, 'minimum valid ritual ID'], + [27, 'default devnet ritual ID'], + [6, 'default testnet ritual ID'], + [42, 'custom mainnet ritual ID'], + [999, 'large ritual ID for devnet'], + ])('should validate ritual ID %d (%s)', (ritualId: number) => { + expect(AccessConfigValidator.isValidRitualId(ritualId)).toBe(true); + }); + + it.each([ + [-1, 'negative ritual ID'], + [5.4, 'floating point ritual ID'], + ])('should invalidate ritual ID %d (%s)', (ritualId: number) => { + expect(AccessConfigValidator.isValidRitualId(ritualId)).toBe(false); + }); + }); + + describe('Fast Configuration Validation', () => { + it('should create AccessClient with viem configuration', () => { + const result = AccessConfigValidator.validateFast({ + domain: 'tapir', + ritualId: 6, + viemClient: fakeViemPublicClient(), + viemSignerAccount: fakeViemAccount(), + }); + + expect(result.isValid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should fail validation for invalid domain configuration', () => { + const result = AccessConfigValidator.validateFast({ + domain: 'INVALID_DOMAIN' as DomainName, + ritualId: 999, + viemClient: fakeViemPublicClient(), + viemSignerAccount: fakeViemAccount(), + }); + + expect(result.isValid).toBe(false); + expect(result.errors.length).toBeGreaterThan(0); + }); + + it('should fail validation when domain is missing', () => { + const result = AccessConfigValidator.validateFast({ + ritualId: 6, + viemClient: fakeViemPublicClient(), + viemSignerAccount: fakeViemAccount(), + } as AccessClientConfig); + + expect(result.isValid).toBe(false); + expect(result.errors).toContain('The property `domain` is required'); + }); + }); +}); + +// Test helpers for accessing AccessClient's private static members +const getAccessClientStatics = () => + AccessClient as unknown as { + initializationPromise: Promise | undefined; + }; + +const resetAccessClientStatics = () => { + delete (AccessClient as unknown as { initializationPromise?: Promise }) + .initializationPromise; +}; + +describe('AccessClient', () => { + beforeAll(async () => { + // Ensure AccessClient is initialized before running tests + await AccessClient.initialize(); + }); + + let validViemConfig: AccessClientViemConfig; + let validEthersConfig: AccessClientEthersConfig; + + beforeEach(() => { + validViemConfig = { + domain: 'tapir', + ritualId: 6, + viemClient: fakeViemPublicClient(), + viemSignerAccount: fakeViemAccount(), + }; + + const ethersProvider = fakeProvider(); + validEthersConfig = { + domain: 'tapir', + ritualId: 6, + ethersProvider, + ethersSigner: ethersProvider.getSigner(), + }; + }); + + describe('Client Construction', () => { + it('should successfully create client with valid viem configuration', async () => { + const client = new AccessClient(validViemConfig); + await client.validateConfig(); + expect(client).toBeInstanceOf(AccessClient); + }); + + it('should successfully create client with valid ethers configuration', async () => { + const client = new AccessClient(validEthersConfig); + await client.validateConfig(); + expect(client).toBeInstanceOf(AccessClient); + }); + + it('should throw error for invalid domain name', () => { + expect( + () => + new AccessClient({ + ...validViemConfig, + domain: 'INVALID' as DomainName, + }), + ).toThrow('Invalid domain name'); + }); + + it('should throw error for invalid ritual ID', () => { + expect( + () => + new AccessClient({ + ...validViemConfig, + ritualId: -1, + }), + ).toThrow('Invalid ritual ID'); + }); + + it.each([ + { + configModifications: { domain: undefined }, + baseConfig: 'viem', + expectedError: 'The property `domain` is required', + description: 'missing domain from viem config', + }, + { + configModifications: { ritualId: undefined }, + baseConfig: 'viem', + expectedError: 'The property `ritualId` is required', + description: 'missing ritual ID from viem config', + }, + { + configModifications: { viemClient: undefined }, + baseConfig: 'viem', + expectedError: 'viemClient is required for viem configuration', + description: 'missing viemClient from viem config', + }, + { + configModifications: { viemSignerAccount: undefined }, + baseConfig: 'viem', + expectedError: 'viemSignerAccount is required for viem configuration', + description: 'missing viemSignerAccount from viem config', + }, + { + configModifications: { domain: undefined }, + baseConfig: 'ethers', + expectedError: 'The property `domain` is required', + description: 'missing domain from ethers config', + }, + { + configModifications: { ritualId: undefined }, + baseConfig: 'ethers', + expectedError: 'The property `ritualId` is required', + description: 'missing ritual ID from ethers config', + }, + { + configModifications: { ethersProvider: undefined }, + baseConfig: 'ethers', + expectedError: 'ethersProvider is required for ethers configuration', + description: 'missing ethersProvider from ethers config', + }, + { + configModifications: { ethersSigner: undefined }, + baseConfig: 'ethers', + expectedError: 'ethersSigner is required for ethers configuration', + description: 'missing ethersSigner from ethers config', + }, + ])( + 'should throw error for $description', + ({ configModifications, baseConfig, expectedError }) => { + const baseConfigObject = + baseConfig === 'viem' ? validViemConfig : validEthersConfig; + const invalidConfig = { ...baseConfigObject, ...configModifications }; + + expect( + () => new AccessClient(invalidConfig as AccessClientConfig), + ).toThrow(expectedError); + }, + ); + + it('should throw error for mixed/invalid configuration types', () => { + expect( + () => + new AccessClient({ + domain: 'tapir', + ritualId: 6, + viemClient: fakeViemPublicClient(), + ethersProvider: fakeProvider(), + } as unknown as AccessClientConfig), + ).toThrow( + 'Invalid configuration: Configuration must include either viem objects (viemClient + viemSignerAccount) or ethers objects (ethersProvider + ethersSigner)', + ); + }); + }); + + describe('Configuration Access', () => { + it.each([ + { + configType: 'viem', + config: () => validViemConfig, + expectedProperties: ['viemClient', 'viemSignerAccount'], + description: 'viem client configuration', + }, + { + configType: 'ethers', + config: () => validEthersConfig, + expectedProperties: ['ethersProvider', 'ethersSigner'], + description: 'ethers client configuration', + }, + ])( + 'should return readonly configuration object for $description', + ({ config, expectedProperties }) => { + const client = new AccessClient(config()); + const clientConfig = client.getConfig(); + + // Verify common properties + expect(clientConfig.domain).toBe('tapir'); + expect(clientConfig.ritualId).toBe(6); + + // Verify config-specific properties + expectedProperties.forEach((prop) => { + expect(prop in clientConfig).toBe(true); + }); + + // Should be frozen/readonly + expect(() => { + (clientConfig as Record).domain = 'lynx'; + }).toThrow(); + }, + ); + }); + + describe('Initialization Lifecycle', () => { + it('should trigger automatic AccessClient initialization on client construction', async () => { + // Reset static initialization state to verify automatic initialization + // occurs when AccessClient constructor is called + resetAccessClientStatics(); + + new AccessClient(validViemConfig); + + // Initialization should be triggered by constructor + expect(getAccessClientStatics().initializationPromise).toBeDefined(); + }); + + it('should share initialization across multiple AccessClient instances', async () => { + new AccessClient(validViemConfig); + new AccessClient({ + ...validViemConfig, + ritualId: 27, // Different ritual ID + }); + + // Both clients should share the same initialization promise + const initPromise1 = getAccessClientStatics().initializationPromise; + const initPromise2 = getAccessClientStatics().initializationPromise; + + expect(initPromise1).toBe(initPromise2); + expect(initPromise1).toBeDefined(); + }); + + it('should provide static initialize method with proper promise handling', async () => { + // Verify AccessClient.initialize() method exists and returns a promise + const initPromise = AccessClient.initialize(); + expect(initPromise).toBeInstanceOf(Promise); + + // Wait for initialization to complete + await initPromise; + + // Verify that repeated calls return the same promise (singleton pattern) + const initPromise2 = AccessClient.initialize(); + expect(initPromise2).toBeInstanceOf(Promise); + }); + }); + + describe('Full Configuration Validation', () => { + it.each([ + { + configType: 'viem', + config: () => validViemConfig, + description: 'correct viem configuration', + }, + { + configType: 'ethers', + config: () => validEthersConfig, + description: 'correct ethers configuration', + }, + ])('should pass full validation for $description', async ({ config }) => { + const result = AccessConfigValidator.validate(config()); + expect(result).resolves.not.toThrow(); + }); + + it('should detect and report missing blockchain dependencies', async () => { + const result = await AccessConfigValidator.validate({ + domain: 'tapir', + ritualId: 6, + // Missing blockchain objects + } as unknown as AccessClientConfig); + + expect(result.isValid).toBe(false); + expect(result.errors).toContain( + 'Configuration must include either viem objects (viemClient + viemSignerAccount) or ethers objects (ethersProvider + ethersSigner)', + ); + }); + + it('should detect and report invalid domain in full validation', async () => { + const result = await AccessConfigValidator.validate({ + ...validViemConfig, + domain: 'INVALID' as DomainName, + }); + + expect(result.isValid).toBe(false); + expect( + result.errors.some((error) => error.includes('Invalid domain name')), + ).toBe(true); + }); + + it('should detect and report invalid ritual ID during construction', async () => { + expect( + () => + new AccessClient({ + domain: 'tapir', + ritualId: -5, + viemClient: fakeViemPublicClient(), + viemSignerAccount: fakeViemAccount(), + }), + ).toThrow('Invalid ritual ID'); + }); + }); + + describe('Domain Support', () => { + it('should provide domain name via getConfig method', () => { + const client = new AccessClient(validViemConfig); + const config = client.getConfig(); + + expect(config.domain).toBe('tapir'); + }); + }); +}); diff --git a/packages/taco/test/taco.test.ts b/packages/taco/test/taco.test.ts index e035f211f..89260b847 100644 --- a/packages/taco/test/taco.test.ts +++ b/packages/taco/test/taco.test.ts @@ -11,11 +11,15 @@ import { fakePorterUri, fakeProvider, fakeTDecFlow, + fakeViemAccount, + fakeViemPublicClient, mockGetRitualIdFromPublicKey, mockTacoDecrypt, TEST_CHAIN_ID, TEST_SIWE_PARAMS, } from '@nucypher/test-utils'; +import { ethers } from 'ethers'; +import type { LocalAccount, PublicClient } from 'viem'; import { beforeAll, describe, expect, it } from 'vitest'; import * as taco from '../src'; @@ -30,110 +34,170 @@ import { mockMakeSessionKey, } from './test-utils'; -// Shared test variables -const message = 'this is a secret'; +// Test fixtures +const TEST_MESSAGE = 'this is a secret'; +const TEST_NFT_CONTRACT = '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77'; +const TEST_NFT_TOKEN_ID = 3591; + const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({ - contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', - parameters: [3591], + contractAddress: TEST_NFT_CONTRACT, + parameters: [TEST_NFT_TOKEN_ID], chain: TEST_CHAIN_ID, }); -describe('taco', () => { +describe('TACo SDK', () => { beforeAll(async () => { await initialize(); }); - it('encrypts and decrypts', async () => { - const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); - const mockedDkgRitual = fakeDkgRitual(mockedDkg); - const provider = fakeProvider(aliceSecretKeyBytes); - const signer = provider.getSigner(); - const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); - - const messageKit = await taco.encrypt( - provider, - domains.DEVNET, - message, - ownsNFT, - mockedDkg.ritualId, - signer, - ); - expect(getFinalizedRitualSpy).toHaveBeenCalled(); - - const { decryptionShares } = fakeTDecFlow({ - ...mockedDkg, - message: toBytes(message), - dkgPublicKey: mockedDkg.dkg.publicKey(), - thresholdMessageKit: messageKit, - }); - const { participantSecrets, participants } = await mockDkgParticipants( - mockedDkg.ritualId, - ); - const requesterSessionKey = SessionStaticSecret.random(); - const decryptSpy = mockTacoDecrypt( - mockedDkg.ritualId, - decryptionShares, - participantSecrets, - requesterSessionKey.publicKey(), - ); - const getParticipantsSpy = mockGetParticipants(participants); - const sessionKeySpy = mockMakeSessionKey(requesterSessionKey); - const getRitualIdFromPublicKey = mockGetRitualIdFromPublicKey( - mockedDkg.ritualId, - ); - const getRitualSpy = mockGetActiveRitual(mockedDkgRitual); - - const authProvider = new tacoAuth.EIP4361AuthProvider( - provider, - signer, - TEST_SIWE_PARAMS, - ); - const conditionContext = ConditionContext.fromMessageKit(messageKit); - conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider); - const decryptedMessage = await taco.decrypt( - provider, - domains.DEVNET, - messageKit, - conditionContext, - [fakePorterUri], - ); - expect(decryptedMessage).toEqual(toBytes(message)); - expect(getParticipantsSpy).toHaveBeenCalled(); - expect(sessionKeySpy).toHaveBeenCalled(); - expect(getRitualIdFromPublicKey).toHaveBeenCalled(); - expect(getRitualSpy).toHaveBeenCalled(); - expect(decryptSpy).toHaveBeenCalled(); - }, 9000); // test timeout 9s (TODO: not sure why this test takes so long on CI) - - it('exposes requested parameters', async () => { - const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); - const mockedDkgRitual = fakeDkgRitual(mockedDkg); - const provider = fakeProvider(aliceSecretKeyBytes); - const signer = provider.getSigner(); - const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); - - const customParamKey = ':nftId'; - const ownsNFTWithCustomParams = - new conditions.predefined.erc721.ERC721Ownership({ - contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77', - parameters: [customParamKey], - chain: TEST_CHAIN_ID, + describe.each< + | ['ethers', () => ethers.providers.Provider, () => ethers.Signer] + | ['viem', () => PublicClient, () => LocalAccount] + >([ + [ + 'ethers', + () => fakeProvider(aliceSecretKeyBytes), + () => fakeProvider(aliceSecretKeyBytes).getSigner(), + ], + [ + 'viem', + () => fakeViemPublicClient(), + () => fakeViemAccount(aliceSecretKeyBytes), + ], + ])('Provider: %s', (providerType, createProvider, createSigner) => { + it('should encrypt and decrypt a message with conditions', async () => { + // Setup + const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); + const mockedDkgRitual = fakeDkgRitual(mockedDkg); + const provider = createProvider(); + const signer = createSigner(); + const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); + + // Encrypt + const messageKit = await taco.encrypt( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + domains.DEVNET, + TEST_MESSAGE, + ownsNFT, + mockedDkg.ritualId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + expect(getFinalizedRitualSpy).toHaveBeenCalled(); + + // Setup decryption mocks + const { decryptionShares } = fakeTDecFlow({ + ...mockedDkg, + message: toBytes(TEST_MESSAGE), + dkgPublicKey: mockedDkg.dkg.publicKey(), + thresholdMessageKit: messageKit, }); + const { participantSecrets, participants } = await mockDkgParticipants( + mockedDkg.ritualId, + ); + const requesterSessionKey = SessionStaticSecret.random(); + const decryptSpy = mockTacoDecrypt( + mockedDkg.ritualId, + decryptionShares, + participantSecrets, + requesterSessionKey.publicKey(), + ); + const getParticipantsSpy = mockGetParticipants(participants); + const sessionKeySpy = mockMakeSessionKey(requesterSessionKey); + const getRitualIdFromPublicKey = mockGetRitualIdFromPublicKey( + mockedDkg.ritualId, + ); + const getRitualSpy = mockGetActiveRitual(mockedDkgRitual); + + // Setup authentication + const authProvider = new tacoAuth.EIP4361AuthProvider( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + TEST_SIWE_PARAMS, + ); + const conditionContext = ConditionContext.fromMessageKit(messageKit); + conditionContext.addAuthProvider( + USER_ADDRESS_PARAM_DEFAULT, + authProvider, + ); + + // Decrypt + const decryptedMessage = await taco.decrypt( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + domains.DEVNET, + messageKit, + conditionContext, + [fakePorterUri], + ); + + // Verify + expect(decryptedMessage).toEqual(toBytes(TEST_MESSAGE)); + expect(getParticipantsSpy).toHaveBeenCalled(); + expect(sessionKeySpy).toHaveBeenCalled(); + expect(getRitualIdFromPublicKey).toHaveBeenCalled(); + expect(getRitualSpy).toHaveBeenCalled(); + expect(decryptSpy).toHaveBeenCalled(); + }, 15000); // Extended timeout for CI + + it('should handle custom condition parameters', async () => { + // Setup + const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); + const mockedDkgRitual = fakeDkgRitual(mockedDkg); + const provider = createProvider(); + const signer = createSigner(); + const getFinalizedRitualSpy = mockGetActiveRitual(mockedDkgRitual); - const messageKit = await taco.encrypt( - provider, - domains.DEVNET, - message, - ownsNFTWithCustomParams, - mockedDkg.ritualId, - signer, - ); - expect(getFinalizedRitualSpy).toHaveBeenCalled(); - - const conditionContext = ConditionContext.fromMessageKit(messageKit); - const requestedParameters = conditionContext.requestedContextParameters; - expect(requestedParameters).toEqual( - new Set([customParamKey, USER_ADDRESS_PARAM_DEFAULT]), - ); + // Create condition with custom parameter + const customParamKey = ':nftId'; + const ownsNFTWithCustomParams = + new conditions.predefined.erc721.ERC721Ownership({ + contractAddress: TEST_NFT_CONTRACT, + parameters: [customParamKey], + chain: TEST_CHAIN_ID, + }); + + // Encrypt with custom condition + const messageKit = await taco.encrypt( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + provider as any, + domains.DEVNET, + TEST_MESSAGE, + ownsNFTWithCustomParams, + mockedDkg.ritualId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + expect(getFinalizedRitualSpy).toHaveBeenCalled(); + + // Verify parameters are exposed + const conditionContext = ConditionContext.fromMessageKit(messageKit); + const requestedParameters = conditionContext.requestedContextParameters; + expect(requestedParameters).toEqual( + new Set([customParamKey, USER_ADDRESS_PARAM_DEFAULT]), + ); + }); + + it('should encrypt with public key directly', async () => { + // Setup + const mockedDkg = fakeDkgFlow(FerveoVariant.precomputed, 0, 4, 4); + const signer = createSigner(); + + // Encrypt with public key + const messageKit = await taco.encryptWithPublicKey( + TEST_MESSAGE, + ownsNFT, + mockedDkg.dkg.publicKey(), + // eslint-disable-next-line @typescript-eslint/no-explicit-any + signer as any, + ); + + // Verify + expect(messageKit).toBeDefined(); + expect(messageKit).toBeInstanceOf(Object); + }); }); }); diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 1295e2594..e4ac8c5a5 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -10,6 +10,7 @@ "author": "Piotr Roslaniec ", "exports": { ".": { + "types": "./dist/cjs/index.d.ts", "import": "./dist/es/index.js", "require": "./dist/cjs/index.js" } @@ -21,9 +22,9 @@ "dist" ], "scripts": { - "build": "pnpm build:module && pnpm build:cjs", + "build": "pnpm build:es && pnpm build:cjs", "build:cjs": "tsc --build ./tsconfig.cjs.json --verbose", - "build:module": "tsc --build ./tsconfig.es.json --verbose", + "build:es": "tsc --build ./tsconfig.es.json --verbose", "exports:lint": "ts-unused-exports tsconfig.json --ignoreFiles src/index.ts", "lint": "eslint --ext .ts src", "lint:fix": "pnpm lint --fix" @@ -34,6 +35,7 @@ "@nucypher/taco-auth": "workspace:*", "axios": "^1.11.0", "ethers": "^5.8.0", + "viem": "^2.0.0", "vitest": "^3.0.9" }, "engines": { diff --git a/packages/test-utils/src/utils.ts b/packages/test-utils/src/utils.ts index dc328f23c..24e4e4ed1 100644 --- a/packages/test-utils/src/utils.ts +++ b/packages/test-utils/src/utils.ts @@ -45,6 +45,9 @@ import { SingleSignOnEIP4361AuthProvider, } from '@nucypher/taco-auth'; import { ethers, providers, Wallet } from 'ethers'; +import { createPublicClient, custom, PublicClient } from 'viem'; +import { LocalAccount, privateKeyToAccount } from 'viem/accounts'; +import { polygonAmoy } from 'viem/chains'; import { expect, MockInstance, vi } from 'vitest'; import { TEST_CONTRACT_ADDR, TEST_SIWE_PARAMS } from './variables'; @@ -73,7 +76,7 @@ const makeFakeProvider = ( getBlockNumber: () => Promise.resolve(blockNumber), getBlock: () => Promise.resolve(block), _isProvider: true, - getNetwork: () => Promise.resolve({ name: 'mockNetwork', chainId: 1234 }), + getNetwork: () => Promise.resolve({ name: 'mockNetwork', chainId: 80_002 }), }; }; @@ -99,6 +102,33 @@ export const fakeProvider = ( } as unknown as ethers.providers.Web3Provider; }; +// Viem test utilities +export const fakeViemPublicClient = (): PublicClient => { + // Create public client for reading data + const publicClient = createPublicClient({ + chain: polygonAmoy, + transport: custom({ + request: vi.fn().mockImplementation(async ({ method }) => { + // Network detection calls + if (method === 'eth_chainId') { + return `0x${polygonAmoy.id.toString(16)}`; + } + // Default response for other calls + return null; + }), + }), + }); + return publicClient; +}; + +export const fakeViemAccount = ( + secretKeyBytes = ethers.utils.randomBytes(32), +): LocalAccount => { + // Convert bytes to hex string for viem + const privateKey = `0x${Buffer.from(secretKeyBytes).toString('hex')}`; + return privateKeyToAccount(privateKey as `0x${string}`); +}; + export const fakeAuthProviders = async ( signer?: ethers.providers.JsonRpcSigner, ) => { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 558fd750b..518cc3e1a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -39,7 +39,7 @@ importers: version: 6.21.0(eslint@8.57.1)(typescript@5.8.2) '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0)) + version: 3.2.4(vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)) bundlemon: specifier: ^3.1.0 version: 3.1.0(typescript@5.8.2) @@ -88,6 +88,9 @@ importers: ts-unused-exports: specifier: ^10.1.0 version: 10.1.0(typescript@5.8.2) + tsx: + specifier: ^4.20.5 + version: 4.20.6 typedoc: specifier: ^0.25.13 version: 0.25.13(typescript@5.8.2) @@ -105,7 +108,7 @@ importers: version: 5.8.2 vitest: specifier: ^3.0.9 - version: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0) + version: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) demos/taco-demo: dependencies: @@ -129,7 +132,7 @@ importers: version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 6.2.0(webpack@5.101.3) react: specifier: ^18.3.1 version: 18.3.1 @@ -145,7 +148,7 @@ importers: devDependencies: '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.6.0 - version: 0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack-cli@6.0.1)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1)) + version: 0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2)(webpack@5.101.3) '@types/react': specifier: ^18.3.20 version: 18.3.20 @@ -157,16 +160,16 @@ importers: version: 18.3.5(@types/react@18.3.20) copy-webpack-plugin: specifier: ^13.0.0 - version: 13.0.1(webpack@5.101.3(webpack-cli@6.0.1)) + version: 13.0.1(webpack@5.101.3) crypto-browserify: specifier: ^3.12.1 version: 3.12.1 esbuild-loader: specifier: ^2.21.0 - version: 2.21.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 2.21.0(webpack@5.101.3) html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(webpack@5.101.3(webpack-cli@6.0.1)) + version: 5.6.3(webpack@5.101.3) process: specifier: ^0.11.10 version: 0.11.10 @@ -214,7 +217,7 @@ importers: version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) file-loader: specifier: ^6.2.0 - version: 6.2.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 6.2.0(webpack@5.101.3) react: specifier: ^18.3.1 version: 18.3.1 @@ -230,7 +233,7 @@ importers: devDependencies: '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.6.0 - version: 0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack-cli@6.0.1)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1)) + version: 0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2)(webpack@5.101.3) '@types/react': specifier: ^18.3.20 version: 18.3.20 @@ -242,13 +245,13 @@ importers: version: 18.3.5(@types/react@18.3.20) copy-webpack-plugin: specifier: ^13.0.0 - version: 13.0.1(webpack@5.101.3(webpack-cli@6.0.1)) + version: 13.0.1(webpack@5.101.3) esbuild-loader: specifier: ^2.21.0 - version: 2.21.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 2.21.0(webpack@5.101.3) html-webpack-plugin: specifier: ^5.6.3 - version: 5.6.3(webpack@5.101.3(webpack-cli@6.0.1)) + version: 5.6.3(webpack@5.101.3) react-refresh: specifier: ^0.17.0 version: 0.17.0 @@ -352,10 +355,10 @@ importers: devDependencies: copy-webpack-plugin: specifier: ^13.0.0 - version: 13.0.1(webpack@5.101.3(webpack-cli@6.0.1)) + version: 13.0.1(webpack@5.101.3) esbuild-loader: specifier: ^2.21.0 - version: 2.21.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 2.21.0(webpack@5.101.3) ethers: specifier: ^5.8.0 version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) @@ -460,6 +463,43 @@ importers: specifier: ^5.0.1 version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@types/babel__core@7.20.5)(bufferutil@4.0.9)(eslint@8.57.1)(react@18.3.1)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(type-fest@0.21.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + examples/taco/react-viem: + dependencies: + '@nucypher/shared': + specifier: workspace:* + version: link:../../../packages/shared + '@nucypher/taco': + specifier: workspace:* + version: link:../../../packages/taco + '@nucypher/taco-auth': + specifier: workspace:* + version: link:../../../packages/taco-auth + ethers: + specifier: ^5.7.2 + version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + viem: + specifier: ^2.0.0 + version: 2.37.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + devDependencies: + '@types/node': + specifier: ^20.17.28 + version: 20.19.17 + '@types/react': + specifier: ^18.3.20 + version: 18.3.20 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.5(@types/react@18.3.20) + react-scripts: + specifier: ^5.0.1 + version: 5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@types/babel__core@7.20.5)(bufferutil@4.0.9)(eslint@8.57.1)(react@18.3.1)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(type-fest@0.21.3)(typescript@5.8.2)(utf-8-validate@5.0.10) + examples/taco/webpack-5: dependencies: '@nucypher/taco': @@ -474,10 +514,10 @@ importers: devDependencies: copy-webpack-plugin: specifier: ^13.0.0 - version: 13.0.1(webpack@5.101.3(webpack-cli@6.0.1)) + version: 13.0.1(webpack@5.101.3) esbuild-loader: specifier: ^2.21.0 - version: 2.21.0(webpack@5.101.3(webpack-cli@6.0.1)) + version: 2.21.0(webpack@5.101.3) webpack: specifier: ^5.99.9 version: 5.101.3(webpack-cli@6.0.1) @@ -558,6 +598,11 @@ importers: typechain: specifier: ^8.3.2 version: 8.3.2(typescript@5.8.2) + viem: + specifier: ^2.0.0 + version: 2.37.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) + + packages/shared/dist/es: {} packages/taco: dependencies: @@ -598,6 +643,9 @@ importers: modified-zod2md: specifier: 0.1.5-modified.4 version: 0.1.5-modified.4(zod@3.24.2) + viem: + specifier: ^2.0.0 + version: 2.37.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) packages/taco-auth: dependencies: @@ -609,7 +657,7 @@ importers: version: link:../shared siwe: specifier: ^3.0.0 - version: 3.0.0(ethers@5.8.0) + version: 3.0.0(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^3.24.2 version: 3.24.2 @@ -618,6 +666,10 @@ importers: specifier: workspace:* version: link:../test-utils + packages/taco-auth/dist/es: {} + + packages/taco/dist/es: {} + packages/test-utils: dependencies: '@nucypher/nucypher-core': @@ -635,12 +687,18 @@ importers: ethers: specifier: ^5.8.0 version: 5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) + viem: + specifier: ^2.0.0 + version: 2.37.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2) vitest: specifier: ^3.0.9 - version: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0) + version: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) packages: + '@adraffy/ens-normalize@1.11.0': + resolution: {integrity: sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -2525,15 +2583,23 @@ packages: '@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1': resolution: {integrity: sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==} - '@noble/curves@1.8.1': - resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} + '@noble/ciphers@1.3.0': + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.1': + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + + '@noble/curves@1.9.2': + resolution: {integrity: sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g==} engines: {node: ^14.21.3 || >=16} '@noble/ed25519@1.7.3': resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} - '@noble/hashes@1.7.1': - resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} engines: {node: ^14.21.3 || >=16} '@nodelib/fs.scandir@2.1.5': @@ -2766,14 +2832,14 @@ packages: '@rushstack/eslint-patch@1.11.0': resolution: {integrity: sha512-zxnHvoMQVqewTJr/W4pKjF0bMGiKJv1WX7bSrkl46Hg0QjESbzBROWK0Wg4RphzSOS5Jiy7eFimmM3UgMrMZbQ==} - '@scure/base@1.2.4': - resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} - '@scure/bip32@1.6.2': - resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + '@scure/bip32@1.7.0': + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} - '@scure/bip39@1.5.4': - resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + '@scure/bip39@1.6.0': + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} '@sinclair/typebox@0.24.51': resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==} @@ -3033,6 +3099,9 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/node@20.19.17': + resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==} + '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} @@ -3461,6 +3530,17 @@ packages: resolution: {integrity: sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==} deprecated: Use your platform's native atob() and btoa() methods instead + abitype@1.1.0: + resolution: {integrity: sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==} + peerDependencies: + typescript: '>=5.0.4' + zod: ^3.22.0 || ^4.0.0 + peerDependenciesMeta: + typescript: + optional: true + zod: + optional: true + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -6250,6 +6330,11 @@ packages: peerDependencies: ws: '*' + isows@1.0.7: + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} + peerDependencies: + ws: '*' + istanbul-lib-coverage@3.2.2: resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} engines: {node: '>=8'} @@ -7202,6 +7287,14 @@ packages: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} + ox@0.9.6: + resolution: {integrity: sha512-8SuCbHPvv2eZLYXrNmC0EC12rdzXQLdhnOMlHDW2wiCPLxBrOOJwX5L5E61by+UjTPOryqQiRSnjIKCI+GykKg==} + peerDependencies: + typescript: '>=5.4.0' + peerDependenciesMeta: + typescript: + optional: true + p-cancelable@2.1.1: resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} engines: {node: '>=8'} @@ -9012,6 +9105,11 @@ packages: peerDependencies: typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tweetnacl@1.0.3: resolution: {integrity: sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==} @@ -9141,6 +9239,9 @@ packages: underscore@1.12.1: resolution: {integrity: sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} @@ -9237,6 +9338,14 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + viem@2.37.8: + resolution: {integrity: sha512-mL+5yvCQbRIR6QvngDQMfEiZTfNWfd+/QL5yFaOoYbpH3b1Q2ddwF7YG2eI2AcYSh9LE1gtUkbzZLFUAVyj4oQ==} + peerDependencies: + typescript: '>=5.0.4' + peerDependenciesMeta: + typescript: + optional: true + vite-node@3.0.9: resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -9628,6 +9737,18 @@ packages: utf-8-validate: optional: true + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + wsl-utils@0.1.0: resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} engines: {node: '>=18'} @@ -9686,6 +9807,8 @@ packages: snapshots: + '@adraffy/ens-normalize@1.11.0': {} + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -9720,10 +9843,10 @@ snapshots: '@aptos-labs/aptos-cli': 1.0.2 '@aptos-labs/aptos-client': 1.1.0(axios@1.8.4)(got@11.8.6) '@aptos-labs/script-composer-pack': 0.0.9 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 eventemitter3: 5.0.1 form-data: 4.0.2 js-base64: 3.7.7 @@ -11608,6 +11731,43 @@ snapshots: jest-util: 28.1.3 slash: 3.0.0 + '@jest/core@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10)': + dependencies: + '@jest/console': 27.5.1 + '@jest/reporters': 27.5.1 + '@jest/test-result': 27.5.1 + '@jest/transform': 27.5.1 + '@jest/types': 27.5.1 + '@types/node': 24.3.0 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + emittery: 0.8.1 + exit: 0.1.2 + graceful-fs: 4.2.11 + jest-changed-files: 27.5.1 + jest-config: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + jest-haste-map: 27.5.1 + jest-message-util: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-resolve-dependencies: 27.5.1 + jest-runner: 27.5.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + jest-runtime: 27.5.1 + jest-snapshot: 27.5.1 + jest-util: 27.5.1 + jest-validate: 27.5.1 + jest-watcher: 27.5.1 + micromatch: 4.0.8 + rimraf: 3.0.2 + slash: 3.0.0 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + '@jest/core@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10)': dependencies: '@jest/console': 27.5.1 @@ -11991,13 +12151,19 @@ snapshots: dependencies: eslint-scope: 5.1.1 - '@noble/curves@1.8.1': + '@noble/ciphers@1.3.0': {} + + '@noble/curves@1.9.1': dependencies: - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 + + '@noble/curves@1.9.2': + dependencies: + '@noble/hashes': 1.8.0 '@noble/ed25519@1.7.3': {} - '@noble/hashes@1.7.1': {} + '@noble/hashes@1.8.0': {} '@nodelib/fs.scandir@2.1.5': dependencies: @@ -12037,7 +12203,7 @@ snapshots: dependencies: '@ethersproject/abstract-signer': 5.8.0 '@nucypher/shared': 0.5.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - siwe: 3.0.0(ethers@5.8.0) + siwe: 3.0.0(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.24.2 transitivePeerDependencies: - bufferutil @@ -12072,12 +12238,12 @@ snapshots: react-refresh: 0.11.0 schema-utils: 4.3.0 source-map: 0.7.4 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) optionalDependencies: type-fest: 0.21.3 webpack-dev-server: 4.15.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack@5.101.3) - '@pmmmwh/react-refresh-webpack-plugin@0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack-cli@6.0.1)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1))': + '@pmmmwh/react-refresh-webpack-plugin@0.6.1(react-refresh@0.17.0)(type-fest@0.21.3)(webpack-dev-server@5.2.2)(webpack@5.101.3)': dependencies: anser: 2.3.2 core-js-pure: 3.41.0 @@ -12197,18 +12363,18 @@ snapshots: '@rushstack/eslint-patch@1.11.0': {} - '@scure/base@1.2.4': {} + '@scure/base@1.2.6': {} - '@scure/bip32@1.6.2': + '@scure/bip32@1.7.0': dependencies: - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.4 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 - '@scure/bip39@1.5.4': + '@scure/bip39@1.6.0': dependencies: - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.4 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 '@sinclair/typebox@0.24.51': {} @@ -12234,8 +12400,8 @@ snapshots: '@solana/web3.js@1.98.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)': dependencies: '@babel/runtime': 7.27.0 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 + '@noble/curves': 1.9.2 + '@noble/hashes': 1.8.0 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.6.0 bigint-buffer: 1.1.5 @@ -12255,7 +12421,7 @@ snapshots: '@spruceid/siwe-parser@3.0.0': dependencies: - '@noble/hashes': 1.7.1 + '@noble/hashes': 1.8.0 apg-js: 4.4.0 '@stablelib/binary@1.0.1': @@ -12527,6 +12693,10 @@ snapshots: '@types/node@12.20.55': {} + '@types/node@20.19.17': + dependencies: + undici-types: 6.21.0 + '@types/node@24.3.0': dependencies: undici-types: 7.10.0 @@ -12910,7 +13080,7 @@ snapshots: - node-fetch - supports-color - '@vitest/coverage-v8@3.2.4(vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0))': + '@vitest/coverage-v8@3.2.4(vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -12925,7 +13095,7 @@ snapshots: std-env: 3.9.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0) + vitest: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) transitivePeerDependencies: - supports-color @@ -12936,13 +13106,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.9(vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0))': + '@vitest/mocker@3.0.9(vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0))': dependencies: '@vitest/spy': 3.0.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) '@vitest/pretty-format@3.0.9': dependencies: @@ -13045,17 +13215,17 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1))': + '@webpack-cli/configtest@3.0.1(webpack-cli@6.0.1)(webpack@5.101.3)': dependencies: webpack: 5.101.3(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3) - '@webpack-cli/info@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1))': + '@webpack-cli/info@3.0.1(webpack-cli@6.0.1)(webpack@5.101.3)': dependencies: webpack: 5.101.3(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3) - '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack-cli@6.0.1)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1))': + '@webpack-cli/serve@3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.101.3)': dependencies: webpack: 5.101.3(webpack-cli@6.0.1) webpack-cli: 6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3) @@ -13073,6 +13243,11 @@ snapshots: abab@2.0.6: {} + abitype@1.1.0(typescript@5.8.2)(zod@3.24.2): + optionalDependencies: + typescript: 5.8.2 + zod: 3.24.2 + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -13450,7 +13625,7 @@ snapshots: loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) babel-plugin-istanbul@6.1.1: dependencies: @@ -14199,7 +14374,7 @@ snapshots: dependencies: toggle-selection: 1.0.6 - copy-webpack-plugin@13.0.1(webpack@5.101.3(webpack-cli@6.0.1)): + copy-webpack-plugin@13.0.1(webpack@5.101.3): dependencies: glob-parent: 6.0.2 normalize-path: 3.0.0 @@ -14347,7 +14522,7 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.7.1 optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) css-minimizer-webpack-plugin@3.4.1(webpack@5.101.3): dependencies: @@ -14357,7 +14532,7 @@ snapshots: schema-utils: 4.3.0 serialize-javascript: 6.0.2 source-map: 0.6.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) css-prefers-color-scheme@6.0.3(postcss@8.5.3): dependencies: @@ -14928,7 +15103,7 @@ snapshots: dependencies: es6-promise: 4.2.8 - esbuild-loader@2.21.0(webpack@5.101.3(webpack-cli@6.0.1)): + esbuild-loader@2.21.0(webpack@5.101.3): dependencies: esbuild: 0.16.17 joycon: 3.1.1 @@ -15052,7 +15227,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.2.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) eslint-plugin-react: 7.37.4(eslint@8.57.0) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.0) @@ -15071,7 +15246,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.0) eslint-plugin-react: 7.37.4(eslint@8.57.0) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.0) @@ -15086,6 +15261,33 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2): + dependencies: + '@babel/core': 7.26.10 + '@babel/eslint-parser': 7.27.0(@babel/core@7.26.10)(eslint@8.57.1) + '@rushstack/eslint-patch': 1.11.0 + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2) + '@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@5.8.2) + babel-preset-react-app: 10.1.0 + confusing-browser-globals: 1.0.11 + eslint: 8.57.1 + eslint-plugin-flowtype: 8.0.3(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1) + eslint-plugin-jest: 25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2) + eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) + eslint-plugin-react: 7.37.4(eslint@8.57.1) + eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1) + eslint-plugin-testing-library: 5.11.1(eslint@8.57.1)(typescript@5.8.2) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - '@babel/plugin-syntax-flow' + - '@babel/plugin-transform-react-jsx' + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - jest + - supports-color + eslint-config-react-app@7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2): dependencies: '@babel/core': 7.26.10 @@ -15137,7 +15339,7 @@ snapshots: tinyglobby: 0.2.12 unrs-resolver: 1.3.2 optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -15151,24 +15353,14 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.2.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0): - dependencies: - debug: 3.2.7 - optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.2.2) - eslint: 8.57.0 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) - transitivePeerDependencies: - - supports-color - - eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0) transitivePeerDependencies: - supports-color @@ -15225,7 +15417,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.2.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -15236,7 +15428,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.0)(typescript@5.2.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.0))(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15248,13 +15440,13 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.0)(typescript@5.2.2) + '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -15263,9 +15455,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 8.57.0 + eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -15283,34 +15475,16 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1): + eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2): dependencies: - '@rtsao/scc': 1.1.0 - array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.6 - array.prototype.flat: 1.3.3 - array.prototype.flatmap: 1.3.3 - debug: 3.2.7 - doctrine: 2.1.0 + '@typescript-eslint/experimental-utils': 5.62.0(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) - hasown: 2.0.2 - is-core-module: 2.16.1 - is-glob: 4.0.3 - minimatch: 3.1.2 - object.fromentries: 2.0.8 - object.groupby: 1.0.3 - object.values: 1.2.1 - semver: 6.3.1 - string.prototype.trimend: 1.0.9 - tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2) + jest: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) transitivePeerDependencies: - - eslint-import-resolver-typescript - - eslint-import-resolver-webpack - supports-color + - typescript eslint-plugin-jest@25.7.0(@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2): dependencies: @@ -15459,7 +15633,7 @@ snapshots: micromatch: 4.0.8 normalize-path: 3.0.0 schema-utils: 4.3.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) eslint@8.57.0: dependencies: @@ -15763,17 +15937,11 @@ snapshots: dependencies: flat-cache: 3.2.0 - file-loader@6.2.0(webpack@5.101.3(webpack-cli@6.0.1)): - dependencies: - loader-utils: 2.0.4 - schema-utils: 3.3.0 - webpack: 5.101.3(webpack-cli@6.0.1) - file-loader@6.2.0(webpack@5.101.3): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) file-uri-to-path@1.0.0: {} @@ -15886,7 +16054,7 @@ snapshots: semver: 7.7.1 tapable: 1.1.3 typescript: 5.8.2 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) optionalDependencies: eslint: 8.57.1 @@ -16273,16 +16441,6 @@ snapshots: relateurl: 0.2.7 terser: 5.39.0 - html-webpack-plugin@5.6.3(webpack@5.101.3(webpack-cli@6.0.1)): - dependencies: - '@types/html-minifier-terser': 6.1.0 - html-minifier-terser: 6.1.0 - lodash: 4.17.21 - pretty-error: 4.0.0 - tapable: 2.2.1 - optionalDependencies: - webpack: 5.101.3(webpack-cli@6.0.1) - html-webpack-plugin@5.6.3(webpack@5.101.3): dependencies: '@types/html-minifier-terser': 6.1.0 @@ -16291,7 +16449,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.2.1 optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) htmlparser2@6.1.0: dependencies: @@ -16699,6 +16857,10 @@ snapshots: dependencies: ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + isows@1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + dependencies: + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + istanbul-lib-coverage@3.2.2: {} istanbul-lib-instrument@5.2.1: @@ -16812,6 +16974,27 @@ snapshots: transitivePeerDependencies: - supports-color + jest-cli@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10): + dependencies: + '@jest/core': 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + '@jest/test-result': 27.5.1 + '@jest/types': 27.5.1 + chalk: 4.1.2 + exit: 0.1.2 + graceful-fs: 4.2.11 + import-local: 3.2.0 + jest-config: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + jest-util: 27.5.1 + jest-validate: 27.5.1 + prompts: 2.4.2 + yargs: 16.2.0 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest-cli@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10): dependencies: '@jest/core': 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10) @@ -16833,6 +17016,40 @@ snapshots: - ts-node - utf-8-validate + jest-config@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10): + dependencies: + '@babel/core': 7.26.10 + '@jest/test-sequencer': 27.5.1 + '@jest/types': 27.5.1 + babel-jest: 27.5.1(@babel/core@7.26.10) + chalk: 4.1.2 + ci-info: 3.9.0 + deepmerge: 4.3.1 + glob: 7.2.3 + graceful-fs: 4.2.11 + jest-circus: 27.5.1 + jest-environment-jsdom: 27.5.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + jest-environment-node: 27.5.1 + jest-get-type: 27.5.1 + jest-jasmine2: 27.5.1 + jest-regex-util: 27.5.1 + jest-resolve: 27.5.1 + jest-runner: 27.5.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + jest-util: 27.5.1 + jest-validate: 27.5.1 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 27.5.1 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + ts-node: 10.9.2(@types/node@20.19.17)(typescript@5.8.2) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + jest-config@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.26.10 @@ -17136,6 +17353,17 @@ snapshots: leven: 3.1.0 pretty-format: 27.5.1 + jest-watch-typeahead@1.1.0(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10)): + dependencies: + ansi-escapes: 4.3.2 + chalk: 4.1.2 + jest: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + jest-regex-util: 28.0.2 + jest-watcher: 28.1.3 + slash: 4.0.0 + string-length: 5.0.1 + strip-ansi: 7.1.0 + jest-watch-typeahead@1.1.0(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10)): dependencies: ansi-escapes: 4.3.2 @@ -17186,6 +17414,18 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 + jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10): + dependencies: + '@jest/core': 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + import-local: 3.2.0 + jest-cli: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - ts-node + - utf-8-validate + jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10): dependencies: '@jest/core': 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(utf-8-validate@5.0.10) @@ -17589,7 +17829,7 @@ snapshots: dependencies: schema-utils: 4.3.0 tapable: 2.2.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) minimalistic-assert@1.0.1: {} @@ -17920,6 +18160,21 @@ snapshots: object-keys: 1.1.1 safe-push-apply: 1.0.0 + ox@0.9.6(typescript@5.8.2)(zod@3.24.2): + dependencies: + '@adraffy/ens-normalize': 1.11.0 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - zod + p-cancelable@2.1.1: {} p-filter@2.1.0: @@ -18240,6 +18495,14 @@ snapshots: postcss: 8.5.3 postcss-value-parser: 4.2.0 + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2)): + dependencies: + lilconfig: 3.1.3 + yaml: 2.7.0 + optionalDependencies: + postcss: 8.5.3 + ts-node: 10.9.2(@types/node@20.19.17)(typescript@5.8.2) + postcss-load-config@4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2)): dependencies: lilconfig: 3.1.3 @@ -18254,7 +18517,7 @@ snapshots: klona: 2.0.6 postcss: 8.5.3 semver: 7.7.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) postcss-logical@5.0.4(postcss@8.5.3): dependencies: @@ -18696,7 +18959,7 @@ snapshots: shell-quote: 1.8.2 strip-ansi: 6.0.1 text-table: 0.2.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) optionalDependencies: typescript: 5.8.2 transitivePeerDependencies: @@ -18722,6 +18985,93 @@ snapshots: react-refresh@0.17.0: {} + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@types/babel__core@7.20.5)(bufferutil@4.0.9)(eslint@8.57.1)(react@18.3.1)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(type-fest@0.21.3)(typescript@5.8.2)(utf-8-validate@5.0.10): + dependencies: + '@babel/core': 7.26.10 + '@pmmmwh/react-refresh-webpack-plugin': 0.5.15(react-refresh@0.11.0)(type-fest@0.21.3)(webpack-dev-server@4.15.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack@5.101.3))(webpack@5.101.3) + '@svgr/webpack': 5.5.0 + babel-jest: 27.5.1(@babel/core@7.26.10) + babel-loader: 8.4.1(@babel/core@7.26.10)(webpack@5.101.3) + babel-plugin-named-asset-import: 0.3.8(@babel/core@7.26.10) + babel-preset-react-app: 10.1.0 + bfj: 7.1.0 + browserslist: 4.24.4 + camelcase: 6.3.0 + case-sensitive-paths-webpack-plugin: 2.4.0 + css-loader: 6.11.0(webpack@5.101.3) + css-minimizer-webpack-plugin: 3.4.1(webpack@5.101.3) + dotenv: 10.0.0 + dotenv-expand: 5.1.0 + eslint: 8.57.1 + eslint-config-react-app: 7.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(eslint@8.57.1)(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10))(typescript@5.8.2) + eslint-webpack-plugin: 3.2.0(eslint@8.57.1)(webpack@5.101.3) + file-loader: 6.2.0(webpack@5.101.3) + fs-extra: 10.1.0 + html-webpack-plugin: 5.6.3(webpack@5.101.3) + identity-obj-proxy: 3.0.0 + jest: 27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10) + jest-resolve: 27.5.1 + jest-watch-typeahead: 1.1.0(jest@27.5.1(bufferutil@4.0.9)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2))(utf-8-validate@5.0.10)) + mini-css-extract-plugin: 2.9.2(webpack@5.101.3) + postcss: 8.5.3 + postcss-flexbugs-fixes: 5.0.2(postcss@8.5.3) + postcss-loader: 6.2.1(postcss@8.5.3)(webpack@5.101.3) + postcss-normalize: 10.0.1(browserslist@4.24.4)(postcss@8.5.3) + postcss-preset-env: 7.8.3(postcss@8.5.3) + prompts: 2.4.2 + react: 18.3.1 + react-app-polyfill: 3.0.0 + react-dev-utils: 12.0.1(eslint@8.57.1)(typescript@5.8.2)(webpack@5.101.3) + react-refresh: 0.11.0 + resolve: 1.22.10 + resolve-url-loader: 4.0.0 + sass-loader: 12.6.0(webpack@5.101.3) + semver: 7.7.1 + source-map-loader: 3.0.2(webpack@5.101.3) + style-loader: 3.3.4(webpack@5.101.3) + tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2)) + terser-webpack-plugin: 5.3.14(webpack@5.101.3) + webpack: 5.101.3(webpack-cli@6.0.1) + webpack-dev-server: 4.15.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack@5.101.3) + webpack-manifest-plugin: 4.1.1(webpack@5.101.3) + workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.101.3) + optionalDependencies: + fsevents: 2.3.3 + typescript: 5.8.2 + transitivePeerDependencies: + - '@babel/plugin-syntax-flow' + - '@babel/plugin-transform-react-jsx' + - '@parcel/css' + - '@rspack/core' + - '@swc/core' + - '@types/babel__core' + - '@types/webpack' + - bufferutil + - canvas + - clean-css + - csso + - debug + - esbuild + - eslint-import-resolver-typescript + - eslint-import-resolver-webpack + - fibers + - node-notifier + - node-sass + - rework + - rework-visit + - sass + - sass-embedded + - sockjs-client + - supports-color + - ts-node + - type-fest + - uglify-js + - utf-8-validate + - vue-template-compiler + - webpack-cli + - webpack-hot-middleware + - webpack-plugin-serve + react-scripts@5.0.1(@babel/plugin-syntax-flow@7.26.0(@babel/core@7.26.10))(@babel/plugin-transform-react-jsx@7.25.9(@babel/core@7.26.10))(@types/babel__core@7.20.5)(bufferutil@4.0.9)(eslint@8.57.1)(react@18.3.1)(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2))(type-fest@0.21.3)(typescript@5.8.2)(utf-8-validate@5.0.10): dependencies: '@babel/core': 7.26.10 @@ -18768,7 +19118,7 @@ snapshots: style-loader: 3.3.4(webpack@5.101.3) tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2)) terser-webpack-plugin: 5.3.14(webpack@5.101.3) - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) webpack-dev-server: 4.15.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack@5.101.3) webpack-manifest-plugin: 4.1.1(webpack@5.101.3) workbox-webpack-plugin: 6.6.0(@types/babel__core@7.20.5)(webpack@5.101.3) @@ -19114,7 +19464,7 @@ snapshots: dependencies: klona: 2.0.6 neo-async: 2.6.2 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) sax@1.2.4: {} @@ -19356,7 +19706,7 @@ snapshots: sisteransi@1.0.5: {} - siwe@3.0.0(ethers@5.8.0): + siwe@3.0.0(ethers@5.8.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@spruceid/siwe-parser': 3.0.0 '@stablelib/random': 1.0.2 @@ -19394,7 +19744,7 @@ snapshots: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) source-map-support@0.5.21: dependencies: @@ -19645,7 +19995,7 @@ snapshots: style-loader@3.3.4(webpack@5.101.3): dependencies: - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) styled-jsx@5.1.6(react@18.3.1): dependencies: @@ -19726,6 +20076,33 @@ snapshots: typical: 5.2.0 wordwrapjs: 4.0.1 + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2)): + dependencies: + '@alloc/quick-lru': 5.2.0 + arg: 5.0.2 + chokidar: 3.6.0 + didyoumean: 1.2.2 + dlv: 1.1.3 + fast-glob: 3.3.3 + glob-parent: 6.0.2 + is-glob: 4.0.3 + jiti: 1.21.7 + lilconfig: 3.1.3 + micromatch: 4.0.8 + normalize-path: 3.0.0 + object-hash: 3.0.0 + picocolors: 1.1.1 + postcss: 8.5.3 + postcss-import: 15.1.0(postcss@8.5.3) + postcss-js: 4.0.1(postcss@8.5.3) + postcss-load-config: 4.0.2(postcss@8.5.3)(ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2)) + postcss-nested: 6.2.0(postcss@8.5.3) + postcss-selector-parser: 6.1.2 + resolve: 1.22.10 + sucrase: 3.35.0 + transitivePeerDependencies: + - ts-node + tailwindcss@3.4.17(ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2)): dependencies: '@alloc/quick-lru': 5.2.0 @@ -19773,15 +20150,6 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.3.14(webpack@5.101.3(webpack-cli@6.0.1)): - dependencies: - '@jridgewell/trace-mapping': 0.3.25 - jest-worker: 27.5.1 - schema-utils: 4.3.0 - serialize-javascript: 6.0.2 - terser: 5.39.0 - webpack: 5.101.3(webpack-cli@6.0.1) - terser-webpack-plugin@5.3.14(webpack@5.101.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -19789,7 +20157,7 @@ snapshots: schema-utils: 4.3.0 serialize-javascript: 6.0.2 terser: 5.39.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) terser@5.39.0: dependencies: @@ -19932,6 +20300,25 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@20.19.17)(typescript@5.8.2): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.19.17 + acorn: 8.14.1 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.2 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-node@10.9.2(@types/node@24.3.0)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -19972,6 +20359,13 @@ snapshots: tslib: 1.14.1 typescript: 5.8.2 + tsx@4.20.6: + dependencies: + esbuild: 0.25.1 + get-tsconfig: 4.10.0 + optionalDependencies: + fsevents: 2.3.3 + tweetnacl@1.0.3: {} type-check@0.3.2: @@ -20098,6 +20492,8 @@ snapshots: underscore@1.12.1: {} + undici-types@6.21.0: {} + undici-types@7.10.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -20195,13 +20591,30 @@ snapshots: vary@1.1.2: {} - vite-node@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): + viem@2.37.8(bufferutil@4.0.9)(typescript@5.8.2)(utf-8-validate@5.0.10)(zod@3.24.2): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.1.0(typescript@5.8.2)(zod@3.24.2) + isows: 1.0.7(ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + ox: 0.9.6(typescript@5.8.2)(zod@3.24.2) + ws: 8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10) + optionalDependencies: + typescript: 5.8.2 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + vite-node@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) transitivePeerDependencies: - '@types/node' - jiti @@ -20216,7 +20629,7 @@ snapshots: - tsx - yaml - vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0): + vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0): dependencies: esbuild: 0.25.1 postcss: 8.5.3 @@ -20226,12 +20639,13 @@ snapshots: fsevents: 2.3.3 jiti: 2.4.2 terser: 5.39.0 + tsx: 4.20.6 yaml: 2.7.0 - vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0)(terser@5.39.0)(yaml@2.7.0): + vitest@3.0.9(@types/node@24.3.0)(jiti@2.4.2)(jsdom@16.7.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0): dependencies: '@vitest/expect': 3.0.9 - '@vitest/mocker': 3.0.9(vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0)) + '@vitest/mocker': 3.0.9(vite@6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0)) '@vitest/pretty-format': 3.0.9 '@vitest/runner': 3.0.9 '@vitest/snapshot': 3.0.9 @@ -20247,8 +20661,8 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) - vite-node: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(yaml@2.7.0) + vite: 6.2.3(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) + vite-node: 3.0.9(@types/node@24.3.0)(jiti@2.4.2)(terser@5.39.0)(tsx@4.20.6)(yaml@2.7.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.3.0 @@ -20311,9 +20725,9 @@ snapshots: webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3): dependencies: '@discoveryjs/json-ext': 0.6.3 - '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1)) - '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1)) - '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1(webpack-dev-server@5.2.2)(webpack@5.101.3))(webpack-dev-server@5.2.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)(webpack-cli@6.0.1)(webpack@5.101.3))(webpack@5.101.3(webpack-cli@6.0.1)) + '@webpack-cli/configtest': 3.0.1(webpack-cli@6.0.1)(webpack@5.101.3) + '@webpack-cli/info': 3.0.1(webpack-cli@6.0.1)(webpack@5.101.3) + '@webpack-cli/serve': 3.0.1(webpack-cli@6.0.1)(webpack-dev-server@5.2.2)(webpack@5.101.3) colorette: 2.0.20 commander: 12.1.0 cross-spawn: 7.0.6 @@ -20334,9 +20748,9 @@ snapshots: mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.3.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) - webpack-dev-middleware@7.4.2(webpack@5.101.3(webpack-cli@6.0.1)): + webpack-dev-middleware@7.4.2(webpack@5.101.3): dependencies: colorette: 2.0.20 memfs: 4.38.2 @@ -20380,7 +20794,7 @@ snapshots: webpack-dev-middleware: 5.3.4(webpack@5.101.3) ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) transitivePeerDependencies: - bufferutil - debug @@ -20415,7 +20829,7 @@ snapshots: serve-index: 1.9.1 sockjs: 0.3.24 spdy: 4.0.2 - webpack-dev-middleware: 7.4.2(webpack@5.101.3(webpack-cli@6.0.1)) + webpack-dev-middleware: 7.4.2(webpack@5.101.3) ws: 8.18.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) optionalDependencies: webpack: 5.101.3(webpack-cli@6.0.1) @@ -20429,7 +20843,7 @@ snapshots: webpack-manifest-plugin@4.1.1(webpack@5.101.3): dependencies: tapable: 2.2.1 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) webpack-sources: 2.3.1 webpack-merge@6.0.1: @@ -20450,38 +20864,6 @@ snapshots: webpack-sources@3.3.3: {} - webpack@5.101.3: - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 - '@types/json-schema': 7.0.15 - '@webassemblyjs/ast': 1.14.1 - '@webassemblyjs/wasm-edit': 1.14.1 - '@webassemblyjs/wasm-parser': 1.14.1 - acorn: 8.15.0 - acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.24.4 - chrome-trace-event: 1.0.4 - enhanced-resolve: 5.18.1 - es-module-lexer: 1.6.0 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 4.3.2 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(webpack@5.101.3) - watchpack: 2.4.2 - webpack-sources: 3.3.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - webpack@5.101.3(webpack-cli@6.0.1): dependencies: '@types/eslint-scope': 3.7.7 @@ -20506,7 +20888,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.2 tapable: 2.2.1 - terser-webpack-plugin: 5.3.14(webpack@5.101.3(webpack-cli@6.0.1)) + terser-webpack-plugin: 5.3.14(webpack@5.101.3) watchpack: 2.4.2 webpack-sources: 3.3.3 optionalDependencies: @@ -20733,7 +21115,7 @@ snapshots: fast-json-stable-stringify: 2.1.0 pretty-bytes: 5.6.0 upath: 1.2.0 - webpack: 5.101.3 + webpack: 5.101.3(webpack-cli@6.0.1) webpack-sources: 1.4.3 workbox-build: 6.6.0(@types/babel__core@7.20.5) transitivePeerDependencies: @@ -20787,6 +21169,11 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 + ws@8.18.3(bufferutil@4.0.9)(utf-8-validate@5.0.10): + optionalDependencies: + bufferutil: 4.0.9 + utf-8-validate: 5.0.10 + wsl-utils@0.1.0: dependencies: is-wsl: 3.1.0 diff --git a/scripts/ensure-es-compatible-imports.ts b/scripts/ensure-es-compatible-imports.ts new file mode 100644 index 000000000..df23e4064 --- /dev/null +++ b/scripts/ensure-es-compatible-imports.ts @@ -0,0 +1,108 @@ +#!/usr/bin/env tsx +/** + * This is needed to support ECMA Script modules. + * It fixes TypeScript source imports to use .js extensions for proper ES module support + * Can be called from any package directory with: pnpm tsx ../../scripts/ensure-es-compatible-imports.ts + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +function addJsExtensionToImports(dir: string): void { + const files = fs.readdirSync(dir); + + for (const file of files) { + const fullPath = path.join(dir, file); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + // Recursively process subdirectories + addJsExtensionToImports(fullPath); + } else if (file.endsWith('.ts')) { + // Fix imports in .ts files + let content = fs.readFileSync(fullPath, 'utf8'); + let modified = false; + + // Fix all relative import/export statements to include .js extension + // Match import/export statements that use `from "..."`, including: + // - import ... from './path' + // - export { ... } from './path' + // - export * as ns from './path' + // - export * from './path' + const importExportRegex = + /((?:import|export)(?:\s+type)?\s*(?:\{\s*[^}]*\}\s*|\*\s*as\s+\w+|\*|\w+)?\s*from\s+['"])([^'"]*?)(['"])/g; + + content = content.replace( + importExportRegex, + (match, prefix, importPath, quote) => { + // The relative modules are: './', '../', '.', or '..' + const isRelative = + importPath.startsWith('./') || + importPath.startsWith('../') || + importPath === '.' || + importPath === '..'; + if (!isRelative) { + // Skip external modules + return match; + } + + // Skip if already has extension + if (importPath.endsWith('.js') || importPath.endsWith('.mjs')) { + return match; + } + + // Check if it's a directory with index.ts + const resolvedPath = path.resolve(path.dirname(fullPath), importPath); + const indexPath = path.join(resolvedPath, 'index.ts'); + + if ( + fs.existsSync(resolvedPath) && + fs.statSync(resolvedPath).isDirectory() + ) { + if (fs.existsSync(indexPath)) { + modified = true; + return `${prefix}${importPath}/index.js${quote}`; + } + } else { + // Check if it's a file without extension + const filePath = `${resolvedPath}.ts`; + if (fs.existsSync(filePath)) { + modified = true; + return `${prefix}${importPath}.js${quote}`; + } + } + + return match; + }, + ); + + if (modified) { + fs.writeFileSync(fullPath, content); + console.log( + `Fixed source imports in: ${path.relative(process.cwd(), fullPath)}`, + ); + } + } + } +} + +// Get the calling package directory (where the script is run from) +const packageDir = process.cwd(); +const packageName = path.basename(packageDir); +const packageJsonPath = path.join(packageDir, 'package.json'); +const srcDir = path.join(packageDir, 'src'); +if (!fs.existsSync(packageJsonPath)) { + console.log( + `❌ No package.json found in the current directory (${packageDir}). Please run this script from a package directory.`, + ); + process.exit(1); +} + +if (fs.existsSync(srcDir)) { + console.log(`🔧 Fixing TypeScript source imports for ${packageName}...`); + addJsExtensionToImports(srcDir); + console.log(`✅ Source import fix complete for ${packageName}!`); +} else { + console.log(`❌ Source directory not found for ${packageName}.`); + process.exit(1); +} diff --git a/scripts/ensure-es-package-type.ts b/scripts/ensure-es-package-type.ts new file mode 100644 index 000000000..861ed23be --- /dev/null +++ b/scripts/ensure-es-package-type.ts @@ -0,0 +1,52 @@ +#!/usr/bin/env tsx +/** + * Set dist/es/package.json type to "module" while preserving + * any existing content. This enables proper ES module resolution for dual + * CommonJS/ES module packages. + * + * Usage from within a package directory: + * pnpm tsx ../../scripts/ensure-es-package-type.ts + */ + +import * as fs from 'fs'; +import * as path from 'path'; + +interface ESPackageJson { + type: 'module'; + [key: string]: unknown; +} + +type Action = 'created' | 'updated' | 'verified' | 'recreated'; + +// Get the calling package directory (where the script is run from) +const packageDir = process.cwd(); +const packageName = path.basename(packageDir); +const esDir = path.join(packageDir, 'dist/es'); +const packageJsonPath = path.join(esDir, 'package.json'); + +if (!fs.existsSync(esDir)) { + console.log(`❌ ES directory not found for ${packageName}`); + process.exit(1); +} + +let esPackageJson: ESPackageJson = { type: 'module' }; +let action: Action = 'created'; + +// Preserve existing content if package.json exists +if (fs.existsSync(packageJsonPath)) { + try { + const existingContent = fs.readFileSync(packageJsonPath, 'utf8'); + esPackageJson = JSON.parse(existingContent); + action = esPackageJson.type === 'module' ? 'verified' : 'updated'; + } catch (error) { + // Invalid JSON - will be recreated + esPackageJson = { type: 'module' }; + action = 'recreated'; + } +} + +// Set ES Module type +esPackageJson.type = 'module'; + +fs.writeFileSync(packageJsonPath, JSON.stringify(esPackageJson, null, 2)); +console.log(`✅ ES module package.json ${action} for ${packageName}`);