Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: address PR comments
  • Loading branch information
bradleyplaydon committed Sep 25, 2025
commit 0326af068bbc0697a2dfd24ef84e3eb9c140a299
19 changes: 4 additions & 15 deletions src/utils/inProgressTxCache.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import config from 'config';
import type { TransactionLocal } from 'config/types';
import { isEmptyObject } from 'utils';
import { isEmptyObject, JSONReplacer } from 'utils';

const LOCAL_STORAGE_KEY = 'transactions:inprogress';
const LOCAL_STORAGE_MAX = 3;

// Bigint types cannot be serialized to a string
// That's why we need to provide a replacer function to handle it separately
// Please see for details: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/BigInt_not_serializable
const JSONReplacer = (_, value: any) =>
typeof value === 'bigint' ? value.toString() : value;

// Checks the existance of the props with the given types in a parent object
const validateChildPropTypes = (
Expand Down Expand Up @@ -171,10 +169,7 @@ export const addTxToLocalStorage = (

// Update the list
try {
ls.setItem(
config.cacheKey(LOCAL_STORAGE_KEY),
JSON.stringify(newList, JSONReplacer),
);
ls.setItem(config.cacheKey(LOCAL_STORAGE_KEY), JSONReplacer(newList));
} catch (e: any) {
// We can get two different errors:
// 1- TypeError from JSON.stringify
Expand All @@ -198,10 +193,7 @@ export const removeTxFromLocalStorage = (txHash: string) => {
// remove the item and update localStorage
items.splice(removeIndex, 1);
try {
ls.setItem(
config.cacheKey(LOCAL_STORAGE_KEY),
JSON.stringify(items, JSONReplacer),
);
ls.setItem(config.cacheKey(LOCAL_STORAGE_KEY), JSONReplacer(items));
} catch (e: any) {
// We can get two different errors:
// 1- TypeError from JSON.stringify
Expand Down Expand Up @@ -231,10 +223,7 @@ export const updateTxInLocalStorage = (
// Update item property and put back in local storage
items[idx][key] = value;
try {
ls.setItem(
config.cacheKey(LOCAL_STORAGE_KEY),
JSON.stringify(items, JSONReplacer),
);
ls.setItem(config.cacheKey(LOCAL_STORAGE_KEY), JSONReplacer(items));
} catch (e: any) {
// We can get two different errors:
// 1- TypeError from JSON.stringify
Expand Down
6 changes: 6 additions & 0 deletions src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@
try {
document.execCommand('copy');
return true;
} catch (err) {

Check warning on line 117 in src/utils/index.ts

View workflow job for this annotation

GitHub Actions / ESLint

'err' is defined but never used. Allowed unused caught errors must match /^_/u
return false;
} finally {
document.body.removeChild(textArea);
Expand Down Expand Up @@ -424,3 +424,9 @@
}
return route.endsWith('ExecutorRoute');
};

export const JSONReplacer = (json: any) => {
return JSON.stringify(json, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
);
};
119 changes: 34 additions & 85 deletions src/utils/wallet/solana.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ import {

import config from 'config';

const CONFIRMATION_PROMISE_TIMER = 3_000; // How long to wait for confirmation before resending

import type { SolanaUnsignedTransaction } from '@wormhole-foundation/sdk-solana';
import type { Chain, Network } from '@wormhole-foundation/sdk';
import { setPriorityFeeInstructions } from 'utils/solana';
import { retry } from 'es-toolkit';
import { JSONReplacer } from 'utils';

const getWalletName = (wallet: Wallet) =>
wallet.getName().toLowerCase().replaceAll('wallet', '').trim();
Expand Down Expand Up @@ -140,81 +143,19 @@ export async function signAndSendTransactionWithResends(
commitment,
);

const confirmationPromiseTimer = 3_000; // How long to wait for confirmation before resending
const transaction = { serializedTransaction, sendOptions };

const signature = await signAndConfirmTransactionWhilstResending(
{ serializedTransaction, sendOptions },
const signature = await resendTransactionUntilConfirmed(
connection,
transaction,
blockhash,
lastValidBlockHeight,
commitment,
confirmationPromiseTimer,
);

return signature;
}

async function signAndConfirmTransactionWhilstResending(
transaction: {
serializedTransaction: Uint8Array | Buffer | number[];
sendOptions?: SendOptions;
},
connection: Connection,
blockHash: string,
lastValidBlockHeight: number,
commitment: Commitment,
confirmationPromiseTimer: number,
): Promise<string> {
const { signature, confirmPromise } =
await sendTransactionAndGetConfirmPromise(
transaction,
connection,
blockHash,
lastValidBlockHeight,
commitment,
);

await resendTransactionUntilConfirmed(
signature,
connection,
transaction,
confirmationPromiseTimer,
confirmPromise,
);

return signature;
}

async function sendTransactionAndGetConfirmPromise(
transaction: {
serializedTransaction: Uint8Array | Buffer | number[];
sendOptions?: SendOptions;
},
connection: Connection,
blockHash: string,
lastValidBlockHeight: number,
commitment: Commitment,
): Promise<{
signature: string;
confirmPromise: Promise<RpcResponseAndContext<SignatureResult>>;
}> {
const signature = await connection.sendRawTransaction(
transaction.serializedTransaction,
transaction.sendOptions,
);

const confirmPromise = connection.confirmTransaction(
{
signature,
blockhash: blockHash,
lastValidBlockHeight,
},
commitment,
);

return { signature, confirmPromise };
}

/**
* **We race against the confirmation promise to mitigate the risk of the transaction
* not being confirmed before the blockhash expires.**
Expand All @@ -228,33 +169,42 @@ async function sendTransactionAndGetConfirmPromise(
*/

async function resendTransactionUntilConfirmed(
signature: string,
connection: Connection,
transaction: {
serializedTransaction: Uint8Array | Buffer | number[];
sendOptions?: SendOptions;
},
confirmationPromiseTimer: number,
confirmTransactionPromise: Promise<RpcResponseAndContext<SignatureResult>>,
) {
blockhash: string,
lastValidBlockHeight: number,
commitment: Commitment,
): Promise<string> {
let isTransactionConfirmed: RpcResponseAndContext<SignatureResult> | null =
null;

const signature = await connection.sendRawTransaction(
transaction.serializedTransaction,
transaction.sendOptions,
);

try {
while (!isTransactionConfirmed) {
const confirmPromise = connection.confirmTransaction(
{ signature, blockhash, lastValidBlockHeight },
commitment,
);

isTransactionConfirmed = await Promise.race([
confirmTransactionPromise,
confirmPromise,
new Promise<null>((resolve) =>
setTimeout(() => {
resolve(null);
}, confirmationPromiseTimer),
setTimeout(() => resolve(null), CONFIRMATION_PROMISE_TIMER),
),
]);

if (isTransactionConfirmed) {
break;
}

// This is the same signature so don't need response.
// Need to await since we are explicilty resending before confirming again
// Resend transaction as it's not yet confirmed
await connection.sendRawTransaction(
transaction.serializedTransaction,
transaction.sendOptions,
Expand All @@ -263,18 +213,20 @@ async function resendTransactionUntilConfirmed(
} catch (e) {
if (e instanceof Error) {
if (e.name === 'TransactionExpiredBlockheightExceededError') {
recoverBlockheightExceededTransaction(e, connection, signature);
await recoverBlockheightExceededTransaction(connection, signature);
}
console.error('Failed to resend transaction:', e);
}
}

if (isTransactionConfirmed && isTransactionConfirmed.value.err) {
if (isTransactionConfirmed?.value.err) {
const errorMessage = formatConfirmationError(
isTransactionConfirmed.value.err,
);
throw new Error(`Transaction failed: ${errorMessage}`);
}

return signature;
}

/**
Expand All @@ -288,23 +240,22 @@ async function resendTransactionUntilConfirmed(
* @returns The signature once found, or throws if retries are exhausted
*/
async function recoverBlockheightExceededTransaction(
e: unknown,
connection: Connection,
signature: string,
{ retries = 5, delay = 2000 }: { retries?: number; delay?: number } = {},
): Promise<string | null> {
const findTransaction = async (): Promise<string> => {
) {
const findTransaction = async () => {
const tx = await connection.getTransaction(signature, {
commitment: 'confirmed',
maxSupportedTransactionVersion: 0,
});

if (tx) return signature;
if (tx) return;

throw new Error('Transaction not yet found on chain');
};

return retry(findTransaction, { retries, delay });
retry(findTransaction, { retries, delay });
}

async function createSolanaTransaction(
Expand Down Expand Up @@ -340,9 +291,7 @@ function formatConfirmationError(err: TransactionError): string {

if (typeof err === 'object') {
try {
return JSON.stringify(err, (_key, value) =>
typeof value === 'bigint' ? value.toString() : value,
);
return JSONReplacer(err);
} catch {
return 'Unstringifiable error object';
}
Expand Down
Loading