Skip to content

Commit fcb087d

Browse files
Xarozclaude
andauthored
fix(widgets): use HTTP polling for Solana tx confirmation (#8592)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 3d52b1a commit fcb087d

2 files changed

Lines changed: 57 additions & 9 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@hyperlane-xyz/widgets': patch
3+
---
4+
5+
Replaced WebSocket-based `confirmTransaction` with HTTP polling (`getSignatureStatuses`) for Solana transaction confirmation. Fixed hangs with RPC providers that don't support `signatureSubscribe`.

typescript/widgets/src/walletIntegrations/solana.ts

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import type { ITokenMetadata } from '@hyperlane-xyz/sdk/token/ITokenMetadata';
1111
import type { ChainName } from '@hyperlane-xyz/sdk/types';
1212
import type { WarpTypedTransaction } from '@hyperlane-xyz/sdk/warp/types';
1313

14+
import { sleep } from '@hyperlane-xyz/utils';
15+
1416
import { widgetLogger } from '../logger.js';
1517

1618
import {
@@ -73,22 +75,63 @@ export function useSolanaTransactionFns(
7375
const connection = new Connection(rpcUrl, 'confirmed');
7476
const {
7577
context: { slot: minContextSlot },
76-
value: { blockhash, lastValidBlockHeight },
78+
value: { lastValidBlockHeight },
7779
} = await connection.getLatestBlockhashAndContext();
7880

7981
logger.debug(`Sending tx on chain ${chainName}`);
8082
const signature = await sendSolTransaction(tx.transaction, connection, {
8183
minContextSlot,
8284
});
8385

84-
const confirm = (): Promise<TypedTransactionReceipt> =>
85-
connection
86-
.confirmTransaction({ blockhash, lastValidBlockHeight, signature })
87-
.then(() => connection.getTransaction(signature))
88-
.then((r) => ({
89-
type: ProviderType.SolanaWeb3,
90-
receipt: r!,
91-
}));
86+
const confirm = async (): Promise<TypedTransactionReceipt> => {
87+
// Poll via HTTP instead of connection.confirmTransaction which
88+
// relies on signatureSubscribe (WebSocket) — many RPC providers
89+
// (e.g. Alchemy) don't support that method.
90+
const POLL_INTERVAL_MS = 2000;
91+
while (true) {
92+
try {
93+
const { value } = await connection.getSignatureStatuses([
94+
signature,
95+
]);
96+
const status = value?.[0];
97+
if (status?.err) {
98+
throw new Error(
99+
`Transaction failed: ${JSON.stringify(status.err)}`,
100+
);
101+
}
102+
if (
103+
status?.confirmationStatus === 'confirmed' ||
104+
status?.confirmationStatus === 'finalized'
105+
) {
106+
break;
107+
}
108+
const blockHeight = await connection.getBlockHeight();
109+
if (blockHeight > lastValidBlockHeight) {
110+
throw new Error('Transaction expired: block height exceeded');
111+
}
112+
} catch (error) {
113+
// Re-throw definitive failures (tx error, expiry)
114+
if (
115+
error instanceof Error &&
116+
(error.message.startsWith('Transaction failed') ||
117+
error.message.startsWith('Transaction expired'))
118+
) {
119+
throw error;
120+
}
121+
// Tolerate transient RPC errors (timeouts, rate limits)
122+
logger.warn('Transient error polling tx confirmation', error);
123+
}
124+
await sleep(POLL_INTERVAL_MS);
125+
}
126+
const tx = await connection.getTransaction(signature, {
127+
commitment: 'confirmed',
128+
maxSupportedTransactionVersion: 0,
129+
});
130+
if (!tx) {
131+
throw new Error(`Transaction ${signature} confirmed but not found`);
132+
}
133+
return { type: ProviderType.SolanaWeb3, receipt: tx };
134+
};
92135

93136
return { hash: signature, confirm };
94137
},

0 commit comments

Comments
 (0)