diff --git a/core/definitions/src/protocols/core.ts b/core/definitions/src/protocols/core.ts index f0723574bf..7471793b4a 100644 --- a/core/definitions/src/protocols/core.ts +++ b/core/definitions/src/protocols/core.ts @@ -10,6 +10,7 @@ export interface WormholeCore< P extends Platform, C extends PlatformToChains

, > { + getMessageFee(): Promise; publishMessage( sender: AccountAddress, message: string | Uint8Array, diff --git a/platforms/aptos/protocols/core/src/core.ts b/platforms/aptos/protocols/core/src/core.ts index c9013e8f8c..700c81fd4e 100644 --- a/platforms/aptos/protocols/core/src/core.ts +++ b/platforms/aptos/protocols/core/src/core.ts @@ -36,6 +36,9 @@ export class AptosWormholeCore throw new Error(`CoreBridge contract Address for chain ${chain} not found`); this.coreBridge = coreBridgeAddress; } + getMessageFee(): Promise { + throw new Error("Method not implemented."); + } static async fromRpc( connection: AptosClient, diff --git a/platforms/cosmwasm/protocols/core/src/wormholeCore.ts b/platforms/cosmwasm/protocols/core/src/core.ts similarity index 97% rename from platforms/cosmwasm/protocols/core/src/wormholeCore.ts rename to platforms/cosmwasm/protocols/core/src/core.ts index 3ca55fec59..3d2153f246 100644 --- a/platforms/cosmwasm/protocols/core/src/wormholeCore.ts +++ b/platforms/cosmwasm/protocols/core/src/core.ts @@ -37,6 +37,10 @@ export class CosmwasmWormholeCore this.coreAddress = coreAddress; } + getMessageFee(): Promise { + throw new Error("Method not implemented."); + } + static async fromRpc( rpc: CosmWasmClient, config: ChainsConfig, diff --git a/platforms/cosmwasm/protocols/core/src/index.ts b/platforms/cosmwasm/protocols/core/src/index.ts index 4ddb693ea1..6e330d373e 100644 --- a/platforms/cosmwasm/protocols/core/src/index.ts +++ b/platforms/cosmwasm/protocols/core/src/index.ts @@ -1,6 +1,6 @@ import { registerProtocol } from "@wormhole-foundation/connect-sdk"; import { _platform } from "@wormhole-foundation/connect-sdk-cosmwasm"; -import { CosmwasmWormholeCore } from "./wormholeCore"; +import { CosmwasmWormholeCore } from "./core"; declare global { namespace WormholeNamespace { @@ -12,4 +12,4 @@ declare global { registerProtocol(_platform, "WormholeCore", CosmwasmWormholeCore); -export * from "./wormholeCore"; +export * from "./core"; diff --git a/platforms/evm/protocols/core/src/wormholeCore.ts b/platforms/evm/protocols/core/src/core.ts similarity index 97% rename from platforms/evm/protocols/core/src/wormholeCore.ts rename to platforms/evm/protocols/core/src/core.ts index 74cb43e6ab..9ada952fe9 100644 --- a/platforms/evm/protocols/core/src/wormholeCore.ts +++ b/platforms/evm/protocols/core/src/core.ts @@ -62,6 +62,10 @@ export class EvmWormholeCore< ); } + async getMessageFee(): Promise { + return await this.core.messageFee.staticCall(); + } + static async fromRpc( provider: Provider, config: ChainsConfig, diff --git a/platforms/evm/protocols/core/src/index.ts b/platforms/evm/protocols/core/src/index.ts index 57bb0d8d8e..887a2d3e8a 100644 --- a/platforms/evm/protocols/core/src/index.ts +++ b/platforms/evm/protocols/core/src/index.ts @@ -1,6 +1,6 @@ import { registerProtocol } from '@wormhole-foundation/connect-sdk'; import { _platform } from '@wormhole-foundation/connect-sdk-evm'; -import { EvmWormholeCore } from './wormholeCore'; +import { EvmWormholeCore } from './core'; declare global { namespace WormholeNamespace { @@ -13,4 +13,4 @@ declare global { registerProtocol(_platform, 'WormholeCore', EvmWormholeCore); export * as ethers_contracts from './ethers-contracts'; -export * from './wormholeCore'; +export * from './core'; diff --git a/platforms/solana/protocols/core/src/core.ts b/platforms/solana/protocols/core/src/core.ts index abfc6f6da8..8cc8600a64 100644 --- a/platforms/solana/protocols/core/src/core.ts +++ b/platforms/solana/protocols/core/src/core.ts @@ -35,6 +35,7 @@ import { createReadOnlyWormholeProgramInterface, createVerifySignaturesInstructions, derivePostedVaaKey, + getWormholeBridgeData, } from './utils'; const SOLANA_SEQ_LOG = 'Program log: Sequence: '; @@ -86,6 +87,14 @@ export class SolanaWormholeCore ); } + async getMessageFee(): Promise { + const bd = await getWormholeBridgeData( + this.connection, + this.coreBridge.programId, + ); + return bd.config.fee; + } + async *publishMessage( sender: AnySolanaAddress, message: Uint8Array, diff --git a/platforms/solana/src/platform.ts b/platforms/solana/src/platform.ts index db3a9dc914..c65c187e02 100644 --- a/platforms/solana/src/platform.ts +++ b/platforms/solana/src/platform.ts @@ -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 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 { + // 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 extends PlatformContext< ): Promise { 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 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 { - const { lastValidBlockHeight } = await this.latestBlock(rpc); + const { lastValidBlockHeight } = await this.latestBlock(rpc, 'confirmed'); return lastValidBlockHeight; }