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;
}