diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a4c1855..75934c2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: bun install bun test --coverage - lint: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -67,7 +67,7 @@ jobs: - name: Install Bun uses: oven-sh/setup-bun@v1 - - name: Check formatting + - name: Check run: | bun install - bun run prettier -c src/ + bun run check diff --git a/.npmignore b/.npmignore index f07b8d48..a740d6b5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,4 @@ # Configuration and development tools -.prettierrc .bun tsconfig.json README.md diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b1c7fb5b..00000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "arrowParens": "avoid", - "endOfLine": "lf" -} diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..1b5653ca --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", + "vcs": { + "root": ".", + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "include": ["src/**/*.ts", "examples/**/*.ts", "biome.json"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "ignore": [], + "attributePosition": "auto", + "indentStyle": "tab", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80 + }, + "javascript": { + "globals": ["NodeJS"], + "formatter": { + "enabled": true, + "lineWidth": 80, + "indentWidth": 2, + "indentStyle": "space", + "quoteStyle": "single", + "arrowParentheses": "asNeeded", + "trailingComma": "none" + } + } +} diff --git a/bun.lockb b/bun.lockb index c545cc93..b8ae2fab 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/bunfig.toml b/bunfig.toml index 18335bf9..165304da 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -1,4 +1,4 @@ [test] coverageSkipTestFiles = true -coverageThreshold = 0 +coverageThreshold = 0.8 root = "./src" \ No newline at end of file diff --git a/examples/checkStealthAddress/index.ts b/examples/checkStealthAddress/index.ts index fa5ecabf..d7885525 100644 --- a/examples/checkStealthAddress/index.ts +++ b/examples/checkStealthAddress/index.ts @@ -1,21 +1,21 @@ import { + VALID_SCHEME_ID, checkStealthAddress, generateRandomStealthMetaAddress, - generateStealthAddress, - VALID_SCHEME_ID, + generateStealthAddress } from 'stealth-address-sdk'; // User's keys (for example purposes, real values should be securely generated and stored) const { stealthMetaAddressURI, spendingPublicKey: userSpendingPublicKey, - viewingPrivateKey: userViewingPrivateKey, + viewingPrivateKey: userViewingPrivateKey } = generateRandomStealthMetaAddress(); // Generate a stealth address const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ schemeId: VALID_SCHEME_ID.SCHEME_ID_1, - stealthMetaAddressURI, + stealthMetaAddressURI }); console.log(`Stealth Address: ${stealthAddress}`); @@ -29,7 +29,7 @@ const isForUser = checkStealthAddress({ spendingPublicKey: userSpendingPublicKey, userStealthAddress: stealthAddress, // User's known stealth address viewingPrivateKey: userViewingPrivateKey, - viewTag, // From the announcement + viewTag // From the announcement }); console.log(`Is the announcement for the user? ${isForUser}`); diff --git a/examples/computeStealthKey/index.ts b/examples/computeStealthKey/index.ts index 29c6f602..12929122 100644 --- a/examples/computeStealthKey/index.ts +++ b/examples/computeStealthKey/index.ts @@ -11,5 +11,5 @@ const stealthPrivateKey = computeStealthKey({ ephemeralPublicKey, schemeId, spendingPrivateKey, - viewingPrivateKey, + viewingPrivateKey }); diff --git a/examples/generateDeterministicStealthMetaAddress/.env.example b/examples/generateDeterministicStealthMetaAddress/.env.example new file mode 100644 index 00000000..8e4f87c7 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/.env.example @@ -0,0 +1 @@ +VITE_RPC_URL='Your rpc url' \ No newline at end of file diff --git a/examples/generateDeterministicStealthMetaAddress/README.md b/examples/generateDeterministicStealthMetaAddress/README.md new file mode 100644 index 00000000..7ed21481 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/README.md @@ -0,0 +1 @@ +# Generate Deterministic Stealth Meta-Address Example diff --git a/examples/generateDeterministicStealthMetaAddress/bun.lockb b/examples/generateDeterministicStealthMetaAddress/bun.lockb new file mode 100755 index 00000000..fe1a2c59 Binary files /dev/null and b/examples/generateDeterministicStealthMetaAddress/bun.lockb differ diff --git a/examples/generateDeterministicStealthMetaAddress/index.html b/examples/generateDeterministicStealthMetaAddress/index.html new file mode 100644 index 00000000..02b4dfce --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/index.html @@ -0,0 +1,12 @@ + + + + + + + +

Generate Deterministic Stealth Meta-address Example

+
+ + + diff --git a/examples/generateDeterministicStealthMetaAddress/index.tsx b/examples/generateDeterministicStealthMetaAddress/index.tsx new file mode 100644 index 00000000..75b716d0 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/index.tsx @@ -0,0 +1,81 @@ +import React, { useState } from "react"; +import ReactDOM from "react-dom/client"; +import { Address, createWalletClient, custom } from "viem"; +import { sepolia } from "viem/chains"; +import "viem/window"; + +import { generateStealthMetaAddressFromSignature } from "@scopelift/stealth-address-sdk"; + +/** + * This React component demonstrates the process of generating a stealth meta-address deterministically using a user-signed message + * It's deterministic in that the same stealth meta-address is generated for the same user, chain id, and message + * It utilizes Viem's walletClient for wallet interaction + * + * @returns The component renders a button to first handle connecting the wallet, and a subsequent button to handle stealth meta-address generation + * + * @example + * To run the development server: `bun run dev`. + */ +const Example = () => { + // Initialize your configuration + const chain = sepolia; // Example Viem chain + + // Initialize Viem wallet client if using Viem + const walletClient = createWalletClient({ + chain, + transport: custom(window.ethereum!), + }); + + // State + const [account, setAccount] = useState
(); + const [stealthMetaAddress, setStealthMetaAddress] = useState<`0x${string}`>(); + + const connect = async () => { + const [address] = await walletClient.requestAddresses(); + setAccount(address); + }; + + const signMessage = async () => { + // An example message to sign for generating the stealth meta-address + // Usually this message includes the chain id to mitigate replay attacks across different chains + // The message that is signed should clearly communicate to the user what they are signing and why + const MESSAGE_TO_SIGN = `Generate Stealth Meta-Address on ${chain.id} chain`; + + if (!account) throw new Error("A connected account is required"); + + const signature = await walletClient.signMessage({ + account, + message: MESSAGE_TO_SIGN, + }); + + return signature; + }; + + const handleSignAndGenStealthMetaAddress = async () => { + const signature = await signMessage(); + const stealthMetaAddress = + generateStealthMetaAddressFromSignature(signature); + + setStealthMetaAddress(stealthMetaAddress); + }; + + if (account) + return ( + <> + {!stealthMetaAddress ? ( + + ) : ( +
Stealth Meta-Address: {stealthMetaAddress}
+ )} +
Connected: {account}
+ + ); + + return ; +}; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + +); diff --git a/examples/generateDeterministicStealthMetaAddress/package.json b/examples/generateDeterministicStealthMetaAddress/package.json new file mode 100644 index 00000000..d53fd937 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/package.json @@ -0,0 +1,21 @@ +{ + "name": "example-generate-deterministic-stealth-meta-address", + "private": true, + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@types/react": "^18.2.61", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@scopelift/stealth-address-sdk": "latest", + "viem": "latest", + "vite": "latest" + }, + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/examples/generateDeterministicStealthMetaAddress/tsconfig.json b/examples/generateDeterministicStealthMetaAddress/tsconfig.json new file mode 100644 index 00000000..16b8130e --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "jsx": "react", + }, + "include": ["."], +} diff --git a/examples/generateDeterministicStealthMetaAddress/vite.config.ts b/examples/generateDeterministicStealthMetaAddress/vite.config.ts new file mode 100644 index 00000000..efe63572 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}); diff --git a/examples/getAnnouncements/index.ts b/examples/getAnnouncements/index.ts index db3c2946..fc806961 100644 --- a/examples/getAnnouncements/index.ts +++ b/examples/getAnnouncements/index.ts @@ -2,12 +2,13 @@ import { ERC5564_CONTRACT, VALID_SCHEME_ID, createStealthClient, - getAnnouncements, + getAnnouncements } from 'stealth-address-sdk'; // Example parameters const chainId = 11155111; // Example chain ID for Sepolia -const rpcUrl = process.env.RPC_URL!; // Your env rpc url that aligns with the chainId; +const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); const fromBlock = BigInt(12345678); // Example ERC5564 announcer contract deploy block for Sepolia, or the block in which the user registered their stealth meta address (as an example) // Initialize the stealth client @@ -27,29 +28,31 @@ const schemeId = BigInt(VALID_SCHEME_ID.SCHEME_ID_1); const ERC5564Address = ERC5564_CONTRACT.SEPOLIA; // only for Sepolia for now async function fetchAnnouncements() { + if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); + // Example call to getAnnouncements action on the stealth client // Adjust parameters according to your requirements const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { schemeId, - caller, + caller // Additional args for filtering, if necessary }, - fromBlock, + fromBlock // toBlock: 'latest', }); // Alternatively, you can use the getAnnouncements function directly - const otherAnnouncements = await getAnnouncements({ + await getAnnouncements({ // pass in the rpcUrl and chainId to clientParams clientParams: { rpcUrl, chainId }, ERC5564Address, args: { schemeId, caller, - stealthAddress, - }, + stealthAddress + } }); console.log('Fetched announcements:', announcements); diff --git a/examples/getAnnouncementsForUser/index.ts b/examples/getAnnouncementsForUser/index.ts index 07022abf..3abff8ca 100644 --- a/examples/getAnnouncementsForUser/index.ts +++ b/examples/getAnnouncementsForUser/index.ts @@ -1,16 +1,17 @@ import { ERC5564_CONTRACT, VALID_SCHEME_ID, - createStealthClient, + createStealthClient } from 'stealth-address-sdk'; // Example parameters const chainId = 11155111; // Example chain ID for Sepolia const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); const fromBlock = BigInt(12345678); // Example ERC5564 announcer contract deploy block for Sepolia, or the block in which the user registered their stealth meta address (as an example) // Initialize the stealth client -const stealthClient = createStealthClient({ chainId, rpcUrl: rpcUrl! }); +const stealthClient = createStealthClient({ chainId, rpcUrl }); // Use the address of your calling contract if applicable const caller = '0xYourCallingContractAddress'; @@ -37,11 +38,11 @@ async function fetchAnnouncementsForUser() { ERC5564Address, args: { schemeId, - caller, + caller // Additional args for filtering, if necessary }, fromBlock, // Optional fromBlock parameter (defaults to 0, which can be slow for many blocks) - toBlock: 'latest', // Optional toBlock parameter (defaults to latest) + toBlock: 'latest' // Optional toBlock parameter (defaults to latest) }); // Example call to getAnnouncementsForUser action on the stealth client @@ -51,7 +52,7 @@ async function fetchAnnouncementsForUser() { spendingPublicKey, viewingPrivateKey, includeList: ['0xSomeEthAddress, 0xSomeOtherEthAddress'], // Optional include list to only include announcements for specific "from" addresses - excludeList: ['0xEthAddressToExclude'], // Optional exclude list to exclude announcements for specific "from" addresses + excludeList: ['0xEthAddressToExclude'] // Optional exclude list to exclude announcements for specific "from" addresses }); return userAnnouncements; diff --git a/examples/getStealthMetaAddress/index.ts b/examples/getStealthMetaAddress/index.ts index 5ad7cb3a..ff1dcbcb 100644 --- a/examples/getStealthMetaAddress/index.ts +++ b/examples/getStealthMetaAddress/index.ts @@ -1,16 +1,17 @@ import { - createStealthClient, - getStealthMetaAddress, - VALID_SCHEME_ID, ERC6538_CONTRACT, + VALID_SCHEME_ID, + createStealthClient, + getStealthMetaAddress } from 'stealth-address-sdk'; // Example stealth client parameters const chainId = 11155111; // Example chain ID for Sepolia const rpcUrl = process.env.RPC_URL; // Use your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); // Initialize the stealth client -const stealthClient = createStealthClient({ chainId, rpcUrl: rpcUrl! }); +const stealthClient = createStealthClient({ chainId, rpcUrl }); // Example getting the singleton registry contract address for Sepolia const ERC6538Address = ERC6538_CONTRACT.SEPOLIA; @@ -24,7 +25,7 @@ const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const stealthMetaAddress = await stealthClient.getStealthMetaAddress({ ERC6538Address, registrant, - schemeId, + schemeId }); // Alternatively, you can use the getStealthMetaAddress function directly @@ -33,5 +34,5 @@ const again = await getStealthMetaAddress({ clientParams: { rpcUrl, chainId }, ERC6538Address, registrant, - schemeId, + schemeId }); diff --git a/examples/prepareAnnounce/vite.config.ts b/examples/prepareAnnounce/vite.config.ts index 4e7004eb..efe63572 100644 --- a/examples/prepareAnnounce/vite.config.ts +++ b/examples/prepareAnnounce/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()] }); diff --git a/examples/prepareRegisterKeys/vite.config.ts b/examples/prepareRegisterKeys/vite.config.ts index 4e7004eb..efe63572 100644 --- a/examples/prepareRegisterKeys/vite.config.ts +++ b/examples/prepareRegisterKeys/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()] }); diff --git a/examples/watchAnnouncementsForUser/index.ts b/examples/watchAnnouncementsForUser/index.ts index 3da72536..df48fab8 100644 --- a/examples/watchAnnouncementsForUser/index.ts +++ b/examples/watchAnnouncementsForUser/index.ts @@ -1,12 +1,13 @@ import { - createStealthClient, ERC5564_CONTRACT, VALID_SCHEME_ID, + createStealthClient } from 'stealth-address-sdk'; // Initialize your environment variables or configuration const chainId = 11155111; // Example chain ID -const rpcUrl = process.env.RPC_URL!; // Your Ethereum RPC URL +const rpcUrl = process.env.RPC_URL; // Your Ethereum RPC URL +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); // User's keys and stealth address details const spendingPublicKey = '0xUserSpendingPublicKey'; @@ -24,13 +25,13 @@ const unwatch = await stealthClient.watchAnnouncementsForUser({ ERC5564Address, args: { schemeId: BigInt(VALID_SCHEME_ID.SCHEME_ID_1), // Your scheme ID - caller: '0xYourCallingContractAddress', // Use the address of your calling contract if applicable + caller: '0xYourCallingContractAddress' // Use the address of your calling contract if applicable }, spendingPublicKey, viewingPrivateKey, handleLogsForUser: logs => { console.log(logs); - }, // Your callback function to handle incoming logs + } // Your callback function to handle incoming logs }); // Stop watching for announcements diff --git a/package.json b/package.json index d254e70a..0647845a 100644 --- a/package.json +++ b/package.json @@ -8,19 +8,18 @@ "scripts": { "anvil-fork": "make anvil-fork", "test-fork": "make test-fork", - "test": "bun test src", - "build": "bun tsc", - "format": "prettier --write .", - "watch": "bun test --watch src" + "test": "bun test", + "build": "biome check --apply . && bun tsc", + "watch": "bun test --watch src", + "check": "biome check ." }, "devDependencies": { + "@biomejs/biome": "1.6.4", "@types/bun": "latest", - "eslint": "^8.56.0", - "prettier": "^3.2.4", "typescript": "^5.3.3" }, "dependencies": { "@noble/secp256k1": "^2.0.0", - "viem": "^2.7.19" + "viem": "^2.9.16" } } diff --git a/src/config/contractAddresses.ts b/src/config/contractAddresses.ts index 656f76d0..e41b0496 100644 --- a/src/config/contractAddresses.ts +++ b/src/config/contractAddresses.ts @@ -1,9 +1,9 @@ // Singleton announcer contract addresses export enum ERC5564_CONTRACT { - SEPOLIA = '0x55649E01B5Df198D18D95b5cc5051630cfD45564', + SEPOLIA = '0x55649E01B5Df198D18D95b5cc5051630cfD45564' } // Singleton registry contract addresses export enum ERC6538_CONTRACT { - SEPOLIA = '0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538', + SEPOLIA = '0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538' } diff --git a/src/lib/abi/ERC5564Announcer.ts b/src/lib/abi/ERC5564Announcer.ts index 7b52a5a5..da570bc4 100644 --- a/src/lib/abi/ERC5564Announcer.ts +++ b/src/lib/abi/ERC5564Announcer.ts @@ -6,46 +6,46 @@ export default [ indexed: true, internalType: 'uint256', name: 'schemeId', - type: 'uint256', + type: 'uint256' }, { indexed: true, internalType: 'address', name: 'stealthAddress', - type: 'address', + type: 'address' }, { indexed: true, internalType: 'address', name: 'caller', - type: 'address', + type: 'address' }, { indexed: false, internalType: 'bytes', name: 'ephemeralPubKey', - type: 'bytes', + type: 'bytes' }, { indexed: false, internalType: 'bytes', name: 'metadata', - type: 'bytes', - }, + type: 'bytes' + } ], name: 'Announcement', - type: 'event', + type: 'event' }, { inputs: [ { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, { internalType: 'address', name: 'stealthAddress', type: 'address' }, { internalType: 'bytes', name: 'ephemeralPubKey', type: 'bytes' }, - { internalType: 'bytes', name: 'metadata', type: 'bytes' }, + { internalType: 'bytes', name: 'metadata', type: 'bytes' } ], name: 'announce', outputs: [], stateMutability: 'nonpayable', - type: 'function', - }, + type: 'function' + } ] as const; diff --git a/src/lib/abi/ERC6538Registry.ts b/src/lib/abi/ERC6538Registry.ts index 07507a12..c95c8a11 100644 --- a/src/lib/abi/ERC6538Registry.ts +++ b/src/lib/abi/ERC6538Registry.ts @@ -8,82 +8,82 @@ export default [ indexed: true, internalType: 'address', name: 'registrant', - type: 'address', + type: 'address' }, { indexed: true, internalType: 'uint256', name: 'schemeId', - type: 'uint256', + type: 'uint256' }, { indexed: false, internalType: 'bytes', name: 'stealthMetaAddress', - type: 'bytes', - }, + type: 'bytes' + } ], name: 'StealthMetaAddressSet', - type: 'event', + type: 'event' }, { inputs: [], name: 'DOMAIN_SEPARATOR', outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], stateMutability: 'view', - type: 'function', + type: 'function' }, { inputs: [], name: 'ERC6538REGISTRY_ENTRY_TYPE_HASH', outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], stateMutability: 'view', - type: 'function', + type: 'function' }, { inputs: [], name: 'incrementNonce', outputs: [], stateMutability: 'nonpayable', - type: 'function', + type: 'function' }, { inputs: [{ internalType: 'address', name: 'registrant', type: 'address' }], name: 'nonceOf', outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], stateMutability: 'view', - type: 'function', + type: 'function' }, { inputs: [ { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, - { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' }, + { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' } ], name: 'registerKeys', outputs: [], stateMutability: 'nonpayable', - type: 'function', + type: 'function' }, { inputs: [ { internalType: 'address', name: 'registrant', type: 'address' }, { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, { internalType: 'bytes', name: 'signature', type: 'bytes' }, - { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' }, + { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' } ], name: 'registerKeysOnBehalf', outputs: [], stateMutability: 'nonpayable', - type: 'function', + type: 'function' }, { inputs: [ { internalType: 'address', name: 'registrant', type: 'address' }, - { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, + { internalType: 'uint256', name: 'schemeId', type: 'uint256' } ], name: 'stealthMetaAddressOf', outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], stateMutability: 'view', - type: 'function', - }, + type: 'function' + } ] as const; diff --git a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts index 5c9e990f..4c394b23 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts @@ -1,51 +1,67 @@ -import { describe, test, expect } from 'bun:test'; -import ERC556AnnouncerAbi from '../../abi/ERC5564Announcer'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Account, Address } from 'viem'; import { VALID_SCHEME_ID, generateRandomStealthMetaAddress, - generateStealthAddress, + generateStealthAddress } from '../../..'; +import ERC556AnnouncerAbi from '../../abi/ERC5564Announcer'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; -describe('getAnnouncements', async () => { - const { stealthClient, ERC5564DeployBlock, ERC5564Address } = - await setupTestEnv(); - const walletClient = await setupTestWallet(); +describe('getAnnouncements', () => { + let stealthClient: StealthActions; + let walletClient: SuperWalletClient; + let fromBlock: bigint; + let ERC5564Address: Address; + let account: Account | undefined; + // Set up stealth address details const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddressURI } = generateRandomStealthMetaAddress(); - const fromBlock = ERC5564DeployBlock; - - // Set up stealth address details const { stealthAddress, viewTag, ephemeralPublicKey } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); - // Announce the stealth address, ephemeral public key, and view tag - const hash = await walletClient.writeContract({ - address: ERC5564Address, - functionName: 'announce', - args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], - abi: ERC556AnnouncerAbi, - chain: walletClient.chain, - account: walletClient.account!, - }); + // Set up the test environment and announce the stealth address + beforeAll(async () => { + const { + stealthClient: client, + ERC5564Address, + ERC5564DeployBlock + } = await setupTestEnv(); + walletClient = await setupTestWallet(); + stealthClient = client; + fromBlock = ERC5564DeployBlock; + account = walletClient.account; + + if (!account) throw new Error('No account found'); + + // Announce the stealth address, ephemeral public key, and view tag + const hash = await walletClient.writeContract({ + address: ERC5564Address, + functionName: 'announce', + args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], + abi: ERC556AnnouncerAbi, + chain: walletClient.chain, + account + }); - console.log('Waiting for announcement transaction to be mined...'); - // Wait for the transaction to be mined - const res = await walletClient.waitForTransactionReceipt({ - hash, + // Wait for the transaction to be mined + await walletClient.waitForTransactionReceipt({ + hash + }); }); - console.log('Announcement transaction mined:', res.transactionHash); test('fetches announcements successfully', async () => { const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: {}, - fromBlock, + fromBlock }); expect(announcements.length).toBeGreaterThan(0); @@ -55,32 +71,34 @@ describe('getAnnouncements', async () => { const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { - stealthAddress, + stealthAddress }, - fromBlock, + fromBlock }); expect(announcements[0].stealthAddress).toBe(stealthAddress); }); test('fetches specific announcements successfully using caller', async () => { + if (!account) throw new Error('No account found'); + const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { - caller: walletClient.account?.address, + caller: walletClient.account?.address }, - fromBlock, + fromBlock }); - expect(announcements[0].caller).toBe(walletClient.account?.address!); + expect(announcements[0].caller).toBe(account.address); }); test('fetches specific announcements successfully using schemeId', async () => { const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { - schemeId: BigInt(schemeId), + schemeId: BigInt(schemeId) }, - fromBlock, + fromBlock }); expect(announcements[0].schemeId).toBe(BigInt(schemeId)); @@ -90,9 +108,9 @@ describe('getAnnouncements', async () => { const invalidAnnouncements = await stealthClient.getAnnouncements({ ERC5564Address, args: { - schemeId: invalidSchemeId, + schemeId: invalidSchemeId }, - fromBlock, + fromBlock }); expect(invalidAnnouncements.length).toBe(0); diff --git a/src/lib/actions/getAnnouncements/getAnnouncements.ts b/src/lib/actions/getAnnouncements/getAnnouncements.ts index 1efc7d59..81b63eb0 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.ts @@ -1,12 +1,12 @@ import type { BlockType } from '../types'; import { type PublicClient, parseAbiItem } from 'viem'; -import { getBlock, getLogs } from 'viem/actions'; +import { getBlock, getBlockNumber, getLogs } from 'viem/actions'; import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; import type { AnnouncementLog, GetAnnouncementsParams, - GetAnnouncementsReturnType, + GetAnnouncementsReturnType } from './types'; /** @@ -25,20 +25,20 @@ async function getAnnouncements({ ERC5564Address, args, fromBlock, - toBlock, + toBlock }: GetAnnouncementsParams): Promise { const publicClient = handleViemPublicClient(clientParams); const fetchParams = { address: ERC5564Address, - args, + args }; const logs = await fetchLogsInChunks({ publicClient, fetchParams, fromBlock, - toBlock, + toBlock }); // Extract the relevant data from the logs @@ -51,7 +51,7 @@ async function getAnnouncements({ caller: args.caller, ephemeralPubKey: args.ephemeralPubKey, metadata: args.metadata, - ...log, + ...log }; }); @@ -74,11 +74,12 @@ const fetchLogsInChunks = async ({ fetchParams, fromBlock, toBlock, - chunkSize = 5000, // Default chunk size, can be adjusted + chunkSize = 5000 // Default chunk size, can be adjusted }: { publicClient: PublicClient; fetchParams: { address: `0x${string}`; + // biome-ignore lint/suspicious/noExplicitAny: TODO handle better args: any; fromBlock?: BlockType; toBlock?: BlockType; @@ -90,18 +91,14 @@ const fetchLogsInChunks = async ({ const resolvedFromBlock = (await resolveBlockNumber({ publicClient, - block: fromBlock ?? 'earliest', + block: fromBlock ?? 'earliest' })) || BigInt(0); const resolvedToBlock = await resolveBlockNumber({ publicClient, - block: toBlock ?? 'latest', + block: toBlock ?? 'latest' }); - if (!resolvedToBlock) { - throw new Error('Failed to resolve toBlock'); - } - let currentBlock = resolvedFromBlock; const allLogs = []; @@ -119,7 +116,7 @@ const fetchLogsInChunks = async ({ ), fromBlock: currentBlock, toBlock: endBlock, - strict: true, + strict: true }); allLogs.push(...logs); currentBlock = endBlock + BigInt(1); @@ -134,25 +131,25 @@ const fetchLogsInChunks = async ({ * @param {Object} params - Parameters for resolving the block number. * - `publicClient`: An instance of the viem `PublicClient`. * - `block`: The block number or tag to resolve. - * @returns {Promise} The resolved block number as a bigint or null. + * @returns {Promise} The resolved block number as a bigint or null. */ -async function resolveBlockNumber({ +export async function resolveBlockNumber({ publicClient, - block, + block }: { publicClient: PublicClient; block?: BlockType; -}): Promise { +}): Promise { if (typeof block === 'bigint') { return block; } - try { - const res = await getBlock(publicClient, { blockTag: block }); - return res.number; - } catch (error) { - throw new Error(`Failed to resolve block number: ${error}.`); + const { number } = await getBlock(publicClient, { blockTag: block }); + // Get the latest block number if null, since it is the pending block + if (!number) { + return getBlockNumber(publicClient); } + return number; } export default getAnnouncements; diff --git a/src/lib/actions/getAnnouncements/types.ts b/src/lib/actions/getAnnouncements/types.ts index ed67ffbb..a865f7b2 100644 --- a/src/lib/actions/getAnnouncements/types.ts +++ b/src/lib/actions/getAnnouncements/types.ts @@ -1,7 +1,7 @@ import type { Log } from 'viem'; import type { EthAddress } from '../../../utils/crypto/types'; -import type { BlockType } from '../types'; import type { ClientParams } from '../../stealthClient/types'; +import type { BlockType } from '../types'; export type AnnouncementArgs = { schemeId?: bigint | bigint[] | null | undefined; diff --git a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts index 029201fd..82a2021a 100644 --- a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts +++ b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts @@ -1,62 +1,95 @@ -import { expect, test, describe } from 'bun:test'; -import setupTestEnv from '../../helpers/test/setupTestEnv'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; -import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Account, PublicClient } from 'viem'; +import type { AnnouncementLog } from '..'; +import { getViewTagFromMetadata } from '../../..'; import { VALID_SCHEME_ID, generateStealthAddress } from '../../../utils/crypto'; +import type { HexString } from '../../../utils/crypto/types'; import { ERC5564AnnouncerAbi } from '../../abi'; - -describe('getAnnouncementsForUser', async () => { - const { stealthClient, ERC5564Address, ERC5564DeployBlock } = - await setupTestEnv(); - const walletClient = await setupTestWallet(); - const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; - const { stealthMetaAddressURI, spendingPublicKey, viewingPrivateKey } = - setupTestStealthKeys(schemeId); - const fromBlock = ERC5564DeployBlock; - - // Set up stealth address details - const { stealthAddress, ephemeralPublicKey, viewTag } = - generateStealthAddress({ +import setupTestEnv from '../../helpers/test/setupTestEnv'; +import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; +import { + getTransactionFrom, + processAnnouncement +} from './getAnnouncementsForUser'; +import { FromValueNotFoundError, TransactionHashRequiredError } from './types'; + +const PROCESS_LARGE_NUMBER_OF_ANNOUNCEMENTS_NUM = 100; // Number of announcements to process in the large data set test + +describe('getAnnouncementsForUser', () => { + let stealthClient: StealthActions; + let walletClient: SuperWalletClient; + let account: Account | undefined; + + let stealthAddress: HexString; + let ephemeralPublicKey: HexString; + let viewTag: HexString; + let spendingPublicKey: HexString; + let viewingPrivateKey: HexString; + let stealthMetaAddressURI: string; + + let announcements: AnnouncementLog[] = []; + + beforeAll(async () => { + // Set up the test environment + const { + stealthClient: client, + ERC5564Address, + ERC5564DeployBlock + } = await setupTestEnv(); + walletClient = await setupTestWallet(); + account = walletClient.account; + const chain = walletClient.chain; + if (!account) throw new Error('No account found'); + if (!chain) throw new Error('No chain found'); + stealthClient = client; + + // Set up the stealth keys and generate a stealth address + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + ({ stealthMetaAddressURI, spendingPublicKey, viewingPrivateKey } = + setupTestStealthKeys(schemeId)); + + ({ stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId: VALID_SCHEME_ID.SCHEME_ID_1 + })); + + // Announce the stealth address, ephemeral public key, and view tag + const hash = await walletClient.writeContract({ + address: ERC5564Address, + functionName: 'announce', + args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], + abi: ERC5564AnnouncerAbi, + chain, + account }); - // Announce the stealth address, ephemeral public key, and view tag - const hash = await walletClient.writeContract({ - address: ERC5564Address, - functionName: 'announce', - args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], - abi: ERC5564AnnouncerAbi, - chain: walletClient.chain, - account: walletClient.account!, - }); + // Wait for the transaction to be mined + await walletClient.waitForTransactionReceipt({ + hash + }); - console.log('Waiting for announcement transaction to be mined...'); - // Wait for the transaction to be mined - const res = await walletClient.waitForTransactionReceipt({ - hash, - }); - console.log('Announcement transaction mined:', res.transactionHash); - - // Fetch relevant announcements to check against - console.log('fetching announcements...'); - const announcements = await stealthClient.getAnnouncements({ - ERC5564Address, - args: { - schemeId: BigInt(schemeId), - stealthAddress, - caller: walletClient.account?.address, // Just an example; the caller is the address of the wallet since it called announce - }, - fromBlock, + // Fetch relevant announcements to check against + announcements = await stealthClient.getAnnouncements({ + ERC5564Address, + args: { + schemeId: BigInt(schemeId), + stealthAddress, + caller: walletClient.account?.address // Just an example; the caller is the address of the wallet since it called announce + }, + fromBlock: ERC5564DeployBlock, + toBlock: 'latest' + }); }); - console.log('relevant announcements fetched for testing'); test('filters announcements correctly for the user', async () => { // Fetch announcements for the specific user const results = await stealthClient.getAnnouncementsForUser({ announcements, spendingPublicKey, - viewingPrivateKey, + viewingPrivateKey }); expect(results[0].stealthAddress).toEqual(stealthAddress); @@ -65,7 +98,7 @@ describe('getAnnouncementsForUser', async () => { const results2 = await stealthClient.getAnnouncementsForUser({ announcements, spendingPublicKey: '0x', - viewingPrivateKey, + viewingPrivateKey }); expect(results2.length).toBe(0); @@ -74,15 +107,17 @@ describe('getAnnouncementsForUser', async () => { const results3 = await stealthClient.getAnnouncementsForUser({ announcements, spendingPublicKey, - viewingPrivateKey: '0x', + viewingPrivateKey: '0x' }); expect(results3.length).toBe(0); }); test('handles include and exclude lists correctly', async () => { + if (!account) throw new Error('No account found'); + // Just an example: the 'from' address of the announcement to use for filtering - const fromAddressToTest = walletClient.account?.address!; + const fromAddressToTest = account.address; const someOtherAddress = '0xD945323b7E5071598868989838414e679F29C0AB'; // Test with an exclude list that should filter out the announcement @@ -90,7 +125,7 @@ describe('getAnnouncementsForUser', async () => { announcements, spendingPublicKey, viewingPrivateKey, - excludeList: [fromAddressToTest], + excludeList: [fromAddressToTest] }); expect(excludeListResults.length).toBe(0); @@ -100,7 +135,7 @@ describe('getAnnouncementsForUser', async () => { announcements, spendingPublicKey, viewingPrivateKey, - excludeList: [someOtherAddress], + excludeList: [someOtherAddress] }); expect(excludeListResults2[0].stealthAddress).toEqual(stealthAddress); @@ -110,7 +145,7 @@ describe('getAnnouncementsForUser', async () => { announcements, spendingPublicKey, viewingPrivateKey, - includeList: [fromAddressToTest], + includeList: [fromAddressToTest] }); expect(includeListResults[0].stealthAddress).toEqual(stealthAddress); @@ -120,7 +155,7 @@ describe('getAnnouncementsForUser', async () => { announcements, spendingPublicKey, viewingPrivateKey, - includeList: [someOtherAddress], + includeList: [someOtherAddress] }); expect(includeListResults2.length).toBe(0); @@ -132,7 +167,7 @@ describe('getAnnouncementsForUser', async () => { spendingPublicKey, viewingPrivateKey, includeList: [fromAddressToTest], - excludeList: [fromAddressToTest], + excludeList: [fromAddressToTest] }); expect(includeAndExcludeListResults.length).toBe(0); @@ -140,26 +175,75 @@ describe('getAnnouncementsForUser', async () => { test('efficiently processes a large number of announcements', async () => { // Generate a large set of mock announcements using the first announcement from above - const largeNumberOfAnnouncements = 1000; // Example size const largeAnnouncements = Array.from( - { length: largeNumberOfAnnouncements }, + { length: PROCESS_LARGE_NUMBER_OF_ANNOUNCEMENTS_NUM }, () => announcements[0] ); - const startTime = performance.now(); - const results = await stealthClient.getAnnouncementsForUser({ announcements: largeAnnouncements, spendingPublicKey, - viewingPrivateKey, + viewingPrivateKey }); - const endTime = performance.now(); - // Verify the function handles large data sets correctly - expect(results).toHaveLength(largeNumberOfAnnouncements); - console.log( - `Processed ${largeNumberOfAnnouncements} announcements in ${endTime - startTime} milliseconds.` + expect(results).toHaveLength(PROCESS_LARGE_NUMBER_OF_ANNOUNCEMENTS_NUM); + }); + + test('throws TransactionHashRequiredError when transactionHash is null', async () => { + const announcementWithoutHash: AnnouncementLog = { + ...announcements[0], + transactionHash: null + }; + + expect( + processAnnouncement( + announcementWithoutHash, + walletClient as PublicClient, + { + spendingPublicKey, + viewingPrivateKey, + excludeList: new Set([]), + includeList: new Set([]) + } + ) + ).rejects.toBeInstanceOf(TransactionHashRequiredError); + }); + + test('throws FromValueNotFoundError when the "from" value is not found', async () => { + const invalidHash = '0xinvalidhash'; + + expect( + getTransactionFrom({ + publicClient: walletClient as PublicClient, + hash: invalidHash + }) + ).rejects.toBeInstanceOf(FromValueNotFoundError); + }); + + test('throws error if view tag does not start with 0x', () => { + const metadata = 'invalidmetadata'; + expect(() => getViewTagFromMetadata(metadata as HexString)).toThrow( + 'Invalid metadata format' ); }); }); + +describe('getAnnouncementsForUser error class tests', () => { + test('TransactionHashRequiredError should have the correct message', () => { + const errorMessage = 'The transaction hash is required.'; + const error = new TransactionHashRequiredError(); + + expect(error.message).toBe(errorMessage); + expect(error.name).toBe('TransactionHashRequiredError'); + }); + + test('FromValueNotFoundError should have the correct message', () => { + const errorMessage = + 'The "from" value could not be retrieved for a transaction.'; + const error = new FromValueNotFoundError(); + + expect(error.message).toBe(errorMessage); + expect(error.name).toBe('FromValueNotFoundError'); + }); +}); diff --git a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts index 8fc874fd..23b73856 100644 --- a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts +++ b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts @@ -1,19 +1,19 @@ -import { getAddress, type PublicClient } from 'viem'; +import { type PublicClient, getAddress } from 'viem'; import { - checkStealthAddress, - getViewTagFromMetadata, type EthAddress, + checkStealthAddress, + getViewTagFromMetadata } from '../../..'; +import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; +import type { AnnouncementLog } from '../getAnnouncements/types'; import { - TransactionHashRequiredError, + FromValueNotFoundError, type GetAnnouncementsForUserParams, type GetAnnouncementsForUserReturnType, - FromValueNotFoundError, type ProcessAnnouncementParams, type ProcessAnnouncementReturnType, + TransactionHashRequiredError } from './types'; -import type { AnnouncementLog } from '../getAnnouncements/types'; -import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; /** * @description Fetches and processes a list of announcements to determine which are relevant for the user. @@ -34,7 +34,7 @@ async function getAnnouncementsForUser({ viewingPrivateKey, clientParams, excludeList = [], - includeList = [], + includeList = [] }: GetAnnouncementsForUserParams): Promise { const publicClient = handleViemPublicClient(clientParams); @@ -53,7 +53,7 @@ async function getAnnouncementsForUser({ viewingPrivateKey, clientParams, excludeList: _excludeList, - includeList: _includeList, + includeList: _includeList }) ) ); @@ -63,7 +63,7 @@ async function getAnnouncementsForUser({ >( (acc, result) => result.status === 'fulfilled' && result.value !== null - ? [...acc, result.value] + ? acc.concat(result.value) : acc, [] ); @@ -84,21 +84,21 @@ async function getAnnouncementsForUser({ * - `includeList`: Addresses to specifically include in the results. * @returns {Promise} A promise that resolves to the processed announcement if it is relevant, or null otherwise. */ -async function processAnnouncement( +export async function processAnnouncement( announcement: AnnouncementLog, publicClient: PublicClient, { spendingPublicKey, viewingPrivateKey, excludeList, - includeList, + includeList }: ProcessAnnouncementParams ): Promise { const { ephemeralPubKey: ephemeralPublicKey, metadata, stealthAddress: userStealthAddress, - transactionHash: hash, + transactionHash: hash } = announcement; const viewTag = getViewTagFromMetadata(metadata); @@ -109,7 +109,7 @@ async function processAnnouncement( userStealthAddress, viewingPrivateKey, viewTag, - schemeId: Number(announcement.schemeId), + schemeId: Number(announcement.schemeId) }); // If the announcement is not intended for the user, return null @@ -121,7 +121,7 @@ async function processAnnouncement( hash, excludeList, includeList, - publicClient, + publicClient }); if (!shouldInclude) return null; @@ -145,7 +145,7 @@ async function shouldIncludeAnnouncement({ hash, excludeList, includeList, - publicClient, + publicClient }: { hash: `0x${string}`; excludeList: Set; @@ -173,9 +173,9 @@ async function shouldIncludeAnnouncement({ * @returns {Promise<`0x${string}`>} A promise that resolves to the transaction sender address. * @throws {FromValueNotFoundError} If the transaction or sender address cannot be fetched, indicating a potential issue with the transaction lookup. */ -async function getTransactionFrom({ +export async function getTransactionFrom({ publicClient, - hash, + hash }: { publicClient: PublicClient; hash: `0x${string}`; diff --git a/src/lib/actions/getAnnouncementsForUser/types.ts b/src/lib/actions/getAnnouncementsForUser/types.ts index 1f9e5f42..d558353c 100644 --- a/src/lib/actions/getAnnouncementsForUser/types.ts +++ b/src/lib/actions/getAnnouncementsForUser/types.ts @@ -25,7 +25,7 @@ export type ProcessAnnouncementReturnType = AnnouncementLog | null; export class FromValueNotFoundError extends Error { constructor( - message: string = 'The "from" value could not be retrieved for a transaction.' + message = 'The "from" value could not be retrieved for a transaction.' ) { super(message); this.name = 'FromValueNotFoundError'; @@ -34,7 +34,7 @@ export class FromValueNotFoundError extends Error { } export class TransactionHashRequiredError extends Error { - constructor(message: string = 'The transaction hash is required.') { + constructor(message = 'The transaction hash is required.') { super(message); this.name = 'TransactionHashRequiredError'; Object.setPrototypeOf(this, TransactionHashRequiredError.prototype); diff --git a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts index d07c1ac5..516e8567 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts @@ -1,44 +1,68 @@ -import { describe, test, expect } from 'bun:test'; -import setupTestEnv from '../../helpers/test/setupTestEnv'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Address } from 'viem'; import { ERC6538RegistryAbi, VALID_SCHEME_ID, - generateRandomStealthMetaAddress, + generateRandomStealthMetaAddress } from '../../..'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; +import { GetStealthMetaAddressError } from './types'; -describe('getStealthMetaAddress', async () => { - const { stealthClient, ERC6538Address } = await setupTestEnv(); - const walletClient = await setupTestWallet(); +describe('getStealthMetaAddress', () => { + let stealthClient: StealthActions; + let ERC6538Address: Address; + let walletClient: SuperWalletClient; + let registrant: Address | undefined; // Generate a random stealth meta address just for testing purposes + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddress } = generateRandomStealthMetaAddress(); - // Register the stealth meta address - const registrant = walletClient.account?.address!; - const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + beforeAll(async () => { + // Set up the test environment + ({ stealthClient, ERC6538Address } = await setupTestEnv()); + walletClient = await setupTestWallet(); - const hash = await walletClient.writeContract({ - address: ERC6538Address, - functionName: 'registerKeys', - args: [BigInt(schemeId), stealthMetaAddress], - abi: ERC6538RegistryAbi, - chain: walletClient.chain, - account: walletClient.account!, - }); + // Register the stealth meta address + registrant = walletClient.account?.address; + if (!registrant) throw new Error('No registrant address found'); - console.log('Waiting for registration transaction to be mined...'); - const res = await walletClient.waitForTransactionReceipt({ hash }); + const hash = await walletClient.writeContract({ + address: ERC6538Address, + functionName: 'registerKeys', + args: [BigInt(schemeId), stealthMetaAddress], + abi: ERC6538RegistryAbi, + chain: walletClient.chain, + account: registrant + }); - console.log('Registration transaction mined:', res.transactionHash); + await walletClient.waitForTransactionReceipt({ hash }); + }); test('should return the stealth meta address for a given registrant and scheme ID', async () => { + if (!registrant) throw new Error('No registrant address found'); + const result = await stealthClient.getStealthMetaAddress({ ERC6538Address, registrant, - schemeId, + schemeId }); expect(result).toEqual(stealthMetaAddress); }); + + test('should throw an error if the stealth meta address cannot be fetched', async () => { + const invalidRegistrant = '0xInvalidRegistrant'; + + expect( + stealthClient.getStealthMetaAddress({ + ERC6538Address, + registrant: invalidRegistrant, + schemeId: VALID_SCHEME_ID.SCHEME_ID_1 + }) + ).rejects.toBeInstanceOf(GetStealthMetaAddressError); + }); }); diff --git a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts index 6122b40b..4fbd6649 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts @@ -1,8 +1,9 @@ import { ERC6538RegistryAbi } from '../..'; import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; -import type { - GetStealthMetaAddressParams, - GetStealthMetaAddressReturnType, +import { + GetStealthMetaAddressError, + type GetStealthMetaAddressParams, + type GetStealthMetaAddressReturnType } from './types'; /** @@ -24,7 +25,7 @@ async function getStealthMetaAddress({ clientParams, ERC6538Address, registrant, - schemeId, + schemeId }: GetStealthMetaAddressParams): Promise { const publicClient = handleViemPublicClient(clientParams); try { @@ -32,10 +33,12 @@ async function getStealthMetaAddress({ address: ERC6538Address, functionName: 'stealthMetaAddressOf', args: [registrant, BigInt(schemeId)], - abi: ERC6538RegistryAbi, + abi: ERC6538RegistryAbi }); } catch (error) { - throw new Error(`Error getting stealth meta address: ${error}`); + throw new GetStealthMetaAddressError( + `Error getting stealth meta address: ${error}` + ); } } diff --git a/src/lib/actions/getStealthMetaAddress/types.ts b/src/lib/actions/getStealthMetaAddress/types.ts index 232a4afe..a2a43941 100644 --- a/src/lib/actions/getStealthMetaAddress/types.ts +++ b/src/lib/actions/getStealthMetaAddress/types.ts @@ -8,3 +8,11 @@ export type GetStealthMetaAddressParams = { schemeId: VALID_SCHEME_ID; }; export type GetStealthMetaAddressReturnType = `0x${string}` | undefined; + +export class GetStealthMetaAddressError extends Error { + constructor(message = 'Error getting stealth meta address.') { + super(message); + this.name = 'GetStealthMetaAddressError'; + Object.setPrototypeOf(this, GetStealthMetaAddressError.prototype); + } +} diff --git a/src/lib/actions/index.ts b/src/lib/actions/index.ts index 9ac55555..e2abfc49 100644 --- a/src/lib/actions/index.ts +++ b/src/lib/actions/index.ts @@ -1,11 +1,11 @@ +import type { StealthActions } from '../stealthClient/types'; import getAnnouncements from './getAnnouncements/getAnnouncements'; -import getStealthMetaAddress from './getStealthMetaAddress/getStealthMetaAddress'; import getAnnouncementsForUser from './getAnnouncementsForUser/getAnnouncementsForUser'; -import watchAnnouncementsForUser from './watchAnnouncementsForUser/watchAnnouncementsForUser'; +import getStealthMetaAddress from './getStealthMetaAddress/getStealthMetaAddress'; import prepareAnnounce from './prepareAnnounce/prepareAnnounce'; import prepareRegisterKeys from './prepareRegisterKeys/prepareRegisterKeys'; import prepareRegisterKeysOnBehalf from './prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf'; -import type { StealthActions } from '../stealthClient/types'; +import watchAnnouncementsForUser from './watchAnnouncementsForUser/watchAnnouncementsForUser'; export { default as getAnnouncements } from './getAnnouncements/getAnnouncements'; export { default as getStealthMetaAddress } from './getStealthMetaAddress/getStealthMetaAddress'; export { default as getAnnouncementsForUser } from './getAnnouncementsForUser/getAnnouncementsForUser'; @@ -18,31 +18,31 @@ export { type AnnouncementArgs, type AnnouncementLog, type GetAnnouncementsParams, - type GetAnnouncementsReturnType, + type GetAnnouncementsReturnType } from './getAnnouncements/types'; export { type GetStealthMetaAddressParams, - type GetStealthMetaAddressReturnType, + type GetStealthMetaAddressReturnType } from './getStealthMetaAddress/types'; export { type GetAnnouncementsForUserParams, - type GetAnnouncementsForUserReturnType, + type GetAnnouncementsForUserReturnType } from './getAnnouncementsForUser/types'; export { type WatchAnnouncementsForUserParams, - type WatchAnnouncementsForUserReturnType, + type WatchAnnouncementsForUserReturnType } from './watchAnnouncementsForUser/types'; export { type PrepareAnnounceParams, - type PrepareAnnounceReturnType, + type PrepareAnnounceReturnType } from './prepareAnnounce/types'; export { type PrepareRegisterKeysParams, - type PrepareRegisterKeysReturnType, + type PrepareRegisterKeysReturnType } from './prepareRegisterKeys/types'; export { type PrepareRegisterKeysOnBehalfParams, - type PrepareRegisterKeysOnBehalfReturnType, + type PrepareRegisterKeysOnBehalfReturnType } from './prepareRegisterKeysOnBehalf/types'; export const actions: StealthActions = { @@ -52,5 +52,5 @@ export const actions: StealthActions = { watchAnnouncementsForUser, prepareAnnounce, prepareRegisterKeys, - prepareRegisterKeysOnBehalf, + prepareRegisterKeysOnBehalf }; diff --git a/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts b/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts index 2a757e31..c424c0dc 100644 --- a/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts +++ b/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts @@ -1,46 +1,82 @@ -import { describe, test, expect } from 'bun:test'; -import setupTestEnv from '../../helpers/test/setupTestEnv'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Address, Chain, TransactionReceipt } from 'viem'; import { VALID_SCHEME_ID, generateStealthAddress } from '../../..'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; +import { PrepareError } from '../types'; + +describe('prepareAnnounce', () => { + let stealthClient: StealthActions; + let ERC5564Address: Address; + let walletClient: SuperWalletClient; + let account: Address | undefined; + let chain: Chain | undefined; -describe('prepareAnnounce', async () => { - const { stealthClient, ERC5564Address } = await setupTestEnv(); - const walletClient = await setupTestWallet(); const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); - const account = walletClient.account?.address!; - const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); - const prepared = await stealthClient.prepareAnnounce({ - account, - args: { - schemeId, - stealthAddress, - ephemeralPublicKey, - metadata: viewTag, - }, - ERC5564Address, - }); + const prepareArgs = { + schemeId, + stealthAddress, + ephemeralPublicKey, + metadata: viewTag + }; - // Prepare tx using viem and the prepared payload - const request = await walletClient.prepareTransactionRequest({ - ...prepared, - chain: walletClient.chain, - account: walletClient.account, - }); + // Transaction receipt for writing to the contract with the prepared payload + let res: TransactionReceipt; + + beforeAll(async () => { + // Set up the test environment + ({ stealthClient, ERC5564Address } = await setupTestEnv()); + walletClient = await setupTestWallet(); + account = walletClient.account?.address; + chain = walletClient.chain; - const hash = await walletClient.sendTransaction({ - ...request, - chain: walletClient.chain, + if (!account) throw new Error('No account found'); + if (!chain) throw new Error('No chain found'); + + const prepared = await stealthClient.prepareAnnounce({ + account, + args: prepareArgs, + ERC5564Address + }); + + // Prepare tx using viem and the prepared payload + const request = await walletClient.prepareTransactionRequest({ + ...prepared, + chain, + account + }); + + const hash = await walletClient.sendTransaction({ + ...request, + chain, + account + }); + + res = await walletClient.waitForTransactionReceipt({ hash }); }); - const res = await walletClient.waitForTransactionReceipt({ hash }); + test('should throw PrepareError when given invalid params', () => { + if (!account) throw new Error('No account found'); + + const invalidERC5564Address = '0xinvalid'; + expect( + stealthClient.prepareAnnounce({ + account, + args: prepareArgs, + ERC5564Address: invalidERC5564Address + }) + ).rejects.toBeInstanceOf(PrepareError); + }); test('should successfully announce the stealth address details using the prepare payload', () => { expect(res.status).toEqual('success'); diff --git a/src/lib/actions/prepareAnnounce/prepareAnnounce.ts b/src/lib/actions/prepareAnnounce/prepareAnnounce.ts index 9b752600..c6798c1d 100644 --- a/src/lib/actions/prepareAnnounce/prepareAnnounce.ts +++ b/src/lib/actions/prepareAnnounce/prepareAnnounce.ts @@ -1,8 +1,8 @@ -import type { PrepareAnnounceParams, PrepareAnnounceReturnType } from './types'; -import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; -import { ERC5564AnnouncerAbi } from '../..'; import { encodeFunctionData } from 'viem'; +import { ERC5564AnnouncerAbi } from '../..'; +import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; import { PrepareError } from '../types'; +import type { PrepareAnnounceParams, PrepareAnnounceReturnType } from './types'; /** * Prepares the payload for announcing a stealth address to the ERC5564 contract. @@ -26,7 +26,7 @@ async function prepareAnnounce({ ERC5564Address, args, account, - clientParams, + clientParams }: PrepareAnnounceParams): Promise { const publicClient = handleViemPublicClient(clientParams); const { schemeId, stealthAddress, ephemeralPublicKey, metadata } = args; @@ -35,13 +35,13 @@ async function prepareAnnounce({ schemeIdBigInt, stealthAddress, ephemeralPublicKey, - metadata, + metadata ]; const data = encodeFunctionData({ abi: ERC5564AnnouncerAbi, functionName: 'announce', - args: writeArgs, + args: writeArgs }); try { @@ -50,7 +50,7 @@ async function prepareAnnounce({ address: ERC5564Address, abi: ERC5564AnnouncerAbi, functionName: 'announce', - args: writeArgs, + args: writeArgs }); } catch (error) { throw new PrepareError(`Failed to prepare contract call: ${error}`); @@ -59,7 +59,7 @@ async function prepareAnnounce({ return { to: ERC5564Address, account, - data, + data }; } export default prepareAnnounce; diff --git a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts index ac918491..5f2ffd8c 100644 --- a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts +++ b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts @@ -1,40 +1,77 @@ -import { describe, test, expect } from 'bun:test'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import { + type PrepareRegisterKeysParams, + VALID_SCHEME_ID, + parseStealthMetaAddressURI +} from '../../..'; import setupTestEnv from '../../helpers/test/setupTestEnv'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; -import { VALID_SCHEME_ID, parseStealthMetaAddressURI } from '../../..'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; +import { PrepareError } from '../types'; + +describe('prepareRegisterKeys', () => { + let stealthClient: StealthActions; + let ERC6538Address: Address; + let walletClient: SuperWalletClient; + let account: Address | undefined; + let chain: Chain | undefined; -describe('prepareRegisterKeys', async () => { - const { stealthClient, ERC6538Address } = await setupTestEnv(); - const walletClient = await setupTestWallet(); const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }); - const account = walletClient.account?.address!; - const prepared = await stealthClient.prepareRegisterKeys({ - account, - ERC6538Address, - schemeId, - stealthMetaAddress: stealthMetaAddressToRegister, - }); + // Prepare payload args + let prepareArgs: PrepareRegisterKeysParams; + // Transaction receipt for writing to the contract with the prepared payload + let res: TransactionReceipt; - // Prepare tx using viem and the prepared payload - const request = await walletClient.prepareTransactionRequest({ - ...prepared, - chain: walletClient.chain, - account: walletClient.account, - }); + beforeAll(async () => { + // Set up the test environment + ({ stealthClient, ERC6538Address } = await setupTestEnv()); + walletClient = await setupTestWallet(); + account = walletClient.account?.address; + if (!account) throw new Error('No account found'); + chain = walletClient.chain; + if (!chain) throw new Error('No chain found'); - const hash = await walletClient.sendTransaction({ - ...request, - chain: walletClient.chain, - }); + prepareArgs = { + account, + ERC6538Address, + schemeId, + stealthMetaAddress: stealthMetaAddressToRegister + } satisfies PrepareRegisterKeysParams; + const prepared = await stealthClient.prepareRegisterKeys(prepareArgs); + + // Prepare tx using viem and the prepared payload + const request = await walletClient.prepareTransactionRequest({ + ...prepared, + chain, + account + }); + + const hash = await walletClient.sendTransaction({ + ...request, + chain, + account + }); - const res = await walletClient.waitForTransactionReceipt({ hash }); + res = await walletClient.waitForTransactionReceipt({ hash }); + }); + test('should throw PrepareError when given invalid contract address', () => { + const invalidERC6538Address = '0xinvalid'; + expect( + stealthClient.prepareRegisterKeys({ + ...prepareArgs, + ERC6538Address: invalidERC6538Address + }) + ).rejects.toBeInstanceOf(PrepareError); + }); test('should successfully register a stealth meta-address using the prepare payload', () => { expect(res.status).toEqual('success'); diff --git a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.ts b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.ts index 8ff26f4c..b0d261a5 100644 --- a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.ts +++ b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.ts @@ -1,11 +1,11 @@ +import { encodeFunctionData } from 'viem'; +import { ERC6538RegistryAbi } from '../..'; +import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; +import { PrepareError } from '../types'; import type { PrepareRegisterKeysParams, - PrepareRegisterKeysReturnType, + PrepareRegisterKeysReturnType } from './types'; -import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; -import { ERC6538RegistryAbi } from '../..'; -import { encodeFunctionData } from 'viem'; -import { PrepareError } from '../types'; /** * Prepares the payload for registering keys (setting the stealth meta-address) by simulating the contract call. @@ -27,7 +27,7 @@ async function prepareRegisterKeys({ schemeId, stealthMetaAddress, account, - clientParams, + clientParams }: PrepareRegisterKeysParams): Promise { const publicClient = handleViemPublicClient(clientParams); const args: [bigint, `0x${string}`] = [BigInt(schemeId), stealthMetaAddress]; @@ -35,7 +35,7 @@ async function prepareRegisterKeys({ const data = encodeFunctionData({ abi: ERC6538RegistryAbi, functionName: 'registerKeys', - args, + args }); // Simulate the contract call @@ -45,7 +45,7 @@ async function prepareRegisterKeys({ address: ERC6538Address, abi: ERC6538RegistryAbi, functionName: 'registerKeys', - args, + args }); } catch (error) { throw new PrepareError(`Failed to prepare contract call: ${error}`); @@ -54,7 +54,7 @@ async function prepareRegisterKeys({ return { to: ERC6538Address, account, - data, + data }; } diff --git a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts index 986f1549..9f5dd34a 100644 --- a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts +++ b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts @@ -1,97 +1,132 @@ -import { describe, test, expect } from 'bun:test'; -import setupTestEnv from '../../helpers/test/setupTestEnv'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; +import { beforeAll, describe, expect, test } from 'bun:test'; +import type { Address, TransactionReceipt } from 'viem'; import { ERC6538RegistryAbi, VALID_SCHEME_ID, - parseStealthMetaAddressURI, + parseStealthMetaAddressURI } from '../../..'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { StealthActions } from '../../stealthClient/types'; +import { PrepareError } from '../types'; import type { RegisterKeysOnBehalfArgs } from './types'; -describe('prepareRegisterKeysOnBehalf', async () => { - const { stealthClient, ERC6538Address, chainId } = await setupTestEnv(); - const walletClient = await setupTestWallet(); - const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; - const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); - const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ - stealthMetaAddressURI, - schemeId, - }); - const account = walletClient.account?.address!; - - const generateSignature = async () => { - // Get the registrant's current nonce for the signature - const nonce = await walletClient.readContract({ - address: ERC6538Address, - abi: ERC6538RegistryAbi, - functionName: 'nonceOf', - args: [account], +describe('prepareRegisterKeysOnBehalf', () => { + let stealthClient: StealthActions; + let account: Address | undefined; + let args: RegisterKeysOnBehalfArgs; + + // Transaction receipt for writing to the contract with the prepared payload + let res: TransactionReceipt; + + beforeAll(async () => { + const { + stealthClient: client, + ERC6538Address, + chainId + } = await setupTestEnv(); + stealthClient = client; + const walletClient = await setupTestWallet(); + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); + const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ + stealthMetaAddressURI, + schemeId }); + account = walletClient.account?.address; + if (!account) throw new Error('No account found'); + const chain = walletClient.chain; + if (!chain) throw new Error('No chain found'); - // Prepare the signature domain - const domain = { - name: 'ERC6538Registry', - version: '1.0', - chainId, - verifyingContract: ERC6538Address, - } as const; - - // Taken from the ERC6538Registry contract - const primaryType = 'Erc6538RegistryEntry'; - - // Prepare the signature types - const types = { - [primaryType]: [ - { name: 'schemeId', type: 'uint256' }, - { name: 'stealthMetaAddress', type: 'bytes' }, - { name: 'nonce', type: 'uint256' }, - ], - } as const; - - const message = { - schemeId: BigInt(schemeId), - stealthMetaAddress: stealthMetaAddressToRegister, - nonce, + const generateSignature = async (account: Address) => { + // Get the registrant's current nonce for the signature + const nonce = await walletClient.readContract({ + address: ERC6538Address, + abi: ERC6538RegistryAbi, + functionName: 'nonceOf', + args: [account] + }); + + // Prepare the signature domain + const domain = { + name: 'ERC6538Registry', + version: '1.0', + chainId, + verifyingContract: ERC6538Address + } as const; + + // Taken from the ERC6538Registry contract + const primaryType = 'Erc6538RegistryEntry'; + + // Prepare the signature types + const types = { + [primaryType]: [ + { name: 'schemeId', type: 'uint256' }, + { name: 'stealthMetaAddress', type: 'bytes' }, + { name: 'nonce', type: 'uint256' } + ] + } as const; + + const message = { + schemeId: BigInt(schemeId), + stealthMetaAddress: stealthMetaAddressToRegister, + nonce + }; + + const signature = await walletClient.signTypedData({ + account, + primaryType, + domain, + types, + message + }); + + return signature; }; - const signature = await walletClient.signTypedData({ - account: walletClient.account!, - primaryType, - domain, - types, - message, + args = { + registrant: account, + schemeId, + stealthMetaAddress: stealthMetaAddressToRegister, + signature: await generateSignature(account) + } satisfies RegisterKeysOnBehalfArgs; + + const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ + account, + ERC6538Address, + args }); - return signature; - }; + // Prepare tx using viem and the prepared payload + const request = await walletClient.prepareTransactionRequest({ + ...prepared, + chain, + account + }); - const args: RegisterKeysOnBehalfArgs = { - registrant: account, - schemeId, - stealthMetaAddress: stealthMetaAddressToRegister, - signature: await generateSignature(), - }; + const hash = await walletClient.sendTransaction({ + ...request, + chain, + account + }); - const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ - account, - ERC6538Address, - args, + res = await walletClient.waitForTransactionReceipt({ hash }); }); - // Prepare tx using viem and the prepared payload - const request = await walletClient.prepareTransactionRequest({ - ...prepared, - chain: walletClient.chain, - account: walletClient.account, - }); + test('should throw PrepareError when given invalid contract address', () => { + if (!account) throw new Error('No account found'); - const hash = await walletClient.sendTransaction({ - ...request, - chain: walletClient.chain, - }); + const invalidERC6538Address = '0xinvalid'; - const res = await walletClient.waitForTransactionReceipt({ hash }); + expect( + stealthClient.prepareRegisterKeysOnBehalf({ + account, + ERC6538Address: invalidERC6538Address, + args + }) + ).rejects.toBeInstanceOf(PrepareError); + }); test('should successfully register a stealth meta-address on behalf using the prepare payload', () => { expect(res.status).toEqual('success'); diff --git a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.ts b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.ts index 4e34d763..210f95db 100644 --- a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.ts +++ b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.ts @@ -1,17 +1,17 @@ +import { encodeFunctionData } from 'viem'; +import { ERC6538RegistryAbi } from '../..'; +import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; +import { PrepareError } from '../types'; import type { PrepareRegisterKeysOnBehalfParams, - PrepareRegisterKeysOnBehalfReturnType, + PrepareRegisterKeysOnBehalfReturnType } from './types'; -import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; -import { ERC6538RegistryAbi } from '../..'; -import { encodeFunctionData } from 'viem'; -import { PrepareError } from '../types'; async function prepareRegisterKeysOnBehalf({ ERC6538Address, args, account, - clientParams, + clientParams }: PrepareRegisterKeysOnBehalfParams): Promise { const publicClient = handleViemPublicClient(clientParams); const { registrant, schemeId, stealthMetaAddress, signature } = args; @@ -19,13 +19,13 @@ async function prepareRegisterKeysOnBehalf({ registrant, BigInt(schemeId), signature, - stealthMetaAddress, + stealthMetaAddress ]; const data = encodeFunctionData({ abi: ERC6538RegistryAbi, functionName: 'registerKeysOnBehalf', - args: writeArgs, + args: writeArgs }); try { @@ -34,13 +34,13 @@ async function prepareRegisterKeysOnBehalf({ address: ERC6538Address, abi: ERC6538RegistryAbi, functionName: 'registerKeysOnBehalf', - args: writeArgs, + args: writeArgs }); return { to: ERC6538Address, account, - data, + data }; } catch (error) { throw new PrepareError(`Failed to prepare contract call: ${error}`); diff --git a/src/lib/actions/types.ts b/src/lib/actions/types.ts index ec7f09b2..4ae923b9 100644 --- a/src/lib/actions/types.ts +++ b/src/lib/actions/types.ts @@ -14,7 +14,7 @@ export type PreparePayload = { }; export class PrepareError extends Error { - constructor(message: string = 'error preparing transaction payload') { + constructor(message = 'error preparing transaction payload') { super(message); this.name = 'PrepareError'; Object.setPrototypeOf(this, PrepareError.prototype); diff --git a/src/lib/actions/watchAnnouncementsForUser/types.ts b/src/lib/actions/watchAnnouncementsForUser/types.ts index cfb86c25..eba900a6 100644 --- a/src/lib/actions/watchAnnouncementsForUser/types.ts +++ b/src/lib/actions/watchAnnouncementsForUser/types.ts @@ -1,14 +1,14 @@ -import type { EthAddress } from '../../..'; -import type { - AnnouncementArgs, - AnnouncementLog, -} from '../getAnnouncements/types'; -import type { GetAnnouncementsForUserParams } from '..'; import type { GetPollOptions, Transport, - WatchContractEventReturnType, + WatchContractEventReturnType } from 'viem'; +import type { GetAnnouncementsForUserParams } from '..'; +import type { EthAddress } from '../../..'; +import type { + AnnouncementArgs, + AnnouncementLog +} from '../getAnnouncements/types'; export type WatchAnnouncementsForUserPollingOptions = GetPollOptions; diff --git a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts index 90108f84..b542daec 100644 --- a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts +++ b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts @@ -1,14 +1,19 @@ -import { describe, test, expect, afterAll, beforeAll } from 'bun:test'; -import setupTestEnv from '../../helpers/test/setupTestEnv'; +import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; +import type { Address } from 'viem'; import { - type AnnouncementArgs, type AnnouncementLog, ERC5564AnnouncerAbi, VALID_SCHEME_ID, - generateStealthAddress, + generateStealthAddress } from '../../..'; -import setupTestWallet from '../../helpers/test/setupTestWallet'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; + +const NUM_ANNOUNCEMENTS = 3; +const WATCH_POLLING_INTERVAL = 1000; type WriteAnnounceArgs = { schemeId: bigint; @@ -17,48 +22,107 @@ type WriteAnnounceArgs = { viewTag: `0x${string}`; }; -describe('watchAnnouncementsForUser', async () => { - const { stealthClient, ERC5564Address } = await setupTestEnv(); - const walletClient = await setupTestWallet(); +const announce = async ({ + walletClient, + ERC5564Address, + args +}: { + walletClient: SuperWalletClient; + ERC5564Address: Address; + args: WriteAnnounceArgs; +}) => { + if (!walletClient.account) throw new Error('No account found'); + + // Write to the announcement contract + const hash = await walletClient.writeContract({ + address: ERC5564Address, + functionName: 'announce', + args: [ + args.schemeId, + args.stealthAddress, + args.ephemeralPublicKey, + args.viewTag + ], + abi: ERC5564AnnouncerAbi, + chain: walletClient.chain, + account: walletClient.account + }); + + // Wait for the transaction receipt + await walletClient.waitForTransactionReceipt({ + hash + }); + + return hash; +}; + +// Delay to wait for the announcements to be watched in accordance with the polling interval +const delay = async () => + await new Promise(resolve => setTimeout(resolve, WATCH_POLLING_INTERVAL)); + +describe('watchAnnouncementsForUser', () => { + let stealthClient: StealthActions; + let walletClient: SuperWalletClient; + let ERC5564Address: Address; + + // Set up keys const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + const schemeIdBigInt = BigInt(schemeId); const { spendingPublicKey, viewingPrivateKey, stealthMetaAddressURI } = setupTestStealthKeys(schemeId); // Track the new announcements to see if they are being watched - let newAnnouncements: AnnouncementLog[] = []; + const newAnnouncements: AnnouncementLog[] = []; let unwatch: () => void; - const pollingInterval = 1000; // Override the default polling interval for testing - // Delay to wait for the announcements to be watched in accordance with the polling interval - const delay = async () => - await new Promise(resolve => setTimeout(resolve, 1000)); beforeAll(async () => { - // Set up watching announcements for a user - const watchArgs: AnnouncementArgs = { - schemeId: BigInt(VALID_SCHEME_ID.SCHEME_ID_1), - caller: walletClient.account?.address, - }; + // Set up the testing environment + ({ stealthClient, ERC5564Address } = await setupTestEnv()); + walletClient = await setupTestWallet(); + // Set up watching announcements for a user unwatch = await stealthClient.watchAnnouncementsForUser({ ERC5564Address, - args: watchArgs, + args: { + schemeId: schemeIdBigInt, + caller: walletClient.account?.address // Watch announcements for the user, who is also the caller here as an example + }, handleLogsForUser: logs => { // Add the new announcements to the list // Should be just one log for each call of the announce function - logs.forEach(log => { + for (const log of logs) { newAnnouncements.push(log); - }); + } }, spendingPublicKey, viewingPrivateKey, - pollOptions: { pollingInterval }, + pollOptions: { + pollingInterval: WATCH_POLLING_INTERVAL // Override the default polling interval for testing + } }); - // Sequentially announce 3 times - for (let i = 0; i < 3; i++) { - await announce(); + // Set up the stealth address to announce + const { stealthAddress, ephemeralPublicKey, viewTag } = + generateStealthAddress({ + stealthMetaAddressURI, + schemeId + }); + + // Sequentially announce NUM_ACCOUNCEMENT times + for (let i = 0; i < NUM_ANNOUNCEMENTS; i++) { + await announce({ + walletClient, + ERC5564Address, + args: { + schemeId: schemeIdBigInt, + stealthAddress, + ephemeralPublicKey, + viewTag + } + }); } + // Small wait to let the announcements be watched await delay(); }); @@ -66,57 +130,10 @@ describe('watchAnnouncementsForUser', async () => { unwatch(); }); - // Set up and emit announcement for specific stealth address details - const announce = async (argsOverrides?: Partial) => { - console.log('Announcing...'); - - // Set up stealth address details - const { stealthAddress, ephemeralPublicKey, viewTag } = - generateStealthAddress({ - stealthMetaAddressURI, - schemeId, - }); - - // Default arguments - const defaultArgs: WriteAnnounceArgs = { - schemeId: BigInt(schemeId), - stealthAddress, - ephemeralPublicKey, - viewTag, - }; - - // Merge defaults with overrides - const args = { ...defaultArgs, ...argsOverrides }; - - // Write to the announcement contract - const hash = await walletClient.writeContract({ - address: ERC5564Address, - functionName: 'announce', - args: [ - args.schemeId, - args.stealthAddress, - args.ephemeralPublicKey, - args.viewTag, - ], - abi: ERC5564AnnouncerAbi, - chain: walletClient.chain, - account: walletClient.account!, - }); - - console.log('Waiting for announcement transaction to be mined...'); - - const res = await walletClient.waitForTransactionReceipt({ - hash, - }); - - console.log('Announcement transaction mined:', res.transactionHash); - return hash; - }; - test('should watch announcements for a user', () => { // Check if the announcements were watched - // There should be 3 announcements because there were 3 calls to the announce function - expect(newAnnouncements.length).toEqual(3); + // There should be NUM_ACCOUNCEMENTS announcements because there were NUM_ANNOUNCEMENTS calls to the announce function + expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); }); test('should correctly not update announcements for a user if announcement does not apply to user', async () => { @@ -125,7 +142,7 @@ describe('watchAnnouncementsForUser', async () => { const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); const incrementLastCharOfHexString = (hexStr: `0x${string}`) => { @@ -142,14 +159,19 @@ describe('watchAnnouncementsForUser', async () => { // Write to the announcement contract with an inaccurate ephemeral public key await announce({ - stealthAddress, - ephemeralPublicKey: newEphemeralPublicKey, - viewTag, + walletClient, + ERC5564Address, + args: { + schemeId: BigInt(schemeId), + stealthAddress, + ephemeralPublicKey: newEphemeralPublicKey, + viewTag + } }); await delay(); // Expect no change in the number of announcements watched - expect(newAnnouncements.length).toEqual(3); + expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); }); }); diff --git a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.ts b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.ts index 8222a14b..c3b5798b 100644 --- a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.ts +++ b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.ts @@ -1,9 +1,9 @@ import type { WatchAnnouncementsForUserParams, - WatchAnnouncementsForUserReturnType, + WatchAnnouncementsForUserReturnType } from '..'; -import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; import { ERC5564AnnouncerAbi, getAnnouncementsForUser } from '../..'; +import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; /** * Watches for announcement events relevant to the user. @@ -27,7 +27,7 @@ async function watchAnnouncementsForUser({ excludeList, includeList, handleLogsForUser, - pollOptions, + pollOptions }: WatchAnnouncementsForUserParams): Promise { const publicClient = handleViemPublicClient(clientParams); @@ -43,7 +43,7 @@ async function watchAnnouncementsForUser({ ephemeralPubKey: log.args.ephemeralPubKey, metadata: log.args.metadata, schemeId: log.args.schemeId, - stealthAddress: log.args.stealthAddress, + stealthAddress: log.args.stealthAddress })); const relevantAnnouncements = await getAnnouncementsForUser({ @@ -52,13 +52,13 @@ async function watchAnnouncementsForUser({ viewingPrivateKey, clientParams: { publicClient }, excludeList, - includeList, + includeList }); handleLogsForUser(relevantAnnouncements); }, strict: true, - ...pollOptions, + ...pollOptions }); return unwatch; diff --git a/src/lib/helpers/chains.ts b/src/lib/helpers/chains.ts index a519e499..22727f40 100644 --- a/src/lib/helpers/chains.ts +++ b/src/lib/helpers/chains.ts @@ -1,5 +1,8 @@ -import { type Chain } from 'viem'; +import type { Chain } from 'viem'; import { VALID_CHAINS, type VALID_CHAIN_IDS } from './types'; -export const getChain = (id: VALID_CHAIN_IDS): Chain | undefined => - VALID_CHAINS[id]; +export const getChain = (id: VALID_CHAIN_IDS): Chain => { + const chain = VALID_CHAINS[id]; + if (!chain) throw new Error(`Invalid chainId: ${id}`); + return chain; +}; diff --git a/src/lib/helpers/test/setupTestEnv.test.ts b/src/lib/helpers/test/setupTestEnv.test.ts new file mode 100644 index 00000000..3fd7d884 --- /dev/null +++ b/src/lib/helpers/test/setupTestEnv.test.ts @@ -0,0 +1,89 @@ +import { beforeEach, describe, expect, mock, test } from 'bun:test'; +import { VALID_CHAINS } from '../types'; +import { LOCAL_ENDPOINT } from './setupTestEnv'; + +describe('setupTestEnv with different environment configurations', () => { + test('should use local node endpoint url when USE_FORK is true and RPC_URL is defined', async () => { + const exampleRpcUrl = 'http://example-rpc-url.com'; + process.env.USE_FORK = 'true'; + process.env.RPC_URL = exampleRpcUrl; + const { getRpcUrl } = await import('./setupTestEnv'); + + expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); + }); + + test('throws error when USE_FORK is true and RPC_URL is not defined', async () => { + process.env.USE_FORK = 'true'; + process.env.RPC_URL = undefined; + const { getRpcUrl } = await import('./setupTestEnv'); + + expect(getRpcUrl).toThrow('RPC_URL not defined in env'); + }); + + test('should use local node endpoint when USE_FORK is not true', async () => { + process.env.USE_FORK = 'false'; + const { getRpcUrl } = await import('./setupTestEnv'); + + expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); + }); + + test('should use the default foundry local endpoint when USE_FORK is not defined', async () => { + process.env.USE_FORK = undefined; + const { getRpcUrl } = await import('./setupTestEnv'); + + expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); + }); +}); + +describe('getValidChainId validation', () => { + const { getValidChainId } = require('./setupTestEnv'); + + test('valid chain ID returns correctly', () => { + const validChain = VALID_CHAINS[11155111]; + + expect(getValidChainId(validChain.id)).toBe(validChain.id); + }); + + test('invalid chain ID throws error', () => { + const invalidChainId = 9999; + expect(() => getValidChainId(invalidChainId)).toThrow( + `Invalid chain ID: ${invalidChainId}` + ); + }); +}); + +describe('fetchChainId', async () => { + const { fetchChainId } = await import('./setupTestEnv'); + + beforeEach(() => { + // Set the env vars, which are needed to use a fork and not default to using foundry chain id + process.env.USE_FORK = 'true'; + process.env.RPC_URL = 'http://example-rpc-url.com'; + }); + + test('successful fetch returns chain ID', async () => { + mock.module('./setupTestEnv', () => ({ + fetchJson: () => + Promise.resolve({ + result: '0x1' + }) + })); + + const chainId = await fetchChainId(); + expect(chainId).toBe(1); // '0x1' translates to 1 + }); + + test('fetch failure throws error', async () => { + mock.module('./setupTestEnv', () => ({ + fetchJson: () => Promise.reject(new Error('Network failure')) + })); + + expect(fetchChainId()).rejects.toThrow('Failed to get the chain ID'); + }); + + test('throws error when RPC_URL is not defined', async () => { + process.env.RPC_URL = undefined; + + expect(fetchChainId).toThrow('RPC_URL not defined in env'); + }); +}); diff --git a/src/lib/helpers/test/setupTestEnv.ts b/src/lib/helpers/test/setupTestEnv.ts index f7fa32f7..fac61f4c 100644 --- a/src/lib/helpers/test/setupTestEnv.ts +++ b/src/lib/helpers/test/setupTestEnv.ts @@ -1,11 +1,11 @@ +import { fromHex } from 'viem'; import { foundry } from 'viem/chains'; -import { getChain } from '../chains'; import { createStealthClient } from '../..'; import deployAllContracts from '../../../scripts'; +import { getChain } from '../chains'; import type { VALID_CHAIN_IDS } from '../types'; -import { fromHex } from 'viem'; -const LOCAL_ENDPOINT = 'http://127.0.0.1:8545'; +export const LOCAL_ENDPOINT = 'http://127.0.0.1:8545'; /** * Initializes a test environment for testing purposes. @@ -22,7 +22,7 @@ const setupTestEnv = async () => { const { erc5564ContractAddress: ERC5564Address, erc6538ContractAddress: ERC6538Address, - erc5564DeployBlock: ERC5564DeployBlock, + erc5564DeployBlock: ERC5564DeployBlock } = await deployAllContracts(); return { @@ -30,7 +30,7 @@ const setupTestEnv = async () => { ERC5564Address, ERC5564DeployBlock, ERC6538Address, - stealthClient, + stealthClient }; }; @@ -52,7 +52,8 @@ const getValidChainId = (chainId: number): VALID_CHAIN_IDS => { * @returns {string } The RPC URL. */ const getRpcUrl = (): string => { - if (process.env.USE_FORK) { + const useFork = process.env.USE_FORK === 'true'; + if (useFork) { // Check that the RPC_URL is defined if using a fork if (!process.env.RPC_URL) { throw new Error('RPC_URL not defined in env'); @@ -70,49 +71,49 @@ const getChainInfo = async () => { return { chain: getChain(validChainId), chainId: validChainId }; }; -const fetchChainId = async (): Promise => { +export const fetchChainId = async (): Promise => { // If not running fork test script, use the foundry chain ID - if (!process.env.USE_FORK) { - console.log( - `Using foundry chain ID: ${foundry.id}; make sure you ran the fork test script if that's what you wanted` - ); - return foundry.id; - } + if (!process.env.USE_FORK) return foundry.id; if (!process.env.RPC_URL) { throw new Error('RPC_URL not defined in env'); } + interface ChainIdResponse { + version: string; + id: number; + result: `0x${string}`; + } + try { - const response = await fetch(process.env.RPC_URL, { + const data = await fetchJson(process.env.RPC_URL, { method: 'POST', headers: { Accept: 'application/json', - 'Content-Type': 'application/json', + 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 1, jsonrpc: '2.0', - method: 'eth_chainId', - }), + method: 'eth_chainId' + }) }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - - interface ChainIdResponse { - version: string; - id: number; - result: `0x${string}`; - } - const data = (await response.json()) as ChainIdResponse; - return fromHex(data.result, 'number'); } catch (error) { - throw new Error(`Failed to get the chain ID`); + throw new Error('Failed to get the chain ID'); + } +}; + +const fetchJson = async (url: string, options: FetchRequestInit) => { + const response = await fetch(url, options); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); } + + return response.json() as T; }; -export { getValidChainId, getRpcUrl, getChainInfo }; +export { getValidChainId, getRpcUrl, getChainInfo, fetchJson }; export default setupTestEnv; diff --git a/src/lib/helpers/test/setupTestStealthKeys.ts b/src/lib/helpers/test/setupTestStealthKeys.ts index 60fb254c..5b5828fe 100644 --- a/src/lib/helpers/test/setupTestStealthKeys.ts +++ b/src/lib/helpers/test/setupTestStealthKeys.ts @@ -1,4 +1,7 @@ -import { VALID_SCHEME_ID, generateRandomStealthMetaAddress } from '../../..'; +import { + type VALID_SCHEME_ID, + generateRandomStealthMetaAddress +} from '../../..'; function setupTestStealthKeys(schemeId: VALID_SCHEME_ID) { const { @@ -6,7 +9,7 @@ function setupTestStealthKeys(schemeId: VALID_SCHEME_ID) { spendingPublicKey, stealthMetaAddressURI, viewingPrivateKey, - viewingPublicKey, + viewingPublicKey } = generateRandomStealthMetaAddress(); return { @@ -14,7 +17,7 @@ function setupTestStealthKeys(schemeId: VALID_SCHEME_ID) { spendingPrivateKey, viewingPublicKey, viewingPrivateKey, - stealthMetaAddressURI, + stealthMetaAddressURI }; } diff --git a/src/lib/helpers/test/setupTestWallet.test.ts b/src/lib/helpers/test/setupTestWallet.test.ts new file mode 100644 index 00000000..f53eddcc --- /dev/null +++ b/src/lib/helpers/test/setupTestWallet.test.ts @@ -0,0 +1,54 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; +import { privateKeyToAccount } from 'viem/accounts'; +import { VALID_CHAINS } from '../types'; +import { ANVIL_DEFAULT_PRIVATE_KEY } from './setupTestWallet'; + +describe('setupTestWallet', async () => { + const { setupTestWallet, getAccount } = await import('./setupTestWallet'); + + // Clean up the environment variables before each test + beforeEach(() => { + process.env.USE_FORK = undefined; + process.env.RPC_URL = undefined; + process.env.PRIVATE_KEY = undefined; + }); + + afterEach(() => { + process.env.USE_FORK = undefined; + process.env.RPC_URL = undefined; + process.env.PRIVATE_KEY = undefined; + }); + + test('uses PRIVATE_KEY environment variable when not using foundry', () => { + const ANOTHER_ANVIL_PRIVATE_KEY = + '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; + process.env.PRIVATE_KEY = ANOTHER_ANVIL_PRIVATE_KEY; + const chainId = VALID_CHAINS[11155111].id; + const account = getAccount(chainId); + expect(account.address).toBeDefined(); + expect(account.address).not.toBe( + privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY).address + ); + }); + + test('throws an error when the PRIVATE_KEY environment variable is not set', async () => { + // Set the fork env variables + process.env.USE_FORK = 'true'; + process.env.RPC_URL = 'http://example-rpc-url.com'; + + process.env.PRIVATE_KEY = undefined; + const validChainId = VALID_CHAINS[11155111].id; + + // Mock the fetchJson function to return a valid chain ID + mock.module('./setupTestEnv', () => ({ + fetchJson: () => + Promise.resolve({ + result: validChainId + }) + })); + + expect(setupTestWallet()).rejects.toThrow( + 'Missing PRIVATE_KEY environment variable; make sure to set it when using a remote RPC URL.' + ); + }); +}); diff --git a/src/lib/helpers/test/setupTestWallet.ts b/src/lib/helpers/test/setupTestWallet.ts index ea9b176c..521b6d97 100644 --- a/src/lib/helpers/test/setupTestWallet.ts +++ b/src/lib/helpers/test/setupTestWallet.ts @@ -1,11 +1,11 @@ -import { createWalletClient, http, publicActions } from 'viem'; +import { http, createWalletClient, publicActions } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { foundry } from 'viem/chains'; import { getChain as _getChain } from '../chains'; import type { SuperWalletClient } from '../types'; -import { privateKeyToAccount } from 'viem/accounts'; import { getChainInfo, getRpcUrl } from './setupTestEnv'; -import { foundry } from 'viem/chains'; -const ANVIL_DEFAULT_PRIVATE_KEY = +export const ANVIL_DEFAULT_PRIVATE_KEY = '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; /** @@ -22,7 +22,7 @@ const setupTestWallet = async (): Promise => { return createWalletClient({ account, chain, - transport: http(rpcUrl), + transport: http(rpcUrl) }).extend(publicActions); }; @@ -42,4 +42,5 @@ const getAccount = (chainId: number) => { return privateKeyToAccount(privKey); }; +export { setupTestWallet, getAccount }; export default setupTestWallet; diff --git a/src/lib/helpers/types.ts b/src/lib/helpers/types.ts index ca94ab6f..7c8f5dc5 100644 --- a/src/lib/helpers/types.ts +++ b/src/lib/helpers/types.ts @@ -4,21 +4,21 @@ import type { PublicActions, RpcSchema, Transport, - WalletActions, + WalletActions } from 'viem'; -import { sepolia, type Chain, foundry } from 'viem/chains'; +import { type Chain, foundry, sepolia } from 'viem/chains'; export type VALID_CHAIN_IDS = typeof sepolia.id | typeof foundry.id; export const VALID_CHAINS: Record = { [sepolia.id]: sepolia, - [foundry.id]: foundry, + [foundry.id]: foundry }; // A Viem WalletClient with public actions export type SuperWalletClient< transport extends Transport = Transport, chain extends Chain | undefined = Chain | undefined, - account extends Account | undefined = Account | undefined, + account extends Account | undefined = Account | undefined > = Client< transport, chain, diff --git a/src/lib/stealthClient/createStealthClient.test.ts b/src/lib/stealthClient/createStealthClient.test.ts new file mode 100644 index 00000000..c0557ab7 --- /dev/null +++ b/src/lib/stealthClient/createStealthClient.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'bun:test'; +import { http, createPublicClient } from 'viem'; +import { foundry } from 'viem/chains'; +import { LOCAL_ENDPOINT } from '../helpers/test/setupTestEnv'; +import type { VALID_CHAIN_IDS } from '../helpers/types'; +import createStealthClient, { + handleViemPublicClient +} from './createStealthClient'; +import { type ClientParams, PublicClientRequiredError } from './types'; + +describe('createStealthClient', () => { + test('throws error when invalid chain id is provided', () => { + const invalidChainId = 9999; + expect(() => + createStealthClient({ + chainId: invalidChainId as VALID_CHAIN_IDS, // Cast as valid chain to trigger error + rpcUrl: LOCAL_ENDPOINT + }) + ).toThrow(new Error('Invalid chainId: 9999')); + }); +}); + +describe('handleViemPublicClient', () => { + test('throws error when clientParams is undefined', () => { + expect(() => handleViemPublicClient(undefined)).toThrow( + new PublicClientRequiredError( + 'publicClient or chainId and rpcUrl must be provided' + ) + ); + }); + test('returns publicClient when provided', () => { + const mockPublicClient = createPublicClient({ + chain: foundry, + transport: http(LOCAL_ENDPOINT) + }); + const client = handleViemPublicClient({ publicClient: mockPublicClient }); + expect(client).toBe(mockPublicClient); + }); + + test('throws error when chainId is not set', () => { + const exampleRpcUrl = 'https://example.com'; + expect(() => + handleViemPublicClient({ + chainId: undefined as unknown as VALID_CHAIN_IDS, // Cast as valid chain to trigger error + rpcUrl: exampleRpcUrl + }) + ).toThrow( + new PublicClientRequiredError('public client could not be created.') + ); + }); + + test('throws error when clientParams does not have publicClient or both chainId and rpcUrl', () => { + // Example of incorrect structure: missing 'publicClient', 'chainId', and 'rpcUrl' + // Intentionally cast as ClientParams to trigger error + const incorrectParams = { someKey: 'someValue' } as unknown as ClientParams; + + // Attempting to call the function with incorrectParams should lead to the expected error + expect(() => handleViemPublicClient(incorrectParams)).toThrow( + new PublicClientRequiredError( + 'Either publicClient or both chainId and rpcUrl must be provided' + ) + ); + }); +}); diff --git a/src/lib/stealthClient/createStealthClient.ts b/src/lib/stealthClient/createStealthClient.ts index ef9ebb61..d95991a3 100644 --- a/src/lib/stealthClient/createStealthClient.ts +++ b/src/lib/stealthClient/createStealthClient.ts @@ -1,11 +1,11 @@ -import { createPublicClient, http, type PublicClient } from 'viem'; -import { getChain } from '../helpers/chains'; +import { http, type PublicClient, createPublicClient } from 'viem'; import { actions as stealthActions } from '../actions'; +import { getChain } from '../helpers/chains'; import { - PublicClientRequiredError, type ClientParams, + PublicClientRequiredError, type StealthClientInitParams, - type StealthClientReturnType, + type StealthClientReturnType } from './types'; /** @@ -26,56 +26,52 @@ import { */ function createStealthClient({ chainId, - rpcUrl, + rpcUrl }: StealthClientInitParams): StealthClientReturnType { const chain = getChain(chainId); - if (!chain) { - throw new Error(`Invalid chainId: ${chainId}`); - } - // Init viem client const publicClient = createPublicClient({ chain, - transport: http(rpcUrl), + transport: http(rpcUrl) }); const initializedActions: StealthClientReturnType = { getAnnouncements: params => stealthActions.getAnnouncements({ clientParams: { publicClient }, - ...params, + ...params }), getStealthMetaAddress: params => stealthActions.getStealthMetaAddress({ clientParams: { publicClient }, - ...params, + ...params }), getAnnouncementsForUser: params => stealthActions.getAnnouncementsForUser({ clientParams: { publicClient }, - ...params, + ...params }), watchAnnouncementsForUser: params => stealthActions.watchAnnouncementsForUser({ clientParams: { publicClient }, - ...params, + ...params }), prepareAnnounce: params => stealthActions.prepareAnnounce({ clientParams: { publicClient }, - ...params, + ...params }), prepareRegisterKeys: params => stealthActions.prepareRegisterKeys({ clientParams: { publicClient }, - ...params, + ...params }), prepareRegisterKeysOnBehalf: params => stealthActions.prepareRegisterKeysOnBehalf({ clientParams: { publicClient }, - ...params, - }), + ...params + }) }; return initializedActions; @@ -97,7 +93,7 @@ const handleViemPublicClient = (clientParams?: ClientParams): PublicClient => { try { return createPublicClient({ chain: getChain(clientParams.chainId), - transport: http(clientParams.rpcUrl), + transport: http(clientParams.rpcUrl) }); } catch (error) { throw new PublicClientRequiredError( diff --git a/src/lib/stealthClient/types.ts b/src/lib/stealthClient/types.ts index 54602f06..1a74a022 100644 --- a/src/lib/stealthClient/types.ts +++ b/src/lib/stealthClient/types.ts @@ -1,5 +1,4 @@ import type { PublicClient } from 'viem'; -import type { VALID_CHAIN_IDS } from '../helpers/types'; import type { GetAnnouncementsForUserParams, GetAnnouncementsParams, @@ -13,8 +12,9 @@ import type { PrepareRegisterKeysParams, PrepareRegisterKeysReturnType, WatchAnnouncementsForUserParams, - WatchAnnouncementsForUserReturnType, + WatchAnnouncementsForUserReturnType } from '../actions/'; +import type { VALID_CHAIN_IDS } from '../helpers/types'; export type ClientParams = | { @@ -37,19 +37,19 @@ export type StealthActions = { ERC5564Address, args, fromBlock, - toBlock, + toBlock }: GetAnnouncementsParams) => Promise; getStealthMetaAddress: ({ ERC6538Address, registrant, - schemeId, + schemeId }: GetStealthMetaAddressParams) => Promise; getAnnouncementsForUser: ({ announcements, spendingPublicKey, viewingPrivateKey, excludeList, - includeList, + includeList }: GetAnnouncementsForUserParams) => Promise; watchAnnouncementsForUser: ({ ERC5564Address, @@ -57,28 +57,28 @@ export type StealthActions = { handleLogsForUser, spendingPublicKey, viewingPrivateKey, - pollOptions, + pollOptions }: WatchAnnouncementsForUserParams) => Promise; prepareAnnounce: ({ account, args, - ERC5564Address, + ERC5564Address }: PrepareAnnounceParams) => Promise; prepareRegisterKeys: ({ ERC6538Address, schemeId, stealthMetaAddress, - account, + account }: PrepareRegisterKeysParams) => Promise; prepareRegisterKeysOnBehalf: ({ ERC6538Address, args, - account, + account }: PrepareRegisterKeysOnBehalfParams) => Promise; }; export class PublicClientRequiredError extends Error { - constructor(message: string = 'publicClient is required') { + constructor(message = 'publicClient is required') { super(message); this.name = 'PublicClientRequiredError'; Object.setPrototypeOf(this, PublicClientRequiredError.prototype); diff --git a/src/scripts/deployContract.ts b/src/scripts/deployContract.ts index 7aa0cbb3..3699bd02 100644 --- a/src/scripts/deployContract.ts +++ b/src/scripts/deployContract.ts @@ -1,8 +1,8 @@ -import { +import type { ERC5564AnnouncerAbi, ERC5564_CONTRACT, - ERC6538_CONTRACT, ERC6538RegistryAbi, + ERC6538_CONTRACT } from '..'; import setupTestWallet from '../lib/helpers/test/setupTestWallet'; @@ -19,7 +19,7 @@ const deployContract = async ({ address, abi, bytecode, - name, + name }: { address: ERC5564_CONTRACT | ERC6538_CONTRACT; abi: typeof ERC5564AnnouncerAbi | typeof ERC6538RegistryAbi; @@ -30,13 +30,16 @@ const deployContract = async ({ deployBlock: bigint; }> => { const walletClient = await setupTestWallet(); + if (!walletClient.account || !walletClient.chain) { + throw new Error('No account or chain found'); + } const hash = await walletClient.deployContract({ - account: walletClient.account!, - chain: walletClient.chain!, + account: walletClient.account, + chain: walletClient.chain, abi, bytecode, - gas: BigInt(1_000_000), + gas: BigInt(1_000_000) }); const { contractAddress, blockNumber } = @@ -46,8 +49,6 @@ const deployContract = async ({ throw new Error(`Failed to deploy ${name} contract`); } - console.log(`${name} contract deployed to: ${contractAddress}`); - return { address: contractAddress, deployBlock: blockNumber }; }; diff --git a/src/scripts/index.ts b/src/scripts/index.ts index 87c991c2..eaa87999 100644 --- a/src/scripts/index.ts +++ b/src/scripts/index.ts @@ -4,7 +4,7 @@ import { ERC5564_CONTRACT, ERC6538RegistryAbi, ERC6538_BYTECODE, - ERC6538_CONTRACT, + ERC6538_CONTRACT } from '..'; import deployContract from './deployContract'; @@ -14,20 +14,20 @@ const deployAllContracts = async () => { address: ERC5564_CONTRACT.SEPOLIA, abi: ERC5564AnnouncerAbi, name: 'ERC5564', - bytecode: ERC5564_BYTECODE, + bytecode: ERC5564_BYTECODE }); const { address: erc6538ContractAddress } = await deployContract({ address: ERC6538_CONTRACT.SEPOLIA, abi: ERC6538RegistryAbi, name: 'ERC6538', - bytecode: ERC6538_BYTECODE, + bytecode: ERC6538_BYTECODE }); return { erc5564ContractAddress, erc6538ContractAddress, - erc5564DeployBlock, + erc5564DeployBlock }; }; diff --git a/src/test/helpers/index.ts b/src/test/helpers/index.ts new file mode 100644 index 00000000..18f96512 --- /dev/null +++ b/src/test/helpers/index.ts @@ -0,0 +1,167 @@ +import { + http, + type Address, + type Client, + type WalletClient, + createWalletClient, + publicActions +} from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { getRpcUrl } from '../../lib/helpers/test/setupTestEnv'; +import setupTestWallet from '../../lib/helpers/test/setupTestWallet'; +import { type SuperWalletClient, VALID_CHAINS } from '../../lib/helpers/types'; +import { generateKeysFromSignature } from '../../utils/helpers'; + +// Default private key for testing; the setupTestWallet function uses the first anvil default key, so the below will be different +const ANVIL_DEFAULT_PRIVATE_KEY_2 = + '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; + +/* Gets the signature to be able to generate reproducible public/private viewing/spending keys */ +export const getSignature = async ({ + walletClient +}: { + walletClient: WalletClient; +}) => { + if (!walletClient.chain) throw new Error('Chain not found'); + if (!walletClient.account) throw new Error('Account not found'); + + const MESSAGE = `Signing message for stealth transaction on chain id: ${walletClient.chain.id}`; + const signature = await walletClient.signMessage({ + message: MESSAGE, + account: walletClient.account + }); + + return signature; +}; + +/* Generates the public/private viewing/spending keys from the signature */ +export const getKeys = async ({ + walletClient +}: { + walletClient: WalletClient; +}) => { + const signature = await getSignature({ walletClient }); + const keys = generateKeysFromSignature(signature); + return keys; +}; + +/* Sets up the sending and receiving wallet clients for testing */ +export const getWalletClients = async () => { + const sendingWalletClient = await setupTestWallet(); + + const chain = sendingWalletClient.chain; + if (!chain) throw new Error('Chain not found'); + if (!(chain.id in VALID_CHAINS)) { + throw new Error('Invalid chain'); + } + + const rpcUrl = getRpcUrl(); + + const receivingWalletClient: SuperWalletClient = createWalletClient({ + account: privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY_2), + chain, + transport: http(rpcUrl) + }).extend(publicActions); + + return { sendingWalletClient, receivingWalletClient }; +}; + +export const getAccount = (walletClient: WalletClient | Client) => { + if (!walletClient.account) throw new Error('Account not found'); + return walletClient.account; +}; + +/* Gets the wallet clients, accounts, and keys for the sending and receiving wallets */ +export const getWalletClientsAndKeys = async () => { + const { sendingWalletClient, receivingWalletClient } = + await getWalletClients(); + + const sendingAccount = getAccount(sendingWalletClient); + const receivingAccount = getAccount(receivingWalletClient); + + const receivingAccountKeys = await getKeys({ + walletClient: receivingWalletClient + }); + + return { + sendingWalletClient, + receivingWalletClient, + sendingAccount, + receivingAccount, + receivingAccountKeys + }; +}; + +/* Set up the initial balance details for the sending and receiving wallets */ +export const setupInitialBalances = async ({ + sendingWalletClient, + receivingWalletClient +}: { + sendingWalletClient: SuperWalletClient; + receivingWalletClient: SuperWalletClient; +}) => { + const sendingAccount = getAccount(sendingWalletClient); + const receivingAccount = getAccount(receivingWalletClient); + const sendingWalletStartingBalance = await sendingWalletClient.getBalance({ + address: sendingAccount.address + }); + const receivingWalletStartingBalance = await receivingWalletClient.getBalance( + { + address: receivingAccount.address + } + ); + + return { + sendingWalletStartingBalance, + receivingWalletStartingBalance + }; +}; + +/* Send ETH and wait for the transaction to be confirmed */ +export const sendEth = async ({ + sendingWalletClient, + to, + value +}: { + sendingWalletClient: SuperWalletClient; + to: Address; + value: bigint; +}) => { + const account = getAccount(sendingWalletClient); + const hash = await sendingWalletClient.sendTransaction({ + value, + to, + account, + chain: sendingWalletClient.chain + }); + + const receipt = await sendingWalletClient.waitForTransactionReceipt({ hash }); + + const gasPriceSend = receipt.effectiveGasPrice; + const gasEstimate = receipt.gasUsed * gasPriceSend; + + return { hash, gasEstimate }; +}; + +/* Get the ending balances for the sending and receiving wallets */ +export const getEndingBalances = async ({ + sendingWalletClient, + receivingWalletClient +}: { + sendingWalletClient: SuperWalletClient; + receivingWalletClient: SuperWalletClient; +}) => { + const sendingAccount = getAccount(sendingWalletClient); + const receivingAccount = getAccount(receivingWalletClient); + const sendingWalletEndingBalance = await sendingWalletClient.getBalance({ + address: sendingAccount.address + }); + const receivingWalletEndingBalance = await receivingWalletClient.getBalance({ + address: receivingAccount.address + }); + + return { + sendingWalletEndingBalance, + receivingWalletEndingBalance + }; +}; diff --git a/src/test/sendReceive.test.ts b/src/test/sendReceive.test.ts new file mode 100644 index 00000000..5b00bbe0 --- /dev/null +++ b/src/test/sendReceive.test.ts @@ -0,0 +1,106 @@ +import { beforeAll, describe, expect, test } from 'bun:test'; +import { http, createWalletClient, parseEther, publicActions } from 'viem'; +import { privateKeyToAccount } from 'viem/accounts'; +import { getRpcUrl } from '../lib/helpers/test/setupTestEnv'; +import { + VALID_SCHEME_ID, + computeStealthKey, + generateStealthAddress +} from '../utils'; +import { generateStealthMetaAddressFromSignature } from '../utils/helpers'; +import { + getEndingBalances, + getSignature, + getWalletClientsAndKeys, + sendEth, + setupInitialBalances +} from './helpers'; + +/** + * @description Tests for sending and receiving a payment + * Sending means generating a stealth address using the sdk, then sending funds to that stealth address; the sending account is the account that sends the funds + * Withdrawing means computing the stealth address private key using the sdk, then withdrawing funds from the stealth address; the receiving account is the account that receives the funds + * + * The tests need to be run using foundry because the tests utilize the default anvil private keys + */ + +describe('Send and receive payment', () => { + const sendAmount = parseEther('1.0'); + const withdrawBuffer = parseEther('0.01'); + const withdrawAmount = sendAmount - withdrawBuffer; + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + + let gasEstimateSend: bigint; + let sendingWalletBalanceChange: bigint; + let receivingWalletBalanceChange: bigint; + + beforeAll(async () => { + const { + receivingAccount, + receivingAccountKeys, + receivingWalletClient, + sendingWalletClient + } = await getWalletClientsAndKeys(); + + const { stealthAddress, ephemeralPublicKey } = generateStealthAddress({ + stealthMetaAddressURI: generateStealthMetaAddressFromSignature( + await getSignature({ walletClient: receivingWalletClient }) + ), + schemeId + }); + + const { sendingWalletStartingBalance, receivingWalletStartingBalance } = + await setupInitialBalances({ + receivingWalletClient, + sendingWalletClient + }); + + // Send ETH to the stealth address + const { gasEstimate } = await sendEth({ + sendingWalletClient, + to: stealthAddress, + value: sendAmount + }); + + gasEstimateSend = gasEstimate; + + // Compute the stealth key to be able to withdraw the funds from the stealth address to the receiving account + const stealthAddressPrivateKey = computeStealthKey({ + schemeId, + ephemeralPublicKey, + spendingPrivateKey: receivingAccountKeys.spendingPrivateKey, + viewingPrivateKey: receivingAccountKeys.viewingPrivateKey + }); + + // Set up a wallet client using the stealth address private key + const stealthAddressWalletClient = createWalletClient({ + account: privateKeyToAccount(stealthAddressPrivateKey), + chain: sendingWalletClient.chain, + transport: http(getRpcUrl()) + }).extend(publicActions); + + // Withdraw from the stealth address to the receiving account + await sendEth({ + sendingWalletClient: stealthAddressWalletClient, + to: receivingAccount.address, + value: withdrawAmount + }); + + const { sendingWalletEndingBalance, receivingWalletEndingBalance } = + await getEndingBalances({ + sendingWalletClient, + receivingWalletClient + }); + + // Get the balance changes for the sending and receiving wallets + sendingWalletBalanceChange = + sendingWalletEndingBalance - sendingWalletStartingBalance; + receivingWalletBalanceChange = + receivingWalletEndingBalance - receivingWalletStartingBalance; + }); + + test('Can successfully send a stealth transaction from an account and withdraw from a different account by computing the stealth key', () => { + expect(sendingWalletBalanceChange).toBe(-(sendAmount + gasEstimateSend)); + expect(receivingWalletBalanceChange).toBe(withdrawAmount); + }); +}); diff --git a/src/utils/crypto/checkStealthAddress.ts b/src/utils/crypto/checkStealthAddress.ts index 30586a35..20c28adc 100644 --- a/src/utils/crypto/checkStealthAddress.ts +++ b/src/utils/crypto/checkStealthAddress.ts @@ -1,13 +1,13 @@ import { getSharedSecret } from '@noble/secp256k1'; -import type { ICheckStealthAddressParams } from './types'; +import { hexToBytes } from 'viem'; import { getHashedSharedSecret, getStealthPublicKey, getViewTag, handleSchemeId, - publicKeyToAddress, + publicKeyToAddress } from '.'; -import { hexToBytes } from 'viem'; +import type { ICheckStealthAddressParams } from './types'; /** * @description Checks if a given announcement is intended for the user. @@ -27,7 +27,7 @@ function checkStealthAddress({ spendingPublicKey, userStealthAddress, viewingPrivateKey, - viewTag, + viewTag }: ICheckStealthAddressParams) { handleSchemeId(schemeId); @@ -48,13 +48,13 @@ function checkStealthAddress({ const stealthPublicKey = getStealthPublicKey({ spendingPublicKey: hexToBytes(spendingPublicKey), hashedSharedSecret, - schemeId, + schemeId }); // Derive the stealth address from the stealth public key const stealthAddress = publicKeyToAddress({ publicKey: stealthPublicKey, - schemeId, + schemeId }); // Compare derived stealth address with the user's stealth address diff --git a/src/utils/crypto/computeStealthKey.ts b/src/utils/crypto/computeStealthKey.ts index e215ecb0..2aec16d3 100644 --- a/src/utils/crypto/computeStealthKey.ts +++ b/src/utils/crypto/computeStealthKey.ts @@ -1,16 +1,16 @@ -import { getSharedSecret, CURVE } from '@noble/secp256k1'; -import type { HexString, IComputeStealthKeyParams } from './types'; +import { CURVE, getSharedSecret } from '@noble/secp256k1'; +import { hexToBytes } from 'viem'; import { getHashedSharedSecret, - handleSchemeId, + handleSchemeId } from './generateStealthAddress'; -import { hexToBytes } from 'viem'; +import type { HexString, IComputeStealthKeyParams } from './types'; function computeStealthKey({ ephemeralPublicKey, schemeId, spendingPrivateKey, - viewingPrivateKey, + viewingPrivateKey }: IComputeStealthKeyParams): HexString { handleSchemeId(schemeId); @@ -27,11 +27,12 @@ function computeStealthKey({ // Compute the stealth private key by summing the spending private key and the hashed shared secret. const stealthPrivateKeyBigInt = addPriv({ a: spendingPrivateKeyBigInt, - b: hashedSecretBigInt, + b: hashedSecretBigInt }); - const stealthPrivateKeyHex = - `0x${stealthPrivateKeyBigInt.toString(16).padStart(64, '0')}` as HexString; + const stealthPrivateKeyHex = `0x${stealthPrivateKeyBigInt + .toString(16) + .padStart(64, '0')}` as HexString; return stealthPrivateKeyHex; } diff --git a/src/utils/crypto/generateStealthAddress.ts b/src/utils/crypto/generateStealthAddress.ts index 6b0ea3ae..75792f8f 100644 --- a/src/utils/crypto/generateStealthAddress.ts +++ b/src/utils/crypto/generateStealthAddress.ts @@ -1,23 +1,23 @@ import { + ProjectivePoint, getPublicKey as getPublicKeySecp256k1, getSharedSecret, - ProjectivePoint, - utils, + utils } from '@noble/secp256k1'; import { + bytesToHex, + hexToBytes, + keccak256, + publicKeyToAddress as publicKeyToAddressViem +} from 'viem/utils'; +import { + type EthAddress, type GenerateStealthAddressReturnType, type Hex, type HexString, - VALID_SCHEME_ID, - type EthAddress, type IGenerateStealthAddressParams, + VALID_SCHEME_ID } from './types'; -import { - publicKeyToAddress as publicKeyToAddressViem, - keccak256, - bytesToHex, - hexToBytes, -} from 'viem/utils'; /** * @description Generates a stealth address from a given stealth meta-address. @@ -38,11 +38,11 @@ import { function generateStealthAddress({ stealthMetaAddressURI, schemeId = VALID_SCHEME_ID.SCHEME_ID_1, - ephemeralPrivateKey, + ephemeralPrivateKey }: IGenerateStealthAddressParams): GenerateStealthAddressReturnType { const stealthMetaAddress = parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }); if (!validateStealthMetaAddress({ stealthMetaAddress, schemeId })) { @@ -51,26 +51,26 @@ function generateStealthAddress({ const ephemeralPrivateKeyToUse = generatePrivateKey({ ephemeralPrivateKey, - schemeId, + schemeId }); // Compute the ephemeral public key from the ephemeral private key const ephemeralPublicKey = getPublicKey({ privateKey: ephemeralPrivateKeyToUse, compressed: true, - schemeId, + schemeId }); const { spendingPublicKey, viewingPublicKey } = parseKeysFromStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }); const sharedSecret = computeSharedSecret({ ephemeralPrivateKey: ephemeralPrivateKeyToUse, recipientViewingPublicKey: viewingPublicKey, - schemeId, + schemeId }); const hashedSharedSecret = getHashedSharedSecret({ sharedSecret, schemeId }); @@ -80,18 +80,18 @@ function generateStealthAddress({ const stealthPublicKey = getStealthPublicKey({ spendingPublicKey, hashedSharedSecret, - schemeId, + schemeId }); const stealthAddress = publicKeyToAddress({ publicKey: stealthPublicKey, - schemeId, + schemeId }); return { stealthAddress, ephemeralPublicKey: bytesToHex(ephemeralPublicKey), - viewTag, + viewTag }; } @@ -100,23 +100,27 @@ function generateStealthAddress({ * Validates the structure and format of the stealth meta-address. * * @param {object} params - Parameters for parsing the stealth meta-address URI: - * - stealthMetaAddressURI: The URI containing the stealth meta-address. + * - stealthMetaAddressURI: The URI containing the stealth meta-address, or alternatively, the stealth meta-address itself. * - schemeId: The scheme identifier. * @returns {HexString} The extracted stealth meta-address. */ function parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }: { - stealthMetaAddressURI: string; + stealthMetaAddressURI: string | HexString; schemeId: VALID_SCHEME_ID; }): HexString { handleSchemeId(schemeId); + // If the stealth meta-address is provided directly + if (stealthMetaAddressURI.startsWith('0x')) + return stealthMetaAddressURI as HexString; + const parts = stealthMetaAddressURI.split(':'); if (parts.length !== 3 || parts[0] !== 'st') { - throw new Error('Invalid stealth meta-address format'); + throw new Error('Invalid stealth meta-address URI format'); } return parts[2] as HexString; @@ -132,13 +136,14 @@ function parseStealthMetaAddressURI({ */ function validateStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }: { stealthMetaAddress: string; schemeId: VALID_SCHEME_ID; }): boolean { handleSchemeId(schemeId); + // Remove the '0x' prefix if present const cleanedStealthMetaAddress = stealthMetaAddress.startsWith('0x') ? stealthMetaAddress.substring(2) : stealthMetaAddress; @@ -155,18 +160,18 @@ function validateStealthMetaAddress({ // Validate the format of each public key const singlePublicKeyHexLength = 66; // Length for compressed keys - const spendingPublicKeyHex = cleanedStealthMetaAddress.slice( + const spendingPublicKey = cleanedStealthMetaAddress.slice( 0, singlePublicKeyHexLength - ) as HexString; - const viewingPublicKeyHex = + ); + const viewingPublicKey = cleanedStealthMetaAddress.length === 132 - ? (cleanedStealthMetaAddress.slice(singlePublicKeyHexLength) as HexString) - : (spendingPublicKeyHex as HexString); // Use the same key for spending and viewing if only one is provided + ? cleanedStealthMetaAddress.slice(singlePublicKeyHexLength) + : spendingPublicKey; // Use the same key for spending and viewing if only one is provided if ( - !isValidCompressedPublicKey(spendingPublicKeyHex) || - !isValidCompressedPublicKey(viewingPublicKeyHex) + !isValidCompressedPublicKey(spendingPublicKey) || + !isValidCompressedPublicKey(viewingPublicKey) ) { return false; } @@ -174,10 +179,17 @@ function validateStealthMetaAddress({ return true; } -function isValidCompressedPublicKey(publicKeyHex: HexString): boolean { +/** + * @description Validates a compressed public key. + * A compressed public key is a 66-character hexadecimal string that starts with '02' or '03'. + * The function takes a non '0x' prefixed public key as input. + * @param publicKey + * @returns + */ +function isValidCompressedPublicKey(publicKey: string): boolean { return ( - (publicKeyHex.startsWith('02') || publicKeyHex.startsWith('03')) && - publicKeyHex.length === 66 + (publicKey.startsWith('02') || publicKey.startsWith('03')) && + publicKey.length === 66 ); } @@ -185,7 +197,7 @@ function isValidCompressedPublicKey(publicKeyHex: HexString): boolean { * @description Extracts and validates the spending and viewing public keys from a stealth meta-address. * * @param {object} params - Parameters for extracting keys from a stealth meta-address: - * - stealthMetaAddress: The stealth meta-address. + * - stealthMetaAddress: The stealth meta-address as a hex string (prefixed with `0x`). * - schemeId: The scheme identifier. * @returns {object} An object containing: * - spendingPublicKey: The extracted spending public key. @@ -193,29 +205,29 @@ function isValidCompressedPublicKey(publicKeyHex: HexString): boolean { */ function parseKeysFromStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }: { stealthMetaAddress: HexString; schemeId: VALID_SCHEME_ID; }) { handleSchemeId(schemeId); + // Remove the '0x' prefix const cleanedStealthMetaAddress = stealthMetaAddress.slice(2); - const singlePublicKeyHexLength = 66; // Length for compressed keys - const spendingPublicKeyHex = cleanedStealthMetaAddress.slice( + const singlePublicKeyLength = 66; // Length for compressed keys + const spendingPublicKey = cleanedStealthMetaAddress.slice( 0, - singlePublicKeyHexLength + singlePublicKeyLength ); - const viewingPublicKeyHex = + const viewingPublicKey = cleanedStealthMetaAddress.length === 132 - ? cleanedStealthMetaAddress.slice(singlePublicKeyHexLength) - : spendingPublicKeyHex; // Use the same key for spending and viewing if only one is provided + ? cleanedStealthMetaAddress.slice(singlePublicKeyLength) + : spendingPublicKey; // Use the same key for spending and viewing if only one is provided return { spendingPublicKey: - ProjectivePoint.fromHex(spendingPublicKeyHex).toRawBytes(true), // Compressed - viewingPublicKey: - ProjectivePoint.fromHex(viewingPublicKeyHex).toRawBytes(true), // Compressed + ProjectivePoint.fromHex(spendingPublicKey).toRawBytes(true), // Compressed + viewingPublicKey: ProjectivePoint.fromHex(viewingPublicKey).toRawBytes(true) // Compressed }; } @@ -231,7 +243,7 @@ function parseKeysFromStealthMetaAddress({ function computeSharedSecret({ ephemeralPrivateKey, recipientViewingPublicKey, - schemeId, + schemeId }: { ephemeralPrivateKey: Uint8Array; recipientViewingPublicKey: Uint8Array; @@ -253,7 +265,7 @@ function computeSharedSecret({ */ function getHashedSharedSecret({ sharedSecret, - schemeId, + schemeId }: { sharedSecret: Hex; schemeId: VALID_SCHEME_ID; @@ -282,7 +294,7 @@ function handleSchemeId(schemeId: VALID_SCHEME_ID) { */ function generatePrivateKey({ ephemeralPrivateKey, - schemeId, + schemeId }: { ephemeralPrivateKey?: Uint8Array; schemeId: VALID_SCHEME_ID; @@ -311,7 +323,7 @@ function generatePrivateKey({ function getPublicKey({ privateKey, compressed, - schemeId, + schemeId }: { privateKey: Uint8Array; compressed: boolean; @@ -331,7 +343,7 @@ function getPublicKey({ */ function getViewTag({ hashedSharedSecret, - schemeId, + schemeId }: { hashedSharedSecret: Hex; schemeId: VALID_SCHEME_ID; @@ -354,7 +366,7 @@ function getViewTag({ function getStealthPublicKey({ spendingPublicKey, hashedSharedSecret, - schemeId, + schemeId }: { spendingPublicKey: Uint8Array; hashedSharedSecret: HexString; @@ -379,7 +391,7 @@ function getStealthPublicKey({ */ function publicKeyToAddress({ publicKey, - schemeId, + schemeId }: { publicKey: Hex; schemeId: VALID_SCHEME_ID; @@ -401,5 +413,6 @@ export { parseKeysFromStealthMetaAddress, parseStealthMetaAddressURI, publicKeyToAddress, + isValidCompressedPublicKey }; export default generateStealthAddress; diff --git a/src/utils/crypto/index.ts b/src/utils/crypto/index.ts index 655c52bc..ed229d8c 100644 --- a/src/utils/crypto/index.ts +++ b/src/utils/crypto/index.ts @@ -7,7 +7,7 @@ export { handleSchemeId, parseKeysFromStealthMetaAddress, parseStealthMetaAddressURI, - publicKeyToAddress, + publicKeyToAddress } from './generateStealthAddress'; export { default as computeStealthKey } from './computeStealthKey'; @@ -20,5 +20,5 @@ export { type ICheckStealthAddressParams, type IComputeStealthKeyParams, type IGenerateStealthAddressParams, - VALID_SCHEME_ID, + VALID_SCHEME_ID } from './types'; diff --git a/src/utils/crypto/test/checkStealthAddress.test.ts b/src/utils/crypto/test/checkStealthAddress.test.ts index ecfb4f2d..420a9a70 100644 --- a/src/utils/crypto/test/checkStealthAddress.test.ts +++ b/src/utils/crypto/test/checkStealthAddress.test.ts @@ -1,12 +1,12 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; +import { bytesToHex } from 'viem'; import { VALID_SCHEME_ID, checkStealthAddress, generateStealthAddress, parseKeysFromStealthMetaAddress, - parseStealthMetaAddressURI, + parseStealthMetaAddressURI } from '..'; -import { bytesToHex } from 'viem'; describe('checkStealthAddress', () => { const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; @@ -14,7 +14,7 @@ describe('checkStealthAddress', () => { 'st:eth:0x02f1f006a160b934c1d71479ce7d57f1c4ec10018230e35ca10ab65db68e8f037b0305d4725c7784262a38af11a9aef490b1307b82b17866f08d66c38db04c946ab1'; const stealthMetaAddress = parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }); const viewingPrivateKey = '0x2f8fcb2d1e06f52695e06a792b6d59c80a81ad70fc11b03b5236eed5cff09670'; @@ -22,12 +22,12 @@ describe('checkStealthAddress', () => { const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); const { spendingPublicKey } = parseKeysFromStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }); test('successfully identifies an announcement for the user', () => { @@ -37,7 +37,7 @@ describe('checkStealthAddress', () => { spendingPublicKey: bytesToHex(spendingPublicKey), userStealthAddress: stealthAddress, viewingPrivateKey, - viewTag, + viewTag }); expect(isForUser).toBe(true); @@ -51,7 +51,7 @@ describe('checkStealthAddress', () => { spendingPublicKey: bytesToHex(spendingPublicKey), userStealthAddress: stealthAddress, viewingPrivateKey, - viewTag: mismatchedViewTag, + viewTag: mismatchedViewTag }); expect(isForUser).toBe(false); @@ -65,7 +65,7 @@ describe('checkStealthAddress', () => { spendingPublicKey: bytesToHex(spendingPublicKey), userStealthAddress: differentStealthAddress, viewingPrivateKey, - viewTag, + viewTag }); expect(isForUser).toBe(false); diff --git a/src/utils/crypto/test/computeStealthKey.test.ts b/src/utils/crypto/test/computeStealthKey.test.ts index f78dd141..e6557a0c 100644 --- a/src/utils/crypto/test/computeStealthKey.test.ts +++ b/src/utils/crypto/test/computeStealthKey.test.ts @@ -1,13 +1,13 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; +import { CURVE, getPublicKey, utils } from '@noble/secp256k1'; +import { bytesToHex, hexToBytes } from 'viem'; +import { publicKeyToAddress } from 'viem/accounts'; import { + VALID_SCHEME_ID, computeStealthKey, generatePrivateKey, - generateStealthAddress, - VALID_SCHEME_ID, + generateStealthAddress } from '..'; -import { publicKeyToAddress } from 'viem/accounts'; -import { getPublicKey, CURVE, utils } from '@noble/secp256k1'; -import { bytesToHex, hexToBytes } from 'viem'; import { addPriv } from '../computeStealthKey'; const formatPrivKey = (privateKey: bigint) => @@ -28,14 +28,14 @@ describe('generateStealthAddress and computeStealthKey', () => { const generatedStealthAddressResult = generateStealthAddress({ ephemeralPrivateKey, schemeId, - stealthMetaAddressURI, + stealthMetaAddressURI }); const computedStealthPrivateKeyHex = computeStealthKey({ ephemeralPublicKey: generatedStealthAddressResult.ephemeralPublicKey, schemeId, spendingPrivateKey, - viewingPrivateKey, + viewingPrivateKey }); const computedStealthPublicKey = getPublicKey( @@ -96,7 +96,7 @@ describe('adding private keys', () => { const sumWithModulo = addPriv({ a: privateKey1, - b: privateKey2, + b: privateKey2 }); // The sum is within the curve's order, which is valid expect(sumWithModulo).toBeLessThanOrEqual(curveOrder); diff --git a/src/utils/crypto/test/generateStealthAddress.test.ts b/src/utils/crypto/test/generateStealthAddress.test.ts index 8f81286d..c793853e 100644 --- a/src/utils/crypto/test/generateStealthAddress.test.ts +++ b/src/utils/crypto/test/generateStealthAddress.test.ts @@ -1,12 +1,13 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, expect, test } from 'bun:test'; import { bytesToHex } from 'viem'; import { generatePrivateKey, generateStealthAddress, getViewTag, parseKeysFromStealthMetaAddress, + parseStealthMetaAddressURI } from '..'; -import { VALID_SCHEME_ID, type HexString } from '../types'; +import { type HexString, VALID_SCHEME_ID } from '../types'; describe('generateStealthAddress', () => { const validStealthMetaAddressURI = @@ -14,11 +15,31 @@ describe('generateStealthAddress', () => { const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + test('parseStealthMetaAddressURI should return the stealth meta-address', () => { + const expectedStealthMetaAddress = + '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; + // Passing the valid stealth meta-address URI and the scheme ID + const result = parseStealthMetaAddressURI({ + stealthMetaAddressURI: validStealthMetaAddressURI, + schemeId + }); + + expect(result).toBe(expectedStealthMetaAddress); + + // Passing only the stealth meta-address + const result2 = parseStealthMetaAddressURI({ + stealthMetaAddressURI: expectedStealthMetaAddress, + schemeId + }); + + expect(result2).toBe(expectedStealthMetaAddress); + }); + test('should generate a valid stealth address given a valid stealth meta-address URI', () => { // TODO compute the expected stealth address using computeStealthAddress (not yet implemented in the SDK) const result = generateStealthAddress({ stealthMetaAddressURI: validStealthMetaAddressURI, - schemeId, + schemeId }); expect(result.stealthAddress).toBeDefined(); @@ -29,19 +50,19 @@ describe('generateStealthAddress', () => { const firstPrivateKey = generatePrivateKey({ schemeId }); const secondPrivateKey = generatePrivateKey({ ephemeralPrivateKey: firstPrivateKey, - schemeId, + schemeId }); const result = generateStealthAddress({ stealthMetaAddressURI: validStealthMetaAddressURI, schemeId, - ephemeralPrivateKey: firstPrivateKey, + ephemeralPrivateKey: firstPrivateKey }); const result2 = generateStealthAddress({ stealthMetaAddressURI: validStealthMetaAddressURI, schemeId, - ephemeralPrivateKey: secondPrivateKey, + ephemeralPrivateKey: secondPrivateKey }); expect(result.stealthAddress).toBe(result2.stealthAddress); @@ -56,7 +77,7 @@ describe('generateStealthAddress', () => { const result = parseKeysFromStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }); expect(bytesToHex(result.spendingPublicKey)).toBe( @@ -75,7 +96,7 @@ describe('generateStealthAddress', () => { const result = getViewTag({ hashedSharedSecret, - schemeId, + schemeId }); expect(result).toBe(expectedViewTag); diff --git a/src/utils/crypto/types/index.ts b/src/utils/crypto/types/index.ts index 08202ee0..5b2b5d06 100644 --- a/src/utils/crypto/types/index.ts +++ b/src/utils/crypto/types/index.ts @@ -1,7 +1,7 @@ import type { Address } from 'viem'; export enum VALID_SCHEME_ID { - SCHEME_ID_1 = 1, + SCHEME_ID_1 = 1 } export type HexString = `0x${string}`; diff --git a/src/utils/helpers/generateKeysFromSignature.ts b/src/utils/helpers/generateKeysFromSignature.ts new file mode 100644 index 00000000..d1206410 --- /dev/null +++ b/src/utils/helpers/generateKeysFromSignature.ts @@ -0,0 +1,61 @@ +import { getPublicKey } from '@noble/secp256k1'; +import { bytesToHex, hexToBytes, isHex, keccak256 } from 'viem'; +import type { HexString } from '../crypto/types'; + +/** + * Generates spending and viewing public and private keys from a signature. + * @param signature as a hexadecimal string. + * @returns The spending and viewing public and private keys as hexadecimal strings. + */ +function generateKeysFromSignature(signature: HexString): { + spendingPublicKey: HexString; + spendingPrivateKey: HexString; + viewingPublicKey: HexString; + viewingPrivateKey: HexString; +} { + if (!isValidSignature(signature)) { + throw new Error(`Invalid signature: ${signature}`); + } + + // Extract signature portions + const { portion1, portion2, lastByte } = extractPortions(signature); + + if (`0x${portion1}${portion2}${lastByte}` !== signature) { + throw new Error('Signature incorrectly generated or parsed'); + } + + // Generate private keys from the signature portions + // Convert from hex to bytes to be used with the noble library + const spendingPrivateKey = hexToBytes(keccak256(`0x${portion1}`)); + const viewingPrivateKey = hexToBytes(keccak256(`0x${portion2}`)); + + // Generate the compressed public keys from the private keys + const spendingPublicKey = bytesToHex(getPublicKey(spendingPrivateKey, true)); + const viewingPublicKey = bytesToHex(getPublicKey(viewingPrivateKey, true)); + + return { + spendingPublicKey, + spendingPrivateKey: bytesToHex(spendingPrivateKey), + viewingPublicKey, + viewingPrivateKey: bytesToHex(viewingPrivateKey) + }; +} + +function isValidSignature(sig: string) { + return isHex(sig) && sig.length === 132; +} + +export function extractPortions(signature: HexString) { + const startIndex = 2; // first two characters are 0x, so skip these + const length = 64; // each 32 byte chunk is in hex, so 64 characters + const portion1 = signature.slice(startIndex, startIndex + length); + const portion2 = signature.slice( + startIndex + length, + startIndex + length + length + ); + const lastByte = signature.slice(signature.length - 2); + + return { portion1, portion2, lastByte }; +} + +export default generateKeysFromSignature; diff --git a/src/utils/helpers/generateRandomStealthMetaAddress.ts b/src/utils/helpers/generateRandomStealthMetaAddress.ts index 4b4e6959..ca077784 100644 --- a/src/utils/helpers/generateRandomStealthMetaAddress.ts +++ b/src/utils/helpers/generateRandomStealthMetaAddress.ts @@ -1,4 +1,4 @@ -import { utils, getPublicKey } from '@noble/secp256k1'; +import { getPublicKey, utils } from '@noble/secp256k1'; import { bytesToHex } from 'viem'; import type { HexString } from '../crypto/types'; @@ -23,7 +23,7 @@ function generateRandomStealthMetaAddress() { stealthMetaAddress, stealthMetaAddressURI, viewingPrivateKey: bytesToHex(viewingPrivateKey), - viewingPublicKey, + viewingPublicKey }; } diff --git a/src/utils/helpers/generateStealthMetaAddressFromKeys.ts b/src/utils/helpers/generateStealthMetaAddressFromKeys.ts new file mode 100644 index 00000000..3fc13760 --- /dev/null +++ b/src/utils/helpers/generateStealthMetaAddressFromKeys.ts @@ -0,0 +1,32 @@ +import type { HexString } from '../crypto/types'; +import isValidPublicKey from './isValidPublicKey'; + +/** + * Concatenates the spending and viewing public keys to create a stealth meta address. + * @param spendingPublicKey + * @param viewingPublicKey + * @returns The stealth meta address as a hexadecimal string. + */ +function generateStealthMetaAddressFromKeys({ + spendingPublicKey, + viewingPublicKey +}: { + spendingPublicKey: HexString; + viewingPublicKey: HexString; +}): HexString { + if (!isValidPublicKey(spendingPublicKey)) { + throw new Error('Invalid spending public key'); + } + + if (!isValidPublicKey(viewingPublicKey)) { + throw new Error('Invalid viewing public key'); + } + + const stealthMetaAddress: HexString = `0x${spendingPublicKey.slice( + 2 + )}${viewingPublicKey.slice(2)}`; + + return stealthMetaAddress; +} + +export default generateStealthMetaAddressFromKeys; diff --git a/src/utils/helpers/generateStealthMetaAddressFromSignature.ts b/src/utils/helpers/generateStealthMetaAddressFromSignature.ts new file mode 100644 index 00000000..f81bcda2 --- /dev/null +++ b/src/utils/helpers/generateStealthMetaAddressFromSignature.ts @@ -0,0 +1,26 @@ +import type { HexString } from '../crypto/types'; +import generateKeysFromSignature from './generateKeysFromSignature'; +import generateStealthMetaAddressFromKeys from './generateStealthMetaAddressFromKeys'; + +/** + * Generates a stealth meta-address from a signature by: + * 1. Generating the spending and viewing public keys from the signature. + * 2. Concatenating the public keys from step 1. + * @param signature as a hexadecimal string. + * @returns The stealth meta-address as a hexadecimal string. + */ +function generateStealthMetaAddressFromSignature( + signature: HexString +): HexString { + const { spendingPublicKey, viewingPublicKey } = + generateKeysFromSignature(signature); + + const stealthMetaAddress = generateStealthMetaAddressFromKeys({ + spendingPublicKey, + viewingPublicKey + }); + + return stealthMetaAddress; +} + +export default generateStealthMetaAddressFromSignature; diff --git a/src/utils/helpers/index.ts b/src/utils/helpers/index.ts index 842de4aa..1bacbd01 100644 --- a/src/utils/helpers/index.ts +++ b/src/utils/helpers/index.ts @@ -1,2 +1,6 @@ +export { default as generateKeysFromSignature } from './generateKeysFromSignature'; export { default as generateRandomStealthMetaAddress } from './generateRandomStealthMetaAddress'; +export { default as generateStealthMetaAddressFromKeys } from './generateStealthMetaAddressFromKeys'; +export { default as generateStealthMetaAddressFromSignature } from './generateStealthMetaAddressFromSignature'; export { default as getViewTagFromMetadata } from './getViewTagFromMetadata'; +export { default as isValidPublicKey } from './isValidPublicKey'; diff --git a/src/utils/helpers/isValidPublicKey.ts b/src/utils/helpers/isValidPublicKey.ts new file mode 100644 index 00000000..1afcd6a0 --- /dev/null +++ b/src/utils/helpers/isValidPublicKey.ts @@ -0,0 +1,19 @@ +import { ProjectivePoint } from '@noble/secp256k1'; +import type { HexString } from '../crypto/types'; + +/** + * Validates a hex string as a public key using the noble/secp256k1 library. + * @param publicKey The public key to validate. + * @returns True if the public key is valid, false otherwise. + */ + +function isValidPublicKey(publicKey: HexString): boolean { + try { + ProjectivePoint.fromHex(publicKey.slice(2)); + return true; + } catch { + return false; + } +} + +export default isValidPublicKey; diff --git a/src/utils/helpers/test/generateKeysFromSignature.test.ts b/src/utils/helpers/test/generateKeysFromSignature.test.ts new file mode 100644 index 00000000..3f6d5d79 --- /dev/null +++ b/src/utils/helpers/test/generateKeysFromSignature.test.ts @@ -0,0 +1,56 @@ +import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test'; +import { signMessage } from 'viem/actions'; +import setupTestWallet from '../../../lib/helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../../lib/helpers/types'; +import type { HexString } from '../../crypto/types'; +import generateKeysFromSignature from '../generateKeysFromSignature'; +import isValidPublicKey from '../isValidPublicKey'; + +describe('generateKeysFromSignature', () => { + let walletClient: SuperWalletClient; + let signature: HexString; + + beforeAll(async () => { + walletClient = await setupTestWallet(); + if (!walletClient.account) throw new Error('No account found'); + + // Generate a signature to use in the tests + signature = await signMessage(walletClient, { + account: walletClient.account, + message: + 'Sign this message to generate your stealth address keys.\nChain ID: 31337' + }); + }); + + afterAll(() => { + mock.restore(); + }); + + test('should generate valid public keys from a correct signature', () => { + const result = generateKeysFromSignature(signature); + + expect(isValidPublicKey(result.spendingPublicKey)).toBe(true); + expect(isValidPublicKey(result.viewingPublicKey)).toBe(true); + }); + + test('should throw an error for an invalid signature', () => { + const invalidSignature = '0x123'; + + expect(() => { + generateKeysFromSignature(invalidSignature); + }).toThrow('Invalid signature'); + }); + + test('should throw an error for incorrectly parsed signatures', () => { + const notMatchingSignature = '0x123'; + + // Mock the output from extractPortions to return an signature that doesn't match the one passed in + mock.module('../generateKeysFromSignature', () => ({ + extractPortions: () => notMatchingSignature + })); + + expect(() => { + generateKeysFromSignature(signature); + }).toThrow('Signature incorrectly generated or parsed'); + }); +}); diff --git a/src/utils/helpers/test/generateStealthMetaAddressFromKeys.test.ts b/src/utils/helpers/test/generateStealthMetaAddressFromKeys.test.ts new file mode 100644 index 00000000..35bb3a35 --- /dev/null +++ b/src/utils/helpers/test/generateStealthMetaAddressFromKeys.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, test } from 'bun:test'; +import { toHex } from 'viem'; +import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; +import generateStealthMetaAddressFromKeys from '../generateStealthMetaAddressFromKeys'; + +const STEALTH_META_ADDRESS = + '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; + +describe('getStealthMetaAddressFromKeys', () => { + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + const { + spendingPublicKey: _spendingPublicKey, + viewingPublicKey: _viewingPublicKey + } = parseKeysFromStealthMetaAddress({ + stealthMetaAddress: STEALTH_META_ADDRESS, + schemeId + }); + + const spendingPublicKey = toHex(_spendingPublicKey); + const viewingPublicKey = toHex(_viewingPublicKey); + + test('should return the correct stealth meta address', () => { + const expected = STEALTH_META_ADDRESS; + const actual = generateStealthMetaAddressFromKeys({ + spendingPublicKey, + viewingPublicKey + }); + expect(actual).toBe(expected); + }); + + test('should throw an error if the spending public key is invalid', () => { + // Invalid compressed public key + const spendingPublicKey = '0x02a7'; + // Valid compressed public key + const viewingPublicKey = '0x03b8'; + expect(() => + generateStealthMetaAddressFromKeys({ + spendingPublicKey, + viewingPublicKey + }) + ).toThrow('Invalid spending public key'); + }); +}); diff --git a/src/utils/helpers/test/generateStealthMetaAddressFromSignature.test.ts b/src/utils/helpers/test/generateStealthMetaAddressFromSignature.test.ts new file mode 100644 index 00000000..4dde8da2 --- /dev/null +++ b/src/utils/helpers/test/generateStealthMetaAddressFromSignature.test.ts @@ -0,0 +1,38 @@ +import { beforeAll, describe, expect, test } from 'bun:test'; +import { signMessage } from 'viem/actions'; +import setupTestWallet from '../../../lib/helpers/test/setupTestWallet'; +import type { SuperWalletClient } from '../../../lib/helpers/types'; +import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; +import type { HexString } from '../../crypto/types'; +import generateStealthMetaAddressFromSignature from '../generateStealthMetaAddressFromSignature'; + +describe('getStealthMetaAddressFromSignature', () => { + let walletClient: SuperWalletClient; + let signature: HexString; + + beforeAll(async () => { + walletClient = await setupTestWallet(); + if (!walletClient.account) throw new Error('No account found'); + + // Generate a signature to use in the tests + signature = await signMessage(walletClient, { + account: walletClient.account, + message: + 'Sign this message to generate your stealth address keys.\nChain ID: 31337' + }); + }); + + test('should generate a stealth meta-address from a signature', () => { + const result = generateStealthMetaAddressFromSignature(signature); + + expect(result).toBeTruthy(); + + // Can parse the keys from the stealth meta-address + expect(() => + parseKeysFromStealthMetaAddress({ + stealthMetaAddress: result, + schemeId: VALID_SCHEME_ID.SCHEME_ID_1 + }) + ).not.toThrow(); + }); +}); diff --git a/src/utils/helpers/test/isValidPublicKey.test.ts b/src/utils/helpers/test/isValidPublicKey.test.ts new file mode 100644 index 00000000..506a2c73 --- /dev/null +++ b/src/utils/helpers/test/isValidPublicKey.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, test } from 'bun:test'; +import { toHex } from 'viem'; +import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; +import isValidPublicKey from '../isValidPublicKey'; + +describe('isValidPublicKey', () => { + const VALID_STEALTH_META_ADDRESS = + '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; + const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; + + const { + spendingPublicKey: _spendingPublicKey, + viewingPublicKey: _viewingPublicKey + } = parseKeysFromStealthMetaAddress({ + stealthMetaAddress: VALID_STEALTH_META_ADDRESS, + schemeId + }); + + const spendingPublicKey = toHex(_spendingPublicKey); + + test('should return true for a valid public key', () => { + expect(isValidPublicKey(spendingPublicKey)).toBe(true); + }); + + test('should return false for an invalid public key', () => { + // Invalid public key + const invalidPublicKey = '0x02a7'; + expect(isValidPublicKey(invalidPublicKey)).toBe(false); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts index 630e501b..baa4a3c4 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -5,12 +5,12 @@ export { generateStealthAddress, getViewTag, parseKeysFromStealthMetaAddress, - parseStealthMetaAddressURI, + parseStealthMetaAddressURI } from './crypto'; export { generateRandomStealthMetaAddress, - getViewTagFromMetadata, + getViewTagFromMetadata } from './helpers'; export { @@ -18,5 +18,5 @@ export { type GenerateStealthAddressReturnType, type ICheckStealthAddressParams, type IGenerateStealthAddressParams, - VALID_SCHEME_ID, + VALID_SCHEME_ID } from './crypto/types';