///
import { Middleware } from "../compose.middleware.js";
import { Context } from "../context.js";
import {
CHAIN_ID_BSC,
CHAIN_ID_SOLANA,
ChainId,
isEVMChain,
} from "@certusone/wormhole-sdk";
import { Logger } from "winston";
import { Environment } from "../environment.js";
import { LRUCache } from "lru-cache";
import { ParsedVaaWithBytes, defaultWormscanUrl } from "../application.js";
import { WormholescanClient } from "../rpc/wormholescan-client.js";
import { printError } from "../utils.js";
export interface SourceTxOpts {
wormscanEndpoint: string;
retries: number;
initialDelay: number;
maxDelay: number;
timeout: number;
}
export interface SourceTxContext extends Context {
sourceTxHash?: string;
}
const defaultOptsByEnv = {
[Environment.MAINNET]: {
wormscanEndpoint: defaultWormscanUrl[Environment.MAINNET],
retries: 5,
initialDelay: 1_000,
maxDelay: 45_000,
timeout: 5_000,
},
[Environment.TESTNET]: {
wormscanEndpoint: defaultWormscanUrl[Environment.TESTNET],
retries: 3,
initialDelay: 1_000,
maxDelay: 30_000,
timeout: 3_000,
},
[Environment.DEVNET]: {
wormscanEndpoint: defaultWormscanUrl[Environment.DEVNET],
retries: 3,
initialDelay: 500,
maxDelay: 10_000,
timeout: 2_000,
},
} satisfies { [k in Environment]: Partial };
function ifVAAFinalized(vaa: ParsedVaaWithBytes) {
const { consistencyLevel, emitterChain } = vaa;
if (emitterChain === CHAIN_ID_SOLANA) {
return consistencyLevel === 32;
} else if (emitterChain === CHAIN_ID_BSC) {
return consistencyLevel > 15;
}
return consistencyLevel !== 200 && consistencyLevel !== 201;
}
let wormholescan: WormholescanClient;
export function sourceTx(
optsWithoutDefaults?: SourceTxOpts,
): Middleware {
let opts: SourceTxOpts;
const alreadyFetchedHashes = new LRUCache({ max: 1_000 });
return async (ctx, next) => {
if (!opts) {
// initialize options now that we know the environment from context
opts = {
...defaultOptsByEnv[ctx.env],
...optsWithoutDefaults,
};
}
if (ctx.vaa === undefined) {
ctx.logger?.debug("Didn't get a VAA id. Skipping tx hash fetch.");
await next();
return;
}
const vaaId = `${ctx.vaa.id.emitterChain}-${ctx.vaa.id.emitterAddress}-${ctx.vaa.id.sequence}`;
const txHashFromCache = alreadyFetchedHashes.get(vaaId) as
| string
| undefined;
if (txHashFromCache) {
ctx.logger?.debug(`Already fetched tx hash: ${txHashFromCache}`);
ctx.sourceTxHash = txHashFromCache;
await next();
return;
}
const { emitterChain, emitterAddress, sequence } = ctx.vaa;
ctx.logger?.debug("Fetching tx hash...");
let txHash = await fetchVaaHash(
emitterChain,
emitterAddress,
sequence,
ctx.env,
ctx.logger,
opts,
);
if (txHash === "") {
ctx.logger?.debug("Could not retrieve tx hash.");
} else {
// TODO look at consistency level before using cache? (not sure what the checks are)
if (ifVAAFinalized(ctx.vaa)) {
alreadyFetchedHashes.set(vaaId, txHash);
}
ctx.logger?.debug(`Retrieved tx hash: ${txHash}`);
}
ctx.sourceTxHash = txHash;
await next();
};
}
export async function fetchVaaHash(
emitterChain: number,
emitterAddress: Buffer,
sequence: bigint,
env: Environment,
logger?: Logger,
sourceTxOpts?: SourceTxOpts,
) {
if (!wormholescan) {
const opts = sourceTxOpts || defaultOptsByEnv[env];
wormholescan = new WormholescanClient(new URL(opts.wormscanEndpoint), {
retries: opts.retries,
initialDelay: opts.initialDelay,
maxDelay: opts.maxDelay,
timeout: opts.timeout,
});
}
const response = await wormholescan.getVaa(
emitterChain,
emitterAddress.toString("hex"),
sequence,
);
if ("error" in response) {
logger?.error(`Error fetching tx hash: ${printError(response.error)}`);
throw response.error;
}
let txHash = response.data.txHash || "";
if (
isEVMChain(emitterChain as ChainId) &&
txHash &&
!txHash.startsWith("0x")
) {
txHash = `0x${txHash}`;
}
logger?.debug("Source Transaction Hash: " + txHash || "Not Found");
return txHash;
}