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