From 818b377f9ae927f2af20b3ff704f1437f96c7c5e Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 12:14:55 -0700 Subject: [PATCH 01/10] fix: use beforeall --- .../getAnnouncements/getAnnouncements.test.ts | 56 ++++++++++++------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts index 21a2c658..f56bd7f3 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect, mock, afterEach } from 'bun:test'; +import { describe, test, expect, mock, afterEach, beforeAll } from 'bun:test'; import ERC556AnnouncerAbi from '../../abi/ERC5564Announcer'; import { VALID_SCHEME_ID, @@ -7,39 +7,53 @@ import { } from '../../..'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; +import type { StealthActions } from '../../stealthClient/types'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { Address } from 'viem'; describe('getAnnouncements', async () => { - const { stealthClient, ERC5564DeployBlock, ERC5564Address } = - await setupTestEnv(); - const walletClient = await setupTestWallet(); + let stealthClient: StealthActions; + let walletClient: SuperWalletClient; + let fromBlock: bigint; + let ERC5564Address: Address; + // 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, }); - // 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; + + // 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!, + }); - console.log('Waiting for announcement transaction to be mined...'); - // Wait for the transaction to be mined - const res = 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); }); - console.log('Announcement transaction mined:', res.transactionHash); afterEach(() => { mock.restore(); From f1d1a2de4dccc1101a2fde1881de3312de153faa Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 12:22:31 -0700 Subject: [PATCH 02/10] fix: use beforeAll --- .../getStealthMetaAddress.test.ts | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts index 7e63b28b..0b427283 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'bun:test'; +import { describe, test, expect, beforeAll } from 'bun:test'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; import { @@ -7,31 +7,39 @@ import { generateRandomStealthMetaAddress, } from '../../..'; import { GetStealthMetaAddressError } from './types'; +import type { StealthActions } from '../../stealthClient/types'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { Address } from 'viem'; describe('getStealthMetaAddress', async () => { - const { stealthClient, ERC6538Address } = await setupTestEnv(); - const walletClient = await setupTestWallet(); + let stealthClient: StealthActions, + ERC6538Address: Address, + walletClient: SuperWalletClient, + registrant: Address; // 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!; - 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 () => { const result = await stealthClient.getStealthMetaAddress({ From 6047f640be3d80f6c392bdcc822c7a7c43bb6b57 Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 12:28:23 -0700 Subject: [PATCH 03/10] fix: use beforeAll --- .../getAnnouncements/getAnnouncements.test.ts | 2 +- .../getStealthMetaAddress.test.ts | 2 +- .../prepareAnnounce/prepareAnnounce.test.ts | 63 ++++++++++++------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts index f56bd7f3..aa4dace1 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts @@ -11,7 +11,7 @@ import type { StealthActions } from '../../stealthClient/types'; import type { SuperWalletClient } from '../../helpers/types'; import type { Address } from 'viem'; -describe('getAnnouncements', async () => { +describe('getAnnouncements', () => { let stealthClient: StealthActions; let walletClient: SuperWalletClient; let fromBlock: bigint; diff --git a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts index 0b427283..82725175 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts @@ -11,7 +11,7 @@ import type { StealthActions } from '../../stealthClient/types'; import type { SuperWalletClient } from '../../helpers/types'; import type { Address } from 'viem'; -describe('getStealthMetaAddress', async () => { +describe('getStealthMetaAddress', () => { let stealthClient: StealthActions, ERC6538Address: Address, walletClient: SuperWalletClient, diff --git a/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts b/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts index a6b65e52..5dbb1851 100644 --- a/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts +++ b/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts @@ -1,18 +1,22 @@ -import { describe, test, expect } from 'bun:test'; +import { beforeAll, describe, test, expect } from 'bun:test'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; import { VALID_SCHEME_ID, generateStealthAddress } from '../../..'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; import { PrepareError } from '../types'; +import type { StealthActions } from '../../stealthClient/types'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import type { SuperWalletClient } from '../../helpers/types'; + +describe('prepareAnnounce', () => { + let stealthClient: StealthActions, + ERC5564Address: Address, + walletClient: SuperWalletClient, + account: Address, + chain: Chain; -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 chain = walletClient.chain!; - const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, @@ -26,26 +30,37 @@ describe('prepareAnnounce', async () => { metadata: viewTag, }; - const prepared = await stealthClient.prepareAnnounce({ - account, - args: prepareArgs, - ERC5564Address, - }); + // 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, - account, - }); + 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, - account, - }); + const prepared = await stealthClient.prepareAnnounce({ + account, + args: prepareArgs, + ERC5564Address, + }); - const res = await walletClient.waitForTransactionReceipt({ hash }); + // 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 }); + }); test('should throw PrepareError when given invalid params', () => { const invalidERC5564Address = '0xinvalid'; From ffe974738b070d71b9ce38a8dbbbd2e7c4bfdc5a Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 12:32:27 -0700 Subject: [PATCH 04/10] fix: use beforeAll --- .../prepareRegisterKeys.test.ts | 72 ++++++++++++------- 1 file changed, 46 insertions(+), 26 deletions(-) diff --git a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts index 5c2c20e0..50d78202 100644 --- a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts +++ b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts @@ -1,46 +1,66 @@ -import { describe, test, expect } from 'bun:test'; +import { beforeAll, describe, test, expect } from 'bun:test'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; -import { VALID_SCHEME_ID, parseStealthMetaAddressURI } from '../../..'; +import { + VALID_SCHEME_ID, + parseStealthMetaAddressURI, + type PrepareRegisterKeysParams, +} from '../../..'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; import { PrepareError } from '../types'; +import type { Address, Chain, TransactionReceipt } from 'viem'; +import type { SuperWalletClient } from '../../helpers/types'; +import type { StealthActions } from '../../stealthClient/types'; + +describe('prepareRegisterKeys', () => { + let stealthClient: StealthActions, + ERC6538Address: Address, + walletClient: SuperWalletClient, + account: Address, + chain: Chain; -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, }); - const account = walletClient.account?.address!; - const chain = walletClient.chain!; - const prepareArgs = { - 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; - const prepared = await stealthClient.prepareRegisterKeys(prepareArgs); + beforeAll(async () => { + // Set up the test environment + ({ stealthClient, ERC6538Address } = await setupTestEnv()); + walletClient = await setupTestWallet(); + account = walletClient.account?.address!; + chain = walletClient.chain!; - // Prepare tx using viem and the prepared payload - const request = await walletClient.prepareTransactionRequest({ - ...prepared, - chain, - account, - }); + prepareArgs = { + account, + ERC6538Address, + schemeId, + stealthMetaAddress: stealthMetaAddressToRegister, + } satisfies PrepareRegisterKeysParams; + const prepared = await stealthClient.prepareRegisterKeys(prepareArgs); - const hash = await walletClient.sendTransaction({ - ...request, - chain, - account, - }); + // Prepare tx using viem and the prepared payload + const request = await walletClient.prepareTransactionRequest({ + ...prepared, + chain, + account, + }); - const res = await walletClient.waitForTransactionReceipt({ hash }); + const hash = await walletClient.sendTransaction({ + ...request, + chain, + account, + }); + res = await walletClient.waitForTransactionReceipt({ hash }); + }); test('should throw PrepareError when given invalid contract address', () => { const invalidERC6538Address = '0xinvalid'; expect( From 67263d97ca98d71293745867795e5219592e4d3f Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 14:40:07 -0700 Subject: [PATCH 05/10] fix: use beforeAll --- .../prepareRegisterKeysOnBehalf.test.ts | 171 ++++++++++-------- 1 file changed, 94 insertions(+), 77 deletions(-) diff --git a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts index 2f850a19..697aad8f 100644 --- a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts +++ b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts @@ -1,4 +1,4 @@ -import { describe, test, expect } from 'bun:test'; +import { beforeAll, describe, test, expect } from 'bun:test'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestWallet from '../../helpers/test/setupTestWallet'; import { @@ -9,95 +9,112 @@ import { import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; import type { RegisterKeysOnBehalfArgs } from './types'; import { PrepareError } from '../types'; +import type { Address, TransactionReceipt } from 'viem'; +import type { StealthActions } from '../../stealthClient/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 chain = walletClient.chain!; - - 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, + account: Address, + args: RegisterKeysOnBehalfArgs; - // 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, - }; + // Transaction receipt for writing to the contract with the prepared payload + let res: TransactionReceipt; - const signature = await walletClient.signTypedData({ - account: walletClient.account!, - primaryType, - domain, - types, - message, + 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!; + const chain = walletClient.chain!; - return signature; - }; + 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], + }); - const args: RegisterKeysOnBehalfArgs = { - registrant: account, - schemeId, - stealthMetaAddress: stealthMetaAddressToRegister, - signature: await generateSignature(), - }; + // Prepare the signature domain + const domain = { + name: 'ERC6538Registry', + version: '1.0', + chainId, + verifyingContract: ERC6538Address, + } as const; - const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ - account, - ERC6538Address, - args, - }); + // Taken from the ERC6538Registry contract + const primaryType = 'Erc6538RegistryEntry'; - // Prepare tx using viem and the prepared payload - const request = await walletClient.prepareTransactionRequest({ - ...prepared, - chain, - account, - }); + // Prepare the signature types + const types = { + [primaryType]: [ + { name: 'schemeId', type: 'uint256' }, + { name: 'stealthMetaAddress', type: 'bytes' }, + { name: 'nonce', type: 'uint256' }, + ], + } as const; - const hash = await walletClient.sendTransaction({ - ...request, - chain, - account, - }); + const message = { + schemeId: BigInt(schemeId), + stealthMetaAddress: stealthMetaAddressToRegister, + nonce, + }; - const res = await walletClient.waitForTransactionReceipt({ hash }); + const signature = await walletClient.signTypedData({ + account, + primaryType, + domain, + types, + message, + }); + + return signature; + }; + + args = { + registrant: account, + schemeId, + stealthMetaAddress: stealthMetaAddressToRegister, + signature: await generateSignature(), + } satisfies RegisterKeysOnBehalfArgs; + + const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ + account, + ERC6538Address, + args, + }); + + // 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 }); + }); test('should throw PrepareError when given invalid contract address', () => { const invalidERC6538Address = '0xinvalid'; + expect( stealthClient.prepareRegisterKeysOnBehalf({ account, From 78b10b357644fb7918b97a32166e0cb6e6f3a45d Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 15:14:27 -0700 Subject: [PATCH 06/10] fix: beforeAll --- .../watchAnnouncementsForUser.test.ts | 162 ++++++++++-------- 1 file changed, 91 insertions(+), 71 deletions(-) diff --git a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts index 90108f84..a556fc63 100644 --- a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts +++ b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts @@ -1,7 +1,6 @@ import { describe, test, expect, afterAll, beforeAll } from 'bun:test'; import setupTestEnv from '../../helpers/test/setupTestEnv'; import { - type AnnouncementArgs, type AnnouncementLog, ERC5564AnnouncerAbi, VALID_SCHEME_ID, @@ -9,6 +8,12 @@ import { } from '../../..'; import setupTestWallet from '../../helpers/test/setupTestWallet'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; +import type { StealthActions } from '../../stealthClient/types'; +import type { Address } from 'viem'; +import { type SuperWalletClient } from '../../helpers/types'; + +const NUM_ACCOUNCEMENTS = 3; +const WATCH_POLLING_INTERVAL = 1000; type WriteAnnounceArgs = { schemeId: bigint; @@ -17,31 +22,69 @@ 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; +}) => { + // 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, + walletClient: SuperWalletClient, + 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[] = []; 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 @@ -51,72 +94,44 @@ describe('watchAnnouncementsForUser', async () => { }, 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(); - } - - await delay(); - }); - - afterAll(() => { - unwatch(); - }); - - // Set up and emit announcement for specific stealth address details - const announce = async (argsOverrides?: Partial) => { - console.log('Announcing...'); - - // Set up stealth address details + // Set up the stealth address to announce 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...'); + // Sequentially announce NUM_ACCOUNCEMENT times + for (let i = 0; i < NUM_ACCOUNCEMENTS; i++) { + await announce({ + walletClient, + ERC5564Address, + args: { + schemeId: schemeIdBigInt, + stealthAddress, + ephemeralPublicKey, + viewTag, + }, + }); + } - const res = await walletClient.waitForTransactionReceipt({ - hash, - }); + // Small wait to let the announcements be watched + await delay(); + }); - console.log('Announcement transaction mined:', res.transactionHash); - return hash; - }; + afterAll(() => { + unwatch(); + }); 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_ACCOUNCEMENTS); }); test('should correctly not update announcements for a user if announcement does not apply to user', async () => { @@ -142,14 +157,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_ACCOUNCEMENTS); }); }); From 843639e9dbf2456d96a3dc59667c02244005fd70 Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 15:29:27 -0700 Subject: [PATCH 07/10] chore: remove logs --- src/lib/actions/getAnnouncements/getAnnouncements.test.ts | 4 +--- .../getAnnouncementsForUser.test.ts | 6 +----- src/lib/helpers/test/setupTestEnv.ts | 7 +------ src/scripts/deployContract.ts | 2 -- 4 files changed, 3 insertions(+), 16 deletions(-) diff --git a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts index aa4dace1..85e40913 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts @@ -47,12 +47,10 @@ describe('getAnnouncements', () => { account: walletClient.account!, }); - console.log('Waiting for announcement transaction to be mined...'); // Wait for the transaction to be mined - const res = await walletClient.waitForTransactionReceipt({ + await walletClient.waitForTransactionReceipt({ hash, }); - console.log('Announcement transaction mined:', res.transactionHash); }); afterEach(() => { diff --git a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts index 4629f137..1d2c651e 100644 --- a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts +++ b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts @@ -59,15 +59,12 @@ describe('getAnnouncementsForUser', () => { account: walletClient.account!, }); - console.log('Waiting for announcement transaction to be mined...'); // Wait for the transaction to be mined - const res = await walletClient.waitForTransactionReceipt({ + await walletClient.waitForTransactionReceipt({ hash, }); - console.log('Announcement transaction mined:', res.transactionHash); // Fetch relevant announcements to check against - console.log('fetching announcements...'); announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { @@ -78,7 +75,6 @@ describe('getAnnouncementsForUser', () => { fromBlock: ERC5564DeployBlock, toBlock: 'latest', }); - console.log('relevant announcements fetched for testing'); }); test('filters announcements correctly for the user', async () => { diff --git a/src/lib/helpers/test/setupTestEnv.ts b/src/lib/helpers/test/setupTestEnv.ts index 6b86d588..ad310d25 100644 --- a/src/lib/helpers/test/setupTestEnv.ts +++ b/src/lib/helpers/test/setupTestEnv.ts @@ -73,12 +73,7 @@ const getChainInfo = async () => { 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'); diff --git a/src/scripts/deployContract.ts b/src/scripts/deployContract.ts index 7aa0cbb3..fc7f1ace 100644 --- a/src/scripts/deployContract.ts +++ b/src/scripts/deployContract.ts @@ -46,8 +46,6 @@ const deployContract = async ({ throw new Error(`Failed to deploy ${name} contract`); } - console.log(`${name} contract deployed to: ${contractAddress}`); - return { address: contractAddress, deployBlock: blockNumber }; }; From cb96f996274c65ea363d08cd19b67fdd4bff125f Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Fri, 12 Apr 2024 15:32:40 -0700 Subject: [PATCH 08/10] feat: large num of announcements to 100 for testing speed --- .../getAnnouncementsForUser.test.ts | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts index 1d2c651e..cdabe431 100644 --- a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts +++ b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts @@ -16,6 +16,8 @@ import type { HexString } from '../../../utils/crypto/types'; import type { StealthActions } from '../../stealthClient/types'; import type { SuperWalletClient } from '../../helpers/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; @@ -166,27 +168,19 @@ describe('getAnnouncementsForUser', () => { 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, }); - 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 () => { From 4ccac661018f287d2928dab2175955a6ac802eea Mon Sep 17 00:00:00 2001 From: marcomariscal Date: Tue, 16 Apr 2024 09:29:13 -0700 Subject: [PATCH 09/10] chore: spelling --- .../watchAnnouncementsForUser.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts index a556fc63..de571df8 100644 --- a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts +++ b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts @@ -12,7 +12,7 @@ import type { StealthActions } from '../../stealthClient/types'; import type { Address } from 'viem'; import { type SuperWalletClient } from '../../helpers/types'; -const NUM_ACCOUNCEMENTS = 3; +const NUM_ANNOUNCEMENTS = 3; const WATCH_POLLING_INTERVAL = 1000; type WriteAnnounceArgs = { @@ -107,7 +107,7 @@ describe('watchAnnouncementsForUser', () => { }); // Sequentially announce NUM_ACCOUNCEMENT times - for (let i = 0; i < NUM_ACCOUNCEMENTS; i++) { + for (let i = 0; i < NUM_ANNOUNCEMENTS; i++) { await announce({ walletClient, ERC5564Address, @@ -131,7 +131,7 @@ describe('watchAnnouncementsForUser', () => { test('should watch announcements for a user', () => { // Check if the announcements were watched // There should be NUM_ACCOUNCEMENTS announcements because there were NUM_ANNOUNCEMENTS calls to the announce function - expect(newAnnouncements.length).toEqual(NUM_ACCOUNCEMENTS); + expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); }); test('should correctly not update announcements for a user if announcement does not apply to user', async () => { @@ -170,6 +170,6 @@ describe('watchAnnouncementsForUser', () => { await delay(); // Expect no change in the number of announcements watched - expect(newAnnouncements.length).toEqual(NUM_ACCOUNCEMENTS); + expect(newAnnouncements.length).toEqual(NUM_ANNOUNCEMENTS); }); }); From 4ab38f856ebc42b4d09b26f3f0e62feaf7ddf6ed Mon Sep 17 00:00:00 2001 From: marcomariscal <42938673+marcomariscal@users.noreply.github.com> Date: Thu, 2 May 2024 12:53:56 -0700 Subject: [PATCH 10/10] feat: biome for linting and formatting (#56) * feat: biome * chore: biome format * chore: biome no trailing comma * chore: format examples with biome * chore: format * feat: biome format * feat: biome lint script * feat: biome check * fix: lint errors * feat: remove lint since build already runs biome check * chore: format * feat: apply * chore: import sort * fix: use biome * feat: deterministic gen stealth meta address (#60) * fix: valid compressed key input type clarity * fix: comment and variable clarity * feat: get stealth meta address from keys * feat: is valid pub key * feat: is valid key tests * feat: gen keys from sig * chore: default export * chore: fix import * fix: test * feat: extract portions into own func for testing * feat: get stealth meta address from signature * feat: test * feat: example for gen stealth meta address from sig * chore: lint * fix: comment * feat: change get to generate (verbiage) * fix: generate verbiage * chore: clean * Update examples/generateDeterministicStealthMetaAddress/README.md Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com> * feat: send and receive test (#62) * feat: handle just passing the stealth meta-address directly * chore: format * chore: format * feat: send and receive test * chore: format * chore: comment * chore: comment * chore: clean * chore: clean * fix: just wait for the receipt --------- Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com> * fix: ci workflow for check * fix: dont apply with check * fix: check to end * chore: check --------- Co-authored-by: Gary Ghayrat <61768337+garyghayrat@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +- .npmignore | 1 - .prettierrc | 11 -- biome.json | 43 +++++ bun.lockb | Bin 44096 -> 10905 bytes examples/checkStealthAddress/index.ts | 10 +- examples/computeStealthKey/index.ts | 2 +- .../.env.example | 1 + .../README.md | 1 + .../bun.lockb | Bin 0 -> 52163 bytes .../index.html | 12 ++ .../index.tsx | 81 +++++++++ .../package.json | 21 +++ .../tsconfig.json | 17 ++ .../vite.config.ts | 7 + examples/getAnnouncements/index.ts | 17 +- examples/getAnnouncementsForUser/index.ts | 11 +- examples/getStealthMetaAddress/index.ts | 13 +- examples/prepareAnnounce/vite.config.ts | 2 +- examples/prepareRegisterKeys/vite.config.ts | 2 +- examples/watchAnnouncementsForUser/index.ts | 9 +- package.json | 9 +- src/config/contractAddresses.ts | 4 +- src/lib/abi/ERC5564Announcer.ts | 20 +-- src/lib/abi/ERC6538Registry.ts | 32 ++-- .../getAnnouncements/getAnnouncements.test.ts | 44 +++-- .../getAnnouncements/getAnnouncements.ts | 27 +-- src/lib/actions/getAnnouncements/types.ts | 2 +- .../getAnnouncementsForUser.test.ts | 83 +++++---- .../getAnnouncementsForUser.ts | 32 ++-- .../actions/getAnnouncementsForUser/types.ts | 4 +- .../getStealthMetaAddress.test.ts | 33 ++-- .../getStealthMetaAddress.ts | 6 +- .../actions/getStealthMetaAddress/types.ts | 2 +- src/lib/actions/index.ts | 22 +-- .../prepareAnnounce/prepareAnnounce.test.ts | 43 +++-- .../prepareAnnounce/prepareAnnounce.ts | 16 +- .../prepareRegisterKeys.test.ts | 40 +++-- .../prepareRegisterKeys.ts | 18 +- .../prepareRegisterKeysOnBehalf.test.ts | 56 +++--- .../prepareRegisterKeysOnBehalf.ts | 20 +-- src/lib/actions/types.ts | 2 +- .../watchAnnouncementsForUser/types.ts | 14 +- .../watchAnnouncementsForUser.test.ts | 52 +++--- .../watchAnnouncementsForUser.ts | 12 +- src/lib/helpers/chains.ts | 2 +- src/lib/helpers/test/setupTestEnv.test.ts | 14 +- src/lib/helpers/test/setupTestEnv.ts | 18 +- src/lib/helpers/test/setupTestStealthKeys.ts | 9 +- src/lib/helpers/test/setupTestWallet.test.ts | 20 +-- src/lib/helpers/test/setupTestWallet.ts | 8 +- src/lib/helpers/types.ts | 8 +- .../stealthClient/createStealthClient.test.ts | 18 +- src/lib/stealthClient/createStealthClient.ts | 30 ++-- src/lib/stealthClient/types.ts | 20 +-- src/scripts/deployContract.ts | 15 +- src/scripts/index.ts | 8 +- src/test/helpers/index.ts | 167 ++++++++++++++++++ src/test/sendReceive.test.ts | 106 +++++++++++ src/utils/crypto/checkStealthAddress.ts | 12 +- src/utils/crypto/computeStealthKey.ts | 17 +- src/utils/crypto/generateStealthAddress.ts | 116 ++++++------ src/utils/crypto/index.ts | 4 +- .../crypto/test/checkStealthAddress.test.ts | 18 +- .../crypto/test/computeStealthKey.test.ts | 18 +- .../test/generateStealthAddress.test.ts | 81 +++------ src/utils/crypto/types/index.ts | 2 +- .../helpers/generateKeysFromSignature.ts | 61 +++++++ .../generateRandomStealthMetaAddress.ts | 4 +- .../generateStealthMetaAddressFromKeys.ts | 32 ++++ ...generateStealthMetaAddressFromSignature.ts | 26 +++ src/utils/helpers/index.ts | 4 + src/utils/helpers/isValidPublicKey.ts | 19 ++ .../test/generateKeysFromSignature.test.ts | 56 ++++++ ...generateStealthMetaAddressFromKeys.test.ts | 43 +++++ ...ateStealthMetaAddressFromSignature.test.ts | 38 ++++ .../helpers/test/isValidPublicKey.test.ts | 30 ++++ src/utils/index.ts | 6 +- 78 files changed, 1331 insertions(+), 559 deletions(-) delete mode 100644 .prettierrc create mode 100644 biome.json create mode 100644 examples/generateDeterministicStealthMetaAddress/.env.example create mode 100644 examples/generateDeterministicStealthMetaAddress/README.md create mode 100755 examples/generateDeterministicStealthMetaAddress/bun.lockb create mode 100644 examples/generateDeterministicStealthMetaAddress/index.html create mode 100644 examples/generateDeterministicStealthMetaAddress/index.tsx create mode 100644 examples/generateDeterministicStealthMetaAddress/package.json create mode 100644 examples/generateDeterministicStealthMetaAddress/tsconfig.json create mode 100644 examples/generateDeterministicStealthMetaAddress/vite.config.ts create mode 100644 src/test/helpers/index.ts create mode 100644 src/test/sendReceive.test.ts create mode 100644 src/utils/helpers/generateKeysFromSignature.ts create mode 100644 src/utils/helpers/generateStealthMetaAddressFromKeys.ts create mode 100644 src/utils/helpers/generateStealthMetaAddressFromSignature.ts create mode 100644 src/utils/helpers/isValidPublicKey.ts create mode 100644 src/utils/helpers/test/generateKeysFromSignature.test.ts create mode 100644 src/utils/helpers/test/generateStealthMetaAddressFromKeys.test.ts create mode 100644 src/utils/helpers/test/generateStealthMetaAddressFromSignature.test.ts create mode 100644 src/utils/helpers/test/isValidPublicKey.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a4c1855..75934c2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -59,7 +59,7 @@ jobs: bun install bun test --coverage - lint: + check: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -67,7 +67,7 @@ jobs: - name: Install Bun uses: oven-sh/setup-bun@v1 - - name: Check formatting + - name: Check run: | bun install - bun run prettier -c src/ + bun run check diff --git a/.npmignore b/.npmignore index f07b8d48..a740d6b5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,5 +1,4 @@ # Configuration and development tools -.prettierrc .bun tsconfig.json README.md diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index b1c7fb5b..00000000 --- a/.prettierrc +++ /dev/null @@ -1,11 +0,0 @@ -{ - "semi": true, - "trailingComma": "es5", - "singleQuote": true, - "printWidth": 80, - "tabWidth": 2, - "useTabs": false, - "bracketSpacing": true, - "arrowParens": "avoid", - "endOfLine": "lf" -} diff --git a/biome.json b/biome.json new file mode 100644 index 00000000..1b5653ca --- /dev/null +++ b/biome.json @@ -0,0 +1,43 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", + "vcs": { + "root": ".", + "enabled": true, + "clientKind": "git", + "useIgnoreFile": true + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true + } + }, + "files": { + "include": ["src/**/*.ts", "examples/**/*.ts", "biome.json"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "ignore": [], + "attributePosition": "auto", + "indentStyle": "tab", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 80 + }, + "javascript": { + "globals": ["NodeJS"], + "formatter": { + "enabled": true, + "lineWidth": 80, + "indentWidth": 2, + "indentStyle": "space", + "quoteStyle": "single", + "arrowParentheses": "asNeeded", + "trailingComma": "none" + } + } +} diff --git a/bun.lockb b/bun.lockb index 1b545cdc36796762a89d368bc1ca10bf5bb85630..b8ae2fab8773dbde56f3e76f26b8012763ad728f 100755 GIT binary patch delta 3445 zcmbtW3s93+7XJV614(KG#YcDwLJVhIJSanKy6mTel)Yg_osoRQL(JFRXC_d1_N(J`ZB)=iW?sj(1%%3m!Jny;p z+|>gE97R1$s*XTl5LS67Ue9IYIE1`S6DejO~Cpz#jlj0sR421KMij94tpJ)OOTv z=n#DTNNPjI1^cs-GI7D#H}2YY9!}!EUYT-1Gy|(uaLl(aR9~GlLKNw}20ip^y}+Ke)OuVT2hH_ILpFzaqB% zeTga5Gg_DtHpb)sGqc~9m;;Um^j|Y=?r^T8(3#DlsM?)_^dmw^BNQGHg@Oef)C!|z zh?KmA9tl<|hS{T%TMaS2y@P)W{G;43_`9*&(GffDZU zTwN<@uYgoq_e3E3Yw_I&Z}$n!FlV+s~obw_F)U*8t1xHxHhHN1aYompV`rq0nVS29|sF8@59 zke?RDePzeFxzBcVSO(m$U<^E1^<5J+RI*YTQ1&Pz>c}wTK$tJ~3;8p#V|*xSqB{3y zDF^O#dpj(x$jNRx{9 z-M7J`>G4J;Wul;$SP2g4!;O7YF-Je&`+DKnT)~fZYxo-;DTBjX;N@=1)sg8}`G*JnL&s)gEBnC-t_dX|z8z?y{=7c#d!LR% z&mz$$bzbo{CDTUNSYJ3x5AVuhR{(&hX*a{3`>;u28r9r%Xm_{LZATovK$*C8>E;>FiXPl)xloz4#t{4}uV*V>vuK zGFHI%<+}@sd~ku^?=trH-kdbKD+8RnyYbx!q6$6CG4(nDa@<%5d%G=e8qO48xd&eW zT)NfB{%Y&LEF0u(t)vUB&rK$Uh?8eZPND8xQwBkz8@U1%%ZarMc3#!9pbenViMHgK zn$!KtnXb1d-g~X4oV(?ndnofyqK$4tbj`&6Ws{0(gQAzg&#f#re1sjUVc?)11m%K zB-D#-p?e>?Ytc&R?uhPdG!lGw(G3*c`e=>l7K?6|(Hp*uOc z1;Y|!bvcPOM?h(Vek{&3^MRLe2_*Qwk5@k_Upn`(MojWhX31;nLzR-{sybZFZ6y(U z=-^3<=G;>*Br8EGQ}a^9NI*8VlOI3$3T+@-xF6FJPy_W1`|h;djJ3c literal 44096 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 R@F52^A_~8n_`mP}{|C=_CL90& diff --git a/examples/checkStealthAddress/index.ts b/examples/checkStealthAddress/index.ts index fa5ecabf..d7885525 100644 --- a/examples/checkStealthAddress/index.ts +++ b/examples/checkStealthAddress/index.ts @@ -1,21 +1,21 @@ import { + VALID_SCHEME_ID, checkStealthAddress, generateRandomStealthMetaAddress, - generateStealthAddress, - VALID_SCHEME_ID, + generateStealthAddress } from 'stealth-address-sdk'; // User's keys (for example purposes, real values should be securely generated and stored) const { stealthMetaAddressURI, spendingPublicKey: userSpendingPublicKey, - viewingPrivateKey: userViewingPrivateKey, + viewingPrivateKey: userViewingPrivateKey } = generateRandomStealthMetaAddress(); // Generate a stealth address const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ schemeId: VALID_SCHEME_ID.SCHEME_ID_1, - stealthMetaAddressURI, + stealthMetaAddressURI }); console.log(`Stealth Address: ${stealthAddress}`); @@ -29,7 +29,7 @@ const isForUser = checkStealthAddress({ spendingPublicKey: userSpendingPublicKey, userStealthAddress: stealthAddress, // User's known stealth address viewingPrivateKey: userViewingPrivateKey, - viewTag, // From the announcement + viewTag // From the announcement }); console.log(`Is the announcement for the user? ${isForUser}`); diff --git a/examples/computeStealthKey/index.ts b/examples/computeStealthKey/index.ts index 29c6f602..12929122 100644 --- a/examples/computeStealthKey/index.ts +++ b/examples/computeStealthKey/index.ts @@ -11,5 +11,5 @@ const stealthPrivateKey = computeStealthKey({ ephemeralPublicKey, schemeId, spendingPrivateKey, - viewingPrivateKey, + viewingPrivateKey }); diff --git a/examples/generateDeterministicStealthMetaAddress/.env.example b/examples/generateDeterministicStealthMetaAddress/.env.example new file mode 100644 index 00000000..8e4f87c7 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/.env.example @@ -0,0 +1 @@ +VITE_RPC_URL='Your rpc url' \ No newline at end of file diff --git a/examples/generateDeterministicStealthMetaAddress/README.md b/examples/generateDeterministicStealthMetaAddress/README.md new file mode 100644 index 00000000..7ed21481 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/README.md @@ -0,0 +1 @@ +# Generate Deterministic Stealth Meta-Address Example diff --git a/examples/generateDeterministicStealthMetaAddress/bun.lockb b/examples/generateDeterministicStealthMetaAddress/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..fe1a2c591c6d6f5d6fecd9b6eb32fe336a5fc058 GIT binary patch literal 52163 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 LeJ=n1IQIVmLc!(K literal 0 HcmV?d00001 diff --git a/examples/generateDeterministicStealthMetaAddress/index.html b/examples/generateDeterministicStealthMetaAddress/index.html new file mode 100644 index 00000000..02b4dfce --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/index.html @@ -0,0 +1,12 @@ + + + + + + + +

Generate Deterministic Stealth Meta-address Example

+
+ + + diff --git a/examples/generateDeterministicStealthMetaAddress/index.tsx b/examples/generateDeterministicStealthMetaAddress/index.tsx new file mode 100644 index 00000000..75b716d0 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/index.tsx @@ -0,0 +1,81 @@ +import React, { useState } from "react"; +import ReactDOM from "react-dom/client"; +import { Address, createWalletClient, custom } from "viem"; +import { sepolia } from "viem/chains"; +import "viem/window"; + +import { generateStealthMetaAddressFromSignature } from "@scopelift/stealth-address-sdk"; + +/** + * This React component demonstrates the process of generating a stealth meta-address deterministically using a user-signed message + * It's deterministic in that the same stealth meta-address is generated for the same user, chain id, and message + * It utilizes Viem's walletClient for wallet interaction + * + * @returns The component renders a button to first handle connecting the wallet, and a subsequent button to handle stealth meta-address generation + * + * @example + * To run the development server: `bun run dev`. + */ +const Example = () => { + // Initialize your configuration + const chain = sepolia; // Example Viem chain + + // Initialize Viem wallet client if using Viem + const walletClient = createWalletClient({ + chain, + transport: custom(window.ethereum!), + }); + + // State + const [account, setAccount] = useState
(); + const [stealthMetaAddress, setStealthMetaAddress] = useState<`0x${string}`>(); + + const connect = async () => { + const [address] = await walletClient.requestAddresses(); + setAccount(address); + }; + + const signMessage = async () => { + // An example message to sign for generating the stealth meta-address + // Usually this message includes the chain id to mitigate replay attacks across different chains + // The message that is signed should clearly communicate to the user what they are signing and why + const MESSAGE_TO_SIGN = `Generate Stealth Meta-Address on ${chain.id} chain`; + + if (!account) throw new Error("A connected account is required"); + + const signature = await walletClient.signMessage({ + account, + message: MESSAGE_TO_SIGN, + }); + + return signature; + }; + + const handleSignAndGenStealthMetaAddress = async () => { + const signature = await signMessage(); + const stealthMetaAddress = + generateStealthMetaAddressFromSignature(signature); + + setStealthMetaAddress(stealthMetaAddress); + }; + + if (account) + return ( + <> + {!stealthMetaAddress ? ( + + ) : ( +
Stealth Meta-Address: {stealthMetaAddress}
+ )} +
Connected: {account}
+ + ); + + return ; +}; + +ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( + +); diff --git a/examples/generateDeterministicStealthMetaAddress/package.json b/examples/generateDeterministicStealthMetaAddress/package.json new file mode 100644 index 00000000..d53fd937 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/package.json @@ -0,0 +1,21 @@ +{ + "name": "example-generate-deterministic-stealth-meta-address", + "private": true, + "type": "module", + "scripts": { + "dev": "vite" + }, + "dependencies": { + "@types/react": "^18.2.61", + "@types/react-dom": "^18.2.19", + "@vitejs/plugin-react": "^4.2.1", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "@scopelift/stealth-address-sdk": "latest", + "viem": "latest", + "vite": "latest" + }, + "devDependencies": { + "typescript": "^5.3.3" + } +} diff --git a/examples/generateDeterministicStealthMetaAddress/tsconfig.json b/examples/generateDeterministicStealthMetaAddress/tsconfig.json new file mode 100644 index 00000000..16b8130e --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "lib": ["ESNext", "DOM", "DOM.Iterable"], + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "noEmit": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "skipLibCheck": true, + "jsx": "react", + }, + "include": ["."], +} diff --git a/examples/generateDeterministicStealthMetaAddress/vite.config.ts b/examples/generateDeterministicStealthMetaAddress/vite.config.ts new file mode 100644 index 00000000..efe63572 --- /dev/null +++ b/examples/generateDeterministicStealthMetaAddress/vite.config.ts @@ -0,0 +1,7 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()] +}); diff --git a/examples/getAnnouncements/index.ts b/examples/getAnnouncements/index.ts index db3c2946..fc806961 100644 --- a/examples/getAnnouncements/index.ts +++ b/examples/getAnnouncements/index.ts @@ -2,12 +2,13 @@ import { ERC5564_CONTRACT, VALID_SCHEME_ID, createStealthClient, - getAnnouncements, + getAnnouncements } from 'stealth-address-sdk'; // Example parameters const chainId = 11155111; // Example chain ID for Sepolia -const rpcUrl = process.env.RPC_URL!; // Your env rpc url that aligns with the chainId; +const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); const fromBlock = BigInt(12345678); // Example ERC5564 announcer contract deploy block for Sepolia, or the block in which the user registered their stealth meta address (as an example) // Initialize the stealth client @@ -27,29 +28,31 @@ const schemeId = BigInt(VALID_SCHEME_ID.SCHEME_ID_1); const ERC5564Address = ERC5564_CONTRACT.SEPOLIA; // only for Sepolia for now async function fetchAnnouncements() { + if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); + // Example call to getAnnouncements action on the stealth client // Adjust parameters according to your requirements const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: { schemeId, - caller, + caller // Additional args for filtering, if necessary }, - fromBlock, + fromBlock // toBlock: 'latest', }); // Alternatively, you can use the getAnnouncements function directly - const otherAnnouncements = await getAnnouncements({ + await getAnnouncements({ // pass in the rpcUrl and chainId to clientParams clientParams: { rpcUrl, chainId }, ERC5564Address, args: { schemeId, caller, - stealthAddress, - }, + stealthAddress + } }); console.log('Fetched announcements:', announcements); diff --git a/examples/getAnnouncementsForUser/index.ts b/examples/getAnnouncementsForUser/index.ts index 07022abf..3abff8ca 100644 --- a/examples/getAnnouncementsForUser/index.ts +++ b/examples/getAnnouncementsForUser/index.ts @@ -1,16 +1,17 @@ import { ERC5564_CONTRACT, VALID_SCHEME_ID, - createStealthClient, + createStealthClient } from 'stealth-address-sdk'; // Example parameters const chainId = 11155111; // Example chain ID for Sepolia const rpcUrl = process.env.RPC_URL; // Your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); const fromBlock = BigInt(12345678); // Example ERC5564 announcer contract deploy block for Sepolia, or the block in which the user registered their stealth meta address (as an example) // Initialize the stealth client -const stealthClient = createStealthClient({ chainId, rpcUrl: rpcUrl! }); +const stealthClient = createStealthClient({ chainId, rpcUrl }); // Use the address of your calling contract if applicable const caller = '0xYourCallingContractAddress'; @@ -37,11 +38,11 @@ async function fetchAnnouncementsForUser() { ERC5564Address, args: { schemeId, - caller, + caller // Additional args for filtering, if necessary }, fromBlock, // Optional fromBlock parameter (defaults to 0, which can be slow for many blocks) - toBlock: 'latest', // Optional toBlock parameter (defaults to latest) + toBlock: 'latest' // Optional toBlock parameter (defaults to latest) }); // Example call to getAnnouncementsForUser action on the stealth client @@ -51,7 +52,7 @@ async function fetchAnnouncementsForUser() { spendingPublicKey, viewingPrivateKey, includeList: ['0xSomeEthAddress, 0xSomeOtherEthAddress'], // Optional include list to only include announcements for specific "from" addresses - excludeList: ['0xEthAddressToExclude'], // Optional exclude list to exclude announcements for specific "from" addresses + excludeList: ['0xEthAddressToExclude'] // Optional exclude list to exclude announcements for specific "from" addresses }); return userAnnouncements; diff --git a/examples/getStealthMetaAddress/index.ts b/examples/getStealthMetaAddress/index.ts index 5ad7cb3a..ff1dcbcb 100644 --- a/examples/getStealthMetaAddress/index.ts +++ b/examples/getStealthMetaAddress/index.ts @@ -1,16 +1,17 @@ import { - createStealthClient, - getStealthMetaAddress, - VALID_SCHEME_ID, ERC6538_CONTRACT, + VALID_SCHEME_ID, + createStealthClient, + getStealthMetaAddress } from 'stealth-address-sdk'; // Example stealth client parameters const chainId = 11155111; // Example chain ID for Sepolia const rpcUrl = process.env.RPC_URL; // Use your env rpc url that aligns with the chainId; +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); // Initialize the stealth client -const stealthClient = createStealthClient({ chainId, rpcUrl: rpcUrl! }); +const stealthClient = createStealthClient({ chainId, rpcUrl }); // Example getting the singleton registry contract address for Sepolia const ERC6538Address = ERC6538_CONTRACT.SEPOLIA; @@ -24,7 +25,7 @@ const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const stealthMetaAddress = await stealthClient.getStealthMetaAddress({ ERC6538Address, registrant, - schemeId, + schemeId }); // Alternatively, you can use the getStealthMetaAddress function directly @@ -33,5 +34,5 @@ const again = await getStealthMetaAddress({ clientParams: { rpcUrl, chainId }, ERC6538Address, registrant, - schemeId, + schemeId }); diff --git a/examples/prepareAnnounce/vite.config.ts b/examples/prepareAnnounce/vite.config.ts index 4e7004eb..efe63572 100644 --- a/examples/prepareAnnounce/vite.config.ts +++ b/examples/prepareAnnounce/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()] }); diff --git a/examples/prepareRegisterKeys/vite.config.ts b/examples/prepareRegisterKeys/vite.config.ts index 4e7004eb..efe63572 100644 --- a/examples/prepareRegisterKeys/vite.config.ts +++ b/examples/prepareRegisterKeys/vite.config.ts @@ -3,5 +3,5 @@ import { defineConfig } from 'vite'; // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [react()] }); diff --git a/examples/watchAnnouncementsForUser/index.ts b/examples/watchAnnouncementsForUser/index.ts index 3da72536..df48fab8 100644 --- a/examples/watchAnnouncementsForUser/index.ts +++ b/examples/watchAnnouncementsForUser/index.ts @@ -1,12 +1,13 @@ import { - createStealthClient, ERC5564_CONTRACT, VALID_SCHEME_ID, + createStealthClient } from 'stealth-address-sdk'; // Initialize your environment variables or configuration const chainId = 11155111; // Example chain ID -const rpcUrl = process.env.RPC_URL!; // Your Ethereum RPC URL +const rpcUrl = process.env.RPC_URL; // Your Ethereum RPC URL +if (!rpcUrl) throw new Error('Missing RPC_URL environment variable'); // User's keys and stealth address details const spendingPublicKey = '0xUserSpendingPublicKey'; @@ -24,13 +25,13 @@ const unwatch = await stealthClient.watchAnnouncementsForUser({ ERC5564Address, args: { schemeId: BigInt(VALID_SCHEME_ID.SCHEME_ID_1), // Your scheme ID - caller: '0xYourCallingContractAddress', // Use the address of your calling contract if applicable + caller: '0xYourCallingContractAddress' // Use the address of your calling contract if applicable }, spendingPublicKey, viewingPrivateKey, handleLogsForUser: logs => { console.log(logs); - }, // Your callback function to handle incoming logs + } // Your callback function to handle incoming logs }); // Stop watching for announcements diff --git a/package.json b/package.json index 5f658415..0647845a 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,13 @@ "anvil-fork": "make anvil-fork", "test-fork": "make test-fork", "test": "bun test", - "build": "bun tsc", - "format": "prettier --write .", - "watch": "bun test --watch src" + "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": { 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 92093a09..4c394b23 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.test.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.test.ts @@ -1,21 +1,22 @@ -import { describe, test, expect, beforeAll } 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 { StealthActions } from '../../stealthClient/types'; import type { SuperWalletClient } from '../../helpers/types'; -import type { Address } from 'viem'; +import type { StealthActions } from '../../stealthClient/types'; 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; @@ -23,7 +24,7 @@ describe('getAnnouncements', () => { const { stealthAddress, viewTag, ephemeralPublicKey } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); // Set up the test environment and announce the stealth address @@ -31,11 +32,14 @@ describe('getAnnouncements', () => { const { stealthClient: client, ERC5564Address, - ERC5564DeployBlock, + 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({ @@ -44,12 +48,12 @@ describe('getAnnouncements', () => { args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], abi: ERC556AnnouncerAbi, chain: walletClient.chain, - account: walletClient.account!, + account }); // Wait for the transaction to be mined await walletClient.waitForTransactionReceipt({ - hash, + hash }); }); @@ -57,7 +61,7 @@ describe('getAnnouncements', () => { const announcements = await stealthClient.getAnnouncements({ ERC5564Address, args: {}, - fromBlock, + fromBlock }); expect(announcements.length).toBeGreaterThan(0); @@ -67,32 +71,34 @@ describe('getAnnouncements', () => { 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)); @@ -102,9 +108,9 @@ describe('getAnnouncements', () => { 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 87fc0938..81b63eb0 100644 --- a/src/lib/actions/getAnnouncements/getAnnouncements.ts +++ b/src/lib/actions/getAnnouncements/getAnnouncements.ts @@ -3,10 +3,10 @@ import type { BlockType } from '../types'; import { type PublicClient, parseAbiItem } from 'viem'; import { getBlock, getBlockNumber, getLogs } from 'viem/actions'; import { handleViemPublicClient } from '../../stealthClient/createStealthClient'; -import { - type AnnouncementLog, - type GetAnnouncementsParams, - type GetAnnouncementsReturnType, +import type { + AnnouncementLog, + GetAnnouncementsParams, + 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,12 +91,12 @@ 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' }); let currentBlock = resolvedFromBlock; @@ -115,7 +116,7 @@ const fetchLogsInChunks = async ({ ), fromBlock: currentBlock, toBlock: endBlock, - strict: true, + strict: true }); allLogs.push(...logs); currentBlock = endBlock + BigInt(1); @@ -134,7 +135,7 @@ const fetchLogsInChunks = async ({ */ export async function resolveBlockNumber({ publicClient, - block, + block }: { publicClient: PublicClient; block?: BlockType; 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 cdabe431..82a2021a 100644 --- a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts +++ b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.test.ts @@ -1,33 +1,34 @@ -import { expect, test, describe, beforeAll } 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'; -import type { AnnouncementLog } from '..'; -import { FromValueNotFoundError, TransactionHashRequiredError } from './types'; +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, + processAnnouncement } from './getAnnouncementsForUser'; -import { type PublicClient } from 'viem'; -import { getViewTagFromMetadata } from '../../..'; -import type { HexString } from '../../../utils/crypto/types'; -import type { StealthActions } from '../../stealthClient/types'; -import type { SuperWalletClient } from '../../helpers/types'; +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, - ephemeralPublicKey: HexString, - viewTag: HexString; - let spendingPublicKey: HexString, - viewingPrivateKey: HexString, - stealthMetaAddressURI: string; + let stealthAddress: HexString; + let ephemeralPublicKey: HexString; + let viewTag: HexString; + let spendingPublicKey: HexString; + let viewingPrivateKey: HexString; + let stealthMetaAddressURI: string; let announcements: AnnouncementLog[] = []; @@ -36,9 +37,13 @@ describe('getAnnouncementsForUser', () => { const { stealthClient: client, ERC5564Address, - ERC5564DeployBlock, + 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 @@ -48,7 +53,7 @@ describe('getAnnouncementsForUser', () => { ({ stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId: VALID_SCHEME_ID.SCHEME_ID_1, + schemeId: VALID_SCHEME_ID.SCHEME_ID_1 })); // Announce the stealth address, ephemeral public key, and view tag @@ -57,13 +62,13 @@ describe('getAnnouncementsForUser', () => { functionName: 'announce', args: [BigInt(schemeId), stealthAddress, ephemeralPublicKey, viewTag], abi: ERC5564AnnouncerAbi, - chain: walletClient.chain, - account: walletClient.account!, + chain, + account }); // Wait for the transaction to be mined await walletClient.waitForTransactionReceipt({ - hash, + hash }); // Fetch relevant announcements to check against @@ -72,10 +77,10 @@ describe('getAnnouncementsForUser', () => { args: { schemeId: BigInt(schemeId), stealthAddress, - caller: walletClient.account?.address, // Just an example; the caller is the address of the wallet since it called announce + caller: walletClient.account?.address // Just an example; the caller is the address of the wallet since it called announce }, fromBlock: ERC5564DeployBlock, - toBlock: 'latest', + toBlock: 'latest' }); }); @@ -84,7 +89,7 @@ describe('getAnnouncementsForUser', () => { const results = await stealthClient.getAnnouncementsForUser({ announcements, spendingPublicKey, - viewingPrivateKey, + viewingPrivateKey }); expect(results[0].stealthAddress).toEqual(stealthAddress); @@ -93,7 +98,7 @@ describe('getAnnouncementsForUser', () => { const results2 = await stealthClient.getAnnouncementsForUser({ announcements, spendingPublicKey: '0x', - viewingPrivateKey, + viewingPrivateKey }); expect(results2.length).toBe(0); @@ -102,15 +107,17 @@ describe('getAnnouncementsForUser', () => { 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 @@ -118,7 +125,7 @@ describe('getAnnouncementsForUser', () => { announcements, spendingPublicKey, viewingPrivateKey, - excludeList: [fromAddressToTest], + excludeList: [fromAddressToTest] }); expect(excludeListResults.length).toBe(0); @@ -128,7 +135,7 @@ describe('getAnnouncementsForUser', () => { announcements, spendingPublicKey, viewingPrivateKey, - excludeList: [someOtherAddress], + excludeList: [someOtherAddress] }); expect(excludeListResults2[0].stealthAddress).toEqual(stealthAddress); @@ -138,7 +145,7 @@ describe('getAnnouncementsForUser', () => { announcements, spendingPublicKey, viewingPrivateKey, - includeList: [fromAddressToTest], + includeList: [fromAddressToTest] }); expect(includeListResults[0].stealthAddress).toEqual(stealthAddress); @@ -148,7 +155,7 @@ describe('getAnnouncementsForUser', () => { announcements, spendingPublicKey, viewingPrivateKey, - includeList: [someOtherAddress], + includeList: [someOtherAddress] }); expect(includeListResults2.length).toBe(0); @@ -160,7 +167,7 @@ describe('getAnnouncementsForUser', () => { spendingPublicKey, viewingPrivateKey, includeList: [fromAddressToTest], - excludeList: [fromAddressToTest], + excludeList: [fromAddressToTest] }); expect(includeAndExcludeListResults.length).toBe(0); @@ -176,7 +183,7 @@ describe('getAnnouncementsForUser', () => { const results = await stealthClient.getAnnouncementsForUser({ announcements: largeAnnouncements, spendingPublicKey, - viewingPrivateKey, + viewingPrivateKey }); // Verify the function handles large data sets correctly @@ -186,7 +193,7 @@ describe('getAnnouncementsForUser', () => { test('throws TransactionHashRequiredError when transactionHash is null', async () => { const announcementWithoutHash: AnnouncementLog = { ...announcements[0], - transactionHash: null, + transactionHash: null }; expect( @@ -197,7 +204,7 @@ describe('getAnnouncementsForUser', () => { spendingPublicKey, viewingPrivateKey, excludeList: new Set([]), - includeList: new Set([]), + includeList: new Set([]) } ) ).rejects.toBeInstanceOf(TransactionHashRequiredError); @@ -209,7 +216,7 @@ describe('getAnnouncementsForUser', () => { expect( getTransactionFrom({ publicClient: walletClient as PublicClient, - hash: invalidHash, + hash: invalidHash }) ).rejects.toBeInstanceOf(FromValueNotFoundError); }); diff --git a/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts b/src/lib/actions/getAnnouncementsForUser/getAnnouncementsForUser.ts index 8ccf3333..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, [] ); @@ -91,14 +91,14 @@ export async function processAnnouncement( 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 @@ export 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 @@ export 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; @@ -175,7 +175,7 @@ async function shouldIncludeAnnouncement({ */ 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 82725175..516e8567 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.test.ts @@ -1,21 +1,21 @@ -import { describe, test, expect, beforeAll } 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 { GetStealthMetaAddressError } from './types'; -import type { StealthActions } from '../../stealthClient/types'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; import type { SuperWalletClient } from '../../helpers/types'; -import type { Address } from 'viem'; +import type { StealthActions } from '../../stealthClient/types'; +import { GetStealthMetaAddressError } from './types'; describe('getStealthMetaAddress', () => { - let stealthClient: StealthActions, - ERC6538Address: Address, - walletClient: SuperWalletClient, - registrant: Address; + 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; @@ -27,7 +27,8 @@ describe('getStealthMetaAddress', () => { walletClient = await setupTestWallet(); // Register the stealth meta address - registrant = walletClient.account?.address!; + registrant = walletClient.account?.address; + if (!registrant) throw new Error('No registrant address found'); const hash = await walletClient.writeContract({ address: ERC6538Address, @@ -35,17 +36,19 @@ describe('getStealthMetaAddress', () => { args: [BigInt(schemeId), stealthMetaAddress], abi: ERC6538RegistryAbi, chain: walletClient.chain, - account: registrant, + account: registrant }); 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); @@ -58,7 +61,7 @@ describe('getStealthMetaAddress', () => { stealthClient.getStealthMetaAddress({ ERC6538Address, registrant: invalidRegistrant, - schemeId: VALID_SCHEME_ID.SCHEME_ID_1, + 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 55589688..4fbd6649 100644 --- a/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts +++ b/src/lib/actions/getStealthMetaAddress/getStealthMetaAddress.ts @@ -3,7 +3,7 @@ import { handleViemPublicClient } from '../../stealthClient/createStealthClient' import { GetStealthMetaAddressError, type GetStealthMetaAddressParams, - type GetStealthMetaAddressReturnType, + type GetStealthMetaAddressReturnType } from './types'; /** @@ -25,7 +25,7 @@ async function getStealthMetaAddress({ clientParams, ERC6538Address, registrant, - schemeId, + schemeId }: GetStealthMetaAddressParams): Promise { const publicClient = handleViemPublicClient(clientParams); try { @@ -33,7 +33,7 @@ async function getStealthMetaAddress({ address: ERC6538Address, functionName: 'stealthMetaAddressOf', args: [registrant, BigInt(schemeId)], - abi: ERC6538RegistryAbi, + abi: ERC6538RegistryAbi }); } catch (error) { throw new GetStealthMetaAddressError( diff --git a/src/lib/actions/getStealthMetaAddress/types.ts b/src/lib/actions/getStealthMetaAddress/types.ts index 33955971..a2a43941 100644 --- a/src/lib/actions/getStealthMetaAddress/types.ts +++ b/src/lib/actions/getStealthMetaAddress/types.ts @@ -10,7 +10,7 @@ export type GetStealthMetaAddressParams = { export type GetStealthMetaAddressReturnType = `0x${string}` | undefined; export class GetStealthMetaAddressError extends Error { - constructor(message: string = 'Error getting stealth meta address.') { + 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 5dbb1851..c424c0dc 100644 --- a/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts +++ b/src/lib/actions/prepareAnnounce/prepareAnnounce.test.ts @@ -1,33 +1,33 @@ -import { beforeAll, 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 { PrepareError } from '../types'; -import type { StealthActions } from '../../stealthClient/types'; -import type { Address, Chain, TransactionReceipt } from 'viem'; +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, - ERC5564Address: Address, - walletClient: SuperWalletClient, - account: Address, - chain: Chain; + let stealthClient: StealthActions; + let ERC5564Address: Address; + let walletClient: SuperWalletClient; + let account: Address | undefined; + let chain: Chain | undefined; const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); const prepareArgs = { schemeId, stealthAddress, ephemeralPublicKey, - metadata: viewTag, + metadata: viewTag }; // Transaction receipt for writing to the contract with the prepared payload @@ -37,38 +37,43 @@ describe('prepareAnnounce', () => { // Set up the test environment ({ stealthClient, ERC5564Address } = await setupTestEnv()); walletClient = await setupTestWallet(); - account = walletClient.account?.address!; - chain = walletClient.chain!; + account = walletClient.account?.address; + 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, + ERC5564Address }); // Prepare tx using viem and the prepared payload const request = await walletClient.prepareTransactionRequest({ ...prepared, chain, - account, + account }); const hash = await walletClient.sendTransaction({ ...request, chain, - account, + account }); 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, + ERC5564Address: invalidERC5564Address }) ).rejects.toBeInstanceOf(PrepareError); }); 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 50d78202..5f2ffd8c 100644 --- a/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts +++ b/src/lib/actions/prepareRegisterKeys/prepareRegisterKeys.test.ts @@ -1,29 +1,29 @@ -import { beforeAll, 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, - parseStealthMetaAddressURI, type PrepareRegisterKeysParams, + VALID_SCHEME_ID, + parseStealthMetaAddressURI } from '../../..'; +import setupTestEnv from '../../helpers/test/setupTestEnv'; import setupTestStealthKeys from '../../helpers/test/setupTestStealthKeys'; -import { PrepareError } from '../types'; -import type { Address, Chain, TransactionReceipt } from 'viem'; +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, - ERC6538Address: Address, - walletClient: SuperWalletClient, - account: Address, - chain: Chain; + let stealthClient: StealthActions; + let ERC6538Address: Address; + let walletClient: SuperWalletClient; + let account: Address | undefined; + let chain: Chain | undefined; const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }); // Prepare payload args @@ -35,14 +35,16 @@ describe('prepareRegisterKeys', () => { // Set up the test environment ({ stealthClient, ERC6538Address } = await setupTestEnv()); walletClient = await setupTestWallet(); - account = walletClient.account?.address!; - chain = walletClient.chain!; + account = walletClient.account?.address; + if (!account) throw new Error('No account found'); + chain = walletClient.chain; + if (!chain) throw new Error('No chain found'); prepareArgs = { account, ERC6538Address, schemeId, - stealthMetaAddress: stealthMetaAddressToRegister, + stealthMetaAddress: stealthMetaAddressToRegister } satisfies PrepareRegisterKeysParams; const prepared = await stealthClient.prepareRegisterKeys(prepareArgs); @@ -50,13 +52,13 @@ describe('prepareRegisterKeys', () => { const request = await walletClient.prepareTransactionRequest({ ...prepared, chain, - account, + account }); const hash = await walletClient.sendTransaction({ ...request, chain, - account, + account }); res = await walletClient.waitForTransactionReceipt({ hash }); @@ -66,7 +68,7 @@ describe('prepareRegisterKeys', () => { expect( stealthClient.prepareRegisterKeys({ ...prepareArgs, - ERC6538Address: invalidERC6538Address, + ERC6538Address: invalidERC6538Address }) ).rejects.toBeInstanceOf(PrepareError); }); 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 697aad8f..9f5dd34a 100644 --- a/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts +++ b/src/lib/actions/prepareRegisterKeysOnBehalf/prepareRegisterKeysOnBehalf.test.ts @@ -1,21 +1,21 @@ -import { beforeAll, 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 type { RegisterKeysOnBehalfArgs } from './types'; -import { PrepareError } from '../types'; -import type { Address, TransactionReceipt } from 'viem'; +import setupTestWallet from '../../helpers/test/setupTestWallet'; import type { StealthActions } from '../../stealthClient/types'; +import { PrepareError } from '../types'; +import type { RegisterKeysOnBehalfArgs } from './types'; describe('prepareRegisterKeysOnBehalf', () => { - let stealthClient: StealthActions, - account: Address, - args: RegisterKeysOnBehalfArgs; + let stealthClient: StealthActions; + let account: Address | undefined; + let args: RegisterKeysOnBehalfArgs; // Transaction receipt for writing to the contract with the prepared payload let res: TransactionReceipt; @@ -24,7 +24,7 @@ describe('prepareRegisterKeysOnBehalf', () => { const { stealthClient: client, ERC6538Address, - chainId, + chainId } = await setupTestEnv(); stealthClient = client; const walletClient = await setupTestWallet(); @@ -32,18 +32,20 @@ describe('prepareRegisterKeysOnBehalf', () => { const { stealthMetaAddressURI } = setupTestStealthKeys(schemeId); const stealthMetaAddressToRegister = parseStealthMetaAddressURI({ stealthMetaAddressURI, - schemeId, + schemeId }); - account = walletClient.account?.address!; - const chain = walletClient.chain!; + account = walletClient.account?.address; + if (!account) throw new Error('No account found'); + const chain = walletClient.chain; + if (!chain) throw new Error('No chain found'); - const generateSignature = async () => { + 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], + args: [account] }); // Prepare the signature domain @@ -51,7 +53,7 @@ describe('prepareRegisterKeysOnBehalf', () => { name: 'ERC6538Registry', version: '1.0', chainId, - verifyingContract: ERC6538Address, + verifyingContract: ERC6538Address } as const; // Taken from the ERC6538Registry contract @@ -62,14 +64,14 @@ describe('prepareRegisterKeysOnBehalf', () => { [primaryType]: [ { name: 'schemeId', type: 'uint256' }, { name: 'stealthMetaAddress', type: 'bytes' }, - { name: 'nonce', type: 'uint256' }, - ], + { name: 'nonce', type: 'uint256' } + ] } as const; const message = { schemeId: BigInt(schemeId), stealthMetaAddress: stealthMetaAddressToRegister, - nonce, + nonce }; const signature = await walletClient.signTypedData({ @@ -77,7 +79,7 @@ describe('prepareRegisterKeysOnBehalf', () => { primaryType, domain, types, - message, + message }); return signature; @@ -87,39 +89,41 @@ describe('prepareRegisterKeysOnBehalf', () => { registrant: account, schemeId, stealthMetaAddress: stealthMetaAddressToRegister, - signature: await generateSignature(), + signature: await generateSignature(account) } satisfies RegisterKeysOnBehalfArgs; const prepared = await stealthClient.prepareRegisterKeysOnBehalf({ account, ERC6538Address, - args, + args }); // Prepare tx using viem and the prepared payload const request = await walletClient.prepareTransactionRequest({ ...prepared, chain, - account, + account }); const hash = await walletClient.sendTransaction({ ...request, chain, - account, + account }); res = await walletClient.waitForTransactionReceipt({ hash }); }); test('should throw PrepareError when given invalid contract address', () => { + if (!account) throw new Error('No account found'); + const invalidERC6538Address = '0xinvalid'; expect( stealthClient.prepareRegisterKeysOnBehalf({ account, ERC6538Address: invalidERC6538Address, - args, + args }) ).rejects.toBeInstanceOf(PrepareError); }); 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 de571df8..b542daec 100644 --- a/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts +++ b/src/lib/actions/watchAnnouncementsForUser/watchAnnouncementsForUser.test.ts @@ -1,16 +1,16 @@ -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 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'; -import type { Address } from 'viem'; -import { type SuperWalletClient } from '../../helpers/types'; const NUM_ANNOUNCEMENTS = 3; const WATCH_POLLING_INTERVAL = 1000; @@ -25,12 +25,14 @@ type WriteAnnounceArgs = { const announce = async ({ walletClient, ERC5564Address, - args, + 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, @@ -39,16 +41,16 @@ const announce = async ({ args.schemeId, args.stealthAddress, args.ephemeralPublicKey, - args.viewTag, + args.viewTag ], abi: ERC5564AnnouncerAbi, chain: walletClient.chain, - account: walletClient.account!, + account: walletClient.account }); // Wait for the transaction receipt await walletClient.waitForTransactionReceipt({ - hash, + hash }); return hash; @@ -59,9 +61,9 @@ const delay = async () => await new Promise(resolve => setTimeout(resolve, WATCH_POLLING_INTERVAL)); describe('watchAnnouncementsForUser', () => { - let stealthClient: StealthActions, - walletClient: SuperWalletClient, - ERC5564Address: Address; + let stealthClient: StealthActions; + let walletClient: SuperWalletClient; + let ERC5564Address: Address; // Set up keys const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; @@ -70,7 +72,7 @@ describe('watchAnnouncementsForUser', () => { setupTestStealthKeys(schemeId); // Track the new announcements to see if they are being watched - let newAnnouncements: AnnouncementLog[] = []; + const newAnnouncements: AnnouncementLog[] = []; let unwatch: () => void; beforeAll(async () => { @@ -83,27 +85,27 @@ describe('watchAnnouncementsForUser', () => { ERC5564Address, args: { schemeId: schemeIdBigInt, - caller: walletClient.account?.address, // Watch announcements for the user, who is also the caller here as an example + 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: WATCH_POLLING_INTERVAL, // Override the default polling interval for testing - }, + pollingInterval: WATCH_POLLING_INTERVAL // Override the default polling interval for testing + } }); // Set up the stealth address to announce const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); // Sequentially announce NUM_ACCOUNCEMENT times @@ -115,8 +117,8 @@ describe('watchAnnouncementsForUser', () => { schemeId: schemeIdBigInt, stealthAddress, ephemeralPublicKey, - viewTag, - }, + viewTag + } }); } @@ -140,7 +142,7 @@ describe('watchAnnouncementsForUser', () => { const { stealthAddress, ephemeralPublicKey, viewTag } = generateStealthAddress({ stealthMetaAddressURI, - schemeId, + schemeId }); const incrementLastCharOfHexString = (hexStr: `0x${string}`) => { @@ -163,8 +165,8 @@ describe('watchAnnouncementsForUser', () => { schemeId: BigInt(schemeId), stealthAddress, ephemeralPublicKey: newEphemeralPublicKey, - viewTag, - }, + viewTag + } }); await delay(); 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 62d66e6a..22727f40 100644 --- a/src/lib/helpers/chains.ts +++ b/src/lib/helpers/chains.ts @@ -1,4 +1,4 @@ -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 => { diff --git a/src/lib/helpers/test/setupTestEnv.test.ts b/src/lib/helpers/test/setupTestEnv.test.ts index e03629f5..3fd7d884 100644 --- a/src/lib/helpers/test/setupTestEnv.test.ts +++ b/src/lib/helpers/test/setupTestEnv.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, test, mock, beforeEach } from 'bun:test'; -import { LOCAL_ENDPOINT } from './setupTestEnv'; +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 () => { @@ -14,7 +14,7 @@ describe('setupTestEnv with different environment configurations', () => { test('throws error when USE_FORK is true and RPC_URL is not defined', async () => { process.env.USE_FORK = 'true'; - delete process.env.RPC_URL; + process.env.RPC_URL = undefined; const { getRpcUrl } = await import('./setupTestEnv'); expect(getRpcUrl).toThrow('RPC_URL not defined in env'); @@ -65,8 +65,8 @@ describe('fetchChainId', async () => { mock.module('./setupTestEnv', () => ({ fetchJson: () => Promise.resolve({ - result: '0x1', - }), + result: '0x1' + }) })); const chainId = await fetchChainId(); @@ -75,14 +75,14 @@ describe('fetchChainId', async () => { test('fetch failure throws error', async () => { mock.module('./setupTestEnv', () => ({ - fetchJson: () => Promise.reject(new Error('Network failure')), + 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 () => { - delete process.env.RPC_URL; + 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 ad310d25..fac61f4c 100644 --- a/src/lib/helpers/test/setupTestEnv.ts +++ b/src/lib/helpers/test/setupTestEnv.ts @@ -1,9 +1,9 @@ +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'; export const LOCAL_ENDPOINT = 'http://127.0.0.1:8545'; @@ -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 }; }; @@ -90,22 +90,22 @@ export const fetchChainId = async (): Promise => { 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' + }) }); 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: any) => { +const fetchJson = async (url: string, options: FetchRequestInit) => { const response = await fetch(url, options); if (!response.ok) { 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 index 3760f34a..f53eddcc 100644 --- a/src/lib/helpers/test/setupTestWallet.test.ts +++ b/src/lib/helpers/test/setupTestWallet.test.ts @@ -1,22 +1,22 @@ -import { beforeEach, mock, describe, test, expect, afterEach } from 'bun:test'; +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; import { privateKeyToAccount } from 'viem/accounts'; -import { ANVIL_DEFAULT_PRIVATE_KEY } from './setupTestWallet'; 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(() => { - delete process.env.USE_FORK; - delete process.env.RPC_URL; - delete process.env.PRIVATE_KEY; + process.env.USE_FORK = undefined; + process.env.RPC_URL = undefined; + process.env.PRIVATE_KEY = undefined; }); afterEach(() => { - delete process.env.USE_FORK; - delete process.env.RPC_URL; - delete process.env.PRIVATE_KEY; + 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', () => { @@ -43,8 +43,8 @@ describe('setupTestWallet', async () => { mock.module('./setupTestEnv', () => ({ fetchJson: () => Promise.resolve({ - result: validChainId, - }), + result: validChainId + }) })); expect(setupTestWallet()).rejects.toThrow( diff --git a/src/lib/helpers/test/setupTestWallet.ts b/src/lib/helpers/test/setupTestWallet.ts index 366eecdc..521b6d97 100644 --- a/src/lib/helpers/test/setupTestWallet.ts +++ b/src/lib/helpers/test/setupTestWallet.ts @@ -1,9 +1,9 @@ -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'; 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); }; 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 index d9a01ac1..c0557ab7 100644 --- a/src/lib/stealthClient/createStealthClient.test.ts +++ b/src/lib/stealthClient/createStealthClient.test.ts @@ -1,12 +1,12 @@ import { describe, expect, test } from 'bun:test'; -import createStealthClient, { - handleViemPublicClient, -} from './createStealthClient'; -import { PublicClientRequiredError, type ClientParams } from './types'; -import { createPublicClient, http } from 'viem'; +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 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', () => { @@ -14,7 +14,7 @@ describe('createStealthClient', () => { expect(() => createStealthClient({ chainId: invalidChainId as VALID_CHAIN_IDS, // Cast as valid chain to trigger error - rpcUrl: LOCAL_ENDPOINT, + rpcUrl: LOCAL_ENDPOINT }) ).toThrow(new Error('Invalid chainId: 9999')); }); @@ -31,7 +31,7 @@ describe('handleViemPublicClient', () => { test('returns publicClient when provided', () => { const mockPublicClient = createPublicClient({ chain: foundry, - transport: http(LOCAL_ENDPOINT), + transport: http(LOCAL_ENDPOINT) }); const client = handleViemPublicClient({ publicClient: mockPublicClient }); expect(client).toBe(mockPublicClient); @@ -42,7 +42,7 @@ describe('handleViemPublicClient', () => { expect(() => handleViemPublicClient({ chainId: undefined as unknown as VALID_CHAIN_IDS, // Cast as valid chain to trigger error - rpcUrl: exampleRpcUrl, + rpcUrl: exampleRpcUrl }) ).toThrow( new PublicClientRequiredError('public client could not be created.') diff --git a/src/lib/stealthClient/createStealthClient.ts b/src/lib/stealthClient/createStealthClient.ts index 3ef1ca9c..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,52 +26,52 @@ import { */ function createStealthClient({ chainId, - rpcUrl, + rpcUrl }: StealthClientInitParams): StealthClientReturnType { const chain = getChain(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; @@ -93,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 fc7f1ace..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 } = 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 6e6f029a..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,19 +100,23 @@ 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') { @@ -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,6 +413,6 @@ export { parseKeysFromStealthMetaAddress, parseStealthMetaAddressURI, publicKeyToAddress, - isValidCompressedPublicKey, + 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 bb6354e6..c793853e 100644 --- a/src/utils/crypto/test/generateStealthAddress.test.ts +++ b/src/utils/crypto/test/generateStealthAddress.test.ts @@ -1,13 +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, + parseStealthMetaAddressURI } from '..'; -import { VALID_SCHEME_ID, type HexString } from '../types'; +import { type HexString, VALID_SCHEME_ID } from '../types'; describe('generateStealthAddress', () => { const validStealthMetaAddressURI = @@ -15,66 +15,31 @@ describe('generateStealthAddress', () => { const schemeId = VALID_SCHEME_ID.SCHEME_ID_1; - test('should throw an error when given a valid uri format, but an invalid stealth meta-address', () => { - const invalid = 'st:eth:invalid'; - - expect(() => - generateStealthAddress({ - stealthMetaAddressURI: invalid, - schemeId, - }) - ).toThrow(new Error('Invalid stealth meta-address')); - }); - - test('should throw an error when given an invalid uri format', () => { - const invalid = 'invalid'; - - expect(() => - generateStealthAddress({ - stealthMetaAddressURI: invalid, - schemeId, - }) - ).toThrow(new Error('Invalid stealth meta-address URI format')); - }); - - test('should throw an error when given an invalid length stealth meta-address', () => { - const stealthMetaAddress = parseStealthMetaAddressURI({ + 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, + schemeId }); - // Intentionally alter the stealth meta-address to have an invalid length - const invalid = 'st:eth:' + stealthMetaAddress.slice(7, -1) + '0'; - - expect(() => - generateStealthAddress({ - stealthMetaAddressURI: invalid, - schemeId, - }) - ).toThrow(new Error('Invalid stealth meta-address')); - }); - test('should throw an error with stealth meta-address leading to invalid public keys', async () => { - // stealthMetaAddressURI with invalid public key lengths or prefixes - const invalidURIs = [ - 'st:eth:02' + '1'.repeat(63), // Invalid length - 'st:eth:04' + '1'.repeat(64), // Invalid prefix - ]; - - invalidURIs.forEach(uri => { - expect(() => - generateStealthAddress({ - stealthMetaAddressURI: uri, - schemeId: VALID_SCHEME_ID.SCHEME_ID_1, - }) - ).toThrow(new Error('Invalid stealth meta-address')); + 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(); @@ -85,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); @@ -112,7 +77,7 @@ describe('generateStealthAddress', () => { const result = parseKeysFromStealthMetaAddress({ stealthMetaAddress, - schemeId, + schemeId }); expect(bytesToHex(result.spendingPublicKey)).toBe( @@ -131,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';