import { accounts, EscrowStateMarker, XRaydiumBridge__factory } from "."; import { BigNumber, BigNumberish } from "ethers"; import { asyncFilter, dbg, info, warn, _undef } from "./utilities"; import * as context from "./context"; import * as whHelpers from "./wh_helpers"; import * as wh from "@certusone/wormhole-sdk"; import * as grpcWebNodeHttpTransport from "@improbable-eng/grpc-web-node-http-transport"; import { tryFetchEscrowState } from "./fetch_escrow_state"; import BN from "bn.js"; export enum XTxStatus { SourceMessageNotEmitted = "SourceMessageNotEmitted", SourceMessageEmitted = "SourceMessageEmitted", FirstSourceMessageSigned = "FirstSourceMessageSigned", AllSourceMessagesSigned = "AllSourceMessagesSigned", FirstSourceMessageRedeemed = "FirstSourceMessageRedeemed", AllSourceMessagesRedeemed = "AllSourceMessagesRedeemed", TargetProgramInvoked = "TargetProgramInvoked", ReturningTokens_InvocationFailed = "ReturningTokens_InvocationFailed", ReturningTokens_InvocationSucceeded = "ReturningTokens_InvocationSucceeded", FirstReturnMessageSigned = "FirstReturnMessageSigned", AllReturnMessagesSigned = "AllReturnMessagesSigned", FirstReturnMessageRedeemed = "FirstReturnMessageRedeemed", AllReturnMessagesRedeemed = "AllReturnMessagesRedeemed", } async function findLatestXTxId( ctx: context.Context, sender: string, id?: BigNumberish, silent = true, ): Promise<{ id: BN; seqs: string[] } | undefined> { const xRaydium = await XRaydiumBridge__factory.connect( ctx.xRaydiumEvmAddr, ctx.evm.signer, ); const blockNumber = await xRaydium.provider.getBlockNumber(); const filter = xRaydium.filters.XTxID(sender, id); const matches = await xRaydium.queryFilter(filter, blockNumber - 1000); if (matches.length === 0) { info("Found no xTxId events for sender: " + sender); return; } const latest = matches.reduce( (prev, cur) => (prev.blockNumber > cur.blockNumber ? prev : cur), matches[0], ); const parsed = xRaydium.interface.parseLog(latest); if (!silent) { dbg(parsed, "parsed latest log"); } return { id: parsed.args.id, seqs: parsed.args.seqs }; } export async function queryStatusOfXTx( ctx: context.Context, xTxId: BN, lastStatus = XTxStatus.SourceMessageNotEmitted, silent = true, ): Promise { const { outgoingVAAs: maybeOutgoingVAAs, status } = await statusOfSourceMessages(ctx, xTxId, silent); if (status) { return status; } const outgoingVAAs = _undef(maybeOutgoingVAAs); // must be non-null // parse header and fetch solana proxy state const header = await whHelpers.parseHeader( outgoingVAAs[0].transfer.payload3, silent, ); const maybeState = await tryFetchEscrowState( ctx.sol, outgoingVAAs[0].transfer.fromAddress, header, { retries: 1, backoff: 1, silent }, ); // interpret solana proxy state switch (maybeState?.marker.kind) { // not a fallthrough case, this is legit // @ts-ignore case undefined: dbg("EscrowState account not found"); case "Uninitialized": return outgoingVAAs.every(vaa => vaa !== undefined) ? XTxStatus.AllSourceMessagesSigned : XTxStatus.FirstSourceMessageSigned; case "Initialized": return XTxStatus.AllSourceMessagesSigned; case "Ready": return XTxStatus.AllSourceMessagesRedeemed; case "Completed": } const state = _undef(maybeState); // solana proxy state is completed or aborted which indicates Returning tokens at this time return await statusOfTokenReturns(ctx, state, silent); } async function statusOfSourceMessages( ctx: context.Context, xTxId: BN, silent: boolean, ): Promise<{ status?: XTxStatus | undefined; outgoingVAAs?: | { transfer: whHelpers.TransferPayloadWithData; vaaBytes: Uint8Array; baseVAA: wh.ParsedVaa; }[] | undefined; }> { const latest = await findLatestXTxId( ctx, ctx.evm.evmWalletAddr, xTxId.toString(), ); if (!latest) { return dbg( { status: XTxStatus.SourceMessageNotEmitted }, "can't find given xTx in emitted events", ); } const outgoingSeqs = latest.seqs; const outgoingVAAs = await Promise.all( outgoingSeqs.map(seq => tryGetSignedVAAFromEvm(ctx.evm, BigNumber.from(seq), silent), ), ).then(vaas => vaas.flatMap(x => (x ? [x] : []))); dbg(outgoingSeqs.length, "outgoingSeqs.length"); dbg(outgoingVAAs.length, "outgoingVAAs.length"); if (outgoingVAAs.length === 0) { return dbg( { status: XTxStatus.SourceMessageEmitted }, "outgoingVAAs.length === 0", ); } if (outgoingVAAs.length < outgoingSeqs.length) { return dbg( { status: XTxStatus.FirstReturnMessageSigned }, "found an outgoing vaa but not all", ); } return { outgoingVAAs }; } async function statusOfTokenReturns( ctx: context.Context, state: accounts.EscrowState, silent: boolean, ) { const returnSeqs = state?.outputTokens .concat(state.inputTokens) .flatMap(token => token.hasReturned.kind === "ReturnedToEvm" ? [token.hasReturned.value.seq] : [], ); dbg(returnSeqs.length, "returnSeqs.length"); const returnVAAs = await Promise.all( returnSeqs.map(seq => tryGetSignedVAAFromSolana( ctx.sol, BigNumber.from(seq.toString()), silent, ), ), ).then(vaas => vaas.flatMap(vaa => (vaa === undefined ? [] : [vaa]))); dbg(returnVAAs.length, "returnVAAs.length"); if (returnVAAs.length === 0) { if (state.marker.kind === "Completed") { // we know marker variant is Completed, so marker has a value of type CompletedValue const markerValue: EscrowStateMarker.CompletedValue = // @ts-ignore state.marker.value; return markerValue.status.kind === "Success" ? XTxStatus.ReturningTokens_InvocationSucceeded : XTxStatus.ReturningTokens_InvocationFailed; } else { // aborted case return XTxStatus.ReturningTokens_InvocationFailed; } } const completedReturns = await asyncFilter(returnVAAs, vaa => wh.getIsTransferCompletedEth(ctx.tokenBridgeEvm, ctx.evm.provider, vaa), ); dbg(completedReturns.length, "completedReturns.length"); if (completedReturns.length === 0) { return returnVAAs.length === returnSeqs.length ? XTxStatus.AllReturnMessagesSigned : XTxStatus.FirstReturnMessageSigned; } return completedReturns.length === returnSeqs.length ? XTxStatus.AllReturnMessagesRedeemed : XTxStatus.FirstReturnMessageRedeemed; } async function tryGetSignedVAAFromSolana( ctx: context.SolanaContextNoSigner, seq: BigNumber, silent: boolean, ): Promise { const emitterAddress = await wh.getEmitterAddressSolana( ctx.tokenBridgeSolana.toBase58(), ); if (!silent) { console.log("[Sequence]: ", seq.toString()); console.log( "[EmitterAddress]: ", wh.tryHexToNativeAssetString(emitterAddress, wh.CHAIN_ID_SOLANA), ); } try { const resp = await wh.getSignedVAA( ctx.wormholeRPC, wh.CHAIN_ID_SOLANA, emitterAddress, seq.toString(), { transport: grpcWebNodeHttpTransport.NodeHttpTransport() }, ); return resp.vaaBytes; } catch (e) { if (!silent) { info(e, "VAA not found"); } return undefined; } } async function tryGetSignedVAAFromEvm( ctx: context.EvmContextNoSigner, seq: BigNumber, silent: boolean, ): Promise< | { transfer: whHelpers.TransferPayloadWithData; vaaBytes: Uint8Array; baseVAA: wh.ParsedVaa; } | undefined > { const emitterAddress = wh.getEmitterAddressEth(ctx.tokenBridgeEvm); if (!silent) { console.log("[Sequence]: ", seq.toString()); console.log( "[EmitterAddress]: ", wh.tryHexToNativeAssetString(emitterAddress, ctx.chainId), ); } try { const resp = await wh.getSignedVAA( ctx.wormholeRPC, ctx.chainId, emitterAddress, seq.toString(), { transport: grpcWebNodeHttpTransport.NodeHttpTransport() }, ); if (!resp?.vaaBytes) { return undefined; } const baseVAA = wh.parseVaa(resp.vaaBytes) const transfer = await whHelpers.parseTransferTokenWithPayload(baseVAA); return { transfer, baseVAA, vaaBytes: resp.vaaBytes }; } catch (e) { if (!silent) { info("VAA not found"); } return undefined; } }