Allright I need help.. My Web3.js sign and send wouldn't work. I either get BlockHeightExceeded , Blockhash not found, or it just gets stucked in the confirmTransaction..
I have googled and looked around in Stack Exchange, Cookbook etc and implement lastValidBlockheight vs blockheight , retrying tx from etc... none of it works... the code i have works sometimes, but it will not work 80% of the time, either stucked or throw error...
Question I have:
Could this be due to RPC/ So basically do i now need to buy the 500$ RPC package from quicknode. I use the 49$ one currently.
If not what is wrong with my code? This code is from the example in wormhole.. and I have tinkered around with multiple variations.. sendRawTransaction seems to complete (i.e no error thrown).. but the confirmTx will wait forever.. i.e tx doesnt get broadcasted in the network
import {
ComputeBudgetProgram,
Connection,
Keypair,
SendOptions,
SendTransactionError,
TransactionExpiredBlockheightExceededError,
sendAndConfirmRawTransaction
} from '@solana/web3.js';
import {
SignAndSendSigner,
UnsignedTransaction,
} from '@wormhole-foundation/connect-sdk';
import { Network } from '@wormhole-foundation/sdk-base';
import { SolanaPlatform } from '../platform';
import { SolanaChains } from '../types';
import { SolanaUnsignedTransaction } from '../unsignedTransaction';
import { logTxDetails } from './debug';
// Number of blocks to wait before considering a transaction expired
const SOLANA_EXPIRED_BLOCKHEIGHT = 150;
export class SolanaSendSignerV1<
N extends Network,
C extends SolanaChains = 'Solana',
> implements SignAndSendSigner<N, C>
{
constructor(
private _rpc: Connection,
private _chain: C,
private _keypair: Keypair,
private _debug: boolean = false,
private _sendOpts?: SendOptions,
private _priotifyFeeAmount: bigint = 30n,
) {
this._sendOpts = this._sendOpts ?? {
preflightCommitment: this._rpc.commitment,
maxRetries: 5
};
}
chain(): C {
return this._chain;
}
address(): string {
return this._keypair.publicKey.toBase58();
}
// Handles retrying a Transaction if the error is deemed to be
// recoverable. Currently handles:
// - Transaction expired
// - Blockhash not found
// - Not enough bytes (storage account not seen yet)
private retryable(e: any): boolean {
// Tx expired, set a new block hash and retry
console.log("ERROR MESSAGE", e.message)
if (e instanceof TransactionExpiredBlockheightExceededError) return true;
// if (!(e instanceof SendTransactionError)) return false;
if (e.message.includes("Transaction simulation failed:")) return true;
if (e.message.includes("block height exceeded")) return true;
if (e.message.includes('Blockhash not found')) return true;
// Besides tx expiry, only handle SendTransactionError
// if (!(e instanceof SendTransactionError)) return false;
if (e instanceof SendTransactionError) {
console.log("ENTER SendTransactionError block")
// Only handle simulation errors
if (e.message.includes('Transaction simulation failed')) return true;
// if (e.message.includes('Transaction') && e.message.includes('simulation')) return true;
if (e.message.includes('Blockhash not found')) return true;
// return false;
};
// Only handle simulation errors
// if (!e.message.includes('Transaction simulation failed')) return false;
// Blockhash not found, similar to expired, resend with new blockhash
// if (e.message.includes('Blockhash not found')) return true;
// Find the log message with the error details
const loggedErr = e.logs?.find((log: string) =>
log.startsWith('Program log: Error: '),
);
// who knows
if (!loggedErr) return false;
// Probably caused by storage account not seen yet
if (loggedErr.includes('Not enough bytes')) return true;
if (loggedErr.includes('Unexpected length of input')) return true;
console.log("RETRY END")
return false;
}
async signAndSend(tx: UnsignedTransaction[]): Promise<any[]> {
let { blockhash: bh, lastValidBlockHeight: lvbh } = await SolanaPlatform.latestBlock(
this._rpc,
);
console.log("lastValidBlockHeight latestBlock: ", lvbh)
console.log("BH: latestBlock ", bh)
let blockhashResponse = await SolanaPlatform.getLatestBlockhashAndContext(this._rpc);
let lastValidBlockHeight = blockhashResponse.context.slot + 150;
let blockhash = blockhashResponse.value.blockhash;
console.log(`lastValidBlockHeight.. ${lastValidBlockHeight} `)
console.log(`blockhashResponse.value.blockhash ${blockhashResponse.value.blockhash} `)
const txids: string[] = [];
for (const txn of tx) {
// console.log("TXN", txn)
const { description, transaction: { transaction, signers: extraSigners }, } = txn as SolanaUnsignedTransaction<N, C>;
console.log(`Signing: ${description} for ${this.address()}`);
if (this._priotifyFeeAmount)
transaction.add(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: this._priotifyFeeAmount,
}),
);
if (this._debug) logTxDetails(transaction);
// Try to send the transaction up to 5 times
const maxRetries = 5;
for (let i = 0; i < maxRetries; i++) {
console.log(`TRY TX -- tx.. ${i} .. ${blockhash}`)
try {
transaction.recentBlockhash = blockhash;
transaction.partialSign(this._keypair, ...(extraSigners ?? []));
// console.log("Send Raw Tx")
const txid = await this._rpc.sendRawTransaction(
transaction.serialize(),
this._sendOpts,
);
// const txid = await sendAndConfirmRawTransaction(this._rpc,
// transaction.serialize(),
// this._sendOpts,
// );
console.log(`Sent: ${description} for ${this.address()}`);
// console.log("txids", txids)
txids.push(txid);
break;
} catch (e) {
// No point checking if retryable if we're on the last retry
if (i === maxRetries - 1) throw e;
console.log(`Catrch Retrying tx.. ${i} .. ${this.retryable(e)} `)
// If it's not retryable, throw
if (!this.retryable(e)) throw e;
console.log(`NOT THROW.. ${i} .. `)
// If it is retryable, we need to grab a new block hash
const {
blockhash: newBlockhash,
lastValidBlockHeight: newBlockHeight,
} = await SolanaPlatform.latestBlock(this._rpc);
// But we should _not_ submit if the blockhash hasnt expired
if (
newBlockHeight - lastValidBlockHeight <
SOLANA_EXPIRED_BLOCKHEIGHT
) {
throw e;
}
lastValidBlockHeight = newBlockHeight;
blockhash = newBlockhash;
}
}
}
// Wait for finalization
console.log("CONFIRM TX")
const results = await Promise.all(
txids.map((signature) =>
// this._rpc.getSignatureStatus(signature)
this._rpc.confirmTransaction(
{
signature,
blockhash,
lastValidBlockHeight,
},
this._rpc.commitment,
),
),
);
console.log("results", results)
const erroredTxs = results
.filter((result) => result.value.err)
.map((result) => result.value.err);
if (erroredTxs.length > 0)
throw new Error(`Failed to confirm transaction: ${erroredTxs}`);
return txids;
}
}
Allright here is the catch..* BEWARE OF FEES ***
By setting into these high number I was paying like 0.20 and upto 0.60 for some of the transactiosn. I didn't realized until i look at the block explorer.
I run again with default compute limit and 1000000 micro lamport price. It seems to work so far.. Fees are about 0.0012 to create account (for reference only, it will depend on transaction complexity etc)