Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion umbra-js/src/classes/KeyPair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export class KeyPair {
* @param pkx x-coordinate of compressed public key, as BigNumber or hex string
* @param prefix Prefix bit, must be 2 or 3
*/
static getUncompressedFromX(pkx: BigNumberish, prefix: number | string | undefined = undefined) {
static getUncompressedFromX(pkx: BigNumberish, prefix?: number | string) {
// Converting `pkx` to a BigNumber will throw if the value cannot be safely converted to a BigNumber, i.e. if the
// value is of type Number and larger than Number.MAX_SAFE_INTEGER.
pkx = BigNumber.from(pkx);
Expand Down
100 changes: 66 additions & 34 deletions umbra-js/src/classes/Umbra.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,14 @@ export const parseChainConfig = (chainConfig: ChainConfig | number) => {
throw new Error(`Invalid subgraphUrl provided in chainConfig. Got '${String(subgraphUrl)}'`);
}

let checksummedBatchSendAddress: string | null = null;
if (batchSendAddress) {
checksummedBatchSendAddress = getAddress(batchSendAddress);
}

return {
umbraAddress: getAddress(umbraAddress),
batchSendAddress: batchSendAddress ? getAddress(batchSendAddress) : null,
batchSendAddress: checksummedBatchSendAddress,
startBlock,
chainId,
subgraphUrl,
Expand Down Expand Up @@ -187,7 +192,7 @@ export class Umbra {
token: string,
amount: BigNumberish,
recipientId: string,
overrides: SendOverrides = {}
overrides?: SendOverrides
) {
// Check that recipient is valid.
await assertSupportedAddress(recipientId);
Expand Down Expand Up @@ -224,7 +229,7 @@ export class Umbra {
return { tx, stealthKeyPair };
}

async batchSend(signer: JsonRpcSigner | Wallet, sends: SendBatch[], overrides: SendOverrides = {}) {
async batchSend(signer: JsonRpcSigner | Wallet, sends: SendBatch[], overrides?: SendOverrides) {
if (!this.batchSendContract) {
throw new Error('Batch send is not supported on this network');
}
Expand Down Expand Up @@ -293,7 +298,9 @@ export class Umbra {
* @param destination Address where funds will be withdrawn to
* @param overrides Override the gas limit, gas price, or nonce
*/
async withdraw(spendingPrivateKey: string, token: string, destination: string, overrides: Overrides = {}) {
async withdraw(spendingPrivateKey: string, token: string, destination: string, overrides?: Overrides) {
const txOverrides = overrides === undefined ? {} : overrides;

// Address input validations
// token === 'ETH' is valid so we don't verify that, and let ethers verify it during the function call
destination = getAddress(destination);
Expand All @@ -307,17 +314,17 @@ export class Umbra {
if (isEth(token)) {
try {
// First we attempt to execute the withdrawal using the signer from the user's wallet.
return await tryEthWithdraw(txSigner, await txSigner.getAddress(), destination, overrides);
return await tryEthWithdraw(txSigner, await txSigner.getAddress(), destination, txOverrides);
} catch (e) {
// If that fails, we try again using the fallback provider.
console.error("Withdrawal with wallet's provider failed, see error below. Retrying with a fallback provider.");
console.error(e);
const fallbackSigner = stealthWallet.connect(this.fallbackProvider);
return await tryEthWithdraw(fallbackSigner, await txSigner.getAddress(), destination, overrides);
return await tryEthWithdraw(fallbackSigner, await txSigner.getAddress(), destination, txOverrides);
}
} else {
// Withdrawing a token
return await this.umbraContract.connect(txSigner).withdrawToken(destination, token, overrides);
return await this.umbraContract.connect(txSigner).withdrawToken(destination, token, txOverrides);
}
}

Expand Down Expand Up @@ -346,8 +353,13 @@ export class Umbra {
v: number,
r: string,
s: string,
overrides: Overrides = {}
overrides?: Overrides
): Promise<TransactionResponse> {
let txOverrides: Overrides = {};
if (overrides !== undefined) {
txOverrides = overrides;
}

// Address input validations
stealthAddr = getAddress(stealthAddr);
destination = getAddress(destination);
Expand All @@ -359,7 +371,7 @@ export class Umbra {
const txSigner = this.getConnectedSigner(signer);
return await this.umbraContract
.connect(txSigner)
.withdrawTokenOnBehalf(stealthAddr, destination, token, sponsor, sponsorFee, v, r, s, overrides);
.withdrawTokenOnBehalf(stealthAddr, destination, token, sponsor, sponsorFee, v, r, s, txOverrides);
}

/**
Expand All @@ -375,10 +387,12 @@ export class Umbra {
* @returns A list of Announcement events supplemented with additional metadata, such as the sender, block,
* timestamp, and txhash
*/
async *fetchAllAnnouncements(overrides: ScanOverrides = {}): AsyncGenerator<AnnouncementDetail[]> {
async *fetchAllAnnouncements(overrides?: ScanOverrides): AsyncGenerator<AnnouncementDetail[]> {
const scanOverrides = overrides === undefined ? {} : overrides;

// Get start and end blocks to scan events for
const startBlock = overrides.startBlock || this.chainConfig.startBlock;
const endBlock = overrides.endBlock || 'latest';
const startBlock = scanOverrides.startBlock || this.chainConfig.startBlock;
const endBlock = scanOverrides.endBlock || 'latest';

const filterSupportedAddresses = async (announcements: AnnouncementDetail[]) => {
// Check if all senders and receiver addresses are supported.
Expand All @@ -389,7 +403,10 @@ export class Umbra {
const filtered = announcements.map((announcement) => {
const isReceiverSupported = supportedAddrs.has(announcement.receiver);
const isFromSupported = supportedAddrs.has(announcement.from);
return isReceiverSupported && isFromSupported ? announcement : null;
if (isReceiverSupported && isFromSupported) {
return announcement;
}
return null;
});

return filtered.filter((i) => i !== null) as AnnouncementDetail[];
Expand Down Expand Up @@ -428,13 +445,14 @@ export class Umbra {
possibleRegisteredBlockNumber: number | undefined,
Signer: JsonRpcSigner,
address: string,
overrides: ScanOverrides = {}
overrides?: ScanOverrides
): AsyncGenerator<AnnouncementDetail[]> {
const scanOverrides = overrides === undefined ? {} : overrides;
const registeredBlockNumber =
possibleRegisteredBlockNumber || (await getBlockNumberUserRegistered(address, Signer.provider, this.chainConfig));
// Get start and end blocks to scan events for
const startBlock = overrides.startBlock || registeredBlockNumber || this.chainConfig.startBlock;
const endBlock = overrides.endBlock || 'latest';
const startBlock = scanOverrides.startBlock || registeredBlockNumber || this.chainConfig.startBlock;
const endBlock = scanOverrides.endBlock || 'latest';
for await (const announcementBatch of this.fetchAllAnnouncements({ startBlock, endBlock })) {
yield announcementBatch;
}
Expand Down Expand Up @@ -539,9 +557,10 @@ export class Umbra {
* @param viewingPrivateKey Receiver's viewing public key
* @param overrides Override the start and end block used for scanning
*/
async scan(spendingPublicKey: string, viewingPrivateKey: string, overrides: ScanOverrides = {}) {
async scan(spendingPublicKey: string, viewingPrivateKey: string, overrides?: ScanOverrides) {
const scanOverrides = overrides === undefined ? {} : overrides;
const userAnnouncements: UserAnnouncement[] = [];
for await (const announcementsBatch of this.fetchAllAnnouncements(overrides)) {
for await (const announcementsBatch of this.fetchAllAnnouncements(scanOverrides)) {
for (const announcement of announcementsBatch) {
const { amount, from, receiver, timestamp, token: tokenAddr, txHash } = announcement;
const { isForUser, randomNumber } = Umbra.isAnnouncementForUser(
Expand Down Expand Up @@ -641,9 +660,15 @@ export class Umbra {
return { spendingKeyPair, viewingKeyPair };
}

async prepareSend(recipientId: string, lookupOverrides: SendOverrides = {}) {
async prepareSend(recipientId: string, lookupOverrides?: SendOverrides) {
const sendLookupOverrides = lookupOverrides === undefined ? {} : lookupOverrides;

// Lookup recipient's public key
const { spendingPublicKey, viewingPublicKey } = await lookupRecipient(recipientId, this.provider, lookupOverrides);
const { spendingPublicKey, viewingPublicKey } = await lookupRecipient(
recipientId,
this.provider,
sendLookupOverrides
);
if (!spendingPublicKey || !viewingPublicKey) {
throw new Error(`Could not retrieve public keys for recipient ID ${recipientId}`);
}
Expand Down Expand Up @@ -702,31 +727,34 @@ export class Umbra {
token: string,
sponsor: string,
sponsorFee: BigNumberish,
hook: string = AddressZero,
data = '0x'
hook?: string,
data?: string
) {
const hookAddress = hook === undefined ? AddressZero : hook;
const callData = data === undefined ? '0x' : data;

// Address input validations
contract = getAddress(contract);
acceptor = getAddress(acceptor);
sponsor = getAddress(sponsor);
token = getAddress(token);
hook = getAddress(hook);
const hookContract = getAddress(hookAddress);

// Validate chainId
if (typeof chainId !== 'number' || !Number.isInteger(chainId)) {
throw new Error(`Invalid chainId provided in chainConfig. Got '${chainId}'`);
}

// Validate the data string
if (typeof data !== 'string' || !isHexString(data)) {
if (typeof callData !== 'string' || !isHexString(callData)) {
throw new Error('Data string must be null or in hex format with 0x prefix');
}

const stealthWallet = new Wallet(spendingPrivateKey);
const digest = keccak256(
defaultAbiCoder.encode(
['uint256', 'address', 'address', 'address', 'address', 'uint256', 'address', 'bytes'],
[chainId, contract, acceptor, token, sponsor, sponsorFee, hook, data]
[chainId, contract, acceptor, token, sponsor, sponsorFee, hookContract, callData]
)
);
const rawSig = await stealthWallet.signMessage(arrayify(digest));
Expand All @@ -745,12 +773,15 @@ async function tryEthWithdraw(
signer: JsonRpcSigner | Wallet,
from: string, // signer.getAddress() is async, so pass this to reduce number of calls
to: string,
overrides: Overrides = {},
retryCount = 0
overrides?: Overrides,
retryCount?: number
): Promise<TransactionResponse> {
const effectiveOverrides = overrides === undefined ? {} : overrides;
const retries = retryCount === undefined ? 0 : retryCount;

try {
if (retryCount === 20) throw new Error("Failed to estimate Optimism's L1 Fee, please try again later");
const sweepInfo = await getEthSweepGasInfo(from, to, signer.provider as EthersProvider, overrides);
if (retries === 20) throw new Error("Failed to estimate Optimism's L1 Fee, please try again later");
const sweepInfo = await getEthSweepGasInfo(from, to, signer.provider as EthersProvider, effectiveOverrides);
const { gasPrice, gasLimit, txCost, fromBalance, ethToSend, chainId } = sweepInfo;
if (txCost.gt(fromBalance)) {
throw new Error('Stealth address ETH balance is not enough to pay for withdrawal gas cost');
Expand All @@ -760,7 +791,7 @@ async function tryEthWithdraw(
// proportional to the retryCount, i.e. the more retries, the more margin is added, capped at 20% added cost
let adjustedValue = ethToSend;
if (chainId === 10 || chainId === 8453) {
const costWithMargin = txCost.mul(100 + Math.min(retryCount, 20)).div(100);
const costWithMargin = txCost.mul(100 + Math.min(retries, 20)).div(100);
adjustedValue = adjustedValue.sub(costWithMargin);
}

Expand All @@ -770,8 +801,8 @@ async function tryEthWithdraw(
// Retry if the error was insufficient funds, otherwise throw the error
if (!e.stack.includes('insufficient funds')) throw e;
console.log('e', e);
console.warn(`failed with "insufficient funds for gas * price + value", retry attempt ${retryCount}...`);
return await tryEthWithdraw(signer, from, to, overrides, retryCount + 1);
console.warn(`failed with "insufficient funds for gas * price + value", retry attempt ${retries}...`);
return await tryEthWithdraw(signer, from, to, effectiveOverrides, retries + 1);
}
}

Expand Down Expand Up @@ -802,11 +833,12 @@ export function assertValidStealthAddress(stealthAddress: string) {
* When `advanced` is false it looks for public keys in StealthKeyRegistry, and when true it recovers
* them from on-chain transaction when true
*/
export function parseOverrides(overrides: SendOverrides = {}): {
export function parseOverrides(overrides?: SendOverrides): {
localOverrides: SendOverrides;
lookupOverrides: Pick<SendOverrides, 'advanced' | 'supportPubKey' | 'supportTxHash'>;
} {
const localOverrides = { ...overrides }; // avoid mutating the object passed in
const effectiveOverrides = overrides === undefined ? {} : overrides;
const localOverrides = { ...effectiveOverrides }; // avoid mutating the object passed in
const advanced = localOverrides?.advanced || false;
const supportPubKey = localOverrides?.supportPubKey || false;
const supportTxHash = localOverrides?.supportTxHash || false;
Expand Down
55 changes: 34 additions & 21 deletions umbra-js/src/utils/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,10 +328,11 @@ export async function getBlockNumberUserRegistered(
export async function getMostRecentSubgraphStealthKeyChangedEventFromAddress(
address: string,
chainConfig: ChainConfig,
overrides: ScanOverrides = {}
overrides?: ScanOverrides
): Promise<SubgraphStealthKeyChangedEvent> {
const startBlock = overrides.startBlock || chainConfig.startBlock;
const endBlock = overrides.endBlock || 'latest';
const scanOverrides = overrides === undefined ? {} : overrides;
const startBlock = scanOverrides.startBlock || chainConfig.startBlock;
const endBlock = scanOverrides.endBlock || 'latest';

// Fetch stealth key changed events from the subgraph
const stealthKeyChangedEvents = fetchAllStealthKeyChangedEventsForRecipientAddressFromSubgraph(
Expand Down Expand Up @@ -430,13 +431,16 @@ export async function* recursiveGraphFetch(
url: string,
key: string,
query: (filter: string) => string,
before: any[] = [],
before?: any[],
overrides?: GraphFilterOverride
): AsyncGenerator<any[]> {
const previousResults = before === undefined ? [] : before;

// retrieve the last ID we collected to use as the starting point for this query
const fromId = before.length ? (before[before.length - 1].id as string | number) : false;
const fromId = previousResults.length ? (previousResults[previousResults.length - 1].id as string | number) : false;
let startBlockFilter = '';
let endBlockFilter = '';
let idFilter = '';
const startBlock = overrides?.startBlock ? overrides.startBlock.toString() : '';
const endBlock = overrides?.endBlock ? overrides?.endBlock.toString() : '';
const registrantFilter = overrides?.registrant ? 'registrant: "' + overrides.registrant.toLowerCase() + '"' : '';
Expand All @@ -449,6 +453,10 @@ export async function* recursiveGraphFetch(
endBlockFilter = `block_lte: "${endBlock}",`;
}

if (fromId) {
idFilter = `id_lt: "${fromId}",`;
}

// Fetch this 'page' of results - please note that the query MUST return an ID
const res = await fetch(url, {
method: 'POST',
Expand All @@ -459,7 +467,7 @@ export async function* recursiveGraphFetch(
orderBy: id,
orderDirection: desc,
where: {
${fromId ? `id_lt: "${fromId}",` : ''}
${idFilter}
${startBlockFilter}
${endBlockFilter}
${registrantFilter}
Expand All @@ -474,7 +482,7 @@ export async function* recursiveGraphFetch(
// If there were results on this page yield the results then query the next page, otherwise do nothing.
if (json.data[key].length) {
yield json.data[key]; // yield the data for this page
yield* recursiveGraphFetch(url, key, query, [...before, ...json.data[key]], overrides); // yield the data for the next pages
yield* recursiveGraphFetch(url, key, query, [...previousResults, ...json.data[key]], overrides); // yield the data for the next pages
}
}
// Sorts stealth key logs in ascending order by block number
Expand Down Expand Up @@ -610,7 +618,7 @@ export async function getEthSweepGasInfo(
from: string,
to: string,
provider: EthersProvider,
overrides: Overrides = {}
overrides?: Overrides
): Promise<{
gasPrice: BigNumber;
gasLimit: BigNumber;
Expand All @@ -619,6 +627,7 @@ export async function getEthSweepGasInfo(
ethToSend: BigNumber;
chainId: number;
}> {
const gasOverrides = overrides === undefined ? {} : overrides;
const gasLimitOf21k = [1, 4, 5, 10, 137, 1337, 11155111]; // networks where ETH sends cost 21000 gas
const ignoreGasPriceOverride = [10, 42161]; // to maximize ETH sweeps, ignore uer-specified gasPrice overrides

Expand All @@ -639,21 +648,25 @@ export async function getEthSweepGasInfo(

// If a gas limit was provided, use it. Otherwise, if we are sending to an EOA and this is a network where ETH
// transfers always to cost 21000 gas, use 21000. Otherwise, estimate the gas limit.
const gasLimit = overrides.gasLimit
? BigNumber.from(await overrides.gasLimit)
: isEoa && gasLimitOf21k.includes(chainId)
? BigNumber.from('21000')
: await provider.estimateGas({
gasPrice: 0,
to,
from,
value: fromBalance,
});
let gasLimit: BigNumber;
if (gasOverrides.gasLimit) {
gasLimit = BigNumber.from(await gasOverrides.gasLimit);
} else if (isEoa && gasLimitOf21k.includes(chainId)) {
gasLimit = BigNumber.from('21000');
} else {
gasLimit = await provider.estimateGas({
gasPrice: 0,
to,
from,
value: fromBalance,
});
}

// Estimate the gas price, defaulting to the given one unless on a network where we want to use provider gas price
let gasPrice = ignoreGasPriceOverride.includes(chainId)
? providerGasPrice
: BigNumber.from((await overrides.gasPrice) || providerGasPrice);
let gasPrice = providerGasPrice;
if (!ignoreGasPriceOverride.includes(chainId)) {
gasPrice = BigNumber.from((await gasOverrides.gasPrice) || providerGasPrice);
}

// On networks with EIP-1559 gas pricing, the provider will throw an error and refuse to submit
// the tx if the gas price is less than the block-specified base fee. The error is "max fee
Expand Down
Loading