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

Solana: Provide ability to resign transactions with ephemeral keys #195

Merged
merged 5 commits into from
Jan 3, 2024
Merged
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
Next Next commit
add getMessageFee to core protocol, add handlers for some Solana simu…
…late errors
barnjamin committed Jan 3, 2024
commit c841ed5093a8e4eea033f26ed43ad5b0e632b219
68 changes: 60 additions & 8 deletions platforms/solana/src/platform.ts
Original file line number Diff line number Diff line change
@@ -22,6 +22,8 @@ import {
ParsedAccountData,
PublicKey,
SendOptions,
SendTransactionError,
TransactionExpiredBlockheightExceededError,
} from '@solana/web3.js';
import { SolanaAddress, SolanaZeroAddress } from './address';
import {
@@ -160,6 +162,52 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
return balancesArr.reduce((obj, item) => Object.assign(obj, item), {});
}

// Handles retrying a Transaction if the error is deemed to be
// recoverable. Currently handles:
// - Blockhash not found (blockhash too new for the node we submitted to)
// - Not enough bytes (storage account not seen yet)

private static async sendWithRetry(
rpc: Connection,
stxns: SignedTx,
opts: SendOptions,
retries: number = 3,
): Promise<string> {
// Shouldnt get hit but just in case
if (!retries) throw new Error('Too many retries');

try {
const txid = await rpc.sendRawTransaction(stxns.tx, opts);
return txid;
} catch (e) {
retries -= 1;
if (!retries) throw e;

// Would require re-signing, for now bail
if (e instanceof TransactionExpiredBlockheightExceededError) throw e;

// Only handle SendTransactionError
if (!(e instanceof SendTransactionError)) throw e;
const emsg = e.message;

// Only handle simulation errors
if (!emsg.includes('Transaction simulation failed')) throw e;

// Blockhash not found _yet_
if (emsg.includes('Blockhash not found'))
return this.sendWithRetry(rpc, stxns, opts, retries);

// Find the log message with the error details
const loggedErr = e.logs.find((log) =>
log.startsWith('Program log: Error: '),
);

// Probably caused by storage account not seen yet
if (loggedErr && loggedErr.includes('Not enough bytes'))
return this.sendWithRetry(rpc, stxns, opts, retries);
}
}

static async sendWait(
chain: Chain,
rpc: Connection,
@@ -168,14 +216,16 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
): Promise<TxHash[]> {
const { blockhash, lastValidBlockHeight } = await this.latestBlock(rpc);

// Set the commitment level to match the rpc commitment level
// otherwise, it defaults to finalized
if (!opts) opts = { preflightCommitment: rpc.commitment };

const txhashes = await Promise.all(
stxns.map((stxn) => {
return rpc.sendRawTransaction(stxn, opts);
}),
stxns.map((stxn) =>
this.sendWithRetry(
rpc,
stxn,
// Set the commitment level to match the rpc commitment level
// otherwise, it defaults to finalized
opts ?? { preflightCommitment: rpc.commitment },
),
),
);

await Promise.all(
@@ -198,11 +248,13 @@ export class SolanaPlatform<N extends Network> extends PlatformContext<
rpc: Connection,
commitment?: Commitment,
): Promise<{ blockhash: string; lastValidBlockHeight: number }> {
// Use finalized to prevent blockhash not found errors
// Note: this may mean we have less time to submit transactions?
return rpc.getLatestBlockhash(commitment ?? 'finalized');
}

static async getLatestBlock(rpc: Connection): Promise<number> {
const { lastValidBlockHeight } = await this.latestBlock(rpc);
const { lastValidBlockHeight } = await this.latestBlock(rpc, 'confirmed');
return lastValidBlockHeight;
}