├── 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 |
--------------------------------------------------------------------------------