Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: send and receive test #62

Merged
167 changes: 167 additions & 0 deletions src/test/helpers/index.ts
Original file line number Diff line number Diff line change
@@ -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,
};
};
106 changes: 106 additions & 0 deletions src/test/sendReceive.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
8 changes: 6 additions & 2 deletions src/utils/crypto/generateStealthAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}: {
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') {
Expand Down
63 changes: 13 additions & 50 deletions src/utils/crypto/test/generateStealthAddress.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,61 +15,24 @@ 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';
test('parseStealthMetaAddressURI should return the stealth meta-address', () => {
const expectedStealthMetaAddress =
'0x033404e82cd2a92321d51e13064ec13a0fb0192a9fdaaca1cfb47b37bd27ec13970390ad5eca026c05ab5cf4d620a2ac65241b11df004ddca360e954db1b26e3846e';
// Passing the valid stealth meta-address URI and the scheme ID
const result = parseStealthMetaAddressURI({
stealthMetaAddressURI: validStealthMetaAddressURI,
schemeId
});

expect(() =>
generateStealthAddress({
stealthMetaAddressURI: invalid,
schemeId
})
).toThrow(new Error('Invalid stealth meta-address URI format'));
});
expect(result).toBe(expectedStealthMetaAddress);

test('should throw an error when given an invalid length stealth meta-address', () => {
const stealthMetaAddress = parseStealthMetaAddressURI({
stealthMetaAddressURI: validStealthMetaAddressURI,
// Passing only the stealth meta-address
const result2 = parseStealthMetaAddressURI({
stealthMetaAddressURI: expectedStealthMetaAddress,
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
}`
];

for (const uri of invalidURIs) {
expect(() =>
generateStealthAddress({
stealthMetaAddressURI: uri,
schemeId: VALID_SCHEME_ID.SCHEME_ID_1
})
).toThrow(new Error('Invalid stealth meta-address'));
}
expect(result2).toBe(expectedStealthMetaAddress);
});

test('should generate a valid stealth address given a valid stealth meta-address URI', () => {
Expand Down