├── examples ├── getAnnouncements │ ├── .env.example │ ├── README.md │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── getStealthMetaAddress │ ├── .env.example │ ├── README.md │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── computeStealthKey │ ├── README.md │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── prepareAnnounce │ ├── README.md │ ├── .env.example │ ├── bun.lockb │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── package.json │ └── index.tsx ├── watchAnnouncementsForUser │ ├── .env.example │ ├── README.md │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── checkStealthAddress │ ├── README.md │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── prepareRegisterKeys │ ├── README.md │ ├── .env.example │ ├── bun.lockb │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── package.json │ └── index.tsx ├── generateStealthAddress │ ├── README.md │ ├── bun.lockb │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── getAnnouncementsForUser │ ├── README.md │ ├── .env.example │ ├── bun.lockb │ ├── package.json │ ├── tsconfig.json │ └── index.ts ├── prepareRegisterKeysOnBehalf │ ├── README.md │ ├── .env.example │ ├── bun.lockb │ ├── index.html │ ├── package.json │ └── index.tsx ├── generateDeterministicStealthMetaAddress │ ├── .env.example │ ├── README.md │ ├── bun.lockb │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── package.json │ └── index.tsx └── sendToAndTransferFromStealthAddress │ ├── .env.example │ ├── README.md │ ├── bun.lockb │ ├── vite.config.ts │ ├── index.html │ ├── tsconfig.json │ ├── package.json │ └── index.tsx ├── bun.lockb ├── bunfig.toml ├── src ├── index.ts ├── lib │ ├── abi │ │ ├── index.ts │ │ ├── ERC5564Announcer.ts │ │ └── ERC6538Registry.ts │ ├── index.ts │ ├── helpers │ │ ├── chains.ts │ │ ├── test │ │ │ ├── setupTestStealthKeys.ts │ │ │ ├── setupTestWallet.ts │ │ │ ├── setupTestWallet.test.ts │ │ │ ├── setupTestEnv.test.ts │ │ │ └── setupTestEnv.ts │ │ ├── types.ts │ │ └── logs.ts │ ├── actions │ │ ├── types.ts │ │ ├── prepareAnnounce │ │ │ ├── types.ts │ │ │ ├── prepareAnnounce.ts │ │ │ └── prepareAnnounce.test.ts │ │ ├── prepareRegisterKeys │ │ │ ├── types.ts │ │ │ ├── prepareRegisterKeys.ts │ │ │ └── prepareRegisterKeys.test.ts │ │ ├── getStealthMetaAddress │ │ │ ├── types.ts │ │ │ ├── getStealthMetaAddress.ts │ │ │ └── getStealthMetaAddress.test.ts │ │ ├── getAnnouncements │ │ │ ├── types.ts │ │ │ ├── getAnnouncements.ts │ │ │ └── getAnnouncements.test.ts │ │ ├── watchAnnouncementsForUser │ │ │ ├── types.ts │ │ │ ├── watchAnnouncementsForUser.ts │ │ │ └── watchAnnouncementsForUser.test.ts │ │ ├── prepareRegisterKeysOnBehalf │ │ │ ├── types.ts │ │ │ ├── prepareRegisterKeysOnBehalf.ts │ │ │ └── prepareRegisterKeysOnBehalf.test.ts │ │ ├── getAnnouncementsUsingSubgraph │ │ │ ├── types.ts │ │ │ ├── getAnnouncementsUsingSubgraph.ts │ │ │ └── subgraphHelpers.ts │ │ ├── getAnnouncementsForUser │ │ │ └── types.ts │ │ └── index.ts │ └── stealthClient │ │ ├── createStealthClient.test.ts │ │ ├── types.ts │ │ └── createStealthClient.ts ├── config │ ├── index.ts │ ├── contractAddresses.ts │ └── startBlocks.ts ├── utils │ ├── helpers │ │ ├── isValidPublicKey.ts │ │ ├── types │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── getViewTagFromMetadata.ts │ │ ├── generateStealthMetaAddressFromSignature.ts │ │ ├── generateStealthMetaAddressFromKeys.ts │ │ ├── test │ │ │ ├── isValidPublicKey.test.ts │ │ │ ├── generateStealthMetaAddressFromSignature.test.ts │ │ │ ├── generateKeysFromSignature.test.ts │ │ │ ├── generateStealthMetaAddressFromKeys.test.ts │ │ │ └── generateSignatureForRegisterKeysOnBehalf.test.ts │ │ ├── generateRandomStealthMetaAddress.ts │ │ ├── generateKeysFromSignature.ts │ │ └── generateSignatureForRegisterKeysOnBehalf.ts │ ├── crypto │ │ ├── index.ts │ │ ├── computeStealthKey.ts │ │ ├── checkStealthAddress.ts │ │ ├── types │ │ │ └── index.ts │ │ └── test │ │ │ ├── checkStealthAddress.test.ts │ │ │ ├── generateStealthAddress.test.ts │ │ │ └── computeStealthKey.test.ts │ └── index.ts ├── scripts │ ├── index.ts │ └── deployContract.ts └── test │ ├── sendReceive.test.ts │ └── helpers │ └── index.ts ├── .env.example ├── .npmignore ├── CHANGELOG.md ├── makefile ├── tsconfig.json ├── package.json ├── biome.json ├── LICENSE ├── .github └── workflows │ ├── claude-assistant.yml │ └── ci.yml ├── .gitignore ├── CLAUDE.md └── README.md /examples/getAnnouncements/.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL='Your rpc url' -------------------------------------------------------------------------------- /examples/getAnnouncements/README.md: -------------------------------------------------------------------------------- 1 | # Get Announcements Example 2 | -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL='You rpc url' -------------------------------------------------------------------------------- /examples/computeStealthKey/README.md: -------------------------------------------------------------------------------- 1 | # Compute Stealth Key Example 2 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/README.md: -------------------------------------------------------------------------------- 1 | # Prepare Announcement Example 2 | -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/.env.example: -------------------------------------------------------------------------------- 1 | RPC_URL='Your rpc url' -------------------------------------------------------------------------------- /examples/checkStealthAddress/README.md: -------------------------------------------------------------------------------- 1 | # Check Stealth Address Example 2 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/README.md: -------------------------------------------------------------------------------- 1 | # Prepare Register Keys Example 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/bun.lockb -------------------------------------------------------------------------------- /examples/generateStealthAddress/README.md: -------------------------------------------------------------------------------- 1 | # Generate Stealth Address Example 2 | -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/README.md: -------------------------------------------------------------------------------- 1 | # Get Announcements For User Example 2 | -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/README.md: -------------------------------------------------------------------------------- 1 | # Get Stealth Meta Address Example 2 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/README.md: -------------------------------------------------------------------------------- 1 | # Prepare Register Keys Example 2 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/.env.example: -------------------------------------------------------------------------------- 1 | VITE_RPC_URL='Your rpc url' -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/.env.example: -------------------------------------------------------------------------------- 1 | VITE_RPC_URL='Your rpc url' -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/README.md: -------------------------------------------------------------------------------- 1 | # Watch Announcements For User Example 2 | -------------------------------------------------------------------------------- /bunfig.toml: -------------------------------------------------------------------------------- 1 | [test] 2 | coverageSkipTestFiles = true 3 | coverageThreshold = 0.8 4 | root = "./src" -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/README.md: -------------------------------------------------------------------------------- 1 | # Send to and Withdraw from Stealth Address Example 2 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/README.md: -------------------------------------------------------------------------------- 1 | # Generate Deterministic Stealth Meta-Address Example 2 | -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/.env.example: -------------------------------------------------------------------------------- 1 | SPENDING_PUBLIC_KEY='0x...' 2 | VIEWING_PRIVATE_KEY='0x...' 3 | RPC_URL='Your rpc url' -------------------------------------------------------------------------------- /examples/prepareAnnounce/.env.example: -------------------------------------------------------------------------------- 1 | VITE_RPC_URL='Your rpc url' 2 | VITE_STEALTH_META_ADDRESS_URI="Your stealth meta address URI" -------------------------------------------------------------------------------- /examples/prepareAnnounce/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/prepareAnnounce/bun.lockb -------------------------------------------------------------------------------- /examples/computeStealthKey/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/computeStealthKey/bun.lockb -------------------------------------------------------------------------------- /examples/getAnnouncements/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/getAnnouncements/bun.lockb -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/.env.example: -------------------------------------------------------------------------------- 1 | VITE_RPC_URL='Your rpc url' 2 | VITE_STEALTH_META_ADDRESS_URI="Your stealth meta address URI" -------------------------------------------------------------------------------- /examples/checkStealthAddress/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/checkStealthAddress/bun.lockb -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/prepareRegisterKeys/bun.lockb -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/.env.example: -------------------------------------------------------------------------------- 1 | VITE_RPC_URL='Your rpc url' 2 | VITE_STEALTH_META_ADDRESS_URI="Your stealth meta address URI" -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/crypto'; 2 | export * from './utils/helpers'; 3 | export * from './lib'; 4 | export * from './config'; 5 | -------------------------------------------------------------------------------- /examples/generateStealthAddress/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/generateStealthAddress/bun.lockb -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/getStealthMetaAddress/bun.lockb -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/getAnnouncementsForUser/bun.lockb -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/watchAnnouncementsForUser/bun.lockb -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/prepareRegisterKeysOnBehalf/bun.lockb -------------------------------------------------------------------------------- /src/lib/abi/index.ts: -------------------------------------------------------------------------------- 1 | export { default as ERC5564AnnouncerAbi } from './ERC5564Announcer'; 2 | export { default as ERC6538RegistryAbi } from './ERC6538Registry'; 3 | -------------------------------------------------------------------------------- /src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export { default as createStealthClient } from './stealthClient/createStealthClient'; 2 | export * from './actions'; 3 | export * from './abi'; 4 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/sendToAndTransferFromStealthAddress/bun.lockb -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ScopeLift/stealth-address-sdk/HEAD/examples/generateDeterministicStealthMetaAddress/bun.lockb -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Fork testing environment variables 2 | RPC_URL='' # rpc url to run tests with if using a fork 3 | PRIVATE_KEY=0xSomething # private key to run tests with if using a fork (notice not a string here) 4 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react'; 2 | import { defineConfig } from 'vite'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()] 7 | }); 8 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ERC5564_CONTRACT_ADDRESS, 3 | ERC6538_CONTRACT_ADDRESS 4 | } from './contractAddresses'; 5 | export { ERC5564_BYTECODE, ERC6538_BYTECODE } from './bytecode'; 6 | export { ERC5564_StartBlocks, ERC6538_StartBlocks } from './startBlocks'; 7 | -------------------------------------------------------------------------------- /examples/generateStealthAddress/index.ts: -------------------------------------------------------------------------------- 1 | import { generateStealthAddress } from '@scopelift/stealth-address-sdk'; 2 | 3 | const stealthMetaAddressURI = 'your_stealth_meta_address_here'; 4 | const result = generateStealthAddress({ stealthMetaAddressURI }); 5 | 6 | export default result; 7 | -------------------------------------------------------------------------------- /examples/getAnnouncements/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-get-announcements", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/computeStealthKey/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-compute-stealth-key", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/checkStealthAddress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-check-stealth-address", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/generateStealthAddress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-generate-stealth-address", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-get-announcements-for-user", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-get-stealth-meta-address", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-watch-announcements-for-user", 3 | "private": true, 4 | "type": "module", 5 | "dependencies": { 6 | "@scopelift/stealth-address-sdk": "latest" 7 | }, 8 | "devDependencies": { 9 | "typescript": "^5.3.3" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/helpers/chains.ts: -------------------------------------------------------------------------------- 1 | import type { Chain } from 'viem'; 2 | import { VALID_CHAINS, type VALID_CHAIN_IDS } from './types'; 3 | 4 | export const getChain = (id: VALID_CHAIN_IDS): Chain => { 5 | const chain = VALID_CHAINS[id]; 6 | if (!chain) throw new Error(`Invalid chainId: ${id}`); 7 | return chain; 8 | }; 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Env 2 | .env 3 | 4 | # Configuration and development tools 5 | .bun 6 | tsconfig.json 7 | README.md 8 | 9 | # Source files and tests 10 | src/ 11 | examples/ 12 | *.test.ts 13 | *.test.js 14 | 15 | # Miscellaneous 16 | *.log 17 | *.lockb 18 | 19 | # Development and build directories 20 | node_modules/ 21 | dist/test/ 22 | dist/utils/crypto/test/ 23 | -------------------------------------------------------------------------------- /src/config/contractAddresses.ts: -------------------------------------------------------------------------------- 1 | // Singleton announcer contract addresses 2 | // This is the same for all chains 3 | export const ERC5564_CONTRACT_ADDRESS = 4 | '0x55649E01B5Df198D18D95b5cc5051630cfD45564'; 5 | 6 | // Singleton registry contract addresses 7 | // This is the same for all chains 8 | export const ERC6538_CONTRACT_ADDRESS = 9 | '0x6538E6bf4B0eBd30A8Ea093027Ac2422ce5d6538'; 10 | -------------------------------------------------------------------------------- /src/lib/actions/types.ts: -------------------------------------------------------------------------------- 1 | export type PreparePayload = { 2 | to: `0x${string}`; 3 | account: `0x${string}`; 4 | data: `0x${string}`; 5 | value?: bigint; 6 | }; 7 | 8 | export class PrepareError extends Error { 9 | constructor(message = 'error preparing transaction payload') { 10 | super(message); 11 | this.name = 'PrepareError'; 12 | Object.setPrototypeOf(this, PrepareError.prototype); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prepare Announcement Example 7 | 8 | 9 |

Prepare Announcement Example

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/computeStealthKey/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/getAnnouncements/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/checkStealthAddress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/generateStealthAddress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | }, 15 | "include": ["."], 16 | } 17 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | }, 16 | "include": ["."], 17 | } 18 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Prepare Register Keys (Stealth Meta-Address) Example 7 | 8 | 9 |

Prepare Register Keys (Stealth Meta-Address) Example

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0-beta.1] - 2024-07-03 9 | 10 | ### Changed 11 | 12 | - Publish script 13 | - README spelling fix 14 | 15 | ## [1.0.0-beta.0] - 2024-07-02 16 | 17 | ### Added 18 | 19 | - Initial beta release 20 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | }, 16 | "include": ["."], 17 | } 18 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Send to and Withdraw from Stealth Address Example 7 | 8 | 9 |

Send to and Withdraw from Stealth Address Example

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Generate Deterministic Stealth Meta-address Example 7 | 8 | 9 |

Generate Deterministic Stealth Meta-address Example

10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | }, 16 | "include": ["."], 17 | } 18 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "noEmit": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "skipLibCheck": true, 14 | "jsx": "react", 15 | }, 16 | "include": ["."], 17 | } 18 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | -include .env 2 | 3 | # Default command to run when no target is specified 4 | all: test-local 5 | 6 | # Make anvil fork 7 | anvil-fork: 8 | @echo "Starting Anvil with fork..." 9 | # Start Anvil with a fork of the provided rpc url chain 10 | anvil -f $(RPC_URL) 11 | 12 | # Test using the provided rpc url chain 13 | test-fork: 14 | @echo "Running tests on fork..." 15 | # Run tests with environment variables 16 | env RPC_URL=$(RPC_URL) USE_FORK=true bun test src $(FILE) 17 | 18 | .PHONY: all anvil-fork test-fork 19 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Prepare Register Keys (Stealth Meta-Address) On Behalf Example 8 | 9 | 10 | 11 |

Prepare Register Keys (Stealth Meta-Address) On Behalf Example

12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-prepare-announce", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite" 7 | }, 8 | "dependencies": { 9 | "@types/react": "^18.2.61", 10 | "@types/react-dom": "^18.2.19", 11 | "@vitejs/plugin-react": "^4.2.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "@scopelift/stealth-address-sdk": "latest", 15 | "viem": "latest", 16 | "vite": "latest" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-prepare-register-keys", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite" 7 | }, 8 | "dependencies": { 9 | "@types/react": "^18.2.61", 10 | "@types/react-dom": "^18.2.19", 11 | "@vitejs/plugin-react": "^4.2.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "@scopelift/stealth-address-sdk": "latest", 15 | "viem": "latest", 16 | "vite": "latest" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-prepare-register-keys", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite" 7 | }, 8 | "dependencies": { 9 | "@types/react": "^18.2.61", 10 | "@types/react-dom": "^18.2.19", 11 | "@vitejs/plugin-react": "^4.2.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "@scopelift/stealth-address-sdk": "latest", 15 | "viem": "latest", 16 | "vite": "latest" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/helpers/isValidPublicKey.ts: -------------------------------------------------------------------------------- 1 | import { ProjectivePoint } from '@noble/secp256k1'; 2 | import type { HexString } from '../crypto/types'; 3 | 4 | /** 5 | * Validates a hex string as a public key using the noble/secp256k1 library. 6 | * @param publicKey The public key to validate. 7 | * @returns True if the public key is valid, false otherwise. 8 | */ 9 | 10 | function isValidPublicKey(publicKey: HexString): boolean { 11 | try { 12 | ProjectivePoint.fromHex(publicKey.slice(2)); 13 | return true; 14 | } catch { 15 | return false; 16 | } 17 | } 18 | 19 | export default isValidPublicKey; 20 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example-generate-deterministic-stealth-meta-address", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite" 7 | }, 8 | "dependencies": { 9 | "@types/react": "^18.2.61", 10 | "@types/react-dom": "^18.2.19", 11 | "@vitejs/plugin-react": "^4.2.1", 12 | "react": "^18.2.0", 13 | "react-dom": "^18.2.0", 14 | "@scopelift/stealth-address-sdk": "latest", 15 | "viem": "latest", 16 | "vite": "latest" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5.3.3" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/helpers/test/setupTestStealthKeys.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type VALID_SCHEME_ID, 3 | generateRandomStealthMetaAddress 4 | } from '../../..'; 5 | 6 | function setupTestStealthKeys(schemeId: VALID_SCHEME_ID) { 7 | const { 8 | spendingPrivateKey, 9 | spendingPublicKey, 10 | stealthMetaAddressURI, 11 | viewingPrivateKey, 12 | viewingPublicKey 13 | } = generateRandomStealthMetaAddress(); 14 | 15 | return { 16 | spendingPublicKey, 17 | spendingPrivateKey, 18 | viewingPublicKey, 19 | viewingPrivateKey, 20 | stealthMetaAddressURI 21 | }; 22 | } 23 | 24 | export default setupTestStealthKeys; 25 | -------------------------------------------------------------------------------- /src/lib/actions/prepareAnnounce/types.ts: -------------------------------------------------------------------------------- 1 | import type { EthAddress, VALID_SCHEME_ID } from '../../..'; 2 | import type { ClientParams } from '../../stealthClient/types'; 3 | import type { PreparePayload } from '../types'; 4 | 5 | export type PrepareAnnounceParams = { 6 | account: EthAddress; 7 | args: PrepareAnnounceArgs; 8 | clientParams?: ClientParams; 9 | ERC5564Address: EthAddress; 10 | }; 11 | 12 | export type PrepareAnnounceReturnType = PreparePayload; 13 | 14 | export type PrepareAnnounceArgs = { 15 | schemeId: VALID_SCHEME_ID; 16 | stealthAddress: `0x${string}`; 17 | ephemeralPublicKey: `0x${string}`; 18 | metadata: `0x${string}`; 19 | }; 20 | -------------------------------------------------------------------------------- /src/config/startBlocks.ts: -------------------------------------------------------------------------------- 1 | export enum ERC5564_StartBlocks { 2 | ARBITRUM_ONE = 219468264, 3 | ARBITRUM_SEPOLIA = 24849998, 4 | BASE = 15502414, 5 | BASE_SEPOLIA = 7552655, 6 | HOLESKY = 1222405, 7 | MAINNET = 20042207, 8 | MATIC = 57888814, 9 | OPTIMISM = 121097390, 10 | OPTIMISM_SEPOLIA = 9534458, 11 | SEPOLIA = 5486597 12 | } 13 | 14 | export enum ERC6538_StartBlocks { 15 | ARBITRUM_ONE = 219468267, 16 | ARBITRUM_SEPOLIA = 25796636, 17 | BASE = 15502414, 18 | BASE_SEPOLIA = 7675097, 19 | HOLESKY = 1222405, 20 | MAINNET = 20042207, 21 | MATIC = 57888814, 22 | OPTIMISM = 121097390, 23 | OPTIMISM_SEPOLIA = 9658043, 24 | SEPOLIA = 5538412 25 | } 26 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "send-to-and-withdraw-from-stealth-address-example", 3 | "private": true, 4 | "type": "module", 5 | "scripts": { 6 | "dev": "vite" 7 | }, 8 | "dependencies": { 9 | "@scopelift/stealth-address-sdk": "^1.0.0-beta.1", 10 | "@tanstack/react-query": "^5.51.1", 11 | "@types/react": "^18.2.61", 12 | "@types/react-dom": "^18.2.19", 13 | "@vitejs/plugin-react": "^4.2.1", 14 | "react": "^18.2.0", 15 | "react-dom": "^18.2.0", 16 | "viem": "2.x", 17 | "vite": "latest", 18 | "wagmi": "^2.10.10" 19 | }, 20 | "devDependencies": { 21 | "typescript": "^5.3.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | generatePrivateKey, 3 | generateStealthAddress, 4 | getHashedSharedSecret, 5 | getStealthPublicKey, 6 | getViewTag, 7 | handleSchemeId, 8 | parseKeysFromStealthMetaAddress, 9 | parseStealthMetaAddressURI, 10 | publicKeyToAddress 11 | } from './generateStealthAddress'; 12 | 13 | export { default as computeStealthKey } from './computeStealthKey'; 14 | 15 | export { default as checkStealthAddress } from './checkStealthAddress'; 16 | 17 | export { 18 | type EthAddress, 19 | type GenerateStealthAddressReturnType, 20 | type ICheckStealthAddressParams, 21 | type IComputeStealthKeyParams, 22 | type IGenerateStealthAddressParams, 23 | VALID_SCHEME_ID 24 | } from './types'; 25 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | checkStealthAddress, 3 | computeStealthKey, 4 | generatePrivateKey, 5 | generateStealthAddress, 6 | getViewTag, 7 | parseKeysFromStealthMetaAddress, 8 | parseStealthMetaAddressURI 9 | } from './crypto'; 10 | 11 | export { 12 | generateRandomStealthMetaAddress, 13 | generateSignatureForRegisterKeysOnBehalf, 14 | getViewTagFromMetadata, 15 | type GenerateSignatureForRegisterKeysError, 16 | type GenerateSignatureForRegisterKeysParams 17 | } from './helpers'; 18 | 19 | export { 20 | type EthAddress, 21 | type GenerateStealthAddressReturnType, 22 | type ICheckStealthAddressParams, 23 | type IGenerateStealthAddressParams, 24 | VALID_SCHEME_ID 25 | } from './crypto/types'; 26 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeys/types.ts: -------------------------------------------------------------------------------- 1 | import type { EthAddress, VALID_SCHEME_ID } from '../../../utils'; 2 | import type { ClientParams } from '../../stealthClient/types'; 3 | import type { PreparePayload } from '../types'; 4 | 5 | export type PrepareRegisterKeysParams = { 6 | ERC6538Address: EthAddress; // The address of the ERC6538 contract. 7 | schemeId: VALID_SCHEME_ID; // The scheme ID as per the ERC6538 specification. 8 | stealthMetaAddress: `0x${string}`; // The stealth meta-address to be registered. 9 | account: EthAddress; // The address of the account. 10 | clientParams?: ClientParams; // Optional client parameters for direct function usage. 11 | }; 12 | 13 | export type PrepareRegisterKeysReturnType = PreparePayload; 14 | -------------------------------------------------------------------------------- /examples/computeStealthKey/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VALID_SCHEME_ID, 3 | computeStealthKey 4 | } from '@scopelift/stealth-address-sdk'; 5 | 6 | // Example keys (these would be generated or provided as necessary) 7 | const ephemeralPublicKey = '0x02c1ad...'; // Ephemeral public key from the announcement 8 | const viewingPrivateKey = '0x5J1gq...'; // User's viewing private key 9 | const spendingPrivateKey = '0xKwf3e...'; // User's spending private key 10 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; // Scheme ID, typically '1' for the standard implementation 11 | 12 | // Compute the stealth private key 13 | const stealthPrivateKey = computeStealthKey({ 14 | ephemeralPublicKey, 15 | schemeId, 16 | spendingPrivateKey, 17 | viewingPrivateKey 18 | }); 19 | -------------------------------------------------------------------------------- /src/lib/actions/getStealthMetaAddress/types.ts: -------------------------------------------------------------------------------- 1 | import type { EthAddress, VALID_SCHEME_ID } from '../../../utils/crypto/types'; 2 | import type { ClientParams } from '../../stealthClient/types'; 3 | 4 | export type GetStealthMetaAddressParams = { 5 | clientParams?: ClientParams; 6 | ERC6538Address: EthAddress; 7 | registrant: `0x${string}`; 8 | schemeId: VALID_SCHEME_ID; 9 | }; 10 | export type GetStealthMetaAddressReturnType = `0x${string}` | undefined; 11 | 12 | export class GetStealthMetaAddressError extends Error { 13 | constructor(message = 'Error getting stealth meta address.') { 14 | super(message); 15 | this.name = 'GetStealthMetaAddressError'; 16 | Object.setPrototypeOf(this, GetStealthMetaAddressError.prototype); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/helpers/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { WalletClient } from 'viem'; 2 | import type { VALID_CHAIN_IDS } from '../../../lib/helpers/types'; 3 | import type { VALID_SCHEME_ID } from '../../crypto'; 4 | 5 | export interface GenerateSignatureForRegisterKeysParams { 6 | walletClient: WalletClient; 7 | account: `0x${string}`; 8 | ERC6538Address: `0x${string}`; 9 | chainId: VALID_CHAIN_IDS; 10 | schemeId: VALID_SCHEME_ID; 11 | stealthMetaAddressToRegister: `0x${string}`; 12 | } 13 | 14 | export class GenerateSignatureForRegisterKeysError extends Error { 15 | constructor( 16 | message: string, 17 | public readonly cause?: unknown 18 | ) { 19 | super(message); 20 | this.name = 'GenerateSignatureForRegisterKeysError'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext", "DOM", "DOM.Iterable"], 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleDetection": "force", 7 | "jsx": "react-jsx", 8 | "allowJs": true, 9 | "outDir": "./dist", 10 | "declaration": true, 11 | "rootDir": "./src", 12 | "esModuleInterop": true, 13 | 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "verbatimModuleSyntax": true, 17 | "noEmit": false, 18 | 19 | /* Linting */ 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "noUnusedLocals": true, 25 | }, 26 | "include": ["src/**/*"], 27 | "exclude": ["node_modules", "dist", "examples"], 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@scopelift/stealth-address-sdk", 3 | "version": "1.0.0-beta.2", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "module": "dist/index.js", 7 | "type": "module", 8 | "scripts": { 9 | "anvil-fork": "make anvil-fork", 10 | "test-fork": "make test-fork", 11 | "test": "bun test", 12 | "build": "biome check --apply . && bun tsc", 13 | "watch": "bun test --watch src", 14 | "check": "biome check .", 15 | "publish": "bun run build && npm publish" 16 | }, 17 | "devDependencies": { 18 | "@biomejs/biome": "1.6.4", 19 | "@types/bun": "latest", 20 | "typescript": "^5.3.3" 21 | }, 22 | "dependencies": { 23 | "@noble/secp256k1": "^2.0.0", 24 | "graphql-request": "^7.1.0", 25 | "viem": "^2.9.16" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/helpers/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | GenerateSignatureForRegisterKeysError, 3 | GenerateSignatureForRegisterKeysParams 4 | } from './types'; 5 | 6 | export { default as generateKeysFromSignature } from './generateKeysFromSignature'; 7 | export { default as generateRandomStealthMetaAddress } from './generateRandomStealthMetaAddress'; 8 | export { default as generateSignatureForRegisterKeysOnBehalf } from './generateSignatureForRegisterKeysOnBehalf'; 9 | export { default as generateStealthMetaAddressFromKeys } from './generateStealthMetaAddressFromKeys'; 10 | export { default as generateStealthMetaAddressFromSignature } from './generateStealthMetaAddressFromSignature'; 11 | export { default as getViewTagFromMetadata } from './getViewTagFromMetadata'; 12 | export { default as isValidPublicKey } from './isValidPublicKey'; 13 | -------------------------------------------------------------------------------- /src/scripts/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERC5564AnnouncerAbi, 3 | ERC5564_BYTECODE, 4 | ERC6538RegistryAbi, 5 | ERC6538_BYTECODE 6 | } from '..'; 7 | import deployContract from './deployContract'; 8 | 9 | const deployAllContracts = async () => { 10 | const { address: erc5564ContractAddress, deployBlock: erc5564DeployBlock } = 11 | await deployContract({ 12 | abi: ERC5564AnnouncerAbi, 13 | name: 'ERC5564', 14 | bytecode: ERC5564_BYTECODE 15 | }); 16 | 17 | const { address: erc6538ContractAddress } = await deployContract({ 18 | abi: ERC6538RegistryAbi, 19 | name: 'ERC6538', 20 | bytecode: ERC6538_BYTECODE 21 | }); 22 | 23 | return { 24 | erc5564ContractAddress, 25 | erc6538ContractAddress, 26 | erc5564DeployBlock 27 | }; 28 | }; 29 | 30 | export default deployAllContracts; 31 | -------------------------------------------------------------------------------- /src/utils/helpers/getViewTagFromMetadata.ts: -------------------------------------------------------------------------------- 1 | import type { HexString } from '../crypto/types'; 2 | 3 | /** 4 | * Extracts the view tag from the metadata. 5 | * 6 | * @param metadata The hexadecimal string from which the view tag is extracted. 7 | * @returns The view tag as a hexadecimal string. 8 | */ 9 | function getViewTagFromMetadata(metadata: HexString): HexString { 10 | // Ensure metadata starts with "0x" and has enough length 11 | if (!metadata.startsWith('0x') || metadata.length < 4) { 12 | throw new Error('Invalid metadata format'); 13 | } 14 | 15 | // The view tag should be the first byte of the metadata. 16 | // Since each byte is 2 characters in hex notation, we take the first 4 characters 17 | // which includes the "0x" prefix and the first byte. 18 | const viewTag = metadata.substring(0, 4) as HexString; 19 | 20 | return viewTag; 21 | } 22 | 23 | export default getViewTagFromMetadata; 24 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncements/types.ts: -------------------------------------------------------------------------------- 1 | import type { Log } from 'viem'; 2 | import type { EthAddress } from '../../../utils/crypto/types'; 3 | import type { ClientParams } from '../../stealthClient/types'; 4 | 5 | export type AnnouncementArgs = { 6 | schemeId?: bigint | bigint[] | null | undefined; 7 | stealthAddress?: `0x${string}` | `0x${string}`[] | null | undefined; 8 | caller?: `0x${string}` | `0x${string}`[] | null | undefined; 9 | }; 10 | 11 | export interface AnnouncementLog extends Log { 12 | caller: EthAddress; 13 | ephemeralPubKey: `0x${string}`; 14 | metadata: `0x${string}`; 15 | schemeId: bigint; 16 | stealthAddress: EthAddress; 17 | } 18 | 19 | export type GetAnnouncementsParams = { 20 | clientParams?: ClientParams; 21 | ERC5564Address: EthAddress; 22 | args: AnnouncementArgs; 23 | fromBlock?: bigint | 'earliest'; 24 | toBlock?: bigint | 'latest'; 25 | }; 26 | export type GetAnnouncementsReturnType = AnnouncementLog[]; 27 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", 3 | "vcs": { 4 | "root": ".", 5 | "enabled": true, 6 | "clientKind": "git", 7 | "useIgnoreFile": true 8 | }, 9 | "organizeImports": { 10 | "enabled": true 11 | }, 12 | "linter": { 13 | "enabled": true, 14 | "rules": { 15 | "recommended": true 16 | } 17 | }, 18 | "files": { 19 | "include": ["src/**/*.ts", "examples/**/*.ts*", "biome.json"] 20 | }, 21 | "formatter": { 22 | "enabled": true, 23 | "formatWithErrors": false, 24 | "ignore": [], 25 | "attributePosition": "auto", 26 | "indentStyle": "tab", 27 | "indentWidth": 2, 28 | "lineEnding": "lf", 29 | "lineWidth": 80 30 | }, 31 | "javascript": { 32 | "globals": ["NodeJS"], 33 | "formatter": { 34 | "enabled": true, 35 | "lineWidth": 80, 36 | "indentWidth": 2, 37 | "indentStyle": "space", 38 | "quoteStyle": "single", 39 | "arrowParentheses": "asNeeded", 40 | "trailingComma": "none" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/utils/helpers/generateStealthMetaAddressFromSignature.ts: -------------------------------------------------------------------------------- 1 | import type { HexString } from '../crypto/types'; 2 | import generateKeysFromSignature from './generateKeysFromSignature'; 3 | import generateStealthMetaAddressFromKeys from './generateStealthMetaAddressFromKeys'; 4 | 5 | /** 6 | * Generates a stealth meta-address from a signature by: 7 | * 1. Generating the spending and viewing public keys from the signature. 8 | * 2. Concatenating the public keys from step 1. 9 | * @param signature as a hexadecimal string. 10 | * @returns The stealth meta-address as a hexadecimal string. 11 | */ 12 | function generateStealthMetaAddressFromSignature( 13 | signature: HexString 14 | ): HexString { 15 | const { spendingPublicKey, viewingPublicKey } = 16 | generateKeysFromSignature(signature); 17 | 18 | const stealthMetaAddress = generateStealthMetaAddressFromKeys({ 19 | spendingPublicKey, 20 | viewingPublicKey 21 | }); 22 | 23 | return stealthMetaAddress; 24 | } 25 | 26 | export default generateStealthMetaAddressFromSignature; 27 | -------------------------------------------------------------------------------- /src/utils/helpers/generateStealthMetaAddressFromKeys.ts: -------------------------------------------------------------------------------- 1 | import type { HexString } from '../crypto/types'; 2 | import isValidPublicKey from './isValidPublicKey'; 3 | 4 | /** 5 | * Concatenates the spending and viewing public keys to create a stealth meta address. 6 | * @param spendingPublicKey 7 | * @param viewingPublicKey 8 | * @returns The stealth meta address as a hexadecimal string. 9 | */ 10 | function generateStealthMetaAddressFromKeys({ 11 | spendingPublicKey, 12 | viewingPublicKey 13 | }: { 14 | spendingPublicKey: HexString; 15 | viewingPublicKey: HexString; 16 | }): HexString { 17 | if (!isValidPublicKey(spendingPublicKey)) { 18 | throw new Error('Invalid spending public key'); 19 | } 20 | 21 | if (!isValidPublicKey(viewingPublicKey)) { 22 | throw new Error('Invalid viewing public key'); 23 | } 24 | 25 | const stealthMetaAddress: HexString = `0x${spendingPublicKey.slice( 26 | 2 27 | )}${viewingPublicKey.slice(2)}`; 28 | 29 | return stealthMetaAddress; 30 | } 31 | 32 | export default generateStealthMetaAddressFromKeys; 33 | -------------------------------------------------------------------------------- /src/lib/actions/watchAnnouncementsForUser/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | GetPollOptions, 3 | Transport, 4 | WatchContractEventReturnType 5 | } from 'viem'; 6 | import type { GetAnnouncementsForUserParams } from '..'; 7 | import type { EthAddress } from '../../..'; 8 | import type { 9 | AnnouncementArgs, 10 | AnnouncementLog 11 | } from '../getAnnouncements/types'; 12 | 13 | export type WatchAnnouncementsForUserPollingOptions = GetPollOptions; 14 | 15 | export type WatchAnnouncementsForUserParams = { 16 | /** The address of the ERC5564 contract. */ 17 | ERC5564Address: EthAddress; 18 | /** The arguments to filter the announcements. */ 19 | args: AnnouncementArgs; 20 | /** The callback function to handle the filtered announcement logs. */ 21 | handleLogsForUser: (logs: AnnouncementLog[]) => T; 22 | /** Optional polling options */ 23 | pollOptions?: WatchAnnouncementsForUserPollingOptions; 24 | } & Omit; 25 | 26 | export type WatchAnnouncementsForUserReturnType = WatchContractEventReturnType; 27 | -------------------------------------------------------------------------------- /examples/sendToAndTransferFromStealthAddress/index.tsx: -------------------------------------------------------------------------------- 1 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom/client'; 4 | import { WagmiProvider } from 'wagmi'; 5 | import { http, createConfig } from 'wagmi'; 6 | import { sepolia } from 'wagmi/chains'; 7 | import StealthActionsExample from './components/stealth-actions-example'; 8 | 9 | export const RPC_URL = 10 | import.meta.env.VITE_RPC_URL || 'https://1rpc.io/sepolia'; 11 | if (!RPC_URL) throw new Error('VITE_RPC_URL is required'); 12 | 13 | export const config = createConfig({ 14 | chains: [sepolia], 15 | transports: { 16 | [sepolia.id]: http(RPC_URL) 17 | } 18 | }); 19 | 20 | const queryClient = new QueryClient(); 21 | 22 | const App = () => { 23 | return ( 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | }; 31 | 32 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 33 | 34 | ); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ScopeLift 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeysOnBehalf/types.ts: -------------------------------------------------------------------------------- 1 | import type { EthAddress, VALID_SCHEME_ID } from '../../../utils'; 2 | import type { ClientParams } from '../../stealthClient/types'; 3 | import type { PreparePayload } from '../types'; 4 | 5 | export type RegisterKeysOnBehalfArgs = { 6 | registrant: EthAddress; // The address of the user for whom keys are being registered. TODO: this can also be an ens name. 7 | schemeId: VALID_SCHEME_ID; // The scheme ID as per the ERC6538 specification. 8 | stealthMetaAddress: `0x${string}`; // The stealth meta-address to be registered on behalf. 9 | signature: `0x${string}`; // The signature authorizing the registration. 10 | }; 11 | 12 | export type PrepareRegisterKeysOnBehalfParams = { 13 | ERC6538Address: EthAddress; // The address of the ERC6538 contract. 14 | args: RegisterKeysOnBehalfArgs; // The arguments for the registerKeysOnBehalf function. 15 | account: EthAddress; // The address of the account making the call. 16 | clientParams?: ClientParams; // Optional client parameters for direct function usage. 17 | }; 18 | 19 | export type PrepareRegisterKeysOnBehalfReturnType = PreparePayload; 20 | -------------------------------------------------------------------------------- /src/utils/helpers/test/isValidPublicKey.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { toHex } from 'viem'; 3 | import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; 4 | import isValidPublicKey from '../isValidPublicKey'; 5 | 6 | describe('isValidPublicKey', () => { 7 | const VALID_STEALTH_META_ADDRESS = 8 | '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 9 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 10 | 11 | const { 12 | spendingPublicKey: _spendingPublicKey, 13 | viewingPublicKey: _viewingPublicKey 14 | } = parseKeysFromStealthMetaAddress({ 15 | stealthMetaAddress: VALID_STEALTH_META_ADDRESS, 16 | schemeId 17 | }); 18 | 19 | const spendingPublicKey = toHex(_spendingPublicKey); 20 | 21 | test('should return true for a valid public key', () => { 22 | expect(isValidPublicKey(spendingPublicKey)).toBe(true); 23 | }); 24 | 25 | test('should return false for an invalid public key', () => { 26 | // Invalid public key 27 | const invalidPublicKey = '0x02a7'; 28 | expect(isValidPublicKey(invalidPublicKey)).toBe(false); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/utils/helpers/generateRandomStealthMetaAddress.ts: -------------------------------------------------------------------------------- 1 | import { getPublicKey, utils } from '@noble/secp256k1'; 2 | import { bytesToHex } from 'viem'; 3 | import type { HexString } from '../crypto/types'; 4 | 5 | function generateRandomStealthMetaAddress() { 6 | // Generate random spending and viewing private keys 7 | const spendingPrivateKey = utils.randomPrivateKey(); 8 | const viewingPrivateKey = utils.randomPrivateKey(); 9 | 10 | // Derive the public keys from the private keys 11 | const spendingPublicKey = bytesToHex(getPublicKey(spendingPrivateKey, true)); 12 | const viewingPublicKey = bytesToHex(getPublicKey(viewingPrivateKey, true)); 13 | 14 | // Concatenate the spending and viewing public keys for the meta-address 15 | const stealthMetaAddress = (spendingPublicKey + 16 | viewingPublicKey.substring(2)) as HexString; 17 | 18 | const stealthMetaAddressURI = `st:eth:${stealthMetaAddress}`; 19 | 20 | return { 21 | spendingPrivateKey: bytesToHex(spendingPrivateKey), 22 | spendingPublicKey, 23 | stealthMetaAddress, 24 | stealthMetaAddressURI, 25 | viewingPrivateKey: bytesToHex(viewingPrivateKey), 26 | viewingPublicKey 27 | }; 28 | } 29 | 30 | export default generateRandomStealthMetaAddress; 31 | -------------------------------------------------------------------------------- /.github/workflows/claude-assistant.yml: -------------------------------------------------------------------------------- 1 | name: Claude PR Assistant 2 | 3 | on: 4 | issue_comment: 5 | types: [created] 6 | pull_request_review_comment: 7 | types: [created] 8 | issues: 9 | types: [opened, assigned] 10 | pull_request_review: 11 | types: [submitted] 12 | 13 | jobs: 14 | claude-code-action: 15 | if: | 16 | (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || 17 | (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || 18 | (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || 19 | (github.event_name == 'issues' && contains(github.event.issue.body, '@claude')) 20 | runs-on: ubuntu-latest 21 | permissions: 22 | contents: read 23 | pull-requests: read 24 | issues: read 25 | id-token: write 26 | steps: 27 | - name: Checkout repository 28 | uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 1 31 | 32 | - name: Run Claude PR Action 33 | uses: anthropics/claude-code-action@beta 34 | with: 35 | anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} 36 | timeout_minutes: "60" -------------------------------------------------------------------------------- /examples/getStealthMetaAddress/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERC6538_CONTRACT_ADDRESS, 3 | VALID_SCHEME_ID, 4 | createStealthClient, 5 | getStealthMetaAddress 6 | } from '@scopelift/stealth-address-sdk'; 7 | 8 | // Example stealth client parameters 9 | const chainId = 11155111; // Example chain ID for Sepolia 10 | const rpcUrl = process.env.RPC_URL; // Use your env rpc url that aligns with the chainId; 11 | if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); 12 | 13 | // Initialize the stealth client 14 | const stealthClient = createStealthClient({ chainId, rpcUrl }); 15 | 16 | const ERC6538Address = ERC6538_CONTRACT_ADDRESS; 17 | 18 | // Example registrant 19 | const registrant = '0xYourRegistrantAddress'; // can also be an ens name 20 | 21 | // Example getting a valid scheme id 22 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 23 | 24 | const stealthMetaAddress = await stealthClient.getStealthMetaAddress({ 25 | ERC6538Address, 26 | registrant, 27 | schemeId 28 | }); 29 | 30 | // Alternatively, you can use the getStealthMetaAddress function directly 31 | const again = await getStealthMetaAddress({ 32 | // pass in the rpcUrl and chainId to clientParams 33 | clientParams: { rpcUrl, chainId }, 34 | ERC6538Address, 35 | registrant, 36 | schemeId 37 | }); 38 | -------------------------------------------------------------------------------- /examples/checkStealthAddress/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | VALID_SCHEME_ID, 3 | checkStealthAddress, 4 | generateRandomStealthMetaAddress, 5 | generateStealthAddress 6 | } from '@scopelift/stealth-address-sdk'; 7 | 8 | // User's keys (for example purposes, real values should be securely generated and stored) 9 | const { 10 | stealthMetaAddressURI, 11 | spendingPublicKey: userSpendingPublicKey, 12 | viewingPrivateKey: userViewingPrivateKey 13 | } = generateRandomStealthMetaAddress(); 14 | 15 | // Generate a stealth address 16 | const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ 17 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1, 18 | stealthMetaAddressURI 19 | }); 20 | 21 | console.log(`Stealth Address: ${stealthAddress}`); 22 | console.log(`Ephemeral Public Key: ${ephemeralPublicKey}`); 23 | console.log(`View Tag: ${viewTag}`); 24 | 25 | // Check if an announcement (simulated here) is for the user 26 | const isForUser = checkStealthAddress({ 27 | ephemeralPublicKey, // From the announcement 28 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1, 29 | spendingPublicKey: userSpendingPublicKey, 30 | userStealthAddress: stealthAddress, // User's known stealth address 31 | viewingPrivateKey: userViewingPrivateKey, 32 | viewTag // From the announcement 33 | }); 34 | 35 | console.log(`Is the announcement for the user? ${isForUser}`); 36 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncementsUsingSubgraph/types.ts: -------------------------------------------------------------------------------- 1 | import type { GetAnnouncementsReturnType } from '../getAnnouncements/types'; 2 | 3 | export type SubgraphAnnouncementEntity = { 4 | blockNumber: string; 5 | caller: string; 6 | ephemeralPubKey: string; 7 | id: string; 8 | metadata: string; 9 | schemeId: string; 10 | stealthAddress: string; 11 | transactionHash: string; 12 | 13 | // Additional log information 14 | blockHash: string; 15 | data: string; 16 | logIndex: string; 17 | removed: boolean; 18 | topics: string[]; 19 | transactionIndex: string; 20 | }; 21 | 22 | export type GetAnnouncementsUsingSubgraphParams = { 23 | /** The URL of the subgraph to fetch announcements from */ 24 | subgraphUrl: string; 25 | /** (Optional) The filter options to use when fetching announcements */ 26 | filter?: string; 27 | /** (Optional) The number of results to fetch per page; defaults to 1000 (max allowed by subgraph providers usually) */ 28 | pageSize?: number; 29 | }; 30 | 31 | export type GetAnnouncementsUsingSubgraphReturnType = 32 | GetAnnouncementsReturnType; 33 | 34 | export class GetAnnouncementsUsingSubgraphError extends Error { 35 | constructor( 36 | message: string, 37 | public readonly originalError?: unknown 38 | ) { 39 | super(message); 40 | this.name = 'GetAnnouncementsUsingSubgraphError'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/lib/abi/ERC5564Announcer.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | anonymous: false, 4 | inputs: [ 5 | { 6 | indexed: true, 7 | internalType: 'uint256', 8 | name: 'schemeId', 9 | type: 'uint256' 10 | }, 11 | { 12 | indexed: true, 13 | internalType: 'address', 14 | name: 'stealthAddress', 15 | type: 'address' 16 | }, 17 | { 18 | indexed: true, 19 | internalType: 'address', 20 | name: 'caller', 21 | type: 'address' 22 | }, 23 | { 24 | indexed: false, 25 | internalType: 'bytes', 26 | name: 'ephemeralPubKey', 27 | type: 'bytes' 28 | }, 29 | { 30 | indexed: false, 31 | internalType: 'bytes', 32 | name: 'metadata', 33 | type: 'bytes' 34 | } 35 | ], 36 | name: 'Announcement', 37 | type: 'event' 38 | }, 39 | { 40 | inputs: [ 41 | { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, 42 | { internalType: 'address', name: 'stealthAddress', type: 'address' }, 43 | { internalType: 'bytes', name: 'ephemeralPubKey', type: 'bytes' }, 44 | { internalType: 'bytes', name: 'metadata', type: 'bytes' } 45 | ], 46 | name: 'announce', 47 | outputs: [], 48 | stateMutability: 'nonpayable', 49 | type: 'function' 50 | } 51 | ] as const; 52 | -------------------------------------------------------------------------------- /examples/watchAnnouncementsForUser/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERC5564_CONTRACT_ADDRESS, 3 | VALID_SCHEME_ID, 4 | createStealthClient 5 | } from '@scopelift/stealth-address-sdk'; 6 | 7 | // Initialize your environment variables or configuration 8 | const chainId = 11155111; // Example chain ID 9 | const rpcUrl = process.env.RPC_URL; // Your Ethereum RPC URL 10 | if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); 11 | 12 | // User's keys and stealth address details 13 | const spendingPublicKey = '0xUserSpendingPublicKey'; 14 | const viewingPrivateKey = '0xUserViewingPrivateKey'; 15 | 16 | // The contract address of the ERC5564Announcer on your target blockchain 17 | // You can use the provided ERC5564_CONTRACT_ADDRESS get the singleton contract address for a valid chain ID 18 | const ERC5564Address = ERC5564_CONTRACT_ADDRESS; 19 | 20 | // Initialize the stealth client with your configuration 21 | const stealthClient = createStealthClient({ chainId, rpcUrl }); 22 | 23 | // Watch for announcements for the user 24 | const unwatch = await stealthClient.watchAnnouncementsForUser({ 25 | ERC5564Address, 26 | args: { 27 | schemeId: BigInt(VALID_SCHEME_ID.SCHEME_ID_1), // Your scheme ID 28 | caller: '0xYourCallingContractAddress' // Use the address of your calling contract if applicable 29 | }, 30 | spendingPublicKey, 31 | viewingPrivateKey, 32 | handleLogsForUser: logs => { 33 | console.log(logs); 34 | } // Your callback function to handle incoming logs 35 | }); 36 | 37 | // Stop watching for announcements 38 | unwatch(); 39 | -------------------------------------------------------------------------------- /src/utils/helpers/test/generateStealthMetaAddressFromSignature.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import { signMessage } from 'viem/actions'; 3 | import setupTestWallet from '../../../lib/helpers/test/setupTestWallet'; 4 | import type { SuperWalletClient } from '../../../lib/helpers/types'; 5 | import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; 6 | import type { HexString } from '../../crypto/types'; 7 | import generateStealthMetaAddressFromSignature from '../generateStealthMetaAddressFromSignature'; 8 | 9 | describe('generateStealthMetaAddressFromSignature', () => { 10 | let walletClient: SuperWalletClient; 11 | let signature: HexString; 12 | 13 | beforeAll(async () => { 14 | walletClient = await setupTestWallet(); 15 | if (!walletClient.account) throw new Error('No account found'); 16 | 17 | // Generate a signature to use in the tests 18 | signature = await signMessage(walletClient, { 19 | account: walletClient.account, 20 | message: 21 | 'Sign this message to generate your stealth address keys.\nChain ID: 31337' 22 | }); 23 | }); 24 | 25 | test('should generate a stealth meta-address from a signature', () => { 26 | const result = generateStealthMetaAddressFromSignature(signature); 27 | 28 | expect(result).toBeTruthy(); 29 | 30 | // Can parse the keys from the stealth meta-address 31 | expect(() => 32 | parseKeysFromStealthMetaAddress({ 33 | stealthMetaAddress: result, 34 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1 35 | }) 36 | ).not.toThrow(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.ts: -------------------------------------------------------------------------------- 1 | import { encodeFunctionData } from 'viem'; 2 | import { ERC6538RegistryAbi } from '../..'; 3 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 4 | import { PrepareError } from '../types'; 5 | import type { 6 | PrepareRegisterKeysOnBehalfParams, 7 | PrepareRegisterKeysOnBehalfReturnType 8 | } from './types'; 9 | 10 | async function prepareRegisterKeysOnBehalf({ 11 | ERC6538Address, 12 | args, 13 | account, 14 | clientParams 15 | }: PrepareRegisterKeysOnBehalfParams): Promise { 16 | const publicClient = handleViemPublicClient(clientParams); 17 | const { registrant, schemeId, stealthMetaAddress, signature } = args; 18 | const writeArgs: [`0x${string}`, bigint, `0x${string}`, `0x${string}`] = [ 19 | registrant, 20 | BigInt(schemeId), 21 | signature, 22 | stealthMetaAddress 23 | ]; 24 | 25 | const data = encodeFunctionData({ 26 | abi: ERC6538RegistryAbi, 27 | functionName: 'registerKeysOnBehalf', 28 | args: writeArgs 29 | }); 30 | 31 | try { 32 | await publicClient.simulateContract({ 33 | account, 34 | address: ERC6538Address, 35 | abi: ERC6538RegistryAbi, 36 | functionName: 'registerKeysOnBehalf', 37 | args: writeArgs 38 | }); 39 | 40 | return { 41 | to: ERC6538Address, 42 | account, 43 | data 44 | }; 45 | } catch (error) { 46 | throw new PrepareError(`Failed to prepare contract call: ${error}`); 47 | } 48 | } 49 | 50 | export default prepareRegisterKeysOnBehalf; 51 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncementsForUser/types.ts: -------------------------------------------------------------------------------- 1 | import type { EthAddress } from '../../..'; 2 | import type { ClientParams } from '../../stealthClient/types'; 3 | import type { AnnouncementLog } from '../getAnnouncements/types'; 4 | 5 | export type GetAnnouncementsForUserParams = { 6 | announcements: AnnouncementLog[]; 7 | spendingPublicKey: `0x${string}`; 8 | viewingPrivateKey: `0x${string}`; 9 | clientParams?: ClientParams; 10 | excludeList?: EthAddress[]; // Optional: list of "from" values (msg.sender) to exclude 11 | includeList?: EthAddress[]; // Optional: list of "from" values (msg.sender) to include 12 | }; 13 | 14 | export type GetAnnouncementsForUserReturnType = AnnouncementLog[]; 15 | 16 | export type ProcessAnnouncementParams = Omit< 17 | GetAnnouncementsForUserParams, 18 | 'announcements' | 'excludeList' | 'includeList' 19 | > & { 20 | excludeList: Set; 21 | includeList: Set; 22 | }; 23 | 24 | export type ProcessAnnouncementReturnType = AnnouncementLog | null; 25 | 26 | export class FromValueNotFoundError extends Error { 27 | constructor( 28 | message = 'The "from" value could not be retrieved for a transaction.' 29 | ) { 30 | super(message); 31 | this.name = 'FromValueNotFoundError'; 32 | Object.setPrototypeOf(this, FromValueNotFoundError.prototype); 33 | } 34 | } 35 | 36 | export class TransactionHashRequiredError extends Error { 37 | constructor(message = 'The transaction hash is required.') { 38 | super(message); 39 | this.name = 'TransactionHashRequiredError'; 40 | Object.setPrototypeOf(this, TransactionHashRequiredError.prototype); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/crypto/computeStealthKey.ts: -------------------------------------------------------------------------------- 1 | import { CURVE, getSharedSecret } from '@noble/secp256k1'; 2 | import { hexToBytes } from 'viem'; 3 | import { 4 | getHashedSharedSecret, 5 | handleSchemeId 6 | } from './generateStealthAddress'; 7 | import type { HexString, IComputeStealthKeyParams } from './types'; 8 | 9 | function computeStealthKey({ 10 | ephemeralPublicKey, 11 | schemeId, 12 | spendingPrivateKey, 13 | viewingPrivateKey 14 | }: IComputeStealthKeyParams): HexString { 15 | handleSchemeId(schemeId); 16 | 17 | const sharedSecret = getSharedSecret( 18 | hexToBytes(viewingPrivateKey), 19 | hexToBytes(ephemeralPublicKey) 20 | ); 21 | 22 | const hashedSharedSecret = getHashedSharedSecret({ sharedSecret, schemeId }); 23 | 24 | const spendingPrivateKeyBigInt = BigInt(spendingPrivateKey); 25 | const hashedSecretBigInt = BigInt(hashedSharedSecret); 26 | 27 | // Compute the stealth private key by summing the spending private key and the hashed shared secret. 28 | const stealthPrivateKeyBigInt = addPriv({ 29 | a: spendingPrivateKeyBigInt, 30 | b: hashedSecretBigInt 31 | }); 32 | 33 | const stealthPrivateKeyHex = `0x${stealthPrivateKeyBigInt 34 | .toString(16) 35 | .padStart(64, '0')}` as HexString; 36 | 37 | return stealthPrivateKeyHex; 38 | } 39 | 40 | // Adds two private key scalars, ensuring the resulting key is within the elliptic curve's valid scalar range (modulo the curve's order). 41 | function addPriv({ a, b }: { a: bigint; b: bigint }) { 42 | const curveOrderBigInt = BigInt(CURVE.n); 43 | return (a + b) % curveOrderBigInt; 44 | } 45 | 46 | export { addPriv }; 47 | export default computeStealthKey; 48 | -------------------------------------------------------------------------------- /src/utils/helpers/test/generateKeysFromSignature.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import { signMessage } from 'viem/actions'; 3 | import setupTestWallet from '../../../lib/helpers/test/setupTestWallet'; 4 | import type { SuperWalletClient } from '../../../lib/helpers/types'; 5 | import type { HexString } from '../../crypto/types'; 6 | import generateKeysFromSignature from '../generateKeysFromSignature'; 7 | import isValidPublicKey from '../isValidPublicKey'; 8 | 9 | describe('generateKeysFromSignature', () => { 10 | let walletClient: SuperWalletClient; 11 | let signature: HexString; 12 | 13 | beforeAll(async () => { 14 | walletClient = await setupTestWallet(); 15 | if (!walletClient.account) throw new Error('No account found'); 16 | // Generate a signature to use in the tests 17 | signature = await signMessage(walletClient, { 18 | account: walletClient.account, 19 | message: 20 | 'Sign this message to generate your stealth address keys.\nChain ID: 31337' 21 | }); 22 | }); 23 | 24 | test('should generate valid public keys from a correct signature', () => { 25 | const result = generateKeysFromSignature(signature); 26 | expect(isValidPublicKey(result.spendingPublicKey)).toBe(true); 27 | expect(isValidPublicKey(result.viewingPublicKey)).toBe(true); 28 | expect(result.spendingPrivateKey).toBeDefined(); 29 | expect(result.viewingPrivateKey).toBeDefined(); 30 | }); 31 | 32 | test('should throw an error for an invalid signature', () => { 33 | const invalid = '0x123'; 34 | expect(() => { 35 | generateKeysFromSignature(invalid); 36 | }).toThrow(`Invalid signature: ${invalid}`); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/scripts/deployContract.ts: -------------------------------------------------------------------------------- 1 | import type { ERC5564AnnouncerAbi, ERC6538RegistryAbi } from '..'; 2 | import setupTestWallet from '../lib/helpers/test/setupTestWallet'; 3 | 4 | /** 5 | * Deploys a specified contract 6 | * @param param0 - The contract address, abi, and optional name 7 | * @property {`0x${string}`} address - The contract address as a reference to be able to get the bytecode 8 | * @property {typeof ERC5564AnnouncerAbi | typeof ERC6538RegistryAbi} abi - The contract ABI 9 | * @property {`0x${string}`} bytecode - The contract bytecode 10 | * @property {string} name Optional contract name for logging 11 | * @returns {Promise<`0x${string}`>} - The address of the deployed contract 12 | */ 13 | const deployContract = async ({ 14 | abi, 15 | bytecode, 16 | name 17 | }: { 18 | abi: typeof ERC5564AnnouncerAbi | typeof ERC6538RegistryAbi; 19 | bytecode: `0x${string}`; 20 | name: string; 21 | }): Promise<{ 22 | address: `0x${string}`; 23 | deployBlock: bigint; 24 | }> => { 25 | const walletClient = await setupTestWallet(); 26 | if (!walletClient.account || !walletClient.chain) { 27 | throw new Error('No account or chain found'); 28 | } 29 | 30 | const hash = await walletClient.deployContract({ 31 | account: walletClient.account, 32 | chain: walletClient.chain, 33 | abi, 34 | bytecode, 35 | gas: BigInt(1_000_000) 36 | }); 37 | 38 | const { contractAddress, blockNumber } = 39 | await walletClient.waitForTransactionReceipt({ hash }); 40 | 41 | if (!contractAddress) { 42 | throw new Error(`Failed to deploy ${name} contract`); 43 | } 44 | 45 | return { address: contractAddress, deployBlock: blockNumber }; 46 | }; 47 | 48 | export default deployContract; 49 | -------------------------------------------------------------------------------- /src/utils/helpers/test/generateStealthMetaAddressFromKeys.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { toHex } from 'viem'; 3 | import { VALID_SCHEME_ID, parseKeysFromStealthMetaAddress } from '../../crypto'; 4 | import generateStealthMetaAddressFromKeys from '../generateStealthMetaAddressFromKeys'; 5 | 6 | const STEALTH_META_ADDRESS = 7 | '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 8 | 9 | describe('getStealthMetaAddressFromKeys', () => { 10 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 11 | const { 12 | spendingPublicKey: _spendingPublicKey, 13 | viewingPublicKey: _viewingPublicKey 14 | } = parseKeysFromStealthMetaAddress({ 15 | stealthMetaAddress: STEALTH_META_ADDRESS, 16 | schemeId 17 | }); 18 | 19 | const spendingPublicKey = toHex(_spendingPublicKey); 20 | const viewingPublicKey = toHex(_viewingPublicKey); 21 | 22 | test('should return the correct stealth meta address', () => { 23 | const expected = STEALTH_META_ADDRESS; 24 | const actual = generateStealthMetaAddressFromKeys({ 25 | spendingPublicKey, 26 | viewingPublicKey 27 | }); 28 | expect(actual).toBe(expected); 29 | }); 30 | 31 | test('should throw an error if the spending public key is invalid', () => { 32 | // Invalid compressed public key 33 | const spendingPublicKey = '0x02a7'; 34 | // Valid compressed public key 35 | const viewingPublicKey = '0x03b8'; 36 | expect(() => 37 | generateStealthMetaAddressFromKeys({ 38 | spendingPublicKey, 39 | viewingPublicKey 40 | }) 41 | ).toThrow('Invalid spending public key'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/lib/helpers/test/setupTestWallet.ts: -------------------------------------------------------------------------------- 1 | import { http, createWalletClient, publicActions } from 'viem'; 2 | import { privateKeyToAccount } from 'viem/accounts'; 3 | import { foundry } from 'viem/chains'; 4 | import { getChain as _getChain } from '../chains'; 5 | import type { SuperWalletClient } from '../types'; 6 | import { getChainInfo, getRpcUrl } from './setupTestEnv'; 7 | 8 | export const ANVIL_DEFAULT_PRIVATE_KEY = 9 | '0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'; 10 | 11 | /** 12 | * Initializes and configures a wallet client for testing purposes. 13 | * Defaults to local anvil node usage or, alternatively, set the PRIVATE_KEY environment variable to use a remote RPC URL. 14 | * @returns {SuperWalletClient} A configured wallet client. 15 | */ 16 | const setupTestWallet = async (): Promise => { 17 | const { chain, chainId } = await getChainInfo(); 18 | // Retrieve the account from the private key set in environment variables if provided. 19 | const account = getAccount(chainId); 20 | const rpcUrl = getRpcUrl(); 21 | 22 | return createWalletClient({ 23 | account, 24 | chain, 25 | transport: http(rpcUrl) 26 | }).extend(publicActions); 27 | }; 28 | 29 | const getAccount = (chainId: number) => { 30 | // If using foundry anvil, use the default private key 31 | if (chainId === foundry.id) { 32 | return privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY); 33 | } 34 | 35 | // Retrieve the private key from the environment variable 36 | const privKey = process.env.PRIVATE_KEY as `0x${string}` | undefined; 37 | if (!privKey) { 38 | throw new Error( 39 | 'Missing PRIVATE_KEY environment variable; make sure to set it when using a remote RPC URL.' 40 | ); 41 | } 42 | return privateKeyToAccount(privKey); 43 | }; 44 | 45 | export { setupTestWallet, getAccount }; 46 | export default setupTestWallet; 47 | -------------------------------------------------------------------------------- /src/lib/helpers/types.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Account, 3 | Client, 4 | PublicActions, 5 | RpcSchema, 6 | Transport, 7 | WalletActions 8 | } from 'viem'; 9 | import { 10 | type Chain, 11 | arbitrum, 12 | arbitrumSepolia, 13 | base, 14 | baseSepolia, 15 | foundry, 16 | gnosis, 17 | holesky, 18 | mainnet, 19 | optimism, 20 | optimismSepolia, 21 | polygon, 22 | scroll, 23 | sepolia 24 | } from 'viem/chains'; 25 | export type VALID_CHAIN_IDS = 26 | | typeof arbitrum.id 27 | | typeof arbitrumSepolia.id 28 | | typeof base.id 29 | | typeof baseSepolia.id 30 | | typeof foundry.id 31 | | typeof gnosis.id 32 | | typeof holesky.id 33 | | typeof mainnet.id 34 | | typeof optimism.id 35 | | typeof optimismSepolia.id 36 | | typeof polygon.id 37 | | typeof scroll.id 38 | | typeof sepolia.id; 39 | 40 | // Valid chains where the ERC5564 and ERC6538 contracts are deployed 41 | // The contract addresses for each chain can be found here: https://stealthaddress.dev/contracts/deployments 42 | export const VALID_CHAINS: Record = { 43 | [arbitrum.id]: arbitrum, 44 | [arbitrumSepolia.id]: arbitrumSepolia, 45 | [base.id]: base, 46 | [baseSepolia.id]: baseSepolia, 47 | [foundry.id]: foundry, 48 | [gnosis.id]: gnosis, 49 | [holesky.id]: holesky, 50 | [mainnet.id]: mainnet, 51 | [optimism.id]: optimism, 52 | [optimismSepolia.id]: optimismSepolia, 53 | [polygon.id]: polygon, 54 | [scroll.id]: scroll, 55 | [sepolia.id]: sepolia 56 | }; 57 | 58 | // A Viem WalletClient with public actions 59 | export type SuperWalletClient< 60 | transport extends Transport = Transport, 61 | chain extends Chain | undefined = Chain | undefined, 62 | account extends Account | undefined = Account | undefined 63 | > = Client< 64 | transport, 65 | chain, 66 | account, 67 | RpcSchema, 68 | PublicActions & WalletActions 69 | >; 70 | -------------------------------------------------------------------------------- /src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts: -------------------------------------------------------------------------------- 1 | import { ERC6538RegistryAbi } from '../..'; 2 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 3 | import { 4 | GetStealthMetaAddressError, 5 | type GetStealthMetaAddressParams, 6 | type GetStealthMetaAddressReturnType 7 | } from './types'; 8 | 9 | /** 10 | * @description Retrieves a stealth meta address from the ERC-6538 Registry contract. 11 | * 12 | * The `registrant` parameter can represent different types of recipient identifiers, 13 | * including a standard Ethereum address (160-bit) or other formats like ENS names. 14 | * 15 | * @param {GetStealthMetaAddressParams} params - The parameters for fetching the stealth meta address. 16 | * - `clientParams`: (Optional if stealthClient is set up) Client parameters for stealthClient initialization. 17 | * - `ERC6538Address`: The address of the ERC-6538 Registry contract. 18 | * - `registrant`: The registrant identifier (Ethereum address or ENS name). 19 | * - `schemeId`: The ID of the stealth address scheme to use. 20 | * @returns {Promise} The stealth meta address. 21 | * 22 | * @throws {Error} If there is an error fetching the stealth meta address from the contract. 23 | */ 24 | async function getStealthMetaAddress({ 25 | clientParams, 26 | ERC6538Address, 27 | registrant, 28 | schemeId 29 | }: GetStealthMetaAddressParams): Promise { 30 | const publicClient = handleViemPublicClient(clientParams); 31 | try { 32 | return await publicClient.readContract({ 33 | address: ERC6538Address, 34 | functionName: 'stealthMetaAddressOf', 35 | args: [registrant, BigInt(schemeId)], 36 | abi: ERC6538RegistryAbi 37 | }); 38 | } catch (error) { 39 | throw new GetStealthMetaAddressError( 40 | `Error getting stealth meta address: ${error}` 41 | ); 42 | } 43 | } 44 | 45 | export default getStealthMetaAddress; 46 | -------------------------------------------------------------------------------- /src/lib/helpers/test/setupTestWallet.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; 2 | import { privateKeyToAccount } from 'viem/accounts'; 3 | import { VALID_CHAINS } from '../types'; 4 | import { ANVIL_DEFAULT_PRIVATE_KEY } from './setupTestWallet'; 5 | 6 | describe('setupTestWallet', async () => { 7 | const { setupTestWallet, getAccount } = await import('./setupTestWallet'); 8 | 9 | // Clean up the environment variables before each test 10 | beforeEach(() => { 11 | process.env.USE_FORK = undefined; 12 | process.env.RPC_URL = undefined; 13 | process.env.PRIVATE_KEY = undefined; 14 | }); 15 | 16 | afterEach(() => { 17 | process.env.USE_FORK = undefined; 18 | process.env.RPC_URL = undefined; 19 | process.env.PRIVATE_KEY = undefined; 20 | }); 21 | 22 | test('uses PRIVATE_KEY environment variable when not using foundry', () => { 23 | const ANOTHER_ANVIL_PRIVATE_KEY = 24 | '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; 25 | process.env.PRIVATE_KEY = ANOTHER_ANVIL_PRIVATE_KEY; 26 | const chainId = VALID_CHAINS[11155111].id; 27 | const account = getAccount(chainId); 28 | expect(account.address).toBeDefined(); 29 | expect(account.address).not.toBe( 30 | privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY).address 31 | ); 32 | }); 33 | 34 | test('throws an error when the PRIVATE_KEY environment variable is not set', async () => { 35 | // Set the fork env variables 36 | process.env.USE_FORK = 'true'; 37 | process.env.RPC_URL = 'http://example-rpc-url.com'; 38 | 39 | process.env.PRIVATE_KEY = undefined; 40 | const validChainId = VALID_CHAINS[11155111].id; 41 | 42 | // Mock the fetchJson function to return a valid chain ID 43 | mock.module('./setupTestEnv', () => ({ 44 | fetchJson: () => 45 | Promise.resolve({ 46 | result: validChainId 47 | }) 48 | })); 49 | 50 | expect(setupTestWallet()).rejects.toThrow( 51 | 'Missing PRIVATE_KEY environment variable; make sure to set it when using a remote RPC URL.' 52 | ); 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /examples/getAnnouncements/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERC5564_CONTRACT_ADDRESS, 3 | VALID_SCHEME_ID, 4 | createStealthClient, 5 | getAnnouncements 6 | } from '@scopelift/stealth-address-sdk'; 7 | 8 | // Example parameters 9 | const chainId = 11155111; // Example chain ID for Sepolia 10 | const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; 11 | if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); 12 | 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) 13 | 14 | // Initialize the stealth client 15 | const stealthClient = createStealthClient({ chainId, rpcUrl }); 16 | 17 | // Use the address of your calling contract if applicable 18 | const caller = '0xYourCallingContractAddress'; 19 | 20 | // Example stealth address 21 | const stealthAddress = '0xYourStealthAddress'; 22 | 23 | // Your scheme id 24 | const schemeId = BigInt(VALID_SCHEME_ID.SCHEME_ID_1); 25 | 26 | // The contract address of the ERC5564Announcer on your target blockchain 27 | // You can use the provided ERC5564_CONTRACT_ADDRESS get the singleton contract address for a valid chain ID 28 | const ERC5564Address = ERC5564_CONTRACT_ADDRESS; 29 | 30 | async function fetchAnnouncements() { 31 | if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); 32 | 33 | // Example call to getAnnouncements action on the stealth client 34 | // Adjust parameters according to your requirements 35 | const announcements = await stealthClient.getAnnouncements({ 36 | ERC5564Address, 37 | args: { 38 | schemeId, 39 | caller 40 | // Additional args for filtering, if necessary 41 | }, 42 | fromBlock 43 | // toBlock: 'latest', 44 | }); 45 | 46 | // Alternatively, you can use the getAnnouncements function directly 47 | await getAnnouncements({ 48 | // pass in the rpcUrl and chainId to clientParams 49 | clientParams: { rpcUrl, chainId }, 50 | ERC5564Address, 51 | args: { 52 | schemeId, 53 | caller, 54 | stealthAddress 55 | } 56 | }); 57 | 58 | console.log('Fetched announcements:', announcements); 59 | } 60 | 61 | fetchAnnouncements().catch(console.error); 62 | -------------------------------------------------------------------------------- /src/utils/crypto/checkStealthAddress.ts: -------------------------------------------------------------------------------- 1 | import { getSharedSecret } from '@noble/secp256k1'; 2 | import { hexToBytes } from 'viem'; 3 | import { 4 | getHashedSharedSecret, 5 | getStealthPublicKey, 6 | getViewTag, 7 | handleSchemeId, 8 | publicKeyToAddress 9 | } from '.'; 10 | import type { ICheckStealthAddressParams } from './types'; 11 | 12 | /** 13 | * @description Checks if a given announcement is intended for the user. 14 | * @param {ICheckStealthAddressParams} params Parameters for checking if the announcement is intended for the user: 15 | * - `ephemeralPublicKey`: The ephemeral public key from the announcement. 16 | * - `spendingPublicKey`: The user's spending public key. 17 | * - `userStealthAddress`: The user's stealth address, used to verify the derived stealth address. 18 | * - `viewingPrivateKey`: The user's viewing private key. 19 | * - `viewTag`: The view tag from the announcement, used to quickly filter announcements. 20 | * - `schemeId`: The scheme ID. 21 | * @returns {boolean} True if the derived stealth address matches the user's stealth address, indicating 22 | * the announcement is intended for the user; false otherwise. 23 | */ 24 | function checkStealthAddress({ 25 | ephemeralPublicKey, 26 | schemeId, 27 | spendingPublicKey, 28 | userStealthAddress, 29 | viewingPrivateKey, 30 | viewTag 31 | }: ICheckStealthAddressParams) { 32 | handleSchemeId(schemeId); 33 | 34 | const sharedSecret = getSharedSecret( 35 | hexToBytes(viewingPrivateKey), 36 | hexToBytes(ephemeralPublicKey) 37 | ); 38 | 39 | const hashedSharedSecret = getHashedSharedSecret({ sharedSecret, schemeId }); 40 | 41 | const computedViewTag = getViewTag({ hashedSharedSecret, schemeId }); 42 | 43 | if (computedViewTag !== viewTag) { 44 | // View tags do not match; this announcement is not for the user 45 | return false; 46 | } 47 | 48 | const stealthPublicKey = getStealthPublicKey({ 49 | spendingPublicKey: hexToBytes(spendingPublicKey), 50 | hashedSharedSecret, 51 | schemeId 52 | }); 53 | 54 | // Derive the stealth address from the stealth public key 55 | const stealthAddress = publicKeyToAddress({ 56 | publicKey: stealthPublicKey, 57 | schemeId 58 | }); 59 | 60 | return stealthAddress.toLowerCase() === userStealthAddress.toLowerCase(); 61 | } 62 | 63 | export default checkStealthAddress; 64 | -------------------------------------------------------------------------------- /src/utils/crypto/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Address } from 'viem'; 2 | 3 | export enum VALID_SCHEME_ID { 4 | SCHEME_ID_1 = 1 5 | } 6 | 7 | export type HexString = `0x${string}`; 8 | export type EthAddress = Address; 9 | 10 | /** 11 | * Represents the output of the generateStealthAddress function, 12 | * containing the stealth address, ephemeralPublicKey, and viewTag according to the spec. 13 | */ 14 | export type GenerateStealthAddressReturnType = { 15 | /** The generated stealth address. */ 16 | stealthAddress: EthAddress; 17 | /** The ephemeral public key used to generate the stealth address. */ 18 | ephemeralPublicKey: HexString; 19 | /** The view tag derived from the hashed shared secret. */ 20 | viewTag: HexString; 21 | }; 22 | 23 | export interface IGenerateStealthAddressParams { 24 | /** the stealth meta address in format: "" */ 25 | stealthMetaAddressURI: string; 26 | /** the schemeId to use for the stealth address generation; defaults to use schemeId 1 */ 27 | schemeId?: VALID_SCHEME_ID; 28 | /** the ephemeral private key to use for the stealth address generation; defaults to generate a new one */ 29 | ephemeralPrivateKey?: Uint8Array; 30 | } 31 | 32 | export type Hex = Uint8Array | HexString; 33 | 34 | export interface IParseSpendAndViewKeysReturnType { 35 | spendingPublicKey: Hex; 36 | viewingPublicKey: Hex; 37 | } 38 | 39 | export interface IComputeStealthKeyParams { 40 | /** The viewing private key. */ 41 | viewingPrivateKey: HexString; 42 | /** The spending private key. */ 43 | spendingPrivateKey: HexString; 44 | /** The ephemeral public key from the announcement. */ 45 | ephemeralPublicKey: HexString; 46 | /** The schemeId to use for the stealth address generation. */ 47 | schemeId: VALID_SCHEME_ID; 48 | } 49 | 50 | export interface ICheckStealthAddressParams { 51 | /** The stealth address of the user. */ 52 | userStealthAddress: EthAddress; 53 | /** The view tag from the announcement. */ 54 | viewTag: HexString; 55 | /** The ephemeral public key from the announcement. */ 56 | ephemeralPublicKey: HexString; 57 | /** The spending public key of the user. */ 58 | spendingPublicKey: HexString; 59 | /** The viewing private key of the user. */ 60 | viewingPrivateKey: HexString; 61 | /** The scheme ID of the announcement. */ 62 | schemeId: VALID_SCHEME_ID; 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.ts: -------------------------------------------------------------------------------- 1 | import { encodeFunctionData } from 'viem'; 2 | import { ERC6538RegistryAbi } from '../..'; 3 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 4 | import { PrepareError } from '../types'; 5 | import type { 6 | PrepareRegisterKeysParams, 7 | PrepareRegisterKeysReturnType 8 | } from './types'; 9 | 10 | /** 11 | * Prepares the payload for registering keys (setting the stealth meta-address) by simulating the contract call. 12 | * This function generates the necessary payload for signing and sending a transaction. 13 | * 14 | * @param {PrepareRegisterKeysParams} params - Parameters for preparing the key registration. 15 | * @property {EthAddress} ERC6538Address - The Ethereum address of the ERC6538 contract. 16 | * @property {VALID_SCHEME_ID} schemeId - The scheme ID as per the ERC6538 specification. 17 | * @property {`0x${string}`} stealthMetaAddress - The stealth meta-address to be registered. 18 | * @property {`0x${string}`} account - The address of the account. 19 | * @property {ClientParams} [clientParams] - Optional client parameters for direct function usage. 20 | * 21 | * @returns {Promise} - Returns a promise that resolves to the prepared key registration payload. 22 | * 23 | * @throws {PrepareError} - Throws a PrepareError if the contract call simulation fails. 24 | */ 25 | async function prepareRegisterKeys({ 26 | ERC6538Address, 27 | schemeId, 28 | stealthMetaAddress, 29 | account, 30 | clientParams 31 | }: PrepareRegisterKeysParams): Promise { 32 | const publicClient = handleViemPublicClient(clientParams); 33 | const args: [bigint, `0x${string}`] = [BigInt(schemeId), stealthMetaAddress]; 34 | 35 | const data = encodeFunctionData({ 36 | abi: ERC6538RegistryAbi, 37 | functionName: 'registerKeys', 38 | args 39 | }); 40 | 41 | // Simulate the contract call 42 | try { 43 | await publicClient.simulateContract({ 44 | account, 45 | address: ERC6538Address, 46 | abi: ERC6538RegistryAbi, 47 | functionName: 'registerKeys', 48 | args 49 | }); 50 | } catch (error) { 51 | throw new PrepareError(`Failed to prepare contract call: ${error}`); 52 | } 53 | 54 | return { 55 | to: ERC6538Address, 56 | account, 57 | data 58 | }; 59 | } 60 | 61 | export default prepareRegisterKeys; 62 | -------------------------------------------------------------------------------- /src/utils/helpers/generateKeysFromSignature.ts: -------------------------------------------------------------------------------- 1 | import { getPublicKey } from '@noble/secp256k1'; 2 | import { bytesToHex, hexToBytes, isHex, keccak256 } from 'viem'; 3 | import type { HexString } from '../crypto/types'; 4 | 5 | /** 6 | * Generates spending and viewing public and private keys from a signature. 7 | * @param signature as a hexadecimal string. 8 | * @returns The spending and viewing public and private keys as hexadecimal strings. 9 | */ 10 | function generateKeysFromSignature(signature: HexString): { 11 | spendingPublicKey: HexString; 12 | spendingPrivateKey: HexString; 13 | viewingPublicKey: HexString; 14 | viewingPrivateKey: HexString; 15 | } { 16 | if (!isValidSignature(signature)) { 17 | throw new Error(`Invalid signature: ${signature}`); 18 | } 19 | 20 | // Extract signature portions 21 | const { portion1, portion2, lastByte } = extractPortions(signature); 22 | 23 | if (`0x${portion1}${portion2}${lastByte}` !== signature) { 24 | throw new Error('Signature incorrectly generated or parsed'); 25 | } 26 | 27 | // Generate private keys from the signature portions 28 | // Convert from hex to bytes to be used with the noble library 29 | const spendingPrivateKey = hexToBytes(keccak256(`0x${portion1}`)); 30 | const viewingPrivateKey = hexToBytes(keccak256(`0x${portion2}`)); 31 | 32 | // Generate the compressed public keys from the private keys 33 | const spendingPublicKey = bytesToHex(getPublicKey(spendingPrivateKey, true)); 34 | const viewingPublicKey = bytesToHex(getPublicKey(viewingPrivateKey, true)); 35 | 36 | return { 37 | spendingPublicKey, 38 | spendingPrivateKey: bytesToHex(spendingPrivateKey), 39 | viewingPublicKey, 40 | viewingPrivateKey: bytesToHex(viewingPrivateKey) 41 | }; 42 | } 43 | 44 | function isValidSignature(sig: string) { 45 | return isHex(sig) && sig.length === 132; 46 | } 47 | 48 | export function extractPortions(signature: HexString) { 49 | const startIndex = 2; // first two characters are 0x, so skip these 50 | const length = 64; // each 32 byte chunk is in hex, so 64 characters 51 | const portion1 = signature.slice(startIndex, startIndex + length); 52 | const portion2 = signature.slice( 53 | startIndex + length, 54 | startIndex + length + length 55 | ); 56 | const lastByte = signature.slice(signature.length - 2); 57 | 58 | return { portion1, portion2, lastByte }; 59 | } 60 | 61 | export default generateKeysFromSignature; 62 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncements/getAnnouncements.ts: -------------------------------------------------------------------------------- 1 | import type { GetEventArgs } from 'viem'; 2 | import { ERC5564AnnouncerAbi } from '../../abi'; 3 | import { fetchLogsInChunks } from '../../helpers/logs'; 4 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 5 | import type { 6 | AnnouncementArgs, 7 | GetAnnouncementsParams, 8 | GetAnnouncementsReturnType 9 | } from './types'; 10 | 11 | type AnnouncementFilter = GetEventArgs< 12 | typeof ERC5564AnnouncerAbi, 13 | 'Announcement' 14 | >; 15 | 16 | /** 17 | * This function queries logs for the `Announcement` event emitted by the ERC5564 contract. 18 | * 19 | * @param {GetAnnouncementsParams} params - Parameters required for fetching announcements: 20 | * - `clientParams`: Contains either an existing `PublicClient` instance or parameters to create one. 21 | * - `ERC5564Address`: The address of the ERC5564 contract emitting the announcements. 22 | * - `args`: Additional arguments to filter the logs, such as indexed parameters of the event. 23 | * - `fromBlock`: The starting block number (or tag) for log fetching. 24 | * - `toBlock`: The ending block number (or tag) for log fetching. 25 | * @returns {Promise} An array of announcement logs matching the query. 26 | */ 27 | async function getAnnouncements({ 28 | clientParams, 29 | ERC5564Address, 30 | args, 31 | fromBlock, 32 | toBlock 33 | }: GetAnnouncementsParams): Promise { 34 | const publicClient = handleViemPublicClient(clientParams); 35 | 36 | const logs = await fetchLogsInChunks({ 37 | publicClient: publicClient, 38 | abi: ERC5564AnnouncerAbi, 39 | eventName: 'Announcement', 40 | address: ERC5564Address, 41 | args: convertAnnouncementArgs(args), 42 | fromBlock, 43 | toBlock 44 | }); 45 | 46 | return logs.map(log => ({ 47 | schemeId: log.args.schemeId, 48 | stealthAddress: log.args.stealthAddress, 49 | caller: log.args.caller, 50 | ephemeralPubKey: log.args.ephemeralPubKey, 51 | metadata: log.args.metadata, 52 | ...log 53 | })); 54 | } 55 | 56 | // Helper function to convert AnnouncementArgs to the array format Viem expects 57 | function convertAnnouncementArgs(args: AnnouncementArgs) { 58 | return [ 59 | args.schemeId === undefined ? undefined : args.schemeId, 60 | args.stealthAddress === undefined ? undefined : args.stealthAddress, 61 | args.caller === undefined ? undefined : args.caller 62 | ] as AnnouncementFilter; 63 | } 64 | 65 | export default getAnnouncements; 66 | -------------------------------------------------------------------------------- /src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Address } from 'viem'; 3 | import { 4 | ERC6538RegistryAbi, 5 | VALID_SCHEME_ID, 6 | generateRandomStealthMetaAddress 7 | } from '../../..'; 8 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 9 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 10 | import type { SuperWalletClient } from '../../helpers/types'; 11 | import type { StealthActions } from '../../stealthClient/types'; 12 | import { GetStealthMetaAddressError } from './types'; 13 | 14 | describe('getStealthMetaAddress', () => { 15 | let stealthClient: StealthActions; 16 | let ERC6538Address: Address; 17 | let walletClient: SuperWalletClient; 18 | let registrant: Address | undefined; 19 | 20 | // Generate a random stealth meta address just for testing purposes 21 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 22 | const { stealthMetaAddress } = generateRandomStealthMetaAddress(); 23 | 24 | beforeAll(async () => { 25 | // Set up the test environment 26 | ({ stealthClient, ERC6538Address } = await setupTestEnv()); 27 | walletClient = await setupTestWallet(); 28 | 29 | // Register the stealth meta address 30 | registrant = walletClient.account?.address; 31 | if (!registrant) throw new Error('No registrant address found'); 32 | 33 | const hash = await walletClient.writeContract({ 34 | address: ERC6538Address, 35 | functionName: 'registerKeys', 36 | args: [BigInt(schemeId), stealthMetaAddress], 37 | abi: ERC6538RegistryAbi, 38 | chain: walletClient.chain, 39 | account: registrant 40 | }); 41 | 42 | await walletClient.waitForTransactionReceipt({ hash }); 43 | }); 44 | 45 | test('should return the stealth meta address for a given registrant and scheme ID', async () => { 46 | if (!registrant) throw new Error('No registrant address found'); 47 | 48 | const result = await stealthClient.getStealthMetaAddress({ 49 | ERC6538Address, 50 | registrant, 51 | schemeId 52 | }); 53 | 54 | expect(result).toEqual(stealthMetaAddress); 55 | }); 56 | 57 | test('should throw an error if the stealth meta address cannot be fetched', async () => { 58 | const invalidRegistrant = '0xInvalidRegistrant'; 59 | 60 | expect( 61 | stealthClient.getStealthMetaAddress({ 62 | ERC6538Address, 63 | registrant: invalidRegistrant, 64 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1 65 | }) 66 | ).rejects.toBeInstanceOf(GetStealthMetaAddressError); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /src/lib/stealthClient/createStealthClient.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { http, createPublicClient } from 'viem'; 3 | import { foundry } from 'viem/chains'; 4 | import { LOCAL_ENDPOINT } from '../helpers/test/setupTestEnv'; 5 | import type { VALID_CHAIN_IDS } from '../helpers/types'; 6 | import createStealthClient, { 7 | handleViemPublicClient 8 | } from './createStealthClient'; 9 | import { type ClientParams, PublicClientRequiredError } from './types'; 10 | 11 | describe('createStealthClient', () => { 12 | test('throws error when invalid chain id is provided', () => { 13 | const invalidChainId = 9999; 14 | expect(() => 15 | createStealthClient({ 16 | chainId: invalidChainId as VALID_CHAIN_IDS, // Cast as valid chain to trigger error 17 | rpcUrl: LOCAL_ENDPOINT 18 | }) 19 | ).toThrow(new Error('Invalid chainId: 9999')); 20 | }); 21 | }); 22 | 23 | describe('handleViemPublicClient', () => { 24 | test('throws error when clientParams is undefined', () => { 25 | expect(() => handleViemPublicClient(undefined)).toThrow( 26 | new PublicClientRequiredError( 27 | 'publicClient or chainId and rpcUrl must be provided' 28 | ) 29 | ); 30 | }); 31 | test('returns publicClient when provided', () => { 32 | const mockPublicClient = createPublicClient({ 33 | chain: foundry, 34 | transport: http(LOCAL_ENDPOINT) 35 | }); 36 | const client = handleViemPublicClient({ publicClient: mockPublicClient }); 37 | expect(client).toBe(mockPublicClient); 38 | }); 39 | 40 | test('throws error when chainId is not set', () => { 41 | const exampleRpcUrl = 'https://example.com'; 42 | expect(() => 43 | handleViemPublicClient({ 44 | chainId: undefined as unknown as VALID_CHAIN_IDS, // Cast as valid chain to trigger error 45 | rpcUrl: exampleRpcUrl 46 | }) 47 | ).toThrow( 48 | new PublicClientRequiredError('public client could not be created.') 49 | ); 50 | }); 51 | 52 | test('throws error when clientParams does not have publicClient or both chainId and rpcUrl', () => { 53 | // Example of incorrect structure: missing 'publicClient', 'chainId', and 'rpcUrl' 54 | // Intentionally cast as ClientParams to trigger error 55 | const incorrectParams = { someKey: 'someValue' } as unknown as ClientParams; 56 | 57 | // Attempting to call the function with incorrectParams should lead to the expected error 58 | expect(() => handleViemPublicClient(incorrectParams)).toThrow( 59 | new PublicClientRequiredError( 60 | 'Either publicClient or both chainId and rpcUrl must be provided' 61 | ) 62 | ); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/lib/abi/ERC6538Registry.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { inputs: [], stateMutability: 'nonpayable', type: 'constructor' }, 3 | { inputs: [], name: 'ERC6538Registry__InvalidSignature', type: 'error' }, 4 | { 5 | anonymous: false, 6 | inputs: [ 7 | { 8 | indexed: true, 9 | internalType: 'address', 10 | name: 'registrant', 11 | type: 'address' 12 | }, 13 | { 14 | indexed: true, 15 | internalType: 'uint256', 16 | name: 'schemeId', 17 | type: 'uint256' 18 | }, 19 | { 20 | indexed: false, 21 | internalType: 'bytes', 22 | name: 'stealthMetaAddress', 23 | type: 'bytes' 24 | } 25 | ], 26 | name: 'StealthMetaAddressSet', 27 | type: 'event' 28 | }, 29 | { 30 | inputs: [], 31 | name: 'DOMAIN_SEPARATOR', 32 | outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 33 | stateMutability: 'view', 34 | type: 'function' 35 | }, 36 | { 37 | inputs: [], 38 | name: 'ERC6538REGISTRY_ENTRY_TYPE_HASH', 39 | outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }], 40 | stateMutability: 'view', 41 | type: 'function' 42 | }, 43 | { 44 | inputs: [], 45 | name: 'incrementNonce', 46 | outputs: [], 47 | stateMutability: 'nonpayable', 48 | type: 'function' 49 | }, 50 | { 51 | inputs: [{ internalType: 'address', name: 'registrant', type: 'address' }], 52 | name: 'nonceOf', 53 | outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }], 54 | stateMutability: 'view', 55 | type: 'function' 56 | }, 57 | { 58 | inputs: [ 59 | { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, 60 | { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' } 61 | ], 62 | name: 'registerKeys', 63 | outputs: [], 64 | stateMutability: 'nonpayable', 65 | type: 'function' 66 | }, 67 | { 68 | inputs: [ 69 | { internalType: 'address', name: 'registrant', type: 'address' }, 70 | { internalType: 'uint256', name: 'schemeId', type: 'uint256' }, 71 | { internalType: 'bytes', name: 'signature', type: 'bytes' }, 72 | { internalType: 'bytes', name: 'stealthMetaAddress', type: 'bytes' } 73 | ], 74 | name: 'registerKeysOnBehalf', 75 | outputs: [], 76 | stateMutability: 'nonpayable', 77 | type: 'function' 78 | }, 79 | { 80 | inputs: [ 81 | { internalType: 'address', name: 'registrant', type: 'address' }, 82 | { internalType: 'uint256', name: 'schemeId', type: 'uint256' } 83 | ], 84 | name: 'stealthMetaAddressOf', 85 | outputs: [{ internalType: 'bytes', name: '', type: 'bytes' }], 86 | stateMutability: 'view', 87 | type: 'function' 88 | } 89 | ] as const; 90 | -------------------------------------------------------------------------------- /src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | WatchAnnouncementsForUserParams, 3 | WatchAnnouncementsForUserReturnType 4 | } from '..'; 5 | import { ERC5564AnnouncerAbi, getAnnouncementsForUser } from '../..'; 6 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 7 | 8 | /** 9 | * Watches for announcement events relevant to the user. 10 | * 11 | * @template T - The return type of the handleLogsForUser callback function. 12 | * @property {EthAddress} ERC5564Address - The Ethereum address of the ERC5564 contract. 13 | * @property {AnnouncementArgs} args - Arguments to filter the announcements. 14 | * @property {(logs: AnnouncementLog[]) => T} handleLogsForUser - Callback function to handle the filtered announcement logs. 15 | * This function receives an array of AnnouncementLog and returns a generic value. 16 | * @property {WatchAnnouncementsForUserPollingOptions} [pollOptions] - Optional polling options to configure the behavior of watching announcements. 17 | * This includes configurations such as polling frequency. 18 | * @property {Omit} - Inherits all properties from GetAnnouncementsForUserParams except 'announcements'. 19 | * This typically includes cryptographic keys and filter lists for inclusion or exclusion of specific announcements. 20 | */ 21 | async function watchAnnouncementsForUser({ 22 | args, 23 | spendingPublicKey, 24 | viewingPrivateKey, 25 | ERC5564Address, 26 | clientParams, 27 | excludeList, 28 | includeList, 29 | handleLogsForUser, 30 | pollOptions 31 | }: WatchAnnouncementsForUserParams): Promise { 32 | const publicClient = handleViemPublicClient(clientParams); 33 | 34 | const unwatch = publicClient.watchContractEvent({ 35 | address: ERC5564Address, 36 | abi: ERC5564AnnouncerAbi, 37 | eventName: 'Announcement', 38 | args, 39 | onLogs: async logs => { 40 | const announcements = logs.map(log => ({ 41 | ...log, 42 | caller: log.args.caller, 43 | ephemeralPubKey: log.args.ephemeralPubKey, 44 | metadata: log.args.metadata, 45 | schemeId: log.args.schemeId, 46 | stealthAddress: log.args.stealthAddress 47 | })); 48 | 49 | const relevantAnnouncements = await getAnnouncementsForUser({ 50 | announcements, 51 | spendingPublicKey, 52 | viewingPrivateKey, 53 | clientParams: { publicClient }, 54 | excludeList, 55 | includeList 56 | }); 57 | 58 | handleLogsForUser(relevantAnnouncements); 59 | }, 60 | strict: true, 61 | ...pollOptions 62 | }); 63 | 64 | return unwatch; 65 | } 66 | 67 | export default watchAnnouncementsForUser; 68 | -------------------------------------------------------------------------------- /src/lib/actions/prepareAnnounce/prepareAnnounce.ts: -------------------------------------------------------------------------------- 1 | import { encodeFunctionData } from 'viem'; 2 | import { ERC5564AnnouncerAbi } from '../..'; 3 | import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; 4 | import { PrepareError } from '../types'; 5 | import type { PrepareAnnounceParams, PrepareAnnounceReturnType } from './types'; 6 | 7 | /** 8 | * Prepares the payload for announcing a stealth address to the ERC5564 contract. 9 | * It simulates the contract call to generate the necessary payload without actually sending a transaction. 10 | * This payload can then be used for signing and sending a transaction. 11 | * 12 | * @param {PrepareAnnounceParams} params The parameters required to prepare the announcement. 13 | * @property {EthAddress} ERC5564Address The address of the ERC5564 contract. 14 | * @property {AnnounceArgs} args The announcement details, including: 15 | * - {VALID_SCHEME_ID} schemeId The scheme id per ERC5564. 16 | * - {`0x${string}`} stealthAddress The stealth address being announced. 17 | * - {`0x${string}`} ephemeralPublicKey The ephemeral public key for this announcement. 18 | * - {`0x${string}`} metadata Additional metadata for the announcement including the view tag. 19 | * @property {`0x${string}`} account The address of the account making the announcement. 20 | * @property {ClientParams} clientParams Optional client parameters for direct function use. 21 | * @returns {Promise} The prepared announcement payload, including the transaction data. 22 | * 23 | * @throws {PrepareError} - Throws a PrepareError if the contract call simulation fails. 24 | */ 25 | async function prepareAnnounce({ 26 | ERC5564Address, 27 | args, 28 | account, 29 | clientParams 30 | }: PrepareAnnounceParams): Promise { 31 | const publicClient = handleViemPublicClient(clientParams); 32 | const { schemeId, stealthAddress, ephemeralPublicKey, metadata } = args; 33 | const schemeIdBigInt = BigInt(schemeId); 34 | const writeArgs: [bigint, `0x${string}`, `0x${string}`, `0x${string}`] = [ 35 | schemeIdBigInt, 36 | stealthAddress, 37 | ephemeralPublicKey, 38 | metadata 39 | ]; 40 | 41 | const data = encodeFunctionData({ 42 | abi: ERC5564AnnouncerAbi, 43 | functionName: 'announce', 44 | args: writeArgs 45 | }); 46 | 47 | try { 48 | await publicClient.simulateContract({ 49 | account, 50 | address: ERC5564Address, 51 | abi: ERC5564AnnouncerAbi, 52 | functionName: 'announce', 53 | args: writeArgs 54 | }); 55 | } catch (error) { 56 | throw new PrepareError(`Failed to prepare contract call: ${error}`); 57 | } 58 | 59 | return { 60 | to: ERC5564Address, 61 | account, 62 | data 63 | }; 64 | } 65 | export default prepareAnnounce; 66 | -------------------------------------------------------------------------------- /examples/getAnnouncementsForUser/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ERC5564_CONTRACT_ADDRESS, 3 | VALID_SCHEME_ID, 4 | createStealthClient 5 | } from '@scopelift/stealth-address-sdk'; 6 | 7 | // Example parameters 8 | const chainId = 11155111; // Example chain ID for Sepolia 9 | const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; 10 | if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); 11 | 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) 12 | 13 | // Initialize the stealth client 14 | const stealthClient = createStealthClient({ chainId, rpcUrl }); 15 | 16 | // Use the address of your calling contract if applicable 17 | const caller = '0xYourCallingContractAddress'; 18 | 19 | // Your scheme id 20 | const schemeId = BigInt(VALID_SCHEME_ID.SCHEME_ID_1); 21 | 22 | // The contract address of the ERC5564Announcer on your target blockchain 23 | // You can use the provided ERC5564_CONTRACT_ADDRESS get the singleton contract address for a valid chain ID 24 | const ERC5564Address = ERC5564_CONTRACT_ADDRESS; 25 | 26 | // Example keys for the user 27 | // These don't need to be from environment variables 28 | // Example spending public key 29 | const spendingPublicKey = process.env.SPENDING_PUBLIC_KEY as `0x${string}`; 30 | // Example viewing private key 31 | const viewingPrivateKey = process.env.VIEWING_PRIVATE_KEY as `0x${string}`; 32 | 33 | async function fetchAnnouncementsForUser() { 34 | // Example call to getAnnouncements action on the stealth client to get all potential announcements 35 | // Use your preferred method to get announcements if different, and 36 | // adjust parameters according to your requirements 37 | const announcements = await stealthClient.getAnnouncements({ 38 | ERC5564Address, 39 | args: { 40 | schemeId, 41 | caller 42 | // Additional args for filtering, if necessary 43 | }, 44 | fromBlock, // Optional fromBlock parameter (defaults to 0, which can be slow for many blocks) 45 | toBlock: 'latest' // Optional toBlock parameter (defaults to latest) 46 | }); 47 | 48 | // Example call to getAnnouncementsForUser action on the stealth client 49 | // Adjust parameters according to your requirements 50 | const userAnnouncements = await stealthClient.getAnnouncementsForUser({ 51 | announcements, 52 | spendingPublicKey, 53 | viewingPrivateKey, 54 | includeList: ['0xSomeEthAddress, 0xSomeOtherEthAddress'], // Optional include list to only include announcements for specific "from" addresses 55 | excludeList: ['0xEthAddressToExclude'] // Optional exclude list to exclude announcements for specific "from" addresses 56 | }); 57 | 58 | return userAnnouncements; 59 | } 60 | 61 | fetchAnnouncementsForUser().catch(console.error); 62 | -------------------------------------------------------------------------------- /src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Address, Chain, TransactionReceipt } from 'viem'; 3 | import { VALID_SCHEME_ID, generateStealthAddress } from '../../..'; 4 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 5 | import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; 6 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 7 | import type { SuperWalletClient } from '../../helpers/types'; 8 | import type { StealthActions } from '../../stealthClient/types'; 9 | import { PrepareError } from '../types'; 10 | 11 | describe('prepareAnnounce', () => { 12 | let stealthClient: StealthActions; 13 | let ERC5564Address: Address; 14 | let walletClient: SuperWalletClient; 15 | let account: Address | undefined; 16 | let chain: Chain | undefined; 17 | 18 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 19 | const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); 20 | const { stealthAddress, ephemeralPublicKey, viewTag } = 21 | generateStealthAddress({ 22 | stealthMetaAddressURI, 23 | schemeId 24 | }); 25 | 26 | const prepareArgs = { 27 | schemeId, 28 | stealthAddress, 29 | ephemeralPublicKey, 30 | metadata: viewTag 31 | }; 32 | 33 | // Transaction receipt for writing to the contract with the prepared payload 34 | let res: TransactionReceipt; 35 | 36 | beforeAll(async () => { 37 | // Set up the test environment 38 | ({ stealthClient, ERC5564Address } = await setupTestEnv()); 39 | walletClient = await setupTestWallet(); 40 | account = walletClient.account?.address; 41 | chain = walletClient.chain; 42 | 43 | if (!account) throw new Error('No account found'); 44 | if (!chain) throw new Error('No chain found'); 45 | 46 | const prepared = await stealthClient.prepareAnnounce({ 47 | account, 48 | args: prepareArgs, 49 | ERC5564Address 50 | }); 51 | 52 | // Prepare tx using viem and the prepared payload 53 | const request = await walletClient.prepareTransactionRequest({ 54 | ...prepared, 55 | chain, 56 | account 57 | }); 58 | 59 | const hash = await walletClient.sendTransaction({ 60 | ...request, 61 | chain, 62 | account 63 | }); 64 | 65 | res = await walletClient.waitForTransactionReceipt({ hash }); 66 | }); 67 | 68 | test('should throw PrepareError when given invalid params', () => { 69 | if (!account) throw new Error('No account found'); 70 | 71 | const invalidERC5564Address = '0xinvalid'; 72 | expect( 73 | stealthClient.prepareAnnounce({ 74 | account, 75 | args: prepareArgs, 76 | ERC5564Address: invalidERC5564Address 77 | }) 78 | ).rejects.toBeInstanceOf(PrepareError); 79 | }); 80 | 81 | test('should successfully announce the stealth address details using the prepare payload', () => { 82 | expect(res.status).toEqual('success'); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/lib/stealthClient/types.ts: -------------------------------------------------------------------------------- 1 | import type { PublicClient } from 'viem'; 2 | import type { 3 | GetAnnouncementsForUserParams, 4 | GetAnnouncementsParams, 5 | GetAnnouncementsReturnType, 6 | GetAnnouncementsUsingSubgraphParams, 7 | GetAnnouncementsUsingSubgraphReturnType, 8 | GetStealthMetaAddressParams, 9 | GetStealthMetaAddressReturnType, 10 | PrepareAnnounceParams, 11 | PrepareAnnounceReturnType, 12 | PrepareRegisterKeysOnBehalfParams, 13 | PrepareRegisterKeysOnBehalfReturnType, 14 | PrepareRegisterKeysParams, 15 | PrepareRegisterKeysReturnType, 16 | WatchAnnouncementsForUserParams, 17 | WatchAnnouncementsForUserReturnType 18 | } from '../actions/'; 19 | import type { VALID_CHAIN_IDS } from '../helpers/types'; 20 | 21 | export type ClientParams = 22 | | { 23 | publicClient: PublicClient; 24 | } 25 | | { 26 | chainId: VALID_CHAIN_IDS; 27 | rpcUrl: string; 28 | }; 29 | 30 | export type StealthClientInitParams = { 31 | chainId: VALID_CHAIN_IDS; 32 | rpcUrl: string; 33 | }; 34 | 35 | export type StealthClientReturnType = StealthActions; 36 | 37 | export type StealthActions = { 38 | getAnnouncements: ({ 39 | ERC5564Address, 40 | args, 41 | fromBlock, 42 | toBlock 43 | }: GetAnnouncementsParams) => Promise; 44 | getAnnouncementsUsingSubgraph: ({ 45 | subgraphUrl, 46 | filter, 47 | pageSize 48 | }: GetAnnouncementsUsingSubgraphParams) => Promise; 49 | getStealthMetaAddress: ({ 50 | ERC6538Address, 51 | registrant, 52 | schemeId 53 | }: GetStealthMetaAddressParams) => Promise; 54 | getAnnouncementsForUser: ({ 55 | announcements, 56 | spendingPublicKey, 57 | viewingPrivateKey, 58 | excludeList, 59 | includeList 60 | }: GetAnnouncementsForUserParams) => Promise; 61 | watchAnnouncementsForUser: ({ 62 | ERC5564Address, 63 | args, 64 | handleLogsForUser, 65 | spendingPublicKey, 66 | viewingPrivateKey, 67 | pollOptions 68 | }: WatchAnnouncementsForUserParams) => Promise; 69 | prepareAnnounce: ({ 70 | account, 71 | args, 72 | ERC5564Address 73 | }: PrepareAnnounceParams) => Promise; 74 | prepareRegisterKeys: ({ 75 | ERC6538Address, 76 | schemeId, 77 | stealthMetaAddress, 78 | account 79 | }: PrepareRegisterKeysParams) => Promise; 80 | prepareRegisterKeysOnBehalf: ({ 81 | ERC6538Address, 82 | args, 83 | account 84 | }: PrepareRegisterKeysOnBehalfParams) => Promise; 85 | }; 86 | 87 | export class PublicClientRequiredError extends Error { 88 | constructor(message = 'publicClient is required') { 89 | super(message); 90 | this.name = 'PublicClientRequiredError'; 91 | Object.setPrototypeOf(this, PublicClientRequiredError.prototype); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Address, Chain, TransactionReceipt } from 'viem'; 3 | import { 4 | type PrepareRegisterKeysParams, 5 | VALID_SCHEME_ID, 6 | parseStealthMetaAddressURI 7 | } from '../../..'; 8 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 9 | import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; 10 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 11 | import type { SuperWalletClient } from '../../helpers/types'; 12 | import type { StealthActions } from '../../stealthClient/types'; 13 | import { PrepareError } from '../types'; 14 | 15 | describe('prepareRegisterKeys', () => { 16 | let stealthClient: StealthActions; 17 | let ERC6538Address: Address; 18 | let walletClient: SuperWalletClient; 19 | let account: Address | undefined; 20 | let chain: Chain | undefined; 21 | 22 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 23 | const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); 24 | const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ 25 | stealthMetaAddressURI, 26 | schemeId 27 | }); 28 | 29 | // Prepare payload args 30 | let prepareArgs: PrepareRegisterKeysParams; 31 | // Transaction receipt for writing to the contract with the prepared payload 32 | let res: TransactionReceipt; 33 | 34 | beforeAll(async () => { 35 | // Set up the test environment 36 | ({ stealthClient, ERC6538Address } = await setupTestEnv()); 37 | walletClient = await setupTestWallet(); 38 | account = walletClient.account?.address; 39 | if (!account) throw new Error('No account found'); 40 | chain = walletClient.chain; 41 | if (!chain) throw new Error('No chain found'); 42 | 43 | prepareArgs = { 44 | account, 45 | ERC6538Address, 46 | schemeId, 47 | stealthMetaAddress: stealthMetaAddressToRegister 48 | } satisfies PrepareRegisterKeysParams; 49 | const prepared = await stealthClient.prepareRegisterKeys(prepareArgs); 50 | 51 | // Prepare tx using viem and the prepared payload 52 | const request = await walletClient.prepareTransactionRequest({ 53 | ...prepared, 54 | chain, 55 | account 56 | }); 57 | 58 | const hash = await walletClient.sendTransaction({ 59 | ...request, 60 | chain, 61 | account 62 | }); 63 | 64 | res = await walletClient.waitForTransactionReceipt({ hash }); 65 | }); 66 | test('should throw PrepareError when given invalid contract address', () => { 67 | const invalidERC6538Address = '0xinvalid'; 68 | expect( 69 | stealthClient.prepareRegisterKeys({ 70 | ...prepareArgs, 71 | ERC6538Address: invalidERC6538Address 72 | }) 73 | ).rejects.toBeInstanceOf(PrepareError); 74 | }); 75 | 76 | test('should successfully register a stealth meta-address using the prepare payload', () => { 77 | expect(res.status).toEqual('success'); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/lib/actions/index.ts: -------------------------------------------------------------------------------- 1 | import type { StealthActions } from '../stealthClient/types'; 2 | import getAnnouncements from './getAnnouncements/getAnnouncements'; 3 | import getAnnouncementsForUser from './getAnnouncementsForUser/getAnnouncementsForUser'; 4 | import getAnnouncementsUsingSubgraph from './getAnnouncementsUsingSubgraph/getAnnouncementsUsingSubgraph'; 5 | import getStealthMetaAddress from './getStealthMetaAddress/getStealthMetaAddress'; 6 | import prepareAnnounce from './prepareAnnounce/prepareAnnounce'; 7 | import prepareRegisterKeys from './prepareRegisterKeys/prepareRegisterKeys'; 8 | import prepareRegisterKeysOnBehalf from './prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf'; 9 | import watchAnnouncementsForUser from './watchAnnouncementsForUser/watchAnnouncementsForUser'; 10 | export { default as getAnnouncements } from './getAnnouncements/getAnnouncements'; 11 | export { default as getAnnouncementsForUser } from './getAnnouncementsForUser/getAnnouncementsForUser'; 12 | export { default as getAnnouncementsUsingSubgraph } from './getAnnouncementsUsingSubgraph/getAnnouncementsUsingSubgraph'; 13 | export { default as getStealthMetaAddress } from './getStealthMetaAddress/getStealthMetaAddress'; 14 | export { default as prepareAnnounce } from './prepareAnnounce/prepareAnnounce'; 15 | export { default as prepareRegisterKeys } from './prepareRegisterKeys/prepareRegisterKeys'; 16 | export { default as prepareRegisterKeysOnBehalf } from './prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf'; 17 | export { default as watchAnnouncementsForUser } from './watchAnnouncementsForUser/watchAnnouncementsForUser'; 18 | 19 | export { 20 | type AnnouncementArgs, 21 | type AnnouncementLog, 22 | type GetAnnouncementsParams, 23 | type GetAnnouncementsReturnType 24 | } from './getAnnouncements/types'; 25 | export { 26 | type GetStealthMetaAddressParams, 27 | type GetStealthMetaAddressReturnType 28 | } from './getStealthMetaAddress/types'; 29 | export { 30 | type GetAnnouncementsForUserParams, 31 | type GetAnnouncementsForUserReturnType 32 | } from './getAnnouncementsForUser/types'; 33 | export { 34 | type GetAnnouncementsUsingSubgraphParams, 35 | type GetAnnouncementsUsingSubgraphReturnType 36 | } from './getAnnouncementsUsingSubgraph/types'; 37 | export { 38 | type WatchAnnouncementsForUserParams, 39 | type WatchAnnouncementsForUserReturnType 40 | } from './watchAnnouncementsForUser/types'; 41 | export { 42 | type PrepareAnnounceParams, 43 | type PrepareAnnounceReturnType 44 | } from './prepareAnnounce/types'; 45 | export { 46 | type PrepareRegisterKeysParams, 47 | type PrepareRegisterKeysReturnType 48 | } from './prepareRegisterKeys/types'; 49 | export { 50 | type PrepareRegisterKeysOnBehalfParams, 51 | type PrepareRegisterKeysOnBehalfReturnType 52 | } from './prepareRegisterKeysOnBehalf/types'; 53 | 54 | export const actions: StealthActions = { 55 | getAnnouncements, 56 | getAnnouncementsForUser, 57 | getAnnouncementsUsingSubgraph, 58 | getStealthMetaAddress, 59 | prepareAnnounce, 60 | prepareRegisterKeys, 61 | prepareRegisterKeysOnBehalf, 62 | watchAnnouncementsForUser 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/crypto/test/checkStealthAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { bytesToHex } from 'viem'; 3 | import { 4 | VALID_SCHEME_ID, 5 | checkStealthAddress, 6 | generateStealthAddress, 7 | parseKeysFromStealthMetaAddress, 8 | parseStealthMetaAddressURI 9 | } from '..'; 10 | 11 | describe('checkStealthAddress', () => { 12 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 13 | const stealthMetaAddressURI = 14 | 'st:eth:0x02f1f006a160b934c1d71479ce7d57f1c4ec10018230e35ca10ab65db68e8f037b0305d4725c7784262a38af11a9aef490b1307b82b17866f08d66c38db04c946ab1'; 15 | const stealthMetaAddress = parseStealthMetaAddressURI({ 16 | stealthMetaAddressURI, 17 | schemeId 18 | }); 19 | const viewingPrivateKey = 20 | '0x2f8fcb2d1e06f52695e06a792b6d59c80a81ad70fc11b03b5236eed5cff09670'; 21 | 22 | const { stealthAddress, ephemeralPublicKey, viewTag } = 23 | generateStealthAddress({ 24 | stealthMetaAddressURI, 25 | schemeId 26 | }); 27 | 28 | const { spendingPublicKey } = parseKeysFromStealthMetaAddress({ 29 | stealthMetaAddress, 30 | schemeId 31 | }); 32 | 33 | test('successfully identifies an announcement for the user', () => { 34 | const isForUser = checkStealthAddress({ 35 | ephemeralPublicKey, 36 | schemeId, 37 | spendingPublicKey: bytesToHex(spendingPublicKey), 38 | userStealthAddress: stealthAddress, 39 | viewingPrivateKey, 40 | viewTag 41 | }); 42 | 43 | expect(isForUser).toBe(true); 44 | }); 45 | 46 | test('correctly rejects an announcement with a mismatched view tag', () => { 47 | const mismatchedViewTag = '0x123456'; // Some incorrect view tag 48 | const isForUser = checkStealthAddress({ 49 | ephemeralPublicKey, 50 | schemeId, 51 | spendingPublicKey: bytesToHex(spendingPublicKey), 52 | userStealthAddress: stealthAddress, 53 | viewingPrivateKey, 54 | viewTag: mismatchedViewTag 55 | }); 56 | 57 | expect(isForUser).toBe(false); 58 | }); 59 | 60 | test('correctly rejects an announcement for a different stealth address', () => { 61 | const differentStealthAddress = '0xverynice'; // Some incorrect stealth address 62 | const isForUser = checkStealthAddress({ 63 | ephemeralPublicKey, 64 | schemeId, 65 | spendingPublicKey: bytesToHex(spendingPublicKey), 66 | userStealthAddress: differentStealthAddress, 67 | viewingPrivateKey, 68 | viewTag 69 | }); 70 | 71 | expect(isForUser).toBe(false); 72 | }); 73 | 74 | test('matches addresses regardless of case', () => { 75 | // Test with different case variations 76 | const variations = [ 77 | stealthAddress.toLowerCase(), 78 | stealthAddress.toUpperCase() 79 | ]; 80 | 81 | for (const addressVariation of variations) { 82 | const result = checkStealthAddress({ 83 | ephemeralPublicKey, 84 | schemeId, 85 | spendingPublicKey: bytesToHex(spendingPublicKey), 86 | viewingPrivateKey, 87 | userStealthAddress: addressVariation as `0x${string}`, 88 | viewTag 89 | }); 90 | 91 | expect(result).toBe(true); 92 | } 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | # Comment in the public line in if your project uses Gatsby and not Next.js 126 | 127 | # https://nextjs.org/blog/next-9-1#public-directory-support 128 | 129 | # public 130 | 131 | # vuepress build output 132 | 133 | .vuepress/dist 134 | 135 | # vuepress v2.x temp and cache directory 136 | 137 | .temp 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | -------------------------------------------------------------------------------- /src/utils/helpers/generateSignatureForRegisterKeysOnBehalf.ts: -------------------------------------------------------------------------------- 1 | import { readContract } from 'viem/actions'; 2 | import { ERC6538RegistryAbi } from '../../lib'; 3 | import { 4 | GenerateSignatureForRegisterKeysError, 5 | type GenerateSignatureForRegisterKeysParams 6 | } from './types'; 7 | 8 | const DOMAIN_NAME = 'ERC6538Registry'; 9 | const DOMAIN_VERSION = '1.0'; 10 | const PRIMARY_TYPE = 'Erc6538RegistryEntry'; 11 | 12 | const SIGNATURE_TYPES = { 13 | [PRIMARY_TYPE]: [ 14 | { name: 'schemeId', type: 'uint256' }, 15 | { name: 'stealthMetaAddress', type: 'bytes' }, 16 | { name: 'nonce', type: 'uint256' } 17 | ] 18 | } as const; 19 | 20 | /** 21 | * Generates a typed signature for registering keys on behalf of a user (account) in the ERC6538 Registry. 22 | * 23 | * This function creates an EIP-712 compliant signature for the `registerKeysOnBehalf` function 24 | * in the ERC6538 Registry contract. It retrieves the current nonce for the account, prepares 25 | * the domain separator and message, and signs the data using the provided Viem wallet client. 26 | * 27 | * @param {GenerateSignatureForRegisterKeysParams} params - The parameters for generating the signature. 28 | * @returns {Promise<`0x${string}`>} A promise that resolves to the generated signature as a hexadecimal string. 29 | * 30 | * @throws {GenerateSignatureForRegisterKeysError} If the contract read fails or if the signing process encounters an issue. 31 | * 32 | * @example 33 | * const signature = await generateSignatureForRegisterKeysOnBehalf({ 34 | * walletClient, 35 | * account: '0x1234...5678', 36 | * ERC6538Address: '0xabcd...ef01', 37 | * chainId: 1, 38 | * schemeId: 1, 39 | * stealthMetaAddressToRegister: '0x9876...5432' 40 | * }); 41 | */ 42 | async function generateSignatureForRegisterKeysOnBehalf({ 43 | walletClient, 44 | account, 45 | ERC6538Address, 46 | chainId, 47 | schemeId, 48 | stealthMetaAddressToRegister 49 | }: GenerateSignatureForRegisterKeysParams): Promise<`0x${string}`> { 50 | try { 51 | // Get the registrant's current nonce for the signature 52 | const nonce = await readContract(walletClient, { 53 | address: ERC6538Address, 54 | abi: ERC6538RegistryAbi, 55 | functionName: 'nonceOf', 56 | args: [account] 57 | }); 58 | 59 | // Prepare the signature domain 60 | const domain = { 61 | name: DOMAIN_NAME, 62 | version: DOMAIN_VERSION, 63 | chainId, 64 | verifyingContract: ERC6538Address 65 | } as const; 66 | 67 | const message = { 68 | schemeId: BigInt(schemeId), 69 | stealthMetaAddress: stealthMetaAddressToRegister, 70 | nonce 71 | }; 72 | 73 | const signature = await walletClient.signTypedData({ 74 | account, 75 | primaryType: PRIMARY_TYPE, 76 | domain, 77 | types: SIGNATURE_TYPES, 78 | message 79 | }); 80 | 81 | return signature; 82 | } catch (error) { 83 | console.error('Error generating signature:', error); 84 | throw new GenerateSignatureForRegisterKeysError( 85 | 'Failed to generate signature for registerKeysOnBehalf', 86 | error 87 | ); 88 | } 89 | } 90 | 91 | export default generateSignatureForRegisterKeysOnBehalf; 92 | -------------------------------------------------------------------------------- /examples/generateDeterministicStealthMetaAddress/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { type Address, createWalletClient, custom } from 'viem'; 4 | import { sepolia } from 'viem/chains'; 5 | import 'viem/window'; 6 | 7 | import { generateStealthMetaAddressFromSignature } from '@scopelift/stealth-address-sdk'; 8 | 9 | /** 10 | * This React component demonstrates the process of generating a stealth meta-address deterministically using a user-signed message 11 | * It's deterministic in that the same stealth meta-address is generated for the same user, chain id, and message 12 | * It utilizes Viem's walletClient for wallet interaction 13 | * 14 | * @returns The component renders a button to first handle connecting the wallet, and a subsequent button to handle stealth meta-address generation 15 | * 16 | * @example 17 | * To run the development server: `bun run dev`. 18 | */ 19 | const Example = () => { 20 | // Initialize your configuration 21 | const chain = sepolia; // Example Viem chain 22 | 23 | if (!window.ethereum) throw new Error('window.ethereum is required'); 24 | 25 | // Initialize Viem wallet client if using Viem 26 | const walletClient = createWalletClient({ 27 | chain, 28 | transport: custom(window.ethereum) 29 | }); 30 | 31 | // State 32 | const [account, setAccount] = useState
(); 33 | const [stealthMetaAddress, setStealthMetaAddress] = useState<`0x${string}`>(); 34 | 35 | const connect = async () => { 36 | const [address] = await walletClient.requestAddresses(); 37 | setAccount(address); 38 | }; 39 | 40 | const signMessage = async () => { 41 | // An example message to sign for generating the stealth meta-address 42 | // Usually this message includes the chain id to mitigate replay attacks across different chains 43 | // The message that is signed should clearly communicate to the user what they are signing and why 44 | const MESSAGE_TO_SIGN = `Generate Stealth Meta-Address on ${chain.id} chain`; 45 | 46 | if (!account) throw new Error('A connected account is required'); 47 | 48 | const signature = await walletClient.signMessage({ 49 | account, 50 | message: MESSAGE_TO_SIGN 51 | }); 52 | 53 | return signature; 54 | }; 55 | 56 | const handleSignAndGenStealthMetaAddress = async () => { 57 | const signature = await signMessage(); 58 | const stealthMetaAddress = 59 | generateStealthMetaAddressFromSignature(signature); 60 | 61 | setStealthMetaAddress(stealthMetaAddress); 62 | }; 63 | 64 | if (account) 65 | return ( 66 | <> 67 | {!stealthMetaAddress ? ( 68 | 71 | ) : ( 72 |
Stealth Meta-Address: {stealthMetaAddress}
73 | )} 74 |
Connected: {account}
75 | 76 | ); 77 | 78 | return ( 79 | 82 | ); 83 | }; 84 | 85 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 86 | 87 | ); 88 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncementsUsingSubgraph/getAnnouncementsUsingSubgraph.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLClient } from 'graphql-request'; 2 | import type { AnnouncementLog } from '../getAnnouncements/types'; 3 | import { 4 | convertSubgraphEntityToAnnouncementLog, 5 | fetchPages 6 | } from './subgraphHelpers'; 7 | import { 8 | GetAnnouncementsUsingSubgraphError, 9 | type GetAnnouncementsUsingSubgraphParams, 10 | type GetAnnouncementsUsingSubgraphReturnType, 11 | type SubgraphAnnouncementEntity 12 | } from './types'; 13 | 14 | /** 15 | * Fetches announcement data from a specified subgraph URL. 16 | * 17 | * Supports filtering results based on a filter string that looks like: 18 | * " 19 | * blockNumber_gte: 123456789 20 | * caller: "0x123456789" 21 | * " 22 | * 23 | * `pageSize` can also be adjusted to optimize performance and/or align with the subgraph's pagination limits. 24 | * The default value is 1000, which is a common maximum allowed by the subgraph providers. 25 | * If the subgraph provider has a higher limit, it can be adjusted accordingly. 26 | * 27 | * @param {Object} params - The parameters for the function. 28 | * @param {string} params.subgraphUrl - The URL of the subgraph to query. 29 | * @param {string} [params.filter=''] - Optional filter string for the query. 30 | * @param {number} [params.pageSize=1000] - Number of items to fetch per page. 31 | * 32 | * @returns {Promise} A promise that resolves to an array of AnnouncementLog objects. 33 | * The return value here is the same as the `getAnnouncements` function to be able to seamlessly fallback to fetching via logs 34 | * if the subgraph is not available. 35 | * 36 | * @throws {GetAnnouncementsUsingSubgraphError} If there's an issue fetching the announcements. 37 | */ 38 | async function getAnnouncementsUsingSubgraph({ 39 | subgraphUrl, 40 | filter = '', 41 | pageSize = 1000 42 | }: GetAnnouncementsUsingSubgraphParams): Promise { 43 | const client = new GraphQLClient(subgraphUrl); 44 | const gqlQuery = ` 45 | query GetAnnouncements($first: Int, $id_lt: ID) { 46 | announcements( 47 | where: { __WHERE_CLAUSE__ } 48 | first: $first, 49 | orderBy: id, 50 | orderDirection: desc 51 | ) { 52 | id 53 | blockNumber 54 | blockHash 55 | caller 56 | data 57 | ephemeralPubKey 58 | logIndex 59 | metadata 60 | removed 61 | schemeId 62 | stealthAddress 63 | topics 64 | transactionHash 65 | transactionIndex 66 | } 67 | } 68 | `; 69 | 70 | const allAnnouncements: AnnouncementLog[] = []; 71 | 72 | try { 73 | for await (const batch of fetchPages({ 74 | client, 75 | gqlQuery, 76 | pageSize, 77 | filter, 78 | entity: 'announcements' 79 | })) { 80 | allAnnouncements.push( 81 | ...batch.map(convertSubgraphEntityToAnnouncementLog) 82 | ); 83 | } 84 | } catch (error) { 85 | throw new GetAnnouncementsUsingSubgraphError( 86 | 'Failed to fetch announcements from the subgraph', 87 | error 88 | ); 89 | } 90 | 91 | return allAnnouncements; 92 | } 93 | 94 | export default getAnnouncementsUsingSubgraph; 95 | -------------------------------------------------------------------------------- /src/utils/helpers/test/generateSignatureForRegisterKeysOnBehalf.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { WalletClient } from 'viem'; 3 | import { signMessage } from 'viem/actions'; 4 | import setupTestEnv from '../../../lib/helpers/test/setupTestEnv'; 5 | import setupTestWallet from '../../../lib/helpers/test/setupTestWallet'; 6 | import type { VALID_CHAIN_IDS } from '../../../lib/helpers/types'; 7 | import { VALID_SCHEME_ID } from '../../crypto'; 8 | import generateSignatureForRegisterKeysOnBehalf from '../generateSignatureForRegisterKeysOnBehalf'; 9 | import generateStealthMetaAddressFromSignature from '../generateStealthMetaAddressFromSignature'; 10 | import { GenerateSignatureForRegisterKeysError } from '../types'; 11 | 12 | describe('generateSignatureForRegisterKeysOnBehalf', () => { 13 | let params: Parameters[0]; 14 | let walletClient: WalletClient; 15 | 16 | beforeAll(async () => { 17 | walletClient = await setupTestWallet(); 18 | const { ERC6538Address } = await setupTestEnv(); 19 | const account = walletClient.account; 20 | const chainId = walletClient.chain?.id as VALID_CHAIN_IDS | undefined; 21 | if (!account) throw new Error('No account found'); 22 | if (!chainId) throw new Error('No chain found'); 23 | 24 | const signatureForStealthMetaAddress = await signMessage(walletClient, { 25 | account, 26 | message: 27 | 'Sign this message to generate your stealth address keys.\nChain ID: 31337' 28 | }); 29 | 30 | const stealthMetaAddressToRegister = 31 | generateStealthMetaAddressFromSignature(signatureForStealthMetaAddress); 32 | 33 | params = { 34 | walletClient, 35 | account: account.address, 36 | ERC6538Address, 37 | chainId, 38 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1, 39 | stealthMetaAddressToRegister 40 | }; 41 | }); 42 | 43 | test('should generate signature successfully', async () => { 44 | const result = await generateSignatureForRegisterKeysOnBehalf(params); 45 | expect(result).toBeTypeOf('string'); 46 | expect(result.startsWith('0x')).toBe(true); 47 | }); 48 | 49 | test('should throw GenerateSignatureForRegisterKeysError when contract read fails', async () => { 50 | const invalidParams = { 51 | ...params, 52 | ERC6538Address: 53 | '0x1234567890123456789012345678901234567890' as `0x${string}` 54 | }; 55 | 56 | expect( 57 | generateSignatureForRegisterKeysOnBehalf(invalidParams) 58 | ).rejects.toBeInstanceOf(GenerateSignatureForRegisterKeysError); 59 | }); 60 | 61 | test('should throw GenerateSignatureForRegisterKeysError when signing fails', async () => { 62 | // Create a wallet client that will fail on signTypedData 63 | const failingWalletClient = { 64 | ...walletClient, 65 | signTypedData: async () => { 66 | throw new Error('Signing failed'); 67 | } 68 | } as WalletClient; 69 | 70 | const failingParams = { 71 | ...params, 72 | walletClient: failingWalletClient 73 | }; 74 | 75 | expect( 76 | generateSignatureForRegisterKeysOnBehalf(failingParams) 77 | ).rejects.toBeInstanceOf(GenerateSignatureForRegisterKeysError); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Install Bun 14 | uses: oven-sh/setup-bun@v1 15 | - name: Build 16 | run: | 17 | bun install 18 | bun run build 19 | test: 20 | runs-on: ubuntu-latest 21 | environment: Testing All Networks 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install Foundry 25 | uses: foundry-rs/foundry-toolchain@v1 26 | - name: Install Bun 27 | uses: oven-sh/setup-bun@v1 28 | - name: Start anvil node 29 | run: anvil & 30 | - name: Run tests 31 | env: 32 | SUBGRAPH_URL_PREFIX: ${{ secrets.SUBGRAPH_URL_PREFIX }} 33 | SUBGRAPH_NAME_ARBITRUM_ONE: ${{ secrets.SUBGRAPH_NAME_ARBITRUM_ONE }} 34 | SUBGRAPH_NAME_ARBITRUM_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_ARBITRUM_SEPOLIA }} 35 | SUBGRAPH_NAME_BASE: ${{ secrets.SUBGRAPH_NAME_BASE }} 36 | SUBGRAPH_NAME_BASE_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_BASE_SEPOLIA }} 37 | SUBGRAPH_NAME_HOLESKY: ${{ secrets.SUBGRAPH_NAME_HOLESKY }} 38 | SUBGRAPH_NAME_MAINNET: ${{ secrets.SUBGRAPH_NAME_MAINNET }} 39 | SUBGRAPH_NAME_MATIC: ${{ secrets.SUBGRAPH_NAME_MATIC }} 40 | SUBGRAPH_NAME_OPTIMISM: ${{ secrets.SUBGRAPH_NAME_OPTIMISM }} 41 | SUBGRAPH_NAME_OPTIMISM_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_OPTIMISM_SEPOLIA }} 42 | SUBGRAPH_NAME_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_SEPOLIA }} 43 | run: | 44 | bun install 45 | bun test 46 | coverage: 47 | runs-on: ubuntu-latest 48 | environment: Testing All Networks 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Install Foundry 52 | uses: foundry-rs/foundry-toolchain@v1 53 | - name: Install Bun 54 | uses: oven-sh/setup-bun@v1 55 | - name: Start anvil node 56 | run: anvil & 57 | - name: Run test coverage 58 | env: 59 | SUBGRAPH_URL_PREFIX: ${{ secrets.SUBGRAPH_URL_PREFIX }} 60 | SUBGRAPH_NAME_ARBITRUM_ONE: ${{ secrets.SUBGRAPH_NAME_ARBITRUM_ONE }} 61 | SUBGRAPH_NAME_ARBITRUM_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_ARBITRUM_SEPOLIA }} 62 | SUBGRAPH_NAME_BASE: ${{ secrets.SUBGRAPH_NAME_BASE }} 63 | SUBGRAPH_NAME_BASE_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_BASE_SEPOLIA }} 64 | SUBGRAPH_NAME_HOLESKY: ${{ secrets.SUBGRAPH_NAME_HOLESKY }} 65 | SUBGRAPH_NAME_MAINNET: ${{ secrets.SUBGRAPH_NAME_MAINNET }} 66 | SUBGRAPH_NAME_MATIC: ${{ secrets.SUBGRAPH_NAME_MATIC }} 67 | SUBGRAPH_NAME_OPTIMISM: ${{ secrets.SUBGRAPH_NAME_OPTIMISM }} 68 | SUBGRAPH_NAME_OPTIMISM_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_OPTIMISM_SEPOLIA }} 69 | SUBGRAPH_NAME_SEPOLIA: ${{ secrets.SUBGRAPH_NAME_SEPOLIA }} 70 | run: | 71 | bun install 72 | bun test --coverage 73 | check: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Install Bun 78 | uses: oven-sh/setup-bun@v1 79 | - name: Check 80 | run: | 81 | bun install 82 | bun run check 83 | -------------------------------------------------------------------------------- /src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Address, TransactionReceipt } from 'viem'; 3 | import { 4 | VALID_SCHEME_ID, 5 | generateSignatureForRegisterKeysOnBehalf, 6 | parseStealthMetaAddressURI 7 | } from '../../..'; 8 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 9 | import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; 10 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 11 | import type { StealthActions } from '../../stealthClient/types'; 12 | import { PrepareError } from '../types'; 13 | import type { RegisterKeysOnBehalfArgs } from './types'; 14 | 15 | describe('prepareRegisterKeysOnBehalf', () => { 16 | let stealthClient: StealthActions; 17 | let account: Address | undefined; 18 | let args: RegisterKeysOnBehalfArgs; 19 | 20 | // Transaction receipt for writing to the contract with the prepared payload 21 | let res: TransactionReceipt; 22 | 23 | beforeAll(async () => { 24 | const { 25 | stealthClient: client, 26 | ERC6538Address, 27 | chainId 28 | } = await setupTestEnv(); 29 | stealthClient = client; 30 | const walletClient = await setupTestWallet(); 31 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 32 | const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); 33 | const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ 34 | stealthMetaAddressURI, 35 | schemeId 36 | }); 37 | account = walletClient.account?.address; 38 | if (!account) throw new Error('No account found'); 39 | const chain = walletClient.chain; 40 | if (!chain) throw new Error('No chain found'); 41 | 42 | const signature = await generateSignatureForRegisterKeysOnBehalf({ 43 | walletClient, 44 | account, 45 | ERC6538Address, 46 | chainId, 47 | schemeId, 48 | stealthMetaAddressToRegister 49 | }); 50 | 51 | args = { 52 | registrant: account, 53 | schemeId, 54 | stealthMetaAddress: stealthMetaAddressToRegister, 55 | signature 56 | } satisfies RegisterKeysOnBehalfArgs; 57 | 58 | const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ 59 | account, 60 | ERC6538Address, 61 | args 62 | }); 63 | 64 | // Prepare tx using viem and the prepared payload 65 | const request = await walletClient.prepareTransactionRequest({ 66 | ...prepared, 67 | chain, 68 | account 69 | }); 70 | 71 | const hash = await walletClient.sendTransaction({ 72 | ...request, 73 | chain, 74 | account 75 | }); 76 | 77 | res = await walletClient.waitForTransactionReceipt({ hash }); 78 | }); 79 | 80 | test('should throw PrepareError when given invalid contract address', () => { 81 | if (!account) throw new Error('No account found'); 82 | 83 | const invalidERC6538Address = '0xinvalid'; 84 | 85 | expect( 86 | stealthClient.prepareRegisterKeysOnBehalf({ 87 | account, 88 | ERC6538Address: invalidERC6538Address, 89 | args 90 | }) 91 | ).rejects.toBeInstanceOf(PrepareError); 92 | }); 93 | 94 | test('should successfully register a stealth meta-address on behalf using the prepare payload', () => { 95 | expect(res.status).toEqual('success'); 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeys/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { type Address, createWalletClient, custom } from 'viem'; 4 | import { sepolia } from 'viem/chains'; 5 | import 'viem/window'; 6 | 7 | import { 8 | ERC6538_CONTRACT_ADDRESS, 9 | VALID_SCHEME_ID, 10 | createStealthClient, 11 | parseStealthMetaAddressURI 12 | } from '@scopelift/stealth-address-sdk'; 13 | 14 | /** 15 | * This React component demonstrates the process of connecting to a wallet and registering a stealth meta-address. 16 | * It utilizes Viem's walletClient for wallet interaction and the stealth-address-sdk for stealth address operations. 17 | * 18 | * @returns The component renders a button to connect the wallet and announce the stealth address. 19 | * 20 | * @example 21 | * To run this example, ensure you have set up your environment variables VITE_RPC_URL and VITE_STEALTH_META_ADDRESS_URI. 22 | * Run the development server using Vite, `vite run dev`. 23 | */ 24 | const Example = () => { 25 | // Initialize your environment variables or configuration 26 | const chainId = 11155111; // Example chain ID 27 | const rpcUrl = import.meta.env.VITE_RPC_URL; // Your Ethereum RPC URL 28 | if (!rpcUrl) throw new Error('VITE_RPC_URL is required'); 29 | 30 | // Example URI; see the getStealthMetaAddress example and generateRandomStealthMetaAddress helper for more details 31 | const stealthMetaAddressURI = import.meta.env.VITE_STEALTH_META_ADDRESS_URI; 32 | if (!stealthMetaAddressURI) 33 | throw new Error('VITE_STEALTH_META_ADDRESS_URI is required'); 34 | 35 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 36 | const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ 37 | stealthMetaAddressURI, 38 | schemeId 39 | }); 40 | const chain = sepolia; // Example Viem chain 41 | 42 | if (!window.ethereum) throw new Error('window.ethereum is required'); 43 | 44 | // Initialize Viem wallet client if using Viem 45 | const walletClient = createWalletClient({ 46 | chain, 47 | transport: custom(window.ethereum) 48 | }); 49 | 50 | // Initialize the stealth client with your RPC URL and chain ID 51 | const stealthClient = createStealthClient({ rpcUrl, chainId }); 52 | const [account, setAccount] = useState
(); 53 | 54 | const connect = async () => { 55 | const [address] = await walletClient.requestAddresses(); 56 | setAccount(address); 57 | }; 58 | 59 | const registerKeys = async () => { 60 | if (!account) return; 61 | 62 | // Prepare the registerKeys payload 63 | const preparedPayload = await stealthClient.prepareRegisterKeys({ 64 | account, 65 | ERC6538Address: ERC6538_CONTRACT_ADDRESS, 66 | schemeId, 67 | stealthMetaAddress: stealthMetaAddressToRegister 68 | }); 69 | 70 | await walletClient.sendTransaction({ 71 | ...preparedPayload 72 | }); 73 | }; 74 | 75 | if (account) 76 | return ( 77 | <> 78 |
Connected: {account}
79 | 82 | 83 | ); 84 | return ( 85 | 88 | ); 89 | }; 90 | 91 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 92 | 93 | ); 94 | -------------------------------------------------------------------------------- /examples/prepareRegisterKeysOnBehalf/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { type Address, createWalletClient, custom } from 'viem'; 4 | import { sepolia } from 'viem/chains'; 5 | import 'viem/window'; 6 | 7 | import { 8 | ERC6538_CONTRACT_ADDRESS, 9 | VALID_SCHEME_ID, 10 | createStealthClient, 11 | parseStealthMetaAddressURI 12 | } from '@scopelift/stealth-address-sdk'; 13 | 14 | /** 15 | * This React component demonstrates the process of connecting to a wallet and registering a stealth meta-address. 16 | * It utilizes Viem's walletClient for wallet interaction and the stealth-address-sdk for stealth address operations. 17 | * 18 | * @returns The component renders a button to connect the wallet and announce the stealth address. 19 | * 20 | * @example 21 | * To run this example, ensure you have set up your environment variables VITE_RPC_URL and VITE_STEALTH_META_ADDRESS_URI. 22 | * Run the development server using Vite, `vite run dev`. 23 | */ 24 | const Example = () => { 25 | // Initialize your environment variables or configuration 26 | const chainId = 11155111; // Example chain ID 27 | const rpcUrl = import.meta.env.VITE_RPC_URL; // Your Ethereum RPC URL 28 | if (!rpcUrl) throw new Error('VITE_RPC_URL is required'); 29 | 30 | // Example URI; see the getStealthMetaAddress example and generateRandomStealthMetaAddress helper for more details 31 | const stealthMetaAddressURI = import.meta.env.VITE_STEALTH_META_ADDRESS_URI; 32 | if (!stealthMetaAddressURI) 33 | throw new Error('VITE_STEALTH_META_ADDRESS_URI is required'); 34 | 35 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 36 | const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ 37 | stealthMetaAddressURI, 38 | schemeId 39 | }); 40 | const chain = sepolia; // Example Viem chain 41 | 42 | if (!window.ethereum) throw new Error('window.ethereum is required'); 43 | 44 | // Initialize Viem wallet client if using Viem 45 | const walletClient = createWalletClient({ 46 | chain, 47 | transport: custom(window.ethereum) 48 | }); 49 | 50 | // Initialize the stealth client with your RPC URL and chain ID 51 | const stealthClient = createStealthClient({ rpcUrl, chainId }); 52 | const [account, setAccount] = useState
(); 53 | 54 | const connect = async () => { 55 | const [address] = await walletClient.requestAddresses(); 56 | setAccount(address); 57 | }; 58 | 59 | const registerKeysOnBehalf = async () => { 60 | if (!account) return; 61 | 62 | // Prepare the registerKeys payload 63 | const preparedPayload = await stealthClient.prepareRegisterKeysOnBehalf({ 64 | account, // Your wallet address 65 | ERC6538Address: ERC6538_CONTRACT_ADDRESS, 66 | args: { 67 | registrant: '0x', // Add the registrant address here 68 | schemeId, 69 | stealthMetaAddress: stealthMetaAddressToRegister, 70 | signature: '0x' // Add the signature here 71 | } 72 | }); 73 | 74 | await walletClient.sendTransaction({ 75 | ...preparedPayload 76 | }); 77 | }; 78 | 79 | if (account) 80 | return ( 81 | <> 82 |
Connected: {account}
83 | 86 | 87 | ); 88 | return ( 89 | 92 | ); 93 | }; 94 | 95 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 96 | 97 | ); 98 | -------------------------------------------------------------------------------- /examples/prepareAnnounce/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import { type Address, createWalletClient, custom } from 'viem'; 4 | import { sepolia } from 'viem/chains'; 5 | import 'viem/window'; 6 | 7 | import { 8 | ERC5564_CONTRACT_ADDRESS, 9 | VALID_SCHEME_ID, 10 | createStealthClient, 11 | generateStealthAddress 12 | } from '@scopelift/stealth-address-sdk'; 13 | 14 | /** 15 | * This React component demonstrates the process of connecting to a wallet and announcing a stealth address. 16 | * It utilizes Viem's walletClient for wallet interaction and the stealth-address-sdk for stealth address operations. 17 | * 18 | * @returns The component renders a button to connect the wallet and announce the stealth address. 19 | * 20 | * @example 21 | * To run this example, ensure you have set up your environment variables VITE_RPC_URL and VITE_STEALTH_META_ADDRESS_URI. 22 | * Run the development server using Vite, `vite run dev`. 23 | */ 24 | const Example = () => { 25 | // Initialize your environment variables or configuration 26 | const chainId = 11155111; // Example chain ID 27 | const rpcUrl = import.meta.env.VITE_RPC_URL; // Your Ethereum RPC URL 28 | if (!rpcUrl) throw new Error('VITE_RPC_URL is required'); 29 | 30 | // Example URI; see the getStealthMetaAddress example and generateRandomStealthMetaAddress helper for more details 31 | const stealthMetaAddressURI = import.meta.env.VITE_STEALTH_META_ADDRESS_URI; 32 | if (!stealthMetaAddressURI) 33 | throw new Error('VITE_STEALTH_META_ADDRESS_URI is required'); 34 | 35 | const chain = sepolia; // Example Viem chain 36 | 37 | if (!window.ethereum) throw new Error('window.ethereum is required'); 38 | 39 | // Initialize Viem wallet client if using Viem 40 | const walletClient = createWalletClient({ 41 | chain, 42 | transport: custom(window.ethereum) 43 | }); 44 | 45 | // Initialize the stealth client with your RPC URL and chain ID 46 | const stealthClient = createStealthClient({ rpcUrl, chainId }); 47 | const [account, setAccount] = useState
(); 48 | 49 | const connect = async () => { 50 | const [address] = await walletClient.requestAddresses(); 51 | setAccount(address); 52 | }; 53 | 54 | const announce = async () => { 55 | if (!account) return; 56 | 57 | // Generate stealth address details 58 | const { stealthAddress, ephemeralPublicKey, viewTag } = 59 | generateStealthAddress({ 60 | stealthMetaAddressURI, 61 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1 // Example scheme ID 62 | }); 63 | 64 | // Prepare the announce payload 65 | const preparedPayload = await stealthClient.prepareAnnounce({ 66 | account, 67 | ERC5564Address: ERC5564_CONTRACT_ADDRESS, 68 | args: { 69 | schemeId: VALID_SCHEME_ID.SCHEME_ID_1, 70 | stealthAddress, 71 | ephemeralPublicKey, 72 | metadata: viewTag 73 | } 74 | }); 75 | 76 | await walletClient.sendTransaction({ 77 | ...preparedPayload 78 | }); 79 | }; 80 | 81 | if (account) 82 | return ( 83 | <> 84 |
Connected: {account}
85 | 88 | 89 | ); 90 | return ( 91 | 94 | ); 95 | }; 96 | 97 | ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( 98 | 99 | ); 100 | -------------------------------------------------------------------------------- /src/lib/helpers/logs.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type Abi, 3 | type AbiEvent, 4 | type ContractEventName, 5 | type DecodeEventLogReturnType, 6 | type GetEventArgs, 7 | type Log, 8 | type PublicClient, 9 | decodeEventLog 10 | } from 'viem'; 11 | import { getBlockNumber, getLogs } from 'viem/actions'; 12 | 13 | /** 14 | * Parameters for fetching and decoding logs in chunks. 15 | * @template TAbi - The ABI type. 16 | */ 17 | type FetchLogsParams = { 18 | /** An instance of the viem PublicClient. */ 19 | publicClient: PublicClient; 20 | /** The ABI of the contract. */ 21 | abi: TAbi; 22 | /** The name of the event to fetch logs for. */ 23 | eventName: ContractEventName; 24 | /** The address of the contract. */ 25 | address: `0x${string}`; 26 | /** Optional arguments to filter the logs. */ 27 | args?: GetEventArgs>; 28 | /** The starting block number for the fetch. Defaults to 'earliest'. */ 29 | fromBlock?: bigint | 'earliest'; 30 | /** The ending block number for the fetch. Defaults to 'latest'. */ 31 | toBlock?: bigint | 'latest'; 32 | /** The number of blocks to query in each chunk. Defaults to 5000. */ 33 | chunkSize?: number; 34 | }; 35 | 36 | type FetchLogsReturnType = Array< 37 | DecodeEventLogReturnType> & Log 38 | >; 39 | 40 | /** 41 | * Fetches and decodes logs in chunks to handle potentially large range queries efficiently. 42 | * 43 | * @template TAbi - The ABI type. 44 | * @param {FetchLogsParams} params - The parameters for fetching logs in chunks. 45 | * @returns {Promise} - A flattened array of all logs fetched in chunks, including decoded event data. 46 | * 47 | * @example 48 | * const logs = await fetchLogsInChunks({ 49 | * publicClient, 50 | * abi: myContractABI, 51 | * eventName: 'Transfer', 52 | * address: '0x...', 53 | * fromBlock: 1000000n, 54 | * toBlock: 2000000n, 55 | * chunkSize: 10000 56 | * }); 57 | */ 58 | export const fetchLogsInChunks = async ({ 59 | publicClient, 60 | abi, 61 | eventName, 62 | address, 63 | args, 64 | fromBlock = 'earliest', 65 | toBlock = 'latest', 66 | chunkSize = 5000 67 | }: FetchLogsParams): Promise> => { 68 | const [start, end] = await Promise.all([ 69 | fromBlock === 'earliest' 70 | ? 0n 71 | : typeof fromBlock === 'bigint' 72 | ? fromBlock 73 | : getBlockNumber(publicClient), 74 | toBlock === 'latest' ? getBlockNumber(publicClient) : toBlock 75 | ]); 76 | 77 | const eventAbi = abi.find( 78 | (item): item is AbiEvent => item.type === 'event' && item.name === eventName 79 | ); 80 | 81 | if (!eventAbi) throw new Error(`Event ${eventName} not found in ABI`); 82 | 83 | const allLogs = []; 84 | 85 | for ( 86 | let currentBlock = start; 87 | currentBlock <= end; 88 | currentBlock += BigInt(chunkSize) 89 | ) { 90 | const logs = await getLogs(publicClient, { 91 | address, 92 | event: eventAbi, 93 | args, 94 | fromBlock: currentBlock, 95 | toBlock: BigInt( 96 | Math.min(Number(currentBlock) + chunkSize - 1, Number(end)) 97 | ), 98 | strict: true 99 | }); 100 | 101 | allLogs.push( 102 | ...logs.map(log => ({ 103 | ...log, 104 | ...decodeEventLog({ 105 | abi, 106 | eventName, 107 | topics: log.topics, 108 | data: log.data 109 | }) 110 | })) 111 | ); 112 | } 113 | 114 | return allLogs; 115 | }; 116 | -------------------------------------------------------------------------------- /src/lib/helpers/test/setupTestEnv.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | afterAll, 3 | afterEach, 4 | beforeEach, 5 | describe, 6 | expect, 7 | mock, 8 | test 9 | } from 'bun:test'; 10 | import { VALID_CHAINS } from '../types'; 11 | import { LOCAL_ENDPOINT } from './setupTestEnv'; 12 | 13 | describe('setupTestEnv with different environment configurations', () => { 14 | afterEach(() => { 15 | process.env.USE_FORK = undefined; 16 | process.env.RPC_URL = undefined; 17 | }); 18 | 19 | test('should use local node endpoint url when USE_FORK is true and RPC_URL is defined', async () => { 20 | const exampleRpcUrl = 'http://example-rpc-url.com'; 21 | process.env.USE_FORK = 'true'; 22 | process.env.RPC_URL = exampleRpcUrl; 23 | const { getRpcUrl } = await import('./setupTestEnv'); 24 | 25 | expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); 26 | }); 27 | 28 | test('throws error when USE_FORK is true and RPC_URL is not defined', async () => { 29 | process.env.USE_FORK = 'true'; 30 | process.env.RPC_URL = undefined; 31 | const { getRpcUrl } = await import('./setupTestEnv'); 32 | 33 | expect(getRpcUrl).toThrow('RPC_URL not defined in env'); 34 | }); 35 | 36 | test('should use local node endpoint when USE_FORK is not true', async () => { 37 | process.env.USE_FORK = 'false'; 38 | const { getRpcUrl } = await import('./setupTestEnv'); 39 | 40 | expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); 41 | }); 42 | 43 | test('should use the default foundry local endpoint when USE_FORK is not defined', async () => { 44 | process.env.USE_FORK = undefined; 45 | const { getRpcUrl } = await import('./setupTestEnv'); 46 | 47 | expect(getRpcUrl()).toBe(LOCAL_ENDPOINT); 48 | }); 49 | }); 50 | 51 | describe('getValidChainId validation', () => { 52 | const { getValidChainId } = require('./setupTestEnv'); 53 | 54 | test('valid chain ID returns correctly', () => { 55 | const validChain = VALID_CHAINS[11155111]; 56 | 57 | expect(getValidChainId(validChain.id)).toBe(validChain.id); 58 | }); 59 | 60 | test('invalid chain ID throws error', () => { 61 | const invalidChainId = 9999; 62 | expect(() => getValidChainId(invalidChainId)).toThrow( 63 | `Invalid chain ID: ${invalidChainId}` 64 | ); 65 | }); 66 | }); 67 | 68 | describe('fetchChainId', async () => { 69 | const { fetchChainId } = await import('./setupTestEnv'); 70 | 71 | beforeEach(() => { 72 | // Set the env vars, which are needed to use a fork and not default to using foundry chain id 73 | process.env.USE_FORK = 'true'; 74 | process.env.RPC_URL = 'http://example-rpc-url.com'; 75 | }); 76 | 77 | afterAll(() => { 78 | process.env.USE_FORK = undefined; 79 | process.env.RPC_URL = undefined; 80 | }); 81 | 82 | test('successful fetch returns chain ID', async () => { 83 | mock.module('./setupTestEnv', () => ({ 84 | fetchJson: () => 85 | Promise.resolve({ 86 | result: '0x1' 87 | }) 88 | })); 89 | 90 | const chainId = await fetchChainId(); 91 | expect(chainId).toBe(1); // '0x1' translates to 1 92 | }); 93 | 94 | test('fetch failure throws error', async () => { 95 | mock.module('./setupTestEnv', () => ({ 96 | fetchJson: () => Promise.reject(new Error('Network failure')) 97 | })); 98 | 99 | expect(fetchChainId()).rejects.toThrow('Failed to get the chain ID'); 100 | }); 101 | 102 | test('throws error when RPC_URL is not defined', async () => { 103 | process.env.RPC_URL = undefined; 104 | 105 | expect(fetchChainId).toThrow('RPC_URL not defined in env'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /src/lib/stealthClient/createStealthClient.ts: -------------------------------------------------------------------------------- 1 | import { http, type PublicClient, createPublicClient } from 'viem'; 2 | import { actions as stealthActions } from '../actions'; 3 | import { getChain } from '../helpers/chains'; 4 | import { 5 | type ClientParams, 6 | PublicClientRequiredError, 7 | type StealthClientInitParams, 8 | type StealthClientReturnType 9 | } from './types'; 10 | 11 | /** 12 | * Creates a client for use in stealth address operations that align with ERC5564. 13 | * 14 | * @param {StealthClientInitParams} params - Parameters for initializing the stealth client, including chain ID and RPC URL. 15 | * @returns {StealthClientReturnType} - An object containing initialized stealth action functions. 16 | * 17 | * @example 18 | * import { createStealthClient } from 'stealth-address-sdk' 19 | * 20 | * const stealthClient = createStealthClient({ 21 | * chainId, 22 | * rpcUrl, 23 | * }) 24 | * 25 | * stealthClient.getAnnouncements({ params }) 26 | */ 27 | function createStealthClient({ 28 | chainId, 29 | rpcUrl 30 | }: StealthClientInitParams): StealthClientReturnType { 31 | const chain = getChain(chainId); 32 | 33 | // Init viem client 34 | const publicClient = createPublicClient({ 35 | chain, 36 | transport: http(rpcUrl) 37 | }); 38 | 39 | const initializedActions: StealthClientReturnType = { 40 | getAnnouncements: params => 41 | stealthActions.getAnnouncements({ 42 | clientParams: { publicClient }, 43 | ...params 44 | }), 45 | getAnnouncementsUsingSubgraph: params => 46 | stealthActions.getAnnouncementsUsingSubgraph({ 47 | ...params 48 | }), 49 | getStealthMetaAddress: params => 50 | stealthActions.getStealthMetaAddress({ 51 | clientParams: { publicClient }, 52 | ...params 53 | }), 54 | getAnnouncementsForUser: params => 55 | stealthActions.getAnnouncementsForUser({ 56 | clientParams: { publicClient }, 57 | ...params 58 | }), 59 | watchAnnouncementsForUser: params => 60 | stealthActions.watchAnnouncementsForUser({ 61 | clientParams: { publicClient }, 62 | ...params 63 | }), 64 | prepareAnnounce: params => 65 | stealthActions.prepareAnnounce({ 66 | clientParams: { publicClient }, 67 | ...params 68 | }), 69 | prepareRegisterKeys: params => 70 | stealthActions.prepareRegisterKeys({ 71 | clientParams: { publicClient }, 72 | ...params 73 | }), 74 | prepareRegisterKeysOnBehalf: params => 75 | stealthActions.prepareRegisterKeysOnBehalf({ 76 | clientParams: { publicClient }, 77 | ...params 78 | }) 79 | }; 80 | 81 | return initializedActions; 82 | } 83 | 84 | const handleViemPublicClient = (clientParams?: ClientParams): PublicClient => { 85 | if (!clientParams) { 86 | throw new PublicClientRequiredError( 87 | 'publicClient or chainId and rpcUrl must be provided' 88 | ); 89 | } 90 | 91 | if ('publicClient' in clientParams) { 92 | return clientParams.publicClient; 93 | } 94 | 95 | // Type guard for the 'chainId' and 'rpcUrl' properties 96 | if ('chainId' in clientParams && 'rpcUrl' in clientParams) { 97 | try { 98 | return createPublicClient({ 99 | chain: getChain(clientParams.chainId), 100 | transport: http(clientParams.rpcUrl) 101 | }); 102 | } catch (error) { 103 | throw new PublicClientRequiredError( 104 | 'public client could not be created.' 105 | ); 106 | } 107 | } 108 | 109 | throw new PublicClientRequiredError( 110 | 'Either publicClient or both chainId and rpcUrl must be provided' 111 | ); 112 | }; 113 | 114 | export { createStealthClient, handleViemPublicClient }; 115 | export default createStealthClient; 116 | -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncements/getAnnouncements.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Account, Address } from 'viem'; 3 | import { 4 | VALID_SCHEME_ID, 5 | generateRandomStealthMetaAddress, 6 | generateStealthAddress 7 | } from '../../..'; 8 | import ERC556AnnouncerAbi from '../../abi/ERC5564Announcer'; 9 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 10 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 11 | import type { SuperWalletClient } from '../../helpers/types'; 12 | import type { StealthActions } from '../../stealthClient/types'; 13 | 14 | describe('getAnnouncements', () => { 15 | let stealthClient: StealthActions; 16 | let walletClient: SuperWalletClient; 17 | let fromBlock: bigint; 18 | let ERC5564Address: Address; 19 | let account: Account | undefined; 20 | 21 | // Set up stealth address details 22 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 23 | const { stealthMetaAddressURI } = generateRandomStealthMetaAddress(); 24 | const { stealthAddress, viewTag, ephemeralPublicKey } = 25 | generateStealthAddress({ 26 | stealthMetaAddressURI, 27 | schemeId 28 | }); 29 | 30 | // Set up the test environment and announce the stealth address 31 | beforeAll(async () => { 32 | const { 33 | stealthClient: client, 34 | ERC5564Address, 35 | ERC5564DeployBlock 36 | } = await setupTestEnv(); 37 | walletClient = await setupTestWallet(); 38 | stealthClient = client; 39 | fromBlock = ERC5564DeployBlock; 40 | account = walletClient.account; 41 | 42 | if (!account) throw new Error('No account found'); 43 | 44 | // Announce the stealth address, ephemeral public key, and view tag 45 | const hash = await walletClient.writeContract({ 46 | address: ERC5564Address, 47 | functionName: 'announce', 48 | args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], 49 | abi: ERC556AnnouncerAbi, 50 | chain: walletClient.chain, 51 | account 52 | }); 53 | 54 | // Wait for the transaction to be mined 55 | await walletClient.waitForTransactionReceipt({ 56 | hash 57 | }); 58 | }); 59 | 60 | test('fetches announcements successfully', async () => { 61 | const announcements = await stealthClient.getAnnouncements({ 62 | ERC5564Address, 63 | args: {}, 64 | fromBlock 65 | }); 66 | 67 | expect(announcements.length).toBeGreaterThan(0); 68 | }); 69 | 70 | test('fetches specific announcement successfully using stealth address', async () => { 71 | const announcements = await stealthClient.getAnnouncements({ 72 | ERC5564Address, 73 | args: { 74 | stealthAddress 75 | }, 76 | fromBlock 77 | }); 78 | 79 | expect(announcements[0].stealthAddress).toBe(stealthAddress); 80 | }); 81 | test('fetches specific announcements successfully using caller', async () => { 82 | if (!account) throw new Error('No account found'); 83 | 84 | const announcements = await stealthClient.getAnnouncements({ 85 | ERC5564Address, 86 | args: { 87 | caller: walletClient.account?.address 88 | }, 89 | fromBlock 90 | }); 91 | 92 | expect(announcements[0].caller).toBe(account.address); 93 | }); 94 | 95 | test('fetches specific announcements successfully using schemeId', async () => { 96 | const announcements = await stealthClient.getAnnouncements({ 97 | ERC5564Address, 98 | args: { 99 | schemeId: BigInt(schemeId) 100 | }, 101 | fromBlock 102 | }); 103 | 104 | expect(announcements[0].schemeId).toBe(BigInt(schemeId)); 105 | 106 | const invalidSchemeId = BigInt('2'); 107 | 108 | const invalidAnnouncements = await stealthClient.getAnnouncements({ 109 | ERC5564Address, 110 | args: { 111 | schemeId: invalidSchemeId 112 | }, 113 | fromBlock 114 | }); 115 | 116 | expect(invalidAnnouncements.length).toBe(0); 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /src/utils/crypto/test/generateStealthAddress.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { bytesToHex } from 'viem'; 3 | import { 4 | generatePrivateKey, 5 | generateStealthAddress, 6 | getViewTag, 7 | parseKeysFromStealthMetaAddress, 8 | parseStealthMetaAddressURI 9 | } from '..'; 10 | import { type HexString, VALID_SCHEME_ID } from '../types'; 11 | 12 | describe('generateStealthAddress', () => { 13 | const validStealthMetaAddressURI = 14 | 'st:eth:0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 15 | 16 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 17 | 18 | test('parseStealthMetaAddressURI should return the stealth meta-address', () => { 19 | const expectedStealthMetaAddress = 20 | '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 21 | // Passing the valid stealth meta-address URI and the scheme ID 22 | const result = parseStealthMetaAddressURI({ 23 | stealthMetaAddressURI: validStealthMetaAddressURI, 24 | schemeId 25 | }); 26 | 27 | expect(result).toBe(expectedStealthMetaAddress); 28 | 29 | // Passing only the stealth meta-address 30 | const result2 = parseStealthMetaAddressURI({ 31 | stealthMetaAddressURI: expectedStealthMetaAddress, 32 | schemeId 33 | }); 34 | 35 | expect(result2).toBe(expectedStealthMetaAddress); 36 | }); 37 | 38 | test('should generate a valid stealth address given a valid stealth meta-address URI', () => { 39 | // TODO compute the expected stealth address using computeStealthAddress (not yet implemented in the SDK) 40 | const result = generateStealthAddress({ 41 | stealthMetaAddressURI: validStealthMetaAddressURI, 42 | schemeId 43 | }); 44 | 45 | expect(result.stealthAddress).toBeDefined(); 46 | }); 47 | 48 | test('should generate the same stealth address given the same ephemeralPrivateKey', () => { 49 | // First and second private keys are the same 50 | const firstPrivateKey = generatePrivateKey({ schemeId }); 51 | const secondPrivateKey = generatePrivateKey({ 52 | ephemeralPrivateKey: firstPrivateKey, 53 | schemeId 54 | }); 55 | 56 | const result = generateStealthAddress({ 57 | stealthMetaAddressURI: validStealthMetaAddressURI, 58 | schemeId, 59 | ephemeralPrivateKey: firstPrivateKey 60 | }); 61 | 62 | const result2 = generateStealthAddress({ 63 | stealthMetaAddressURI: validStealthMetaAddressURI, 64 | schemeId, 65 | ephemeralPrivateKey: secondPrivateKey 66 | }); 67 | 68 | expect(result.stealthAddress).toBe(result2.stealthAddress); 69 | }); 70 | 71 | test('should correctly parse spending and viewing public keys from valid stealth meta-address', () => { 72 | const stealthMetaAddress = validStealthMetaAddressURI.slice(7) as HexString; 73 | const expectedSpendingPublicKeyHex = 74 | '0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec1397'; 75 | const expectedViewingPublicKeyHex = 76 | '0x0390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 77 | 78 | const result = parseKeysFromStealthMetaAddress({ 79 | stealthMetaAddress, 80 | schemeId 81 | }); 82 | 83 | expect(bytesToHex(result.spendingPublicKey)).toBe( 84 | expectedSpendingPublicKeyHex 85 | ); 86 | expect(bytesToHex(result.viewingPublicKey)).toBe( 87 | expectedViewingPublicKeyHex 88 | ); 89 | }); 90 | 91 | test('should correctly extract the view tag from the hashed shared secret', () => { 92 | // Replace with the hashed shared secret from which you expect to extract the view tag 93 | const hashedSharedSecret = 94 | '0x158ce29a3dd0c8dca524e5776c2ba6361c280e013f87eee5eb799a713a939501'; 95 | const expectedViewTag = '0x15'; 96 | 97 | const result = getViewTag({ 98 | hashedSharedSecret, 99 | schemeId 100 | }); 101 | 102 | expect(result).toBe(expectedViewTag); 103 | }); 104 | }); 105 | -------------------------------------------------------------------------------- /src/lib/helpers/test/setupTestEnv.ts: -------------------------------------------------------------------------------- 1 | import { fromHex } from 'viem'; 2 | import { foundry } from 'viem/chains'; 3 | import { createStealthClient } from '../..'; 4 | import deployAllContracts from '../../../scripts'; 5 | import { getChain } from '../chains'; 6 | import type { VALID_CHAIN_IDS } from '../types'; 7 | 8 | export const LOCAL_ENDPOINT = 'http://127.0.0.1:8545'; 9 | 10 | /** 11 | * Initializes a test environment for testing purposes. 12 | * Defaults to local anvil node usage or, alternatively, use a remote RPC URL by setting the TEST_RPC_URL environment variable 13 | * @returns An object containing the testing environment setup parameters including chain ID, contract addresses, and a stealth client instance. 14 | */ 15 | const setupTestEnv = async () => { 16 | // Setup stealth client 17 | const { chainId } = await getChainInfo(); 18 | const rpcUrl = getRpcUrl(); 19 | const stealthClient = createStealthClient({ rpcUrl, chainId }); 20 | 21 | // Deploy ERC5564 and ERC6538 contracts 22 | const { 23 | erc5564ContractAddress: ERC5564Address, 24 | erc6538ContractAddress: ERC6538Address, 25 | erc5564DeployBlock: ERC5564DeployBlock 26 | } = await deployAllContracts(); 27 | 28 | return { 29 | chainId, 30 | ERC5564Address, 31 | ERC5564DeployBlock, 32 | ERC6538Address, 33 | stealthClient 34 | }; 35 | }; 36 | 37 | /** 38 | * Validates the provided chain ID against a list of valid chain IDs. 39 | * @param {number} chainId - The chain ID to validate. 40 | * @returns {VALID_CHAIN_IDS} - The validated chain ID. 41 | * @throws {Error} If the chain ID is not valid. 42 | */ 43 | const getValidChainId = (chainId: number): VALID_CHAIN_IDS => { 44 | if (chainId === 11155111 || chainId === 1 || chainId === 31337) { 45 | return chainId as VALID_CHAIN_IDS; 46 | } 47 | throw new Error(`Invalid chain ID: ${chainId}`); 48 | }; 49 | 50 | /** 51 | * Retrieves the TEST RPC URL from env or defaults to foundry http. 52 | * @returns {string } The RPC URL. 53 | */ 54 | const getRpcUrl = (): string => { 55 | const useFork = process.env.USE_FORK === 'true'; 56 | if (useFork) { 57 | // Check that the RPC_URL is defined if using a fork 58 | if (!process.env.RPC_URL) { 59 | throw new Error('RPC_URL not defined in env'); 60 | } 61 | // Use the local node endpoint for the rpc url 62 | return LOCAL_ENDPOINT; 63 | } 64 | 65 | return foundry.rpcUrls.default.http[0]; 66 | }; 67 | 68 | const getChainInfo = async () => { 69 | const chainId = await fetchChainId(); 70 | const validChainId = getValidChainId(chainId); 71 | return { chain: getChain(validChainId), chainId: validChainId }; 72 | }; 73 | 74 | export const fetchChainId = async (): Promise => { 75 | // If not running fork test script, use the foundry chain ID 76 | if (!isUsingFork()) return foundry.id; 77 | 78 | if (!process.env.RPC_URL) { 79 | throw new Error('RPC_URL not defined in env'); 80 | } 81 | 82 | interface ChainIdResponse { 83 | version: string; 84 | id: number; 85 | result: `0x${string}`; 86 | } 87 | 88 | try { 89 | const data = await fetchJson(process.env.RPC_URL, { 90 | method: 'POST', 91 | headers: { 92 | Accept: 'application/json', 93 | 'Content-Type': 'application/json' 94 | }, 95 | body: JSON.stringify({ 96 | id: 1, 97 | jsonrpc: '2.0', 98 | method: 'eth_chainId' 99 | }) 100 | }); 101 | 102 | return fromHex(data.result, 'number'); 103 | } catch (error) { 104 | throw new Error('Failed to get the chain ID'); 105 | } 106 | }; 107 | 108 | const fetchJson = async (url: string, options: FetchRequestInit) => { 109 | const response = await fetch(url, options); 110 | 111 | if (!response.ok) { 112 | throw new Error(`HTTP error! status: ${response.status}`); 113 | } 114 | 115 | return response.json() as T; 116 | }; 117 | 118 | function isUsingFork(): boolean { 119 | const useFork = process.env.USE_FORK; 120 | return useFork === 'true'; 121 | } 122 | 123 | export { getValidChainId, getRpcUrl, getChainInfo, fetchJson }; 124 | export default setupTestEnv; 125 | -------------------------------------------------------------------------------- /src/test/sendReceive.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeAll, describe, expect, test } from 'bun:test'; 2 | import { http, createWalletClient, parseEther, publicActions } from 'viem'; 3 | import { privateKeyToAccount } from 'viem/accounts'; 4 | import { getRpcUrl } from '../lib/helpers/test/setupTestEnv'; 5 | import { 6 | VALID_SCHEME_ID, 7 | computeStealthKey, 8 | generateStealthAddress 9 | } from '../utils'; 10 | import { generateStealthMetaAddressFromSignature } from '../utils/helpers'; 11 | import { 12 | getEndingBalances, 13 | getSignature, 14 | getWalletClientsAndKeys, 15 | sendEth, 16 | setupInitialBalances 17 | } from './helpers'; 18 | 19 | /** 20 | * @description Tests for sending and receiving a payment 21 | * 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 22 | * 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 23 | * 24 | * The tests need to be run using foundry because the tests utilize the default anvil private keys 25 | */ 26 | 27 | describe('Send and receive payment', () => { 28 | const sendAmount = parseEther('1.0'); 29 | const withdrawBuffer = parseEther('0.01'); 30 | const withdrawAmount = sendAmount - withdrawBuffer; 31 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 32 | 33 | let gasEstimateSend: bigint; 34 | let sendingWalletBalanceChange: bigint; 35 | let receivingWalletBalanceChange: bigint; 36 | 37 | beforeAll(async () => { 38 | const { 39 | receivingAccount, 40 | receivingAccountKeys, 41 | receivingWalletClient, 42 | sendingWalletClient 43 | } = await getWalletClientsAndKeys(); 44 | 45 | const { stealthAddress, ephemeralPublicKey } = generateStealthAddress({ 46 | stealthMetaAddressURI: generateStealthMetaAddressFromSignature( 47 | await getSignature({ walletClient: receivingWalletClient }) 48 | ), 49 | schemeId 50 | }); 51 | 52 | const { sendingWalletStartingBalance, receivingWalletStartingBalance } = 53 | await setupInitialBalances({ 54 | receivingWalletClient, 55 | sendingWalletClient 56 | }); 57 | 58 | // Send ETH to the stealth address 59 | const { gasEstimate } = await sendEth({ 60 | sendingWalletClient, 61 | to: stealthAddress, 62 | value: sendAmount 63 | }); 64 | 65 | gasEstimateSend = gasEstimate; 66 | 67 | // Compute the stealth key to be able to withdraw the funds from the stealth address to the receiving account 68 | const stealthAddressPrivateKey = computeStealthKey({ 69 | schemeId, 70 | ephemeralPublicKey, 71 | spendingPrivateKey: receivingAccountKeys.spendingPrivateKey, 72 | viewingPrivateKey: receivingAccountKeys.viewingPrivateKey 73 | }); 74 | 75 | // Set up a wallet client using the stealth address private key 76 | const stealthAddressWalletClient = createWalletClient({ 77 | account: privateKeyToAccount(stealthAddressPrivateKey), 78 | chain: sendingWalletClient.chain, 79 | transport: http(getRpcUrl()) 80 | }).extend(publicActions); 81 | 82 | // Withdraw from the stealth address to the receiving account 83 | await sendEth({ 84 | sendingWalletClient: stealthAddressWalletClient, 85 | to: receivingAccount.address, 86 | value: withdrawAmount 87 | }); 88 | 89 | const { sendingWalletEndingBalance, receivingWalletEndingBalance } = 90 | await getEndingBalances({ 91 | sendingWalletClient, 92 | receivingWalletClient 93 | }); 94 | 95 | // Get the balance changes for the sending and receiving wallets 96 | sendingWalletBalanceChange = 97 | sendingWalletEndingBalance - sendingWalletStartingBalance; 98 | receivingWalletBalanceChange = 99 | receivingWalletEndingBalance - receivingWalletStartingBalance; 100 | }); 101 | 102 | test('Can successfully send a stealth transaction from an account and withdraw from a different account by computing the stealth key', () => { 103 | expect(sendingWalletBalanceChange).toBe(-(sendAmount + gasEstimateSend)); 104 | expect(receivingWalletBalanceChange).toBe(withdrawAmount); 105 | }); 106 | }); 107 | -------------------------------------------------------------------------------- /src/utils/crypto/test/computeStealthKey.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'bun:test'; 2 | import { CURVE, getPublicKey, utils } from '@noble/secp256k1'; 3 | import { bytesToHex, hexToBytes } from 'viem'; 4 | import { publicKeyToAddress } from 'viem/accounts'; 5 | import { 6 | VALID_SCHEME_ID, 7 | computeStealthKey, 8 | generatePrivateKey, 9 | generateStealthAddress 10 | } from '..'; 11 | import { addPriv } from '../computeStealthKey'; 12 | 13 | const formatPrivKey = (privateKey: bigint) => 14 | `${privateKey.toString(16).padStart(64, '0')}`; 15 | 16 | describe('generateStealthAddress and computeStealthKey', () => { 17 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 18 | const stealthMetaAddressURI = 19 | 'st:eth:0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e'; 20 | const spendingPrivateKey = 21 | '0x363721eb9e981558c748b824cb32a840da2b3e8957c2fc3bcb8d9c86cb87456'; 22 | const viewingPrivateKey = 23 | '0xb52a0555f6a8663d89f00365893b1ef9e38eaf2e8bc48a63319c9ea5cb4a27c5'; 24 | 25 | test('full cycle stealth address generation and validation', () => { 26 | const ephemeralPrivateKey = generatePrivateKey({ schemeId }); 27 | 28 | const generatedStealthAddressResult = generateStealthAddress({ 29 | ephemeralPrivateKey, 30 | schemeId, 31 | stealthMetaAddressURI 32 | }); 33 | 34 | const computedStealthPrivateKeyHex = computeStealthKey({ 35 | ephemeralPublicKey: generatedStealthAddressResult.ephemeralPublicKey, 36 | schemeId, 37 | spendingPrivateKey, 38 | viewingPrivateKey 39 | }); 40 | 41 | const computedStealthPublicKey = getPublicKey( 42 | hexToBytes(computedStealthPrivateKeyHex), 43 | false 44 | ); 45 | 46 | const computedStealthAddress = publicKeyToAddress( 47 | bytesToHex(computedStealthPublicKey) 48 | ); 49 | 50 | // Validate the generated stealth address matches the computed stealth address 51 | expect(generatedStealthAddressResult.stealthAddress).toEqual( 52 | computedStealthAddress 53 | ); 54 | }); 55 | }); 56 | 57 | describe('adding private keys', () => { 58 | const privateKey1 = BigInt( 59 | '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DEDFE92F46681B20A0' 60 | ); 61 | const privateKey2 = BigInt( 62 | '0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DEDFE92F46681B20A0' 63 | ); 64 | const curveOrder = BigInt(CURVE.n); 65 | 66 | test('demonstrate exceeding curve order results in invalid scalar, and modulo corrects it', () => { 67 | // Curve's order 68 | const curveOrder = BigInt(CURVE.n); 69 | 70 | // Create a scalar that exceeds the curve's order 71 | const exceededScalar = curveOrder + BigInt(1); 72 | 73 | // Check validity without modulo operation 74 | const isValidWithoutModulo = utils.isValidPrivateKey( 75 | formatPrivKey(exceededScalar) 76 | ); 77 | expect(isValidWithoutModulo).toBe(false); 78 | 79 | // Apply modulo operation to bring the scalar within valid range 80 | const correctedScalar = exceededScalar % curveOrder; 81 | 82 | // Check validity with modulo operation 83 | const isValidWithModulo = utils.isValidPrivateKey( 84 | formatPrivKey(correctedScalar) 85 | ); 86 | expect(isValidWithModulo).toBe(true); 87 | 88 | // Additionally, demonstrate that correctedScalar is less than curveOrder 89 | expect(correctedScalar).toBeLessThan(curveOrder); 90 | }); 91 | 92 | test('adding private key scalars with addPriv', () => { 93 | const sumWithoutModulo = privateKey1 + privateKey2; 94 | // The sum exceeds the curve's order, which is invalid 95 | expect(sumWithoutModulo).toBeGreaterThan(curveOrder); 96 | 97 | const sumWithModulo = addPriv({ 98 | a: privateKey1, 99 | b: privateKey2 100 | }); 101 | // The sum is within the curve's order, which is valid 102 | expect(sumWithModulo).toBeLessThanOrEqual(curveOrder); 103 | }); 104 | 105 | test('fuzzTest addPriv', () => { 106 | const iterations = 100000; 107 | 108 | for (let i = 0; i < iterations; i++) { 109 | // Generate two random scalars 110 | const scalarA = BigInt(bytesToHex(utils.randomPrivateKey())); 111 | const scalarB = BigInt(bytesToHex(utils.randomPrivateKey())); 112 | 113 | const result = addPriv({ a: scalarA, b: scalarB }); 114 | 115 | expect(utils.isValidPrivateKey(formatPrivKey(result))).toBe(true); 116 | } 117 | }); 118 | }); 119 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Development Commands 6 | 7 | ### Testing 8 | - `bun test` - Run all tests using local Bun test runner 9 | - `bun test --watch src` - Run tests in watch mode during development 10 | - `bun test src/path/to/specific.test.ts` - Run a specific test file 11 | - `anvil` - Start local Anvil node for testing (required for most tests) 12 | - `bun run anvil-fork` - Start Anvil with fork of provided RPC_URL 13 | - `bun run test-fork` - Run tests against forked network 14 | - `bun run test-fork FILE=src/path/to/test.ts` - Run specific test against fork 15 | 16 | ### Building and Linting 17 | - `bun run build` - Apply Biome linting fixes and compile TypeScript 18 | - `bun run check` - Run Biome linting checks without fixing 19 | - `biome check --apply .` - Apply Biome formatting and linting fixes 20 | - `bun tsc` - Run TypeScript compiler 21 | 22 | ### Publishing 23 | - `bun run publish` - Build and publish to npm 24 | 25 | ## Architecture Overview 26 | 27 | ### Core Structure 28 | This is a TypeScript SDK for Ethereum stealth addresses implementing EIP-5564 and EIP-6538. The codebase is organized into several key layers: 29 | 30 | **Entry Points (`src/index.ts`)**: 31 | - Exports all utilities, client actions, configuration, and types 32 | - Main exports: crypto utilities, helpers, stealth client, contract addresses 33 | 34 | **Stealth Client (`src/lib/stealthClient/`)**: 35 | - `createStealthClient()` - Primary factory function for client instances 36 | - Takes `chainId` and `rpcUrl` parameters, returns initialized action functions 37 | - Uses Viem's `PublicClient` internally for blockchain interactions 38 | - All client actions are pre-bound with the public client instance 39 | 40 | **Actions (`src/lib/actions/`)**: 41 | - Modular blockchain interaction functions organized by domain 42 | - Each action has its own directory with implementation, types, and tests 43 | - Key actions: `getAnnouncements`, `getStealthMetaAddress`, `prepareAnnounce`, etc. 44 | - Actions work with or without the stealth client (can accept raw `PublicClient`) 45 | 46 | **Crypto Utilities (`src/utils/crypto/`)**: 47 | - Pure functions for stealth address cryptography 48 | - Core functions: `generateStealthAddress`, `computeStealthKey`, `checkStealthAddress` 49 | - Uses `@noble/secp256k1` for elliptic curve operations 50 | - All functions return consistent types and handle errors gracefully 51 | 52 | **Helper Utilities (`src/utils/helpers/`)**: 53 | - Supporting functions for key generation, metadata handling, validation 54 | - Includes subgraph integration helpers for off-chain data fetching 55 | - Functions for signature generation and stealth meta-address creation 56 | 57 | **Configuration (`src/config/`)**: 58 | - Contract addresses for different chains 59 | - Contract bytecode and deployment start blocks 60 | - Exports: `ERC5564_CONTRACT_ADDRESS`, `ERC6538_CONTRACT_ADDRESS` 61 | 62 | ### Key Design Patterns 63 | 64 | **Two-Level API Design**: 65 | 1. **High-level**: Use `createStealthClient()` for most applications 66 | 2. **Low-level**: Import individual functions for granular control 67 | 68 | **Error Handling**: 69 | - Custom error classes for different failure modes 70 | - Errors include original error context for debugging 71 | - All async functions properly propagate errors 72 | 73 | **TypeScript Integration**: 74 | - Comprehensive type exports for all parameters and return values 75 | - Uses Viem's types for blockchain interactions 76 | - BigInt used consistently for block numbers and large integers 77 | 78 | **Testing Strategy**: 79 | - Unit tests for pure functions using mocks 80 | - Integration tests against real/forked networks using Anvil 81 | - Real subgraph endpoints used in some tests for accuracy 82 | 83 | ### Subgraph Integration 84 | The SDK supports both direct blockchain queries and subgraph queries for better performance: 85 | - `getAnnouncements()` - Direct blockchain logs 86 | - `getAnnouncementsUsingSubgraph()` - Subgraph queries with pagination 87 | - Subgraph helpers in `src/lib/actions/getAnnouncementsUsingSubgraph/subgraphHelpers.ts` 88 | 89 | ### Contract Interaction Patterns 90 | - Uses Viem for all blockchain interactions 91 | - Contract addresses are environment-specific (pulled from config) 92 | - Actions that modify state return transaction objects (prepare functions) 93 | - Read-only actions return processed data directly 94 | 95 | ## Environment Setup 96 | Tests expect either a local Anvil node or environment variables: 97 | - `RPC_URL` - RPC endpoint for forked testing 98 | - `USE_FORK=true` - Flag to enable fork mode 99 | - Tests default to local Anvil if no environment specified 100 | 101 | ## Code Organization Notes 102 | - Each action/utility has co-located tests in same directory 103 | - Types are defined alongside implementations, exported centrally 104 | - Examples directory contains working usage examples for each major function 105 | - All crypto functions handle both compressed and uncompressed public keys 106 | - Consistent use of hex strings with 0x prefix for addresses and keys -------------------------------------------------------------------------------- /src/lib/actions/getAnnouncementsUsingSubgraph/subgraphHelpers.ts: -------------------------------------------------------------------------------- 1 | import type { GraphQLClient } from 'graphql-request'; 2 | import { ERC5564_CONTRACT_ADDRESS } from '../../../config'; 3 | import type { AnnouncementLog } from '../getAnnouncements/types'; 4 | import type { SubgraphAnnouncementEntity } from './types'; 5 | 6 | /** 7 | * The necessary pagination variables for the subgraph query. 8 | */ 9 | export type PaginationVariables = { 10 | first: number; 11 | skip: number; 12 | }; 13 | 14 | /** 15 | * Asynchronous generator function to fetch paginated data from a subgraph. 16 | * 17 | * This function fetches data in reverse chronological order (newest first) by using 18 | * the 'id_lt' parameter for pagination. It recursively calls itself to fetch all pages 19 | * of data, using the lastId parameter as the starting point for each subsequent page. 20 | * 21 | * @template T - The type of entities being fetched, must have an 'id' property. 22 | * @param {Object} params - The parameters for the fetch operation. 23 | * @param {GraphQLClient} params.client - The GraphQL client instance. 24 | * @param {string} params.gqlQuery - The GraphQL query string with a '__WHERE_CLAUSE__' placeholder. 25 | * @param {number} params.pageSize - The number of items to fetch per page. 26 | * @param {string} params.filter - Additional filter criteria for the query. 27 | * @param {string} params.entity - The name of the entity being queried. 28 | * @param {string} [params.lastId] - The ID of the last item from the previous page, used for pagination. 29 | * @yields {T[]} An array of entities of type T for each page of results. 30 | * @throws {Error} If there's an error fetching the data from the subgraph. 31 | */ 32 | export async function* fetchPages({ 33 | client, 34 | gqlQuery, 35 | pageSize, 36 | filter, 37 | entity, 38 | lastId 39 | }: { 40 | client: GraphQLClient; 41 | gqlQuery: string; 42 | pageSize: number; 43 | filter: string; 44 | entity: string; 45 | lastId?: string; 46 | }): AsyncGenerator { 47 | // Set up variables for the GraphQL query 48 | const variables: { first: number; id_lt?: string } = { 49 | first: pageSize 50 | }; 51 | 52 | // If lastId is provided, set it as the upper bound for the pagination 53 | if (lastId) { 54 | variables.id_lt = lastId; 55 | } 56 | 57 | // Construct the WHERE clause for the GraphQL query 58 | const whereClause = [filter, lastId ? 'id_lt: $id_lt' : null] 59 | .filter(Boolean) 60 | .join(', '); 61 | 62 | // Replace the placeholder in the query with the constructed WHERE clause 63 | const finalQuery = gqlQuery.replace('__WHERE_CLAUSE__', whereClause); 64 | 65 | try { 66 | const response = await client.request<{ [key: string]: T[] }>( 67 | finalQuery, 68 | variables 69 | ); 70 | const batch = response[entity]; 71 | 72 | // If no results, end the generator 73 | if (batch.length === 0) { 74 | return; 75 | } 76 | 77 | yield batch; 78 | 79 | // If we've received fewer items than the page size, we're done 80 | if (batch.length < pageSize) { 81 | return; 82 | } 83 | 84 | // Recursively fetch the next page 85 | const newLastId = batch[batch.length - 1].id; 86 | yield* fetchPages({ 87 | client, 88 | gqlQuery, 89 | pageSize, 90 | filter, 91 | entity, 92 | lastId: newLastId 93 | }); 94 | } catch (error) { 95 | console.error('Error fetching data:', error); 96 | throw error; 97 | } 98 | } 99 | 100 | /** 101 | * Converts a SubgraphAnnouncementEntity to an AnnouncementLog for interoperability 102 | * between `getAnnouncements` and `getAnnouncementsUsingSubgraph`. 103 | * 104 | * This function transforms the data structure returned by the subgraph into the 105 | * standardized AnnouncementLog format used throughout the SDK. It ensures consistency 106 | * in data representation regardless of whether announcements are fetched directly via logs 107 | * or via a subgraph. 108 | * 109 | * @param {SubgraphAnnouncementEntity} entity - The announcement entity from the subgraph. 110 | * @returns {AnnouncementLog} The converted announcement log in the standard format. 111 | */ 112 | export function convertSubgraphEntityToAnnouncementLog( 113 | entity: SubgraphAnnouncementEntity 114 | ): AnnouncementLog { 115 | return { 116 | address: ERC5564_CONTRACT_ADDRESS, // Contract address is the same for all chains 117 | blockHash: entity.blockHash as `0x${string}`, 118 | blockNumber: BigInt(entity.blockNumber), 119 | logIndex: Number(entity.logIndex), 120 | removed: entity.removed, 121 | transactionHash: entity.transactionHash as `0x${string}`, 122 | transactionIndex: Number(entity.transactionIndex), 123 | topics: entity.topics as [`0x${string}`, ...`0x${string}`[]] | [], 124 | data: entity.data as `0x${string}`, 125 | schemeId: BigInt(entity.schemeId), 126 | stealthAddress: entity.stealthAddress as `0x${string}`, 127 | caller: entity.caller as `0x${string}`, 128 | ephemeralPubKey: entity.ephemeralPubKey as `0x${string}`, 129 | metadata: entity.metadata as `0x${string}` 130 | }; 131 | } 132 | -------------------------------------------------------------------------------- /src/test/helpers/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | http, 3 | type Address, 4 | type Client, 5 | type WalletClient, 6 | createWalletClient, 7 | publicActions 8 | } from 'viem'; 9 | import { privateKeyToAccount } from 'viem/accounts'; 10 | import { getRpcUrl } from '../../lib/helpers/test/setupTestEnv'; 11 | import setupTestWallet from '../../lib/helpers/test/setupTestWallet'; 12 | import { type SuperWalletClient, VALID_CHAINS } from '../../lib/helpers/types'; 13 | import { generateKeysFromSignature } from '../../utils/helpers'; 14 | 15 | // Default private key for testing; the setupTestWallet function uses the first anvil default key, so the below will be different 16 | const ANVIL_DEFAULT_PRIVATE_KEY_2 = 17 | '0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d'; 18 | 19 | /* Gets the signature to be able to generate reproducible public/private viewing/spending keys */ 20 | export const getSignature = async ({ 21 | walletClient 22 | }: { 23 | walletClient: WalletClient; 24 | }) => { 25 | if (!walletClient.chain) throw new Error('Chain not found'); 26 | if (!walletClient.account) throw new Error('Account not found'); 27 | 28 | const MESSAGE = `Signing message for stealth transaction on chain id: ${walletClient.chain.id}`; 29 | const signature = await walletClient.signMessage({ 30 | message: MESSAGE, 31 | account: walletClient.account 32 | }); 33 | 34 | return signature; 35 | }; 36 | 37 | /* Generates the public/private viewing/spending keys from the signature */ 38 | export const getKeys = async ({ 39 | walletClient 40 | }: { 41 | walletClient: WalletClient; 42 | }) => { 43 | const signature = await getSignature({ walletClient }); 44 | const keys = generateKeysFromSignature(signature); 45 | return keys; 46 | }; 47 | 48 | /* Sets up the sending and receiving wallet clients for testing */ 49 | export const getWalletClients = async () => { 50 | const sendingWalletClient = await setupTestWallet(); 51 | 52 | const chain = sendingWalletClient.chain; 53 | if (!chain) throw new Error('Chain not found'); 54 | if (!(chain.id in VALID_CHAINS)) { 55 | throw new Error('Invalid chain'); 56 | } 57 | 58 | const rpcUrl = getRpcUrl(); 59 | 60 | const receivingWalletClient: SuperWalletClient = createWalletClient({ 61 | account: privateKeyToAccount(ANVIL_DEFAULT_PRIVATE_KEY_2), 62 | chain, 63 | transport: http(rpcUrl) 64 | }).extend(publicActions); 65 | 66 | return { sendingWalletClient, receivingWalletClient }; 67 | }; 68 | 69 | export const getAccount = (walletClient: WalletClient | Client) => { 70 | if (!walletClient.account) throw new Error('Account not found'); 71 | return walletClient.account; 72 | }; 73 | 74 | /* Gets the wallet clients, accounts, and keys for the sending and receiving wallets */ 75 | export const getWalletClientsAndKeys = async () => { 76 | const { sendingWalletClient, receivingWalletClient } = 77 | await getWalletClients(); 78 | 79 | const sendingAccount = getAccount(sendingWalletClient); 80 | const receivingAccount = getAccount(receivingWalletClient); 81 | 82 | const receivingAccountKeys = await getKeys({ 83 | walletClient: receivingWalletClient 84 | }); 85 | 86 | return { 87 | sendingWalletClient, 88 | receivingWalletClient, 89 | sendingAccount, 90 | receivingAccount, 91 | receivingAccountKeys 92 | }; 93 | }; 94 | 95 | /* Set up the initial balance details for the sending and receiving wallets */ 96 | export const setupInitialBalances = async ({ 97 | sendingWalletClient, 98 | receivingWalletClient 99 | }: { 100 | sendingWalletClient: SuperWalletClient; 101 | receivingWalletClient: SuperWalletClient; 102 | }) => { 103 | const sendingAccount = getAccount(sendingWalletClient); 104 | const receivingAccount = getAccount(receivingWalletClient); 105 | const sendingWalletStartingBalance = await sendingWalletClient.getBalance({ 106 | address: sendingAccount.address 107 | }); 108 | const receivingWalletStartingBalance = await receivingWalletClient.getBalance( 109 | { 110 | address: receivingAccount.address 111 | } 112 | ); 113 | 114 | return { 115 | sendingWalletStartingBalance, 116 | receivingWalletStartingBalance 117 | }; 118 | }; 119 | 120 | /* Send ETH and wait for the transaction to be confirmed */ 121 | export const sendEth = async ({ 122 | sendingWalletClient, 123 | to, 124 | value 125 | }: { 126 | sendingWalletClient: SuperWalletClient; 127 | to: Address; 128 | value: bigint; 129 | }) => { 130 | const account = getAccount(sendingWalletClient); 131 | const hash = await sendingWalletClient.sendTransaction({ 132 | value, 133 | to, 134 | account, 135 | chain: sendingWalletClient.chain 136 | }); 137 | 138 | const receipt = await sendingWalletClient.waitForTransactionReceipt({ hash }); 139 | 140 | const gasPriceSend = receipt.effectiveGasPrice; 141 | const gasEstimate = receipt.gasUsed * gasPriceSend; 142 | 143 | return { hash, gasEstimate }; 144 | }; 145 | 146 | /* Get the ending balances for the sending and receiving wallets */ 147 | export const getEndingBalances = async ({ 148 | sendingWalletClient, 149 | receivingWalletClient 150 | }: { 151 | sendingWalletClient: SuperWalletClient; 152 | receivingWalletClient: SuperWalletClient; 153 | }) => { 154 | const sendingAccount = getAccount(sendingWalletClient); 155 | const receivingAccount = getAccount(receivingWalletClient); 156 | const sendingWalletEndingBalance = await sendingWalletClient.getBalance({ 157 | address: sendingAccount.address 158 | }); 159 | const receivingWalletEndingBalance = await receivingWalletClient.getBalance({ 160 | address: receivingAccount.address 161 | }); 162 | 163 | return { 164 | sendingWalletEndingBalance, 165 | receivingWalletEndingBalance 166 | }; 167 | }; 168 | -------------------------------------------------------------------------------- /src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts: -------------------------------------------------------------------------------- 1 | import { afterAll, beforeAll, describe, expect, test } from 'bun:test'; 2 | import type { Address } from 'viem'; 3 | import { 4 | type AnnouncementLog, 5 | ERC5564AnnouncerAbi, 6 | VALID_SCHEME_ID, 7 | generateStealthAddress 8 | } from '../../..'; 9 | import setupTestEnv from '../../helpers/test/setupTestEnv'; 10 | import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; 11 | import setupTestWallet from '../../helpers/test/setupTestWallet'; 12 | import type { SuperWalletClient } from '../../helpers/types'; 13 | import type { StealthActions } from '../../stealthClient/types'; 14 | 15 | const NUM_ANNOUNCEMENTS = 3; 16 | const WATCH_POLLING_INTERVAL = 1000; 17 | 18 | type WriteAnnounceArgs = { 19 | schemeId: bigint; 20 | stealthAddress: `0x${string}`; 21 | ephemeralPublicKey: `0x${string}`; 22 | viewTag: `0x${string}`; 23 | }; 24 | 25 | const announce = async ({ 26 | walletClient, 27 | ERC5564Address, 28 | args 29 | }: { 30 | walletClient: SuperWalletClient; 31 | ERC5564Address: Address; 32 | args: WriteAnnounceArgs; 33 | }) => { 34 | if (!walletClient.account) throw new Error('No account found'); 35 | 36 | // Write to the announcement contract 37 | const hash = await walletClient.writeContract({ 38 | address: ERC5564Address, 39 | functionName: 'announce', 40 | args: [ 41 | args.schemeId, 42 | args.stealthAddress, 43 | args.ephemeralPublicKey, 44 | args.viewTag 45 | ], 46 | abi: ERC5564AnnouncerAbi, 47 | chain: walletClient.chain, 48 | account: walletClient.account 49 | }); 50 | 51 | // Wait for the transaction receipt 52 | await walletClient.waitForTransactionReceipt({ 53 | hash 54 | }); 55 | 56 | return hash; 57 | }; 58 | 59 | // Delay to wait for the announcements to be watched in accordance with the polling interval 60 | const delay = async () => 61 | await new Promise(resolve => setTimeout(resolve, WATCH_POLLING_INTERVAL * 2)); 62 | 63 | describe('watchAnnouncementsForUser', () => { 64 | let stealthClient: StealthActions; 65 | let walletClient: SuperWalletClient; 66 | let ERC5564Address: Address; 67 | 68 | // Set up keys 69 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; 70 | const schemeIdBigInt = BigInt(schemeId); 71 | const { spendingPublicKey, viewingPrivateKey, stealthMetaAddressURI } = 72 | setupTestStealthKeys(schemeId); 73 | 74 | // Track the new announcements to see if they are being watched 75 | const newAnnouncements: AnnouncementLog[] = []; 76 | let unwatch: () => void; 77 | 78 | beforeAll(async () => { 79 | // Set up the testing environment 80 | ({ stealthClient, ERC5564Address } = await setupTestEnv()); 81 | walletClient = await setupTestWallet(); 82 | 83 | // Set up watching announcements for a user 84 | unwatch = await stealthClient.watchAnnouncementsForUser({ 85 | ERC5564Address, 86 | args: { 87 | schemeId: schemeIdBigInt, 88 | caller: walletClient.account?.address // Watch announcements for the user, who is also the caller here as an example 89 | }, 90 | handleLogsForUser: logs => { 91 | // Add the new announcements to the list 92 | // Should be just one log for each call of the announce function 93 | for (const log of logs) { 94 | newAnnouncements.push(log); 95 | } 96 | }, 97 | spendingPublicKey, 98 | viewingPrivateKey, 99 | pollOptions: { 100 | pollingInterval: WATCH_POLLING_INTERVAL // Override the default polling interval for testing 101 | } 102 | }); 103 | 104 | // Set up the stealth address to announce 105 | const { stealthAddress, ephemeralPublicKey, viewTag } = 106 | generateStealthAddress({ 107 | stealthMetaAddressURI, 108 | schemeId 109 | }); 110 | 111 | // Sequentially announce NUM_ACCOUNCEMENT times 112 | for (let i = 0; i < NUM_ANNOUNCEMENTS; i++) { 113 | await announce({ 114 | walletClient, 115 | ERC5564Address, 116 | args: { 117 | schemeId: schemeIdBigInt, 118 | stealthAddress, 119 | ephemeralPublicKey, 120 | viewTag 121 | } 122 | }); 123 | } 124 | 125 | // Small wait to let the announcements be watched 126 | await delay(); 127 | }); 128 | 129 | afterAll(() => { 130 | unwatch(); 131 | }); 132 | 133 | test('should watch announcements for a user', () => { 134 | // Check if the announcements were watched 135 | // There should be NUM_ACCOUNCEMENTS announcements because there were NUM_ANNOUNCEMENTS calls to the announce function 136 | expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); 137 | }); 138 | 139 | test('should correctly not update announcements for a user if announcement does not apply to user', async () => { 140 | // Announce again, but arbitrarily (just as an example/for testing) change the ephemeral public key, 141 | // so that the announcement does not apply to the user, and is not watched 142 | const { stealthAddress, ephemeralPublicKey, viewTag } = 143 | generateStealthAddress({ 144 | stealthMetaAddressURI, 145 | schemeId 146 | }); 147 | 148 | const incrementLastCharOfHexString = (hexStr: `0x${string}`) => { 149 | const lastChar = hexStr.slice(-1); 150 | const base = '0123456789abcdef'; 151 | const index = base.indexOf(lastChar.toLowerCase()); 152 | const newLastChar = index === 15 ? '0' : base[index + 1]; // Roll over from 'f' to '0' 153 | return `0x${hexStr.slice(2, -1) + newLastChar}` as `0x${string}`; 154 | }; 155 | 156 | // Replace the last character of ephemeralPublicKey with a different character for testing 157 | const newEphemeralPublicKey = 158 | incrementLastCharOfHexString(ephemeralPublicKey); 159 | 160 | // Write to the announcement contract with an inaccurate ephemeral public key 161 | await announce({ 162 | walletClient, 163 | ERC5564Address, 164 | args: { 165 | schemeId: BigInt(schemeId), 166 | stealthAddress, 167 | ephemeralPublicKey: newEphemeralPublicKey, 168 | viewTag 169 | } 170 | }); 171 | 172 | await delay(); 173 | 174 | // Expect no change in the number of announcements watched 175 | expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); 176 | }); 177 | }); 178 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stealth Address SDK 2 | 3 | This TypeScript SDK provides tools for working with Ethereum stealth addresses as defined in [EIP-5564](https://eips.ethereum.org/EIPS/eip-5564) and [EIP-6538](https://eips.ethereum.org/EIPS/eip-6538). It aims to offer a comprehensive suite of functionalities for both generating stealth addresses and interacting with stealth transactions. 4 | 5 | ## Documentation 6 | 7 | For comprehensive documentation and to learn more about stealth addresses, please visit our [official documentation site](https://stealthaddress.dev/). 8 | 9 | ## Contract Deployments 10 | 11 | Information about contract deployments can be found on the [deployments page](https://stealthaddress.dev/contracts/deployments) of our official documentation site. 12 | 13 | ## Features 14 | 15 | - Generate Ethereum stealth addresses. 16 | - Compute stealth address private keys. 17 | - Check stealth address announcements to determine if they are intended for a specific user. 18 | - Look up the stealth meta address for a registrant 19 | - Fetch announcements 20 | - Watch announcements for a user 21 | - Prepare the payload for announcing stealth address details 22 | - Prepare the payload for registering a stealth meta-address on someone's behalf 23 | 24 | ## Installation 25 | 26 | ```bash 27 | npm install @scopelift/stealth-address-sdk 28 | # or 29 | yarn add @scopelift/stealth-address-sdk 30 | # or 31 | bun install @scopelift/stealth-address-sdk 32 | ``` 33 | 34 | ## Testing 35 | 36 | Tests default to using your local [anvil](https://book.getfoundry.sh/anvil/) node 37 | 38 | ```bash 39 | anvil 40 | bun run test 41 | ``` 42 | 43 | Alternatively, run your tests using a fork of your provided (`RPC_URL` in `env`) rpc url 44 | 45 | ```bash 46 | bun run anvil-fork 47 | # run all tests 48 | bun run test-fork 49 | # or for a specific file 50 | bun run test-fork FILE={file path} 51 | ``` 52 | 53 | ## Quick Start 54 | 55 | ### Generating a Stealth Address 56 | 57 | ```ts 58 | import { generateStealthAddress } from "@scopelift/stealth-address-sdk"; 59 | 60 | // Your stealth meta-address URI 61 | // Follows the format: "st::", where is the chain identifier (https://eips.ethereum.org/EIPS/eip-3770#examples) and is the stealth meta-address. 62 | const stealthMetaAddressURI = "..."; 63 | 64 | // Generate a stealth address using the default scheme (1) 65 | // To learn more about the initial implementation scheme using SECP256k1, please see the reference here (https://eips.ethereum.org/EIPS/eip-5564) 66 | const result = generateStealthAddress({ stealthMetaAddressURI }); 67 | 68 | // Use the stealth address 69 | console.log(result.stealthAddress); 70 | ``` 71 | 72 | ### Computing Stealth Key 73 | 74 | ```ts 75 | import { 76 | computeStealthKey, 77 | VALID_SCHEME_ID, 78 | } from "@scopelift/stealth-address-sdk"; 79 | 80 | // Example inputs 81 | const viewingPrivateKey = "0x..."; // Viewing private key of the recipient 82 | const spendingPrivateKey = "0x..."; // Spending private key of the recipient 83 | const ephemeralPublicKey = "0x..."; // Ephemeral public key from the sender's announcement 84 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; // Scheme ID, currently only '1' is supported 85 | 86 | // Compute the stealth private key 87 | const stealthPrivateKey = computeStealthKey({ 88 | viewingPrivateKey, 89 | spendingPrivateKey, 90 | ephemeralPublicKey, 91 | schemeId, 92 | }); 93 | ``` 94 | 95 | ### Checking Stealth Address Announcements 96 | 97 | ```ts 98 | import { 99 | checkStealthAddress, 100 | VALID_SCHEME_ID, 101 | } from "@scopelift/stealth-address-sdk"; 102 | 103 | // Example inputs 104 | const ephemeralPublicKey = "0x..."; // The ephemeral public key from the announcement 105 | const spendingPublicKey = "0x..."; // The user's spending public key 106 | const userStealthAddress = "0x..."; // The user's stealth address 107 | const viewingPrivateKey = "0x..."; // The user's viewing private key 108 | const viewTag = "0x..."; // The view tag from the announcement 109 | const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; // Scheme ID, currently only '1' is supported 110 | 111 | // Check if the announcement is intended for the user 112 | const isForUser = checkStealthAddress({ 113 | ephemeralPublicKey, 114 | schemeId, 115 | spendingPublicKey, 116 | userStealthAddress, 117 | viewingPrivateKey, 118 | viewTag, 119 | }); 120 | 121 | console.log( 122 | isForUser 123 | ? "Announcement is for the user" 124 | : "Announcement is not for the user" 125 | ); 126 | ``` 127 | 128 | ### Fetching announcements, and checking if the associated stealth address is for the user 129 | 130 | ```ts 131 | import { 132 | ERC5564_CONTRACT_ADDRESS, 133 | VALID_SCHEME_ID, 134 | createStealthClient, 135 | } from "@scopelift/stealth-address-sdk"; 136 | 137 | // Example parameters 138 | const chainId = 11155111; // Example chain ID for Sepolia 139 | const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; 140 | 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) 141 | 142 | // Initialize the stealth client 143 | const stealthClient = createStealthClient({ chainId, rpcUrl: rpcUrl! }); 144 | 145 | // Use the address of your calling contract if applicable 146 | const caller = "0xYourCallingContractAddress"; 147 | 148 | // Your scheme id 149 | const schemeId = BigInt(VALID_SCHEME_ID.SCHEME_ID_1); 150 | 151 | // The contract address of the ERC5564Announcer on your target blockchain 152 | // You can use the provided ERC5564_CONTRACT_ADDRESS get the singleton contract address for a valid chain ID 153 | const ERC5564Address = ERC5564_CONTRACT_ADDRESS; 154 | 155 | async function fetchAnnouncementsForUser() { 156 | // Example call to getAnnouncements action on the stealth client to get all potential announcements 157 | // Use your preferred method to get announcements if different, and 158 | // adjust parameters according to your requirements 159 | const announcements = await stealthClient.getAnnouncements({ 160 | ERC5564Address, 161 | args: { 162 | schemeId, 163 | caller, 164 | // Additional args for filtering, if necessary 165 | }, 166 | fromBlock, // Optional fromBlock parameter (defaults to 0, which can be slow for many blocks) 167 | }); 168 | 169 | // Example call to getAnnouncementsForUser action on the stealth client 170 | // Adjust parameters according to your requirements 171 | const userAnnouncements = await stealthClient.getAnnouncementsForUser({ 172 | announcements, 173 | spendingPublicKey: "0xUserSpendingPublicKey", 174 | viewingPrivateKey: "0xUserViewingPrivateKey", 175 | }); 176 | 177 | return userAnnouncements; 178 | } 179 | ``` 180 | 181 | ## License 182 | 183 | [MIT](/LICENSE) License 184 | --------------------------------------------------------------------------------