From 4f23e3f8a39be65d21a91f6ffb8d164f4a9eb2de Mon Sep 17 00:00:00 2001 From: Jon Ator Date: Wed, 18 Sep 2024 20:12:29 -0400 Subject: [PATCH] add skip tx tracking edge functions --- packages/bridge/src/skip/client.ts | 3 +- packages/bridge/src/skip/index.ts | 1 + packages/bridge/src/skip/transfer-status.ts | 87 +++++++++++++-------- packages/web/pages/api/skip-track-tx.ts | 32 ++++++++ packages/web/pages/api/skip-tx-status.ts | 32 ++++++++ packages/web/stores/root.ts | 28 ++++++- 6 files changed, 147 insertions(+), 36 deletions(-) create mode 100644 packages/web/pages/api/skip-track-tx.ts create mode 100644 packages/web/pages/api/skip-tx-status.ts diff --git a/packages/bridge/src/skip/client.ts b/packages/bridge/src/skip/client.ts index 51507064ff..bcf4f2a673 100644 --- a/packages/bridge/src/skip/client.ts +++ b/packages/bridge/src/skip/client.ts @@ -14,6 +14,7 @@ import { export class SkipApiClient { constructor( readonly env: BridgeEnvironment, + protected readonly apiKey = process.env.SKIP_API_KEY, readonly baseUrl = "https://api.skip.money" ) {} @@ -122,7 +123,7 @@ export class SkipApiClient { return apiClient(args[0], args[1]); } - const key = process.env.SKIP_API_KEY; + const key = this.apiKey; if (!key) throw new Error("SKIP_API_KEY is not set"); return apiClient(args[0], { diff --git a/packages/bridge/src/skip/index.ts b/packages/bridge/src/skip/index.ts index 4bce785b66..679f6e05a5 100644 --- a/packages/bridge/src/skip/index.ts +++ b/packages/bridge/src/skip/index.ts @@ -936,4 +936,5 @@ export class SkipBridgeProvider implements BridgeProvider { } } +export * from "./client"; export * from "./transfer-status"; diff --git a/packages/bridge/src/skip/transfer-status.ts b/packages/bridge/src/skip/transfer-status.ts index d937f56135..33039df44e 100644 --- a/packages/bridge/src/skip/transfer-status.ts +++ b/packages/bridge/src/skip/transfer-status.ts @@ -9,8 +9,27 @@ import type { TransferStatusProvider, TransferStatusReceiver, } from "../interface"; -import { SkipApiClient } from "./client"; import { SkipBridgeProvider } from "./index"; +import { SkipTxStatusResponse } from "./types"; + +type Transaction = { + chainID: string; + txHash: string; + env: BridgeEnvironment; +}; + +export type SkipStatusProvider = { + transactionStatus: ({ + chainID, + txHash, + env, + }: Transaction) => Promise; + trackTransaction: ({ + chainID, + txHash, + env, + }: Transaction) => Promise>; +}; /** Tracks (polls skip endpoint) and reports status updates on Skip bridge transfers. */ export class SkipTransferStatusProvider implements TransferStatusProvider { @@ -19,12 +38,13 @@ export class SkipTransferStatusProvider implements TransferStatusProvider { statusReceiverDelegate?: TransferStatusReceiver | undefined; - readonly skipClient: SkipApiClient; readonly axelarScanBaseUrl: string; - constructor(env: BridgeEnvironment, protected readonly chainList: Chain[]) { - this.skipClient = new SkipApiClient(env); - + constructor( + protected readonly env: BridgeEnvironment, + protected readonly chainList: Chain[], + protected readonly skipStatusProvider: SkipStatusProvider + ) { this.axelarScanBaseUrl = env === "mainnet" ? "https://axelarscan.io" @@ -40,39 +60,38 @@ export class SkipTransferStatusProvider implements TransferStatusProvider { await poll({ fn: async () => { - try { - const txStatus = await this.skipClient.transactionStatus({ - chainID: fromChainId.toString(), - txHash: sendTxHash, + const tx = { + chainID: fromChainId.toString(), + txHash: sendTxHash, + env: this.env, + }; + + const txStatus = await this.skipStatusProvider + .transactionStatus(tx) + .catch(async (error) => { + if (error instanceof Error && error.message.includes("not found")) { + // if we get an error that it's not found, prompt skip to track it first + // then try again + await this.skipStatusProvider.trackTransaction(tx); + return this.skipStatusProvider.transactionStatus(tx); + } + + throw error; }); - let status: TransferStatus = "pending"; - if (txStatus.state === "STATE_COMPLETED_SUCCESS") { - status = "success"; - } - - if (txStatus.state === "STATE_COMPLETED_ERROR") { - status = "failed"; - } - - return { - id: sendTxHash, - status, - }; - } catch (error: any) { - if ("message" in error) { - if (error.message.includes("not found")) { - await this.skipClient.trackTransaction({ - chainID: fromChainId.toString(), - txHash: sendTxHash, - }); - - return undefined; - } - } + let status: TransferStatus = "pending"; + if (txStatus.state === "STATE_COMPLETED_SUCCESS") { + status = "success"; + } - throw error; + if (txStatus.state === "STATE_COMPLETED_ERROR") { + status = "failed"; } + + return { + id: sendTxHash, + status, + }; }, validate: (incomingStatus) => { if (!incomingStatus) { diff --git a/packages/web/pages/api/skip-track-tx.ts b/packages/web/pages/api/skip-track-tx.ts new file mode 100644 index 0000000000..c5a551635b --- /dev/null +++ b/packages/web/pages/api/skip-track-tx.ts @@ -0,0 +1,32 @@ +import { BridgeEnvironment, SkipApiClient } from "@osmosis-labs/bridge"; +import { NextApiRequest, NextApiResponse } from "next"; + +/** This edge function is necessary to invoke the SkipApiClient on the server + * as a secret API key is required for the client. + */ +export default async function skipTrackTx( + req: NextApiRequest, + res: NextApiResponse +) { + const { chainID, txHash, env } = req.query as { + chainID: string; + txHash: string; + env: BridgeEnvironment; + }; + + if (!chainID || !txHash || !env) { + return res.status(400).json({ error: "Missing required query parameters" }); + } + + const skipClient = new SkipApiClient(env); + + try { + const status = await skipClient.trackTransaction({ chainID, txHash }); + return res.status(200).json(status); + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ error: error.message }); + } + return res.status(500).json({ error: "An unknown error occurred" }); + } +} diff --git a/packages/web/pages/api/skip-tx-status.ts b/packages/web/pages/api/skip-tx-status.ts new file mode 100644 index 0000000000..7f53bb563e --- /dev/null +++ b/packages/web/pages/api/skip-tx-status.ts @@ -0,0 +1,32 @@ +import { BridgeEnvironment, SkipApiClient } from "@osmosis-labs/bridge"; +import { NextApiRequest, NextApiResponse } from "next"; + +/** This edge function is necessary to invoke the SkipApiClient on the server + * as a secret API key is required for the client. + */ +export default async function skipTxStatus( + req: NextApiRequest, + res: NextApiResponse +) { + const { chainID, txHash, env } = req.query as { + chainID: string; + txHash: string; + env: BridgeEnvironment; + }; + + if (!chainID || !txHash || !env) { + return res.status(400).json({ error: "Missing required query parameters" }); + } + + const skipClient = new SkipApiClient(env); + + try { + const status = await skipClient.transactionStatus({ chainID, txHash }); + return res.status(200).json(status); + } catch (error) { + if (error instanceof Error) { + return res.status(500).json({ error: error.message }); + } + return res.status(500).json({ error: "An unknown error occurred" }); + } +} diff --git a/packages/web/stores/root.ts b/packages/web/stores/root.ts index cd58c88026..a002d7de34 100644 --- a/packages/web/stores/root.ts +++ b/packages/web/stores/root.ts @@ -249,7 +249,33 @@ export class RootStore { ), new SkipTransferStatusProvider( IS_TESTNET ? "testnet" : "mainnet", - ChainList + ChainList, + { + transactionStatus: async ({ chainID, txHash, env }) => { + const response = await fetch( + `/api/skip-tx-status?chainID=${chainID}&txHash=${txHash}&env=${env}` + ); + const responseJson = await response.json(); + if (!response.ok) { + throw new Error( + "Failed to fetch transaction status: " + responseJson.error + ); + } + return responseJson; + }, + trackTransaction: async ({ chainID, txHash, env }) => { + const response = await fetch( + `/api/skip-track-tx?chainID=${chainID}&txHash=${txHash}&env=${env}` + ); + const responseJson = await response.json(); + if (!response.ok) { + throw new Error( + "Failed to track transaction: " + responseJson.error + ); + } + return responseJson; + }, + } ), new IbcTransferStatusProvider(ChainList, AssetLists), ];