+
+const constructJSON = (
+ swappableMap,
+ exclusionList
+): StringifiedBridgeRoutes => {
+ const result = {}
+
+ // Iterate through the chains
+ for (const chainA in swappableMap) {
+ for (const tokenA in swappableMap[chainA]) {
+ const symbolA = swappableMap[chainA][tokenA].symbol
+ const key = `${symbolA}-${chainA}`
+
+ if (exclusionList.includes(key)) {
+ continue
+ }
+
+ // Iterate through other chains to compare
+ for (const chainB in swappableMap) {
+ if (chainA !== chainB) {
+ for (const tokenB in swappableMap[chainB]) {
+ const symbolB = swappableMap[chainB][tokenB].symbol
+ const value = `${symbolB}-${chainB}`
+
+ if (exclusionList.includes(value)) {
+ continue
+ }
+
+ // Check if there's a bridge between the origins and destinations
+ for (const bridgeSymbol of swappableMap[chainA][tokenA].origin) {
+ if (
+ swappableMap[chainA][tokenA].origin.includes(bridgeSymbol) &&
+ swappableMap[chainB][tokenB].destination.includes(bridgeSymbol)
+ ) {
+ // Add to the result if the key exists, else create a new array
+ if (result[key]) {
+ result[key].push(value)
+ } else {
+ result[key] = [value]
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return result
+}
+
+const transformPair = (string: string): any => {
+ const [symbol, chainId] = string.split('-')
+ const token = Object.values(ALL_TOKENS).find((t) => t.routeSymbol === symbol)
+ const address = token?.addresses[chainId]
+ if (token && address) {
+ return {
+ symbol,
+ chainId,
+ address,
+ }
+ }
+}
+
+const transformBridgeRouteValues = (
+ routes: StringifiedBridgeRoutes
+): TransformedBridgeRoutes => {
+ return Object.fromEntries(
+ Object.entries(routes).map(([key, values]) => {
+ const uniquePairs: TokenData[] = []
+ values.forEach((pairStr) => {
+ const transformedPair = transformPair(pairStr)
+ if (
+ transformedPair &&
+ !uniquePairs.some(
+ (pair) =>
+ pair.symbol === transformedPair.symbol &&
+ pair.chainId === transformedPair.chainId
+ )
+ ) {
+ uniquePairs.push(transformedPair)
+ }
+ })
+ return [key, uniquePairs]
+ })
+ )
+}
+
+export const BRIDGE_ROUTE_MAPPING_SYMBOLS = constructJSON(BRIDGE_MAP, [])
+export const BRIDGE_ROUTE_MAPPING = transformBridgeRouteValues(
+ BRIDGE_ROUTE_MAPPING_SYMBOLS
+)
diff --git a/packages/rest-api/src/utils/isTokenAddress.ts b/packages/rest-api/src/utils/isTokenAddress.ts
new file mode 100644
index 0000000000..7945b1ba4c
--- /dev/null
+++ b/packages/rest-api/src/utils/isTokenAddress.ts
@@ -0,0 +1,12 @@
+import { BridgeableToken } from '../types'
+import * as bridgeableTokens from '../constants/bridgeable'
+
+export const isTokenAddress = (address: string): boolean => {
+ const normalizedAddress = address.toLowerCase()
+
+ return Object.values(bridgeableTokens).some((token: BridgeableToken) =>
+ Object.values(token.addresses).some(
+ (tokenAddress: string) => tokenAddress.toLowerCase() === normalizedAddress
+ )
+ )
+}
diff --git a/packages/rest-api/src/utils/isTokenSupportedOnChain.ts b/packages/rest-api/src/utils/isTokenSupportedOnChain.ts
new file mode 100644
index 0000000000..161404a6f6
--- /dev/null
+++ b/packages/rest-api/src/utils/isTokenSupportedOnChain.ts
@@ -0,0 +1,16 @@
+import { BridgeableToken } from '../types'
+import * as bridgeableTokens from '../constants/bridgeable'
+
+export const isTokenSupportedOnChain = (
+ tokenAddress: string,
+ chainId: string
+): boolean => {
+ const normalizedAddress = tokenAddress.toLowerCase()
+ const chainIdNumber = parseInt(chainId, 10)
+
+ return Object.values(bridgeableTokens).some(
+ (token: BridgeableToken) =>
+ token.addresses[chainIdNumber] !== undefined &&
+ token.addresses[chainIdNumber].toLowerCase() === normalizedAddress
+ )
+}
diff --git a/packages/rest-api/src/utils/tokenAddressToToken.ts b/packages/rest-api/src/utils/tokenAddressToToken.ts
new file mode 100644
index 0000000000..6bad892582
--- /dev/null
+++ b/packages/rest-api/src/utils/tokenAddressToToken.ts
@@ -0,0 +1,20 @@
+import { BRIDGE_MAP } from '../constants/bridgeMap'
+
+export const tokenAddressToToken = (chain: string, tokenAddress: string) => {
+ const chainData = BRIDGE_MAP[chain]
+ if (!chainData) {
+ return null
+ }
+
+ const tokenInfo = chainData[tokenAddress]
+
+ if (!tokenInfo) {
+ return null
+ }
+
+ return {
+ address: tokenAddress,
+ symbol: tokenInfo.symbol,
+ decimals: tokenInfo.decimals,
+ }
+}
diff --git a/packages/rest-api/src/utils/findTokenInfo.ts b/packages/rest-api/src/utils/tokenSymbolToToken.ts
similarity index 82%
rename from packages/rest-api/src/utils/findTokenInfo.ts
rename to packages/rest-api/src/utils/tokenSymbolToToken.ts
index 7cd4e9d63d..bed196c851 100644
--- a/packages/rest-api/src/utils/findTokenInfo.ts
+++ b/packages/rest-api/src/utils/tokenSymbolToToken.ts
@@ -1,6 +1,6 @@
import { BRIDGE_MAP } from '../constants/bridgeMap'
-export const findTokenInfo = (chain: string, tokenSymbol: string) => {
+export const tokenSymbolToToken = (chain: string, tokenSymbol: string) => {
const chainData = BRIDGE_MAP[chain]
if (!chainData) {
return null
diff --git a/packages/rest-api/src/validations/validateRouteExists.ts b/packages/rest-api/src/validations/validateRouteExists.ts
new file mode 100644
index 0000000000..4339d9d8c1
--- /dev/null
+++ b/packages/rest-api/src/validations/validateRouteExists.ts
@@ -0,0 +1,20 @@
+import { tokenAddressToToken } from '../utils/tokenAddressToToken'
+import { BRIDGE_ROUTE_MAPPING_SYMBOLS } from '../utils/bridgeRouteMapping'
+
+export const validateRouteExists = (fromChain, fromToken, toChain, toToken) => {
+ const fromTokenInfo = tokenAddressToToken(fromChain.toString(), fromToken)
+ const toTokenInfo = tokenAddressToToken(toChain.toString(), toToken)
+
+ if (!fromTokenInfo || !toTokenInfo) {
+ return false
+ }
+
+ const key = `${fromTokenInfo.symbol}-${fromChain}`
+ const routes = BRIDGE_ROUTE_MAPPING_SYMBOLS[key]
+
+ if (!routes) {
+ return false
+ }
+
+ return routes.includes(`${toTokenInfo.symbol}-${toChain}`)
+}
diff --git a/packages/rest-api/src/validations/validateTokens.ts b/packages/rest-api/src/validations/validateTokens.ts
deleted file mode 100644
index c787115e1e..0000000000
--- a/packages/rest-api/src/validations/validateTokens.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { check } from 'express-validator'
-
-import { findTokenInfo } from '../utils/findTokenInfo'
-
-export const validateTokens = (chainParam, tokenParam, paramName) => {
- return check(tokenParam)
- .isString()
- .exists()
- .withMessage(`${paramName} is required`)
- .custom((value, { req }) => {
- const chain = req.query[chainParam]
- const tokenInfo = findTokenInfo(chain, value)
- if (!tokenInfo) {
- throw new Error(`Invalid ${paramName} symbol`)
- }
- if (!req.res.locals.tokenInfo) {
- req.res.locals.tokenInfo = {}
- }
- req.res.locals.tokenInfo[paramName] = tokenInfo
- return true
- })
-}
diff --git a/packages/rfq-indexer/README.md b/packages/rfq-indexer/README.md
new file mode 100644
index 0000000000..6d70eb8e37
--- /dev/null
+++ b/packages/rfq-indexer/README.md
@@ -0,0 +1,26 @@
+# RFQ Indexer
+
+## Overview
+
+The RFQ (Request for Quote) Indexer is a system designed to index and track bridge events across multiple blockchain networks. It consists of two main parts: the indexer and the API.
+
+1. What does the rfq-indexer do?
+ The rfq-indexer captures and stores bridge events from various blockchain networks, including Ethereum, Optimism, Arbitrum, Base, Blast, Scroll, Linea, and BNB Chain. It indexes events such as bridge requests, relays, proofs, refunds, and claims.
+
+2. Parts of the indexer and their users:
+ - Indexer: Used by developers and system administrators to collect and store blockchain data.
+ - API: Used by front-end applications, other services, or developers to query the indexed data.
+
+## Directory Structure
+
+rfq-indexer
+├── api: API service
+│ ├── src/ : API source code
+│ ├── package.json : API dependencies and scripts
+│ ├── README.md : API documentation
+├── indexer: Indexer service
+│ ├── src/ : Indexer source code
+│ ├── abis/ : Contract ABIs
+│ ├── package.json : Indexer dependencies and scripts
+│ ├── README.md : Indexer documentation
+
diff --git a/packages/rfq-indexer/api/..prettierrc.js b/packages/rfq-indexer/api/..prettierrc.js
new file mode 100644
index 0000000000..3760edb6d2
--- /dev/null
+++ b/packages/rfq-indexer/api/..prettierrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ ...require('../../../.prettierrc.js'),
+}
diff --git a/packages/rfq-indexer/api/.eslintrc.js b/packages/rfq-indexer/api/.eslintrc.js
new file mode 100644
index 0000000000..cce34ed29b
--- /dev/null
+++ b/packages/rfq-indexer/api/.eslintrc.js
@@ -0,0 +1,3 @@
+module.exports = {
+ extends: '../../../.eslintrc.js',
+}
diff --git a/packages/rfq-indexer/api/.gitignore b/packages/rfq-indexer/api/.gitignore
new file mode 100644
index 0000000000..cad54defd8
--- /dev/null
+++ b/packages/rfq-indexer/api/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+.env
+
+.env.*
diff --git a/packages/rfq-indexer/api/CHANGELOG.md b/packages/rfq-indexer/api/CHANGELOG.md
new file mode 100644
index 0000000000..2ffd88a1c8
--- /dev/null
+++ b/packages/rfq-indexer/api/CHANGELOG.md
@@ -0,0 +1,24 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+## [1.0.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.3...@synapsecns/rfq-indexer-api@1.0.4) (2024-09-22)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer-api
+
+
+
+
+
+## [1.0.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer-api@1.0.2...@synapsecns/rfq-indexer-api@1.0.3) (2024-09-20)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer-api
+
+
+
+
+
+## 1.0.2 (2024-09-19)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer-api
diff --git a/packages/rfq-indexer/api/README.md b/packages/rfq-indexer/api/README.md
new file mode 100644
index 0000000000..5e11f7efad
--- /dev/null
+++ b/packages/rfq-indexer/api/README.md
@@ -0,0 +1,40 @@
+# RFQ Indexer API
+
+This API provides access to the indexed bridge event data.
+
+## API Calls
+
+1. GET /api/hello
+ - Description: A simple hello world endpoint
+ - Example: `curl http://localhost:3001/api/hello`
+
+2. GET /api/pending-transactions-missing-relay
+ - Description: Retrieves pending transactions that are missing relay events
+ - Example:
+ ```
+ curl http://localhost:3001/api/pending-transactions-missing-relay
+ ```
+
+3. GET /api/pending-transactions-missing-proof
+ - Description: Retrieves pending transactions that are missing proof events
+ - Example:
+ ```
+ curl http://localhost:3001/api/pending-transactions-missing-proof
+ ```
+
+4. GET /api/pending-transactions-missing-claim
+ - Description: Retrieves pending transactions that are missing claim events
+ - Example:
+ ```
+ curl http://localhost:3001/api/pending-transactions-missing-claim
+ ```
+
+5. GraphQL endpoint: /graphql
+ - Description: Provides a GraphQL interface for querying indexed data, the user is surfaced an interface to query the data via GraphiQL
+
+## Important Scripts
+
+- `yarn dev:local`: Runs the API in development mode using local environment variables
+- `yarn dev:prod`: Runs the API in development mode using production environment variables
+- `yarn start`: Starts the API in production mode
+
diff --git a/packages/rfq-indexer/api/nixpacks.toml b/packages/rfq-indexer/api/nixpacks.toml
new file mode 100644
index 0000000000..0cefad95ff
--- /dev/null
+++ b/packages/rfq-indexer/api/nixpacks.toml
@@ -0,0 +1,4 @@
+providers = ["node"]
+
+[phases.install]
+cmds = ["npm install -g corepack", "corepack enable", "corepack prepare pnpm@9.1.0 --activate", "pnpm install"]
\ No newline at end of file
diff --git a/packages/rfq-indexer/api/package.json b/packages/rfq-indexer/api/package.json
new file mode 100644
index 0000000000..ca054661f8
--- /dev/null
+++ b/packages/rfq-indexer/api/package.json
@@ -0,0 +1,65 @@
+{
+ "name": "@synapsecns/rfq-indexer-api",
+ "private": true,
+ "version": "1.0.4",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "check-env": "dotenv -e .env.local -- printenv | grep DATABASE_URL",
+ "dev:local": "dotenv -e .env.local -- tsx watch src/app.ts",
+ "dev:prod": "dotenv -e .env.production -- tsx watch src/app.ts",
+ "start": "tsx src/app.ts",
+ "start:local": "dotenv -e .env -- tsx src/app.ts",
+ "dev": "dotenv -e .env -- tsx watch src/app.ts",
+ "lint:check": " ",
+ "ci:lint": " ",
+ "build:go": " ",
+ "build": " ",
+ "build:slither": " ",
+ "test": "",
+ "test:coverage": "echo no tests defined"
+ },
+ "keywords": [],
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@graphql-tools/load-files": "^7.0.0",
+ "@graphql-tools/merge": "^9.0.7",
+ "@graphql-tools/schema": "^10.0.6",
+ "@types/express": "^4.17.21",
+ "@types/node": "^22.5.4",
+ "dotenv-cli": "^7.4.2",
+ "express": "^4.21.0",
+ "express-validator": "^7.2.0",
+ "graphql": "^16.9.0",
+ "graphql-yoga": "^5.7.0",
+ "kysely": "^0.27.4",
+ "pg": "^8.12.0",
+ "supertest": "^7.0.0",
+ "ts-node": "^10.9.2",
+ "tsx": "^4.19.1",
+ "typescript": "^5.6.2",
+ "viem": "^2.21.6"
+ },
+ "engines": {
+ "node": ">=18.17"
+ },
+ "devDependencies": {
+ "@babel/core": "^7.25.2",
+ "@babel/preset-env": "^7.25.4",
+ "@babel/preset-typescript": "^7.24.7",
+ "@types/pg": "^8.11.9",
+ "@types/supertest": "^6.0.2",
+ "@types/swagger-jsdoc": "6.0.4",
+ "@types/swagger-ui-express": "4.1.6",
+ "dotenv": "^16.4.5",
+ "express-validator": "^7.2.0",
+ "swagger-jsdoc": "^6.2.8",
+ "swagger-ui-express": "^5.0.1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/synapsecns/sanguine",
+ "directory": "packages/rfq-indexer/api"
+ }
+}
diff --git a/packages/rfq-indexer/api/src/.babelrc b/packages/rfq-indexer/api/src/.babelrc
new file mode 100644
index 0000000000..3313ff9ef0
--- /dev/null
+++ b/packages/rfq-indexer/api/src/.babelrc
@@ -0,0 +1,3 @@
+{
+ "presets": ["@babel/preset-env", "@babel/preset-typescript"]
+}
diff --git a/packages/rfq-indexer/api/src/app.ts b/packages/rfq-indexer/api/src/app.ts
new file mode 100644
index 0000000000..4209656836
--- /dev/null
+++ b/packages/rfq-indexer/api/src/app.ts
@@ -0,0 +1,32 @@
+import express from 'express'
+import swaggerUi from 'swagger-ui-express'
+import { createYoga } from 'graphql-yoga'
+
+import { specs } from './swagger'
+import routes from './routes'
+import { schema } from './graphql/schema'
+import { overrideJsonBigIntSerialization } from './utils/overrideJsonBigIntSerialization'
+
+const app = express()
+const port = process.env.PORT || 3001
+
+overrideJsonBigIntSerialization()
+
+app.use(express.json())
+
+// Swagger UI setup
+app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(specs))
+
+// REST API routes
+app.use('/api', routes)
+
+// GraphQL setup
+const yoga = createYoga({ schema })
+app.use('/graphql', yoga)
+
+export const server = app.listen(port, () => {
+ console.log(`Server listening at ${port}`)
+ console.info('API server runs on http://localhost:3001')
+ console.info('REST requests go through http://localhost:3001/api')
+ console.info('GraphQL requests go through http://localhost:3001/graphql')
+})
diff --git a/packages/rfq-indexer/api/src/constants/abis/FastBridgeV2.ts b/packages/rfq-indexer/api/src/constants/abis/FastBridgeV2.ts
new file mode 100644
index 0000000000..0b5b365a9c
--- /dev/null
+++ b/packages/rfq-indexer/api/src/constants/abis/FastBridgeV2.ts
@@ -0,0 +1,774 @@
+export const FastBridgeV2Abi = [
+ {
+ inputs: [{ internalType: 'address', name: '_owner', type: 'address' }],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ {
+ inputs: [],
+ name: 'AccessControlBadConfirmation',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'account', type: 'address' },
+ { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' },
+ ],
+ name: 'AccessControlUnauthorizedAccount',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'target', type: 'address' }],
+ name: 'AddressEmptyCode',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'account', type: 'address' }],
+ name: 'AddressInsufficientBalance',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'AmountIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'ChainIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineExceeded',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineNotExceeded',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineTooShort',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DisputePeriodNotPassed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DisputePeriodPassed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'FailedInnerCall',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'MsgValueIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [{ internalType: 'address', name: 'token', type: 'address' }],
+ name: 'SafeERC20FailedOperation',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'SenderIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'StatusIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'TokenNotContract',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'TransactionRelayed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'ZeroAddress',
+ type: 'error',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'relayer',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'token',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'amount',
+ type: 'uint256',
+ },
+ ],
+ name: 'BridgeDepositClaimed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'token',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'amount',
+ type: 'uint256',
+ },
+ ],
+ name: 'BridgeDepositRefunded',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'relayer',
+ type: 'address',
+ },
+ ],
+ name: 'BridgeProofDisputed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'relayer',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'bytes32',
+ name: 'transactionHash',
+ type: 'bytes32',
+ },
+ ],
+ name: 'BridgeProofProvided',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'relayer',
+ type: 'address',
+ },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ {
+ indexed: false,
+ internalType: 'uint32',
+ name: 'originChainId',
+ type: 'uint32',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'originToken',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'destToken',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'originAmount',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'destAmount',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'chainGasAmount',
+ type: 'uint256',
+ },
+ ],
+ name: 'BridgeRelayed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'transactionId',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'sender',
+ type: 'address',
+ },
+ { indexed: false, internalType: 'bytes', name: 'request', type: 'bytes' },
+ {
+ indexed: false,
+ internalType: 'uint32',
+ name: 'destChainId',
+ type: 'uint32',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'originToken',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'destToken',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'originAmount',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'destAmount',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'bool',
+ name: 'sendChainGas',
+ type: 'bool',
+ },
+ ],
+ name: 'BridgeRequested',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'oldChainGasAmount',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newChainGasAmount',
+ type: 'uint256',
+ },
+ ],
+ name: 'ChainGasAmountUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'oldFeeRate',
+ type: 'uint256',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'newFeeRate',
+ type: 'uint256',
+ },
+ ],
+ name: 'FeeRateUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'token',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'address',
+ name: 'recipient',
+ type: 'address',
+ },
+ {
+ indexed: false,
+ internalType: 'uint256',
+ name: 'amount',
+ type: 'uint256',
+ },
+ ],
+ name: 'FeesSwept',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'previousAdminRole',
+ type: 'bytes32',
+ },
+ {
+ indexed: true,
+ internalType: 'bytes32',
+ name: 'newAdminRole',
+ type: 'bytes32',
+ },
+ ],
+ name: 'RoleAdminChanged',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'account',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'sender',
+ type: 'address',
+ },
+ ],
+ name: 'RoleGranted',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'account',
+ type: 'address',
+ },
+ {
+ indexed: true,
+ internalType: 'address',
+ name: 'sender',
+ type: 'address',
+ },
+ ],
+ name: 'RoleRevoked',
+ type: 'event',
+ },
+ {
+ inputs: [],
+ name: 'DEFAULT_ADMIN_ROLE',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'DISPUTE_PERIOD',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'FEE_BPS',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'FEE_RATE_MAX',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'GOVERNOR_ROLE',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'GUARD_ROLE',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'MIN_DEADLINE_PERIOD',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'REFUNDER_ROLE',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'REFUND_DELAY',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'RELAYER_ROLE',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'uint32', name: 'dstChainId', type: 'uint32' },
+ { internalType: 'address', name: 'sender', type: 'address' },
+ { internalType: 'address', name: 'to', type: 'address' },
+ { internalType: 'address', name: 'originToken', type: 'address' },
+ { internalType: 'address', name: 'destToken', type: 'address' },
+ { internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { internalType: 'bool', name: 'sendChainGas', type: 'bool' },
+ { internalType: 'uint256', name: 'deadline', type: 'uint256' },
+ ],
+ internalType: 'struct IFastBridge.BridgeParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'bridge',
+ outputs: [],
+ stateMutability: 'payable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ name: 'bridgeProofs',
+ outputs: [
+ { internalType: 'uint96', name: 'timestamp', type: 'uint96' },
+ { internalType: 'address', name: 'relayer', type: 'address' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ name: 'bridgeRelays',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ name: 'bridgeStatuses',
+ outputs: [
+ { internalType: 'enum FastBridge.BridgeStatus', name: '', type: 'uint8' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { internalType: 'address', name: 'relayer', type: 'address' },
+ ],
+ name: 'canClaim',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'chainGasAmount',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ { internalType: 'address', name: 'to', type: 'address' },
+ ],
+ name: 'claim',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'deployBlock',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ ],
+ name: 'dispute',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }],
+ name: 'getBridgeTransaction',
+ outputs: [
+ {
+ components: [
+ { internalType: 'uint32', name: 'originChainId', type: 'uint32' },
+ { internalType: 'uint32', name: 'destChainId', type: 'uint32' },
+ { internalType: 'address', name: 'originSender', type: 'address' },
+ { internalType: 'address', name: 'destRecipient', type: 'address' },
+ { internalType: 'address', name: 'originToken', type: 'address' },
+ { internalType: 'address', name: 'destToken', type: 'address' },
+ { internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'originFeeAmount', type: 'uint256' },
+ { internalType: 'bool', name: 'sendChainGas', type: 'bool' },
+ { internalType: 'uint256', name: 'deadline', type: 'uint256' },
+ { internalType: 'uint256', name: 'nonce', type: 'uint256' },
+ ],
+ internalType: 'struct IFastBridge.BridgeTransaction',
+ name: '',
+ type: 'tuple',
+ },
+ ],
+ stateMutability: 'pure',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }],
+ name: 'getRoleAdmin',
+ outputs: [{ internalType: 'bytes32', name: '', type: 'bytes32' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'uint256', name: 'index', type: 'uint256' },
+ ],
+ name: 'getRoleMember',
+ outputs: [{ internalType: 'address', name: '', type: 'address' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes32', name: 'role', type: 'bytes32' }],
+ name: 'getRoleMemberCount',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'grantRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'hasRole',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'nonce',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'protocolFeeRate',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'address', name: '', type: 'address' }],
+ name: 'protocolFees',
+ outputs: [{ internalType: 'uint256', name: '', type: 'uint256' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ { internalType: 'bytes32', name: 'destTxHash', type: 'bytes32' },
+ ],
+ name: 'prove',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }],
+ name: 'refund',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes', name: 'request', type: 'bytes' }],
+ name: 'relay',
+ outputs: [],
+ stateMutability: 'payable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'callerConfirmation', type: 'address' },
+ ],
+ name: 'renounceRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'revokeRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'newChainGasAmount', type: 'uint256' },
+ ],
+ name: 'setChainGasAmount',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'uint256', name: 'newFeeRate', type: 'uint256' }],
+ name: 'setProtocolFeeRate',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [{ internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' }],
+ name: 'supportsInterface',
+ outputs: [{ internalType: 'bool', name: '', type: 'bool' }],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'token', type: 'address' },
+ { internalType: 'address', name: 'recipient', type: 'address' },
+ ],
+ name: 'sweepProtocolFees',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ ] as const
diff --git a/packages/rfq-indexer/api/src/constants/networkConfig.ts b/packages/rfq-indexer/api/src/constants/networkConfig.ts
new file mode 100644
index 0000000000..8d2dc78e3f
--- /dev/null
+++ b/packages/rfq-indexer/api/src/constants/networkConfig.ts
@@ -0,0 +1,117 @@
+import { createPublicClient, http } from 'viem'
+import {
+ mainnet,
+ arbitrum,
+ optimism,
+ base,
+ scroll,
+ linea,
+ bsc,
+ blast,
+} from 'viem/chains'
+
+import { FastBridgeV2Abi } from './abis/FastBridgeV2'
+
+interface NetworkEntry {
+ name: string
+ FastBridgeV2: {
+ address: string
+ abi: any
+ }
+ client: any
+}
+
+type NetworkConfig = {
+ [chainId: number]: NetworkEntry
+}
+
+export const networkConfig: NetworkConfig = {
+ 1: {
+ name: 'Ethereum',
+ FastBridgeV2: {
+ address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: mainnet,
+ transport: http(),
+ }),
+ },
+ 42161: {
+ name: 'Arbitrum',
+ FastBridgeV2: {
+ address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: arbitrum,
+ transport: http(),
+ }),
+ },
+ 10: {
+ name: 'Optimism',
+ FastBridgeV2: {
+ address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: optimism,
+ transport: http(),
+ }),
+ },
+ 8453: {
+ name: 'Base',
+ FastBridgeV2: {
+ address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: base,
+ transport: http(),
+ }),
+ },
+ 534352: {
+ name: 'Scroll',
+ FastBridgeV2: {
+ address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: scroll,
+ transport: http(),
+ }),
+ },
+ 59144: {
+ name: 'Linea',
+ FastBridgeV2: {
+ address: '0x34F52752975222d5994C206cE08C1d5B329f24dD',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: linea,
+ transport: http(),
+ }),
+ },
+ 56: {
+ name: 'BNB Chain',
+ FastBridgeV2: {
+ address: '0x34F52752975222d5994C206cE08C1d5B329f24dD',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: bsc,
+ transport: http(),
+ }),
+ },
+ 81457: {
+ name: 'Blast',
+ FastBridgeV2: {
+ address: '0x34F52752975222d5994C206cE08C1d5B329f24dD',
+ abi: FastBridgeV2Abi,
+ },
+ client: createPublicClient({
+ chain: blast,
+ transport: http(),
+ }),
+ },
+} as const
diff --git a/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
new file mode 100644
index 0000000000..5079bb0d5f
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/conflictingProofsController.ts
@@ -0,0 +1,50 @@
+import { Request, Response } from 'express'
+import { sql } from 'kysely'
+
+import { db } from '../db'
+import { qDeposits, qRelays, qProofs } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const conflictingProofsController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .where('relayer_proof', 'is not', null)
+ .where('relayer_relay', 'is not', null)
+ .where(
+ (eb) =>
+ sql`LOWER(${eb.ref('relayer_relay')}) != LOWER(${eb.ref(
+ 'relayer_proof'
+ )})`
+ )
+ .orderBy('blockTimestamp_proof', 'desc')
+
+ const results = await query.execute()
+ const conflictingProofs = nest_results(results)
+
+ if (conflictingProofs && conflictingProofs.length > 0) {
+ res.json(conflictingProofs)
+ } else {
+ res.status(200).json({ message: 'No conflicting proofs found' })
+ }
+ } catch (error) {
+ console.error('Error fetching conflicting proofs:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts b/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts
new file mode 100644
index 0000000000..4d17a56711
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/invalidRelaysController.ts
@@ -0,0 +1,52 @@
+import { Request, Response } from 'express'
+
+import { db } from '../db'
+
+export const recentInvalidRelaysController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .selectFrom('BridgeRelayedEvents')
+ .leftJoin(
+ 'BridgeRequestEvents',
+ 'BridgeRelayedEvents.transactionId',
+ 'BridgeRequestEvents.transactionId'
+ )
+ .select([
+ 'BridgeRelayedEvents.transactionId',
+ 'BridgeRelayedEvents.blockNumber',
+ 'BridgeRelayedEvents.blockTimestamp',
+ 'BridgeRelayedEvents.transactionHash',
+ 'BridgeRelayedEvents.originChain',
+ 'BridgeRelayedEvents.destChain',
+ 'BridgeRelayedEvents.originChainId',
+ 'BridgeRelayedEvents.destChainId',
+ 'BridgeRelayedEvents.originToken',
+ 'BridgeRelayedEvents.destToken',
+ 'BridgeRelayedEvents.originAmountFormatted',
+ 'BridgeRelayedEvents.destAmountFormatted',
+ 'BridgeRelayedEvents.to',
+ 'BridgeRelayedEvents.relayer',
+ ])
+ // lookback approx 2 weeks
+ .where(
+ 'BridgeRelayedEvents.blockTimestamp',
+ '>',
+ Math.floor(Date.now() / 1000) - 2 * 7 * 24 * 60 * 60
+ )
+ .where('BridgeRequestEvents.transactionId', 'is', null)
+
+ const results = await query.execute()
+
+ if (results && results.length > 0) {
+ res.json(results)
+ } else {
+ res.status(200).json({ message: 'No recent invalid relays found' })
+ }
+ } catch (error) {
+ console.error('Error fetching recent invalid relays:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts
new file mode 100644
index 0000000000..dfef98c11e
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/pendingTransactionsController.ts
@@ -0,0 +1,129 @@
+import { Request, Response } from 'express'
+
+import { db } from '../db'
+import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const pendingTransactionsMissingClaimController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('claims', () => qClaims())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .innerJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ .where('transactionId_claim', 'is', null)
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_proof', 'desc')
+
+ const results = await query.execute()
+ const nestedResults = nest_results(results)
+
+ if (nestedResults && nestedResults.length > 0) {
+ res.json(nestedResults)
+ } else {
+ res
+ .status(404)
+ .json({ message: 'No pending transactions missing claim found' })
+ }
+ } catch (error) {
+ console.error('Error fetching pending transactions missing claim:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
+
+
+export const pendingTransactionsMissingProofController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .where('transactionId_proof', 'is', null)
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_relay', 'desc')
+
+ const results = await query.execute()
+ const nestedResults = nest_results(results)
+
+ if (nestedResults && nestedResults.length > 0) {
+ res.json(nestedResults)
+ } else {
+ res
+ .status(404)
+ .json({ message: 'No pending transactions missing proof found' })
+ }
+ } catch (error) {
+ console.error('Error fetching pending transactions missing proof:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
+
+export const pendingTransactionsMissingRelayController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('refunds', () => qRefunds())
+ .with(
+ 'combined',
+ (qb) =>
+ qb
+ .selectFrom('deposits')
+ .selectAll('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin(
+ 'refunds',
+ 'transactionId_deposit',
+ 'transactionId_refund'
+ )
+ .where('transactionId_relay', 'is', null) // is not relayed
+ .where('transactionId_refund', 'is', null) // is not refunded
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_deposit', 'desc')
+
+ const results = await query.execute()
+ const nestedResults = nest_results(results)
+
+ if (nestedResults && nestedResults.length > 0) {
+ res.json(nestedResults)
+ } else {
+ res
+ .status(404)
+ .json({ message: 'No pending transactions missing relay found' })
+ }
+ } catch (error) {
+ console.error('Error fetching pending transactions missing relay:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts b/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts
new file mode 100644
index 0000000000..dd9fb2f0af
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/refundedAndRelayedController.ts
@@ -0,0 +1,43 @@
+import { Request, Response } from 'express'
+
+import { db } from '../db'
+import { qDeposits, qRelays, qRefunds } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const refundedAndRelayedTransactionsController = async (
+ req: Request,
+ res: Response
+) => {
+ try {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('refunds', () => qRefunds())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .innerJoin('refunds', 'transactionId_deposit', 'transactionId_refund')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('refunds')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_refund', 'desc')
+
+ const results = await query.execute()
+ const nestedResults = nest_results(results)
+
+ if (nestedResults && nestedResults.length > 0) {
+ res.json(nestedResults)
+ } else {
+ res
+ .status(200)
+ .json({ message: 'No refunded and relayed transactions found' })
+ }
+ } catch (error) {
+ console.error('Error fetching refunded and relayed transactions:', error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/controllers/transactionIdController.ts b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts
new file mode 100644
index 0000000000..73857496fa
--- /dev/null
+++ b/packages/rfq-indexer/api/src/controllers/transactionIdController.ts
@@ -0,0 +1,58 @@
+import { Request, Response } from 'express'
+
+import { db } from '../db'
+import { qDeposits, qRelays, qProofs, qClaims, qRefunds } from '../queries'
+import { nest_results } from '../utils/nestResults'
+
+export const getTransactionById = async (req: Request, res: Response) => {
+ const { transactionId } = req.params
+
+ try {
+ const query = db
+ .with('deposits', () =>
+ qDeposits().where('transactionId', '=', transactionId as string)
+ )
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('claims', () => qClaims())
+ .with('refunds', () => qRefunds())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim')
+ .leftJoin('refunds', 'transactionId_deposit', 'transactionId_refund')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ .selectAll('claims')
+ .selectAll('refunds')
+ )
+ .selectFrom('combined')
+ .selectAll()
+
+ const results = await query.execute()
+ const nestedResult = nest_results(results)[0] || null
+
+ if (nestedResult) {
+ const filteredResult = Object.fromEntries(
+ Object.entries(nestedResult).filter(([_, value]) => {
+ if (value === null) {
+ return false
+ }
+ if (typeof value !== 'object') {
+ return true
+ }
+ return Object.values(value).some((v) => v !== null)
+ })
+ )
+ res.json(filteredResult)
+ } else {
+ res.status(200).json({ message: 'Transaction not found' })
+ }
+ } catch (error) {
+ console.error(error)
+ res.status(500).json({ message: 'Internal server error' })
+ }
+}
diff --git a/packages/rfq-indexer/api/src/db/index.ts b/packages/rfq-indexer/api/src/db/index.ts
new file mode 100644
index 0000000000..6e9826ad35
--- /dev/null
+++ b/packages/rfq-indexer/api/src/db/index.ts
@@ -0,0 +1,26 @@
+import { Kysely, PostgresDialect } from 'kysely'
+import { Pool } from 'pg'
+
+import type {
+ BridgeRequestEvents,
+ BridgeRelayedEvents,
+ BridgeProofProvidedEvents,
+ BridgeDepositRefundedEvents,
+ BridgeDepositClaimedEvents,
+} from '../types'
+
+const { DATABASE_URL } = process.env
+
+const pool = new Pool({ connectionString: DATABASE_URL })
+
+const dialect = new PostgresDialect({ pool })
+
+export interface Database {
+ BridgeRequestEvents: BridgeRequestEvents
+ BridgeRelayedEvents: BridgeRelayedEvents
+ BridgeProofProvidedEvents: BridgeProofProvidedEvents
+ BridgeDepositRefundedEvents: BridgeDepositRefundedEvents
+ BridgeDepositClaimedEvents: BridgeDepositClaimedEvents
+}
+
+export const db = new Kysely({ dialect })
diff --git a/packages/rfq-indexer/api/src/graphql/queries/queries.graphql b/packages/rfq-indexer/api/src/graphql/queries/queries.graphql
new file mode 100644
index 0000000000..89398bb2ab
--- /dev/null
+++ b/packages/rfq-indexer/api/src/graphql/queries/queries.graphql
@@ -0,0 +1,125 @@
+type Transaction {
+ transactionId: String!
+ originChain: String!
+ destChain: String!
+ originChainId: Int!
+ destChainId: Int!
+ originToken: String!
+ destToken: String!
+ originAmountFormatted: String!
+ destAmountFormatted: String!
+ sender: String!
+ sendChainGas: String!
+}
+
+type Deposit {
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+}
+
+type Relay {
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+
+ relayer: String!
+ to: String!
+}
+
+type Proof {
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+
+ relayer: String!
+}
+
+type Claim {
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+
+ relayer: String!
+ to: String!
+ amountFormatted: String!
+}
+
+type Refund {
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+
+ to: String!
+ amountFormatted: String!
+}
+
+type PendingTransactionMissingRelay {
+ Bridge: Transaction!
+ BridgeRequest: Deposit!
+}
+
+type PendingTransactionMissingProof {
+ Bridge: Transaction!
+ BridgeRequest: Deposit!
+ BridgeRelay: Relay!
+}
+
+type PendingTransactionMissingClaim {
+ Bridge: Transaction!
+ BridgeRequest: Deposit!
+ BridgeRelay: Relay!
+ BridgeProof: Proof!
+}
+
+type InvalidRelay {
+ transactionId: String!
+ blockNumber: Int!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChain: String!
+ destChain: String!
+ originChainId: Int!
+ destChainId: Int!
+ originToken: String!
+ destToken: String!
+ originAmountFormatted: String!
+ destAmountFormatted: String!
+ to: String!
+ relayer: String!
+}
+
+type CompleteTransaction {
+ Bridge: Transaction!
+ BridgeRequest: Deposit
+ BridgeRelay: Relay
+ BridgeProof: Proof
+ BridgeClaim: Claim
+ BridgeRefund: Refund
+}
+
+type RefundedAndRelayedTransaction {
+ Bridge: Transaction!
+ BridgeRequest: Deposit!
+ BridgeRelay: Relay!
+ BridgeRefund: Refund!
+}
+
+type ConflictingProof {
+ Bridge: Transaction!
+ BridgeRequest: Deposit!
+ BridgeRelay: Relay!
+ BridgeProof: Proof!
+}
+
+type Query {
+ pendingTransactionsMissingRelay: [PendingTransactionMissingRelay!]!
+ pendingTransactionsMissingProof: [PendingTransactionMissingProof!]!
+ pendingTransactionsMissingClaim: [PendingTransactionMissingClaim!]!
+ transactionById(transactionId: String!): [CompleteTransaction!]!
+ recentInvalidRelays: [InvalidRelay!]!
+ refundedAndRelayedTransactions: [RefundedAndRelayedTransaction!]!
+ conflictingProofs: [ConflictingProof!]!
+}
+
+
diff --git a/packages/rfq-indexer/api/src/graphql/resolvers.ts b/packages/rfq-indexer/api/src/graphql/resolvers.ts
new file mode 100644
index 0000000000..3ef04819ea
--- /dev/null
+++ b/packages/rfq-indexer/api/src/graphql/resolvers.ts
@@ -0,0 +1,495 @@
+import { sql } from 'kysely'
+
+import { db } from '../db'
+
+// typical fields to return for a BridgeRequest
+const qDeposits = () => {
+ return db
+ .selectFrom('BridgeRequestEvents')
+ .select([
+ 'BridgeRequestEvents.transactionId as transactionId_deposit',
+ 'BridgeRequestEvents.blockNumber as blockNumber_deposit',
+ 'BridgeRequestEvents.blockTimestamp as blockTimestamp_deposit',
+ 'BridgeRequestEvents.transactionHash as transactionHash_deposit',
+ 'BridgeRequestEvents.originChain',
+ 'BridgeRequestEvents.destChain',
+ 'BridgeRequestEvents.originChainId',
+ 'BridgeRequestEvents.destChainId',
+ 'BridgeRequestEvents.originToken',
+ 'BridgeRequestEvents.destToken',
+ 'BridgeRequestEvents.originAmountFormatted',
+ 'BridgeRequestEvents.destAmountFormatted',
+ 'BridgeRequestEvents.sender',
+ 'BridgeRequestEvents.sendChainGas',
+ ])
+ .where('BridgeRequestEvents.blockTimestamp', '>', 1722729600)
+ // if index is partially loaded, we must limit lookback or will have various data issues from relays
+ // that happened to be in flight at the point of the index's start.
+ // may also improve query performance
+}
+
+// typical fields to return for a BridgeRelayed event when it is joined to a BridgeRequest
+const qRelays = () => {
+ return db
+ .selectFrom('BridgeRelayedEvents')
+ .select([
+ 'BridgeRelayedEvents.transactionId as transactionId_relay',
+ 'BridgeRelayedEvents.blockNumber as blockNumber_relay',
+ 'BridgeRelayedEvents.blockTimestamp as blockTimestamp_relay',
+ 'BridgeRelayedEvents.transactionHash as transactionHash_relay',
+
+ 'BridgeRelayedEvents.relayer as relayer_relay',
+ 'BridgeRelayedEvents.to as to_relay',
+ ])
+}
+
+// typical fields to return for a BridgeProofProvided event when it is joined to a BridgeRequest
+const qProofs = () => {
+ return db
+ .selectFrom('BridgeProofProvidedEvents')
+ .select([
+ 'BridgeProofProvidedEvents.transactionId as transactionId_proof',
+ 'BridgeProofProvidedEvents.blockNumber as blockNumber_proof',
+ 'BridgeProofProvidedEvents.blockTimestamp as blockTimestamp_proof',
+ 'BridgeProofProvidedEvents.transactionHash as transactionHash_proof',
+
+ 'BridgeProofProvidedEvents.relayer as relayer_proof',
+ ])
+}
+
+// typical fields to return for a BridgeDepositClaimed event when it is joined to a BridgeRequest
+const qClaims = () => {
+ return db
+ .selectFrom('BridgeDepositClaimedEvents')
+ .select([
+ 'BridgeDepositClaimedEvents.transactionId as transactionId_claim',
+ 'BridgeDepositClaimedEvents.blockNumber as blockNumber_claim',
+ 'BridgeDepositClaimedEvents.blockTimestamp as blockTimestamp_claim',
+ 'BridgeDepositClaimedEvents.transactionHash as transactionHash_claim',
+
+ 'BridgeDepositClaimedEvents.to as to_claim',
+ 'BridgeDepositClaimedEvents.relayer as relayer_claim',
+ 'BridgeDepositClaimedEvents.amountFormatted as amountFormatted_claim',
+ ])
+}
+
+// typical fields to return for a BridgeDepositRefunded event when it is joined to a BridgeRequest
+const qRefunds = () => {
+ return db
+ .selectFrom('BridgeDepositRefundedEvents')
+ .select([
+ 'BridgeDepositRefundedEvents.transactionId as transactionId_refund',
+ 'BridgeDepositRefundedEvents.blockNumber as blockNumber_refund',
+ 'BridgeDepositRefundedEvents.blockTimestamp as blockTimestamp_refund',
+ 'BridgeDepositRefundedEvents.transactionHash as transactionHash_refund',
+
+ 'BridgeDepositRefundedEvents.to as to_refund',
+ 'BridgeDepositRefundedEvents.amountFormatted as amountFormatted_refund',
+ ])
+}
+
+// using the suffix of a field, move it into a nested sub-object. This is a cleaner final resultset
+// example: transactionHash_deposit:0xyz would get moved into BridgeRequest{transactionHash:0xyz}
+//
+// also note that transactionId_xxxx will just convert into a single "transactionId"
+const nest_results = (sqlResults: any[]) => {
+ return sqlResults.map((transaction: any) => {
+ const bridgeRequest: { [key: string]: any } = {}
+ const bridgeRelay: { [key: string]: any } = {}
+ const bridgeProof: { [key: string]: any } = {}
+ const bridgeClaim: { [key: string]: any } = {}
+ const bridgeRefund: { [key: string]: any } = {}
+ const bridgeDispute: { [key: string]: any } = {}
+ const transactionFields: { [key: string]: any } = {}
+
+ let transactionIdSet = false
+
+ for (const [key, value] of Object.entries(transaction)) {
+ if (key.startsWith('transactionId')) {
+ if (!transactionIdSet) {
+ transactionFields[key.replace(/_.+$/, '')] = value
+ transactionIdSet = true
+ }
+ // Ignore other transactionId fields
+ } else if (key.endsWith('_deposit')) {
+ bridgeRequest[key.replace('_deposit', '')] = value
+ } else if (key.endsWith('_relay')) {
+ bridgeRelay[key.replace('_relay', '')] = value
+ } else if (key.endsWith('_proof')) {
+ bridgeProof[key.replace('_proof', '')] = value
+ } else if (key.endsWith('_claim')) {
+ bridgeClaim[key.replace('_claim', '')] = value
+ } else if (key.endsWith('_refund')) {
+ bridgeRefund[key.replace('_refund', '')] = value
+ } else if (key.endsWith('_dispute')) {
+ bridgeDispute[key.replace('_dispute', '')] = value
+ } else {
+ transactionFields[key] = value
+ }
+ }
+
+ const result: { [key: string]: any } = { Bridge: transactionFields }
+ if (Object.keys(bridgeRequest).length) {
+ result.BridgeRequest = bridgeRequest
+ }
+ if (Object.keys(bridgeRelay).length) {
+ result.BridgeRelay = bridgeRelay
+ }
+ if (Object.keys(bridgeProof).length) {
+ result.BridgeProof = bridgeProof
+ }
+ if (Object.keys(bridgeClaim).length) {
+ result.BridgeClaim = bridgeClaim
+ }
+ if (Object.keys(bridgeRefund).length) {
+ result.BridgeRefund = bridgeRefund
+ }
+ if (Object.keys(bridgeDispute).length) {
+ result.BridgeDispute = bridgeDispute
+ }
+ return result
+ })
+}
+
+const resolvers = {
+ Query: {
+ events: async (
+ _: any,
+ { first = 10, after, filter }: { first?: any; after?: any; filter?: any }
+ ) => {
+ let query = db
+ .selectFrom('BridgeRequestEvents')
+ .select([
+ 'BridgeRequestEvents.id',
+ 'BridgeRequestEvents.transactionId',
+ 'BridgeRequestEvents.blockNumber',
+ 'BridgeRequestEvents.blockTimestamp',
+ 'BridgeRequestEvents.transactionHash',
+ 'BridgeRequestEvents.originChainId',
+ 'BridgeRequestEvents.originChain',
+ ])
+ .unionAll(
+ db
+ .selectFrom('BridgeRelayedEvents')
+ .select([
+ 'BridgeRelayedEvents.id',
+ 'BridgeRelayedEvents.transactionId',
+ 'BridgeRelayedEvents.blockNumber',
+ 'BridgeRelayedEvents.blockTimestamp',
+ 'BridgeRelayedEvents.transactionHash',
+ 'BridgeRelayedEvents.originChainId',
+ 'BridgeRelayedEvents.originChain',
+ ])
+ )
+ .unionAll(
+ db
+ .selectFrom('BridgeProofProvidedEvents')
+ .select([
+ 'BridgeProofProvidedEvents.id',
+ 'BridgeProofProvidedEvents.transactionId',
+ 'BridgeProofProvidedEvents.blockNumber',
+ 'BridgeProofProvidedEvents.blockTimestamp',
+ 'BridgeProofProvidedEvents.transactionHash',
+ 'BridgeProofProvidedEvents.originChainId',
+ 'BridgeProofProvidedEvents.originChain',
+ ])
+ )
+ .unionAll(
+ db
+ .selectFrom('BridgeDepositRefundedEvents')
+ .select([
+ 'BridgeDepositRefundedEvents.id',
+ 'BridgeDepositRefundedEvents.transactionId',
+ 'BridgeDepositRefundedEvents.blockNumber',
+ 'BridgeDepositRefundedEvents.blockTimestamp',
+ 'BridgeDepositRefundedEvents.transactionHash',
+ 'BridgeDepositRefundedEvents.originChainId',
+ 'BridgeDepositRefundedEvents.originChain',
+ ])
+ )
+ .unionAll(
+ db
+ .selectFrom('BridgeDepositClaimedEvents')
+ .select([
+ 'BridgeDepositClaimedEvents.id',
+ 'BridgeDepositClaimedEvents.transactionId',
+ 'BridgeDepositClaimedEvents.blockNumber',
+ 'BridgeDepositClaimedEvents.blockTimestamp',
+ 'BridgeDepositClaimedEvents.transactionHash',
+ 'BridgeDepositClaimedEvents.originChainId',
+ 'BridgeDepositClaimedEvents.originChain',
+ ])
+ )
+
+ if (filter) {
+ if (filter.transactionId) {
+ query = query.where('transactionId', '=', filter.transactionId)
+ }
+ if (filter.originChainId) {
+ query = query.where('originChainId', '=', filter.originChainId)
+ }
+ // Add more filters as needed
+ }
+
+ if (after) {
+ query = query.where('id', '>', after)
+ }
+
+ const events = await query
+ .orderBy('blockTimestamp', 'desc')
+ .limit(first + 1)
+ .execute()
+
+ const hasNextPage = events.length > first
+ const edges = events.slice(0, first).map((event: any) => ({
+ node: event,
+ cursor: event.id,
+ }))
+
+ return {
+ edges,
+ pageInfo: {
+ hasNextPage,
+ endCursor: edges.length > 0 ? edges[edges.length - 1]?.cursor : null,
+ },
+ }
+ },
+ pendingTransactionsMissingRelay: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('refunds', () => qRefunds())
+ .with(
+ 'combined',
+ (qb) =>
+ qb
+ .selectFrom('deposits')
+ .selectAll('deposits')
+ .leftJoin(
+ 'relays',
+ 'transactionId_deposit',
+ 'transactionId_relay'
+ )
+ .leftJoin(
+ 'refunds',
+ 'transactionId_deposit',
+ 'transactionId_refund'
+ )
+ .where('transactionId_relay', 'is', null) // is not relayed
+ .where('transactionId_refund', 'is', null) // is not refunded
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_deposit', 'desc')
+
+ return nest_results(await query.execute())
+ },
+ pendingTransactionsMissingProof: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .where('transactionId_proof', 'is', null)
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_relay', 'desc')
+
+ return nest_results(await query.execute())
+ },
+ pendingTransactionsMissingClaim: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('claims', () => qClaims())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .innerJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ .where('transactionId_claim', 'is', null)
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_proof', 'desc')
+
+ return nest_results(await query.execute())
+ },
+ recentInvalidRelays: async () => {
+ const query = db
+ .selectFrom('BridgeRelayedEvents')
+ .leftJoin(
+ 'BridgeRequestEvents',
+ 'BridgeRelayedEvents.transactionId',
+ 'BridgeRequestEvents.transactionId'
+ )
+ .select([
+ 'BridgeRelayedEvents.transactionId',
+ 'BridgeRelayedEvents.blockNumber',
+ 'BridgeRelayedEvents.blockTimestamp',
+ 'BridgeRelayedEvents.transactionHash',
+
+ 'BridgeRelayedEvents.originChain',
+ 'BridgeRelayedEvents.destChain',
+ 'BridgeRelayedEvents.originChainId',
+ 'BridgeRelayedEvents.destChainId',
+ 'BridgeRelayedEvents.originToken',
+ 'BridgeRelayedEvents.destToken',
+ 'BridgeRelayedEvents.originAmountFormatted',
+ 'BridgeRelayedEvents.destAmountFormatted',
+
+ 'BridgeRelayedEvents.to',
+ 'BridgeRelayedEvents.relayer',
+ ])
+ // lookback approx 2 weeks
+ .where(
+ 'BridgeRelayedEvents.blockTimestamp',
+ '>',
+ Math.floor(Date.now() / 1000) - 2 * 7 * 24 * 60 * 60
+ )
+ .where('BridgeRequestEvents.transactionId', 'is', null)
+
+ // intentionally do not nest - doesnt make sense w/ this dataset because the whole point is that no Deposit exists
+ return query.execute()
+ },
+ refundedAndRelayedTransactions: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('refunds', () => qRefunds())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .innerJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .innerJoin(
+ 'refunds',
+ 'transactionId_deposit',
+ 'transactionId_refund'
+ )
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('refunds')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .orderBy('blockTimestamp_refund', 'desc')
+
+ return nest_results(await query.execute())
+ },
+ transactionById: async (
+ _: any,
+ { transactionId }: { transactionId: string }
+ ) => {
+ const query = db
+ .with('deposits', () =>
+ qDeposits().where('transactionId', '=', transactionId)
+ )
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('claims', () => qClaims())
+ .with('refunds', () => qRefunds())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .leftJoin('claims', 'transactionId_deposit', 'transactionId_claim')
+ .leftJoin(
+ 'refunds',
+ 'transactionId_deposit',
+ 'transactionId_refund'
+ )
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ .selectAll('claims')
+ .selectAll('refunds')
+ )
+ .selectFrom('combined')
+ .selectAll()
+
+ const nestedResult = nest_results(await query.execute())[0] || null
+
+ if (nestedResult) {
+ return Object.fromEntries(
+ Object.entries(nestedResult).filter(([_, value]) => {
+ if (value === null) {
+ return false
+ }
+ if (typeof value !== 'object') {
+ return true
+ }
+ return Object.values(value).some((v) => v !== null)
+ })
+ )
+ }
+
+ return null
+ },
+ conflictingProofs: async () => {
+ const query = db
+ .with('deposits', () => qDeposits())
+ .with('relays', () => qRelays())
+ .with('proofs', () => qProofs())
+ .with('combined', (qb) =>
+ qb
+ .selectFrom('deposits')
+ .leftJoin('relays', 'transactionId_deposit', 'transactionId_relay')
+ .leftJoin('proofs', 'transactionId_deposit', 'transactionId_proof')
+ .selectAll('deposits')
+ .selectAll('relays')
+ .selectAll('proofs')
+ )
+ .selectFrom('combined')
+ .selectAll()
+ .where('relayer_proof', 'is not', null)
+ .where('relayer_relay', 'is not', null)
+ .where(
+ (eb) =>
+ sql`LOWER(${eb.ref('relayer_relay')}) != LOWER(${eb.ref(
+ 'relayer_proof'
+ )})`
+ )
+ .orderBy('blockTimestamp_proof', 'desc')
+
+ return nest_results(await query.execute())
+ },
+ },
+ BridgeEvent: {
+ // eslint-disable-next-line prefer-arrow/prefer-arrow-functions
+ __resolveType(obj: any) {
+ // Implement logic to determine the event type based on the object properties
+ // For example:
+ if ('sender' in obj) {
+ return 'BridgeRequestEvent'
+ }
+ if ('relayer' in obj && 'to' in obj) {
+ return 'BridgeRelayedEvent'
+ }
+ if ('relayer' in obj && !('to' in obj)) {
+ return 'BridgeProofProvidedEvent'
+ }
+ if ('to' in obj && 'token' in obj) {
+ return 'BridgeDepositRefundedEvent'
+ }
+ if ('relayer' in obj && 'to' in obj && 'token' in obj) {
+ return 'BridgeDepositClaimedEvent'
+ }
+ return null
+ },
+ },
+}
+
+export { resolvers }
diff --git a/packages/rfq-indexer/api/src/graphql/schema.ts b/packages/rfq-indexer/api/src/graphql/schema.ts
new file mode 100644
index 0000000000..b3eac13dbe
--- /dev/null
+++ b/packages/rfq-indexer/api/src/graphql/schema.ts
@@ -0,0 +1,28 @@
+import { makeExecutableSchema } from '@graphql-tools/schema'
+import { loadFilesSync } from '@graphql-tools/load-files'
+import { mergeTypeDefs } from '@graphql-tools/merge'
+
+import { resolvers } from './resolvers'
+
+const typesArray = loadFilesSync(`${__dirname}/types/**/*.graphql`)
+const queriesArray = loadFilesSync(`${__dirname}/queries/**/*.graphql`)
+
+// Define a union type for BridgeEvent
+const additionalTypes = `
+ union BridgeEvent = BridgeRequestEvent | BridgeRelayedEvent | BridgeProofProvidedEvent | BridgeDepositRefundedEvent | BridgeDepositClaimedEvent
+
+ type Query {
+ events: [BridgeEvent!]!
+ }
+`
+
+const typeDefs = mergeTypeDefs([
+ ...typesArray,
+ ...queriesArray,
+ additionalTypes,
+])
+
+export const schema = makeExecutableSchema({
+ typeDefs,
+ resolvers,
+})
diff --git a/packages/rfq-indexer/api/src/graphql/types/events.graphql b/packages/rfq-indexer/api/src/graphql/types/events.graphql
new file mode 100644
index 0000000000..61f0f4cb4b
--- /dev/null
+++ b/packages/rfq-indexer/api/src/graphql/types/events.graphql
@@ -0,0 +1,81 @@
+scalar BigInt
+
+ type BridgeRequestEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ sender: String!
+ originToken: String!
+ destToken: String!
+ originAmount: BigInt!
+ originAmountFormatted: String!
+ destAmount: BigInt!
+ destAmountFormatted: String!
+ destChainId: Int!
+ destChain: String!
+ sendChainGas: Boolean!
+ }
+
+ type BridgeRelayedEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ relayer: String!
+ to: String!
+ originToken: String!
+ destToken: String!
+ originAmount: BigInt!
+ originAmountFormatted: String!
+ destAmount: BigInt!
+ destAmountFormatted: String!
+ destChainId: Int!
+ destChain: String!
+ }
+
+ type BridgeProofProvidedEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ relayer: String!
+ }
+
+ type BridgeDepositRefundedEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ to: String!
+ token: String!
+ amount: BigInt!
+ amountFormatted: String!
+ }
+
+ type BridgeDepositClaimedEvent {
+ id: String!
+ transactionId: String!
+ blockNumber: BigInt!
+ blockTimestamp: Int!
+ transactionHash: String!
+ originChainId: Int!
+ originChain: String!
+ relayer: String!
+ to: String!
+ token: String!
+ amount: BigInt!
+ amountFormatted: String!
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/api/src/jest.config.js b/packages/rfq-indexer/api/src/jest.config.js
new file mode 100644
index 0000000000..ba447263ea
--- /dev/null
+++ b/packages/rfq-indexer/api/src/jest.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ roots: ['/src'],
+ transform: {
+ '^.+\\.(ts|tsx)$': 'babel-jest',
+ },
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
+ moduleDirectories: ['node_modules', ''],
+}
diff --git a/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts b/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts
new file mode 100644
index 0000000000..29ccda3e43
--- /dev/null
+++ b/packages/rfq-indexer/api/src/middleware/showFirstValidationError.ts
@@ -0,0 +1,24 @@
+import { Request, Response, NextFunction } from 'express'
+import { validationResult } from 'express-validator'
+
+export const showFirstValidationError = (
+ req: Request,
+ res: Response,
+ next: NextFunction
+): void => {
+ const errors = validationResult(req)
+ if (!errors.isEmpty()) {
+ const firstError = errors.array({ onlyFirstError: true })[0]
+
+ res.status(400).json({
+ error: {
+ value: (firstError as any).value,
+ message: firstError?.msg,
+ field: firstError?.type === 'field' ? firstError?.path : undefined,
+ location: (firstError as any).location,
+ },
+ })
+ return
+ }
+ next()
+}
diff --git a/packages/rfq-indexer/api/src/queries/claimsQueries.ts b/packages/rfq-indexer/api/src/queries/claimsQueries.ts
new file mode 100644
index 0000000000..8ce64a5e3e
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/claimsQueries.ts
@@ -0,0 +1,17 @@
+import { db } from '../db'
+
+// typical fields to return for a BridgeDepositClaimed event when it is joined to a BridgeRequest
+export const qClaims = () => {
+ return db
+ .selectFrom('BridgeDepositClaimedEvents')
+ .select([
+ 'BridgeDepositClaimedEvents.transactionId as transactionId_claim',
+ 'BridgeDepositClaimedEvents.blockNumber as blockNumber_claim',
+ 'BridgeDepositClaimedEvents.blockTimestamp as blockTimestamp_claim',
+ 'BridgeDepositClaimedEvents.transactionHash as transactionHash_claim',
+
+ 'BridgeDepositClaimedEvents.to as to_claim',
+ 'BridgeDepositClaimedEvents.relayer as relayer_claim',
+ 'BridgeDepositClaimedEvents.amountFormatted as amountFormatted_claim',
+ ])
+}
diff --git a/packages/rfq-indexer/api/src/queries/depositsQueries.ts b/packages/rfq-indexer/api/src/queries/depositsQueries.ts
new file mode 100644
index 0000000000..61e33aa3d1
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/depositsQueries.ts
@@ -0,0 +1,26 @@
+import { db } from '../db'
+
+export const qDeposits = () => {
+ return db
+ .selectFrom('BridgeRequestEvents')
+ .select([
+ 'BridgeRequestEvents.transactionId as transactionId_deposit',
+ 'BridgeRequestEvents.blockNumber as blockNumber_deposit',
+ 'BridgeRequestEvents.blockTimestamp as blockTimestamp_deposit',
+ 'BridgeRequestEvents.transactionHash as transactionHash_deposit',
+ 'BridgeRequestEvents.originChain',
+ 'BridgeRequestEvents.destChain',
+ 'BridgeRequestEvents.originChainId',
+ 'BridgeRequestEvents.destChainId',
+ 'BridgeRequestEvents.originToken',
+ 'BridgeRequestEvents.destToken',
+ 'BridgeRequestEvents.originAmountFormatted',
+ 'BridgeRequestEvents.destAmountFormatted',
+ 'BridgeRequestEvents.sender',
+ 'BridgeRequestEvents.sendChainGas',
+ ])
+ .where('BridgeRequestEvents.blockTimestamp', '>', 1722729600)
+ // if index is partially loaded, we must limit lookback or will have various data issues from relays
+ // that happened to be in flight at the point of the index's start.
+ // may also improve query performance
+}
diff --git a/packages/rfq-indexer/api/src/queries/index.ts b/packages/rfq-indexer/api/src/queries/index.ts
new file mode 100644
index 0000000000..72bad2522e
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/index.ts
@@ -0,0 +1,5 @@
+export { qClaims } from './claimsQueries'
+export { qDeposits } from './depositsQueries'
+export { qProofs } from './proofsQueries'
+export { qRefunds } from './refundsQueries'
+export { qRelays } from './relaysQueries'
diff --git a/packages/rfq-indexer/api/src/queries/proofsQueries.ts b/packages/rfq-indexer/api/src/queries/proofsQueries.ts
new file mode 100644
index 0000000000..e7a1ccc012
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/proofsQueries.ts
@@ -0,0 +1,15 @@
+import { db } from '../db'
+
+// typical fields to return for a BridgeProofProvided event when it is joined to a BridgeRequest
+export const qProofs = () => {
+ return db
+ .selectFrom('BridgeProofProvidedEvents')
+ .select([
+ 'BridgeProofProvidedEvents.transactionId as transactionId_proof',
+ 'BridgeProofProvidedEvents.blockNumber as blockNumber_proof',
+ 'BridgeProofProvidedEvents.blockTimestamp as blockTimestamp_proof',
+ 'BridgeProofProvidedEvents.transactionHash as transactionHash_proof',
+
+ 'BridgeProofProvidedEvents.relayer as relayer_proof',
+ ])
+}
diff --git a/packages/rfq-indexer/api/src/queries/refundsQueries.ts b/packages/rfq-indexer/api/src/queries/refundsQueries.ts
new file mode 100644
index 0000000000..9de6f72773
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/refundsQueries.ts
@@ -0,0 +1,15 @@
+import { db } from '../db'
+
+export const qRefunds = () => {
+ return db
+ .selectFrom('BridgeDepositRefundedEvents')
+ .select([
+ 'BridgeDepositRefundedEvents.transactionId as transactionId_refund',
+ 'BridgeDepositRefundedEvents.blockNumber as blockNumber_refund',
+ 'BridgeDepositRefundedEvents.blockTimestamp as blockTimestamp_refund',
+ 'BridgeDepositRefundedEvents.transactionHash as transactionHash_refund',
+
+ 'BridgeDepositRefundedEvents.to as to_refund',
+ 'BridgeDepositRefundedEvents.amountFormatted as amountFormatted_refund',
+ ])
+}
diff --git a/packages/rfq-indexer/api/src/queries/relaysQueries.ts b/packages/rfq-indexer/api/src/queries/relaysQueries.ts
new file mode 100644
index 0000000000..bd7b3990a0
--- /dev/null
+++ b/packages/rfq-indexer/api/src/queries/relaysQueries.ts
@@ -0,0 +1,16 @@
+import { db } from '../db'
+
+// typical fields to return for a BridgeRelayed event when it is joined to a BridgeRequest
+export const qRelays = () => {
+ return db
+ .selectFrom('BridgeRelayedEvents')
+ .select([
+ 'BridgeRelayedEvents.transactionId as transactionId_relay',
+ 'BridgeRelayedEvents.blockNumber as blockNumber_relay',
+ 'BridgeRelayedEvents.blockTimestamp as blockTimestamp_relay',
+ 'BridgeRelayedEvents.transactionHash as transactionHash_relay',
+
+ 'BridgeRelayedEvents.relayer as relayer_relay',
+ 'BridgeRelayedEvents.to as to_relay',
+ ])
+}
diff --git a/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts b/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts
new file mode 100644
index 0000000000..aa48c89e07
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/conflictingProofsRoute.ts
@@ -0,0 +1,65 @@
+import express from 'express'
+
+import { conflictingProofsController } from '../controllers/conflictingProofsController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /conflicting-proofs:
+ * get:
+ * summary: Get conflicting proofs
+ * description: Retrieves a list of transactions where the relayer in the proof differs from the relayer in the relay event
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * Bridge:
+ * type: object
+ * description: General transaction fields
+ * BridgeRequest:
+ * type: object
+ * description: Deposit information
+ * BridgeRelay:
+ * type: object
+ * description: Relay information
+ * BridgeRefund:
+ * type: object
+ * description: Refund information
+ * BridgeProof:
+ * type: object
+ * description: Proof information (if available)
+ * BridgeClaim:
+ * type: object
+ * description: Claim information (if available)
+ * BridgeDispute:
+ * type: object
+ * description: Dispute information (if available)
+ * 404:
+ * description: No conflicting proofs found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/', conflictingProofsController)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/index.ts b/packages/rfq-indexer/api/src/routes/index.ts
new file mode 100644
index 0000000000..302f878781
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/index.ts
@@ -0,0 +1,17 @@
+import express from 'express'
+
+import pendingTransactionsRoute from './pendingTransactionsRoute'
+import refundedAndRelayedRoute from './refundedAndRelayedRoute'
+import invalidRelaysRoute from './invalidRelaysRoute'
+import conflictingProofsRoute from './conflictingProofsRoute'
+import transactionIdRoute from './transactionIdRoute'
+
+const router = express.Router()
+
+router.use('/pending-transactions', pendingTransactionsRoute)
+router.use('/refunded-and-relayed', refundedAndRelayedRoute)
+router.use('/invalid-relays', invalidRelaysRoute)
+router.use('/conflicting-proofs', conflictingProofsRoute)
+router.use('/transaction-id', transactionIdRoute)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts b/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts
new file mode 100644
index 0000000000..31356156f2
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/invalidRelaysRoute.ts
@@ -0,0 +1,65 @@
+import express from 'express'
+
+import { recentInvalidRelaysController } from '../controllers/invalidRelaysController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /invalid-relays:
+ * get:
+ * summary: Get recent invalid relays
+ * description: Retrieves a list of recent invalid relay events from the past 2 weeks
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * Bridge:
+ * type: object
+ * description: General transaction fields
+ * BridgeRequest:
+ * type: object
+ * description: Deposit information
+ * BridgeRelay:
+ * type: object
+ * description: Relay information
+ * BridgeRefund:
+ * type: object
+ * description: Refund information
+ * BridgeProof:
+ * type: object
+ * description: Proof information (if available)
+ * BridgeClaim:
+ * type: object
+ * description: Claim information (if available)
+ * BridgeDispute:
+ * type: object
+ * description: Dispute information (if available)
+ * 404:
+ * description: No recent invalid relays found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/', recentInvalidRelaysController)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts
new file mode 100644
index 0000000000..2dbeafb121
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/pendingTransactionsRoute.ts
@@ -0,0 +1,149 @@
+import express from 'express'
+
+import {
+ pendingTransactionsMissingClaimController,
+ pendingTransactionsMissingProofController,
+ pendingTransactionsMissingRelayController
+} from '../controllers/pendingTransactionsController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /pending-transactions/missing-claim:
+ * get:
+ * summary: Get pending transactions missing claim
+ * description: Retrieves a list of transactions that have been deposited, relayed, and proven, but not yet claimed
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * Bridge:
+ * type: object
+ * description: General transaction fields
+ * BridgeRequest:
+ * type: object
+ * description: Deposit information
+ * BridgeRelay:
+ * type: object
+ * description: Relay information
+ * BridgeRefund:
+ * type: object
+ * description: Refund information
+ * BridgeProof:
+ * type: object
+ * description: Proof information (if available)
+ * BridgeClaim:
+ * type: object
+ * description: Claim information (if available)
+ * BridgeDispute:
+ * type: object
+ * description: Dispute information (if available)
+ * 404:
+ * description: No pending transactions missing claim found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/missing-claim', pendingTransactionsMissingClaimController)
+
+/**
+ * @openapi
+ * /pending-transactions/missing-proof:
+ * get:
+ * summary: Get pending transactions missing proof
+ * description: Retrieves a list of transactions that have been deposited and relayed, but not yet proven
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * deposit:
+ * type: object
+ * relay:
+ * type: object
+ * 404:
+ * description: No pending transactions missing proof found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/missing-proof', pendingTransactionsMissingProofController)
+
+/**
+ * @openapi
+ * /pending-transactions/missing-relay:
+ * get:
+ * summary: Get pending transactions missing relay
+ * description: Retrieves a list of transactions that have been deposited, but not yet relayed or refunded
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * deposit:
+ * type: object
+ * 404:
+ * description: No pending transactions missing relay found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/missing-relay', pendingTransactionsMissingRelayController)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts b/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts
new file mode 100644
index 0000000000..cd38b3b33e
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/refundedAndRelayedRoute.ts
@@ -0,0 +1,65 @@
+import express from 'express'
+
+import { refundedAndRelayedTransactionsController } from '../controllers/refundedAndRelayedController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /refunded-and-relayed:
+ * get:
+ * summary: Get refunded and relayed transactions
+ * description: Retrieves a list of transactions that have been both refunded and relayed
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: array
+ * items:
+ * type: object
+ * properties:
+ * Bridge:
+ * type: object
+ * description: General transaction fields
+ * BridgeRequest:
+ * type: object
+ * description: Deposit information
+ * BridgeRelay:
+ * type: object
+ * description: Relay information
+ * BridgeRefund:
+ * type: object
+ * description: Refund information
+ * BridgeProof:
+ * type: object
+ * description: Proof information (if available)
+ * BridgeClaim:
+ * type: object
+ * description: Claim information (if available)
+ * BridgeDispute:
+ * type: object
+ * description: Dispute information (if available)
+ * 404:
+ * description: No refunded and relayed transactions found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/', refundedAndRelayedTransactionsController)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts
new file mode 100644
index 0000000000..ef0b4077a4
--- /dev/null
+++ b/packages/rfq-indexer/api/src/routes/transactionIdRoute.ts
@@ -0,0 +1,70 @@
+import express from 'express'
+
+import { getTransactionById } from '../controllers/transactionIdController'
+
+const router = express.Router()
+
+/**
+ * @openapi
+ * /transaction-id/{transactionId}:
+ * get:
+ * summary: Get transaction details by ID
+ * description: Retrieves detailed information about a transaction, including deposit, relay, proof, claim, and refund data if available
+ * parameters:
+ * - in: path
+ * name: transactionId
+ * required: true
+ * schema:
+ * type: string
+ * description: The unique identifier of the transaction
+ * responses:
+ * 200:
+ * description: Successful response
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+* Bridge:
+* type: object
+* description: General transaction fields
+* BridgeRequest:
+* type: object
+* description: Deposit information
+* BridgeRelay:
+* type: object
+* description: Relay information
+* BridgeRefund:
+* type: object
+* description: Refund information
+* BridgeProof:
+* type: object
+* description: Proof information (if available)
+* BridgeClaim:
+* type: object
+* description: Claim information (if available)
+* BridgeDispute:
+* type: object
+* description: Dispute information (if available)
+ * 404:
+ * description: Transaction not found
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ * 500:
+ * description: Server error
+ * content:
+ * application/json:
+ * schema:
+ * type: object
+ * properties:
+ * message:
+ * type: string
+ */
+router.get('/:transactionId', getTransactionById)
+
+export default router
diff --git a/packages/rfq-indexer/api/src/swagger.ts b/packages/rfq-indexer/api/src/swagger.ts
new file mode 100644
index 0000000000..aec6d0fb43
--- /dev/null
+++ b/packages/rfq-indexer/api/src/swagger.ts
@@ -0,0 +1,28 @@
+import swaggerJsdoc from 'swagger-jsdoc'
+
+const isDevelopment = process.env.NODE_ENV === 'development'
+
+const devServer = {
+ url: 'http://localhost:3001/api',
+ description: 'Local Development Server',
+}
+
+const prodServer = {
+ url: 'https://triumphant-magic-production.up.railway.app/api',
+ description: 'Production Server',
+}
+
+const options: swaggerJsdoc.Options = {
+ definition: {
+ openapi: '3.0.0',
+ info: {
+ title: 'RFQ Indexer API',
+ version: '1.0.00',
+ description: 'API documentation for the RFQ Indexer API',
+ },
+ servers: isDevelopment ? [devServer, prodServer] : [prodServer, devServer],
+ },
+ apis: ['./src/routes/*.ts', './src/*.ts'],
+}
+
+export const specs = swaggerJsdoc(options)
diff --git a/packages/rfq-indexer/api/src/tsconfig.json b/packages/rfq-indexer/api/src/tsconfig.json
new file mode 100644
index 0000000000..9ad1c6e784
--- /dev/null
+++ b/packages/rfq-indexer/api/src/tsconfig.json
@@ -0,0 +1,32 @@
+{
+ "compilerOptions": {
+ // Type checking
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+
+ // Interop constraints
+ "verbatimModuleSyntax": false,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+
+ // Language and environment
+ "moduleResolution": "bundler",
+ "module": "ESNext",
+ "noEmit": true,
+ "lib": ["ES2022"],
+ "target": "ES2022",
+
+ // Skip type checking for node modules
+ "skipLibCheck": true,
+
+ // Paths
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"]
+ }
+ },
+ "include": ["./**/*.ts"],
+ "exclude": ["node_modules"]
+}
\ No newline at end of file
diff --git a/packages/rfq-indexer/api/src/types/index.ts b/packages/rfq-indexer/api/src/types/index.ts
new file mode 100644
index 0000000000..a536bead99
--- /dev/null
+++ b/packages/rfq-indexer/api/src/types/index.ts
@@ -0,0 +1,97 @@
+import { ColumnType } from 'kysely'
+
+// Define types for each table in the database
+export interface BridgeRequestEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ originChainId: ColumnType
+ originChain: ColumnType
+ sender: ColumnType
+ originToken: ColumnType
+ destToken: ColumnType
+ originAmount: ColumnType
+ originAmountFormatted: ColumnType
+ destAmount: ColumnType
+ destAmountFormatted: ColumnType
+ destChainId: ColumnType
+ destChain: ColumnType
+ sendChainGas: ColumnType
+}
+
+export interface BridgeRelayedEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ originChainId: ColumnType
+ originChain: ColumnType
+ relayer: ColumnType
+ to: ColumnType
+ originToken: ColumnType
+ destToken: ColumnType
+ originAmount: ColumnType
+ originAmountFormatted: ColumnType
+ destAmount: ColumnType
+ destAmountFormatted: ColumnType
+ destChainId: ColumnType
+ destChain: ColumnType
+}
+
+export interface BridgeProofProvidedEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ originChainId: ColumnType
+ originChain: ColumnType
+ relayer: ColumnType
+}
+
+export interface BridgeDepositRefundedEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ originChainId: ColumnType
+ originChain: ColumnType
+ to: ColumnType
+ token: ColumnType
+ amount: ColumnType
+ amountFormatted: ColumnType
+}
+
+export interface BridgeDepositClaimedEvents {
+ id: ColumnType
+ transactionId: ColumnType
+ blockNumber: ColumnType
+ blockTimestamp: ColumnType
+ transactionHash: ColumnType
+ originChainId: ColumnType
+ originChain: ColumnType
+ relayer: ColumnType
+ to: ColumnType
+ token: ColumnType
+ amount: ColumnType
+ amountFormatted: ColumnType
+}
+
+// Add any other shared types used across the API
+export type EventType =
+ | 'REQUEST'
+ | 'RELAYED'
+ | 'PROOF_PROVIDED'
+ | 'DEPOSIT_REFUNDED'
+ | 'DEPOSIT_CLAIMED'
+
+export interface EventFilter {
+ type?: EventType
+ transactionId?: string
+ originChainId?: number
+ destChainId?: number
+}
diff --git a/packages/rfq-indexer/api/src/utils/isTransaction.ts b/packages/rfq-indexer/api/src/utils/isTransaction.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/rfq-indexer/api/src/utils/nestResults.ts b/packages/rfq-indexer/api/src/utils/nestResults.ts
new file mode 100644
index 0000000000..6d31050362
--- /dev/null
+++ b/packages/rfq-indexer/api/src/utils/nestResults.ts
@@ -0,0 +1,58 @@
+export const nest_results = (sqlResults: any[]) => {
+ return sqlResults.map((transaction: any) => {
+ const bridgeRequest: { [key: string]: any } = {}
+ const bridgeRelay: { [key: string]: any } = {}
+ const bridgeProof: { [key: string]: any } = {}
+ const bridgeClaim: { [key: string]: any } = {}
+ const bridgeRefund: { [key: string]: any } = {}
+ const bridgeDispute: { [key: string]: any } = {}
+ const transactionFields: { [key: string]: any } = {}
+
+ let transactionIdSet = false
+
+ for (const [key, value] of Object.entries(transaction)) {
+ if (key.startsWith('transactionId')) {
+ if (!transactionIdSet) {
+ transactionFields[key.replace(/_.+$/, '')] = value
+ transactionIdSet = true
+ }
+ // Ignore other transactionId fields
+ } else if (key.endsWith('_deposit')) {
+ bridgeRequest[key.replace('_deposit', '')] = value
+ } else if (key.endsWith('_relay')) {
+ bridgeRelay[key.replace('_relay', '')] = value
+ } else if (key.endsWith('_proof')) {
+ bridgeProof[key.replace('_proof', '')] = value
+ } else if (key.endsWith('_claim')) {
+ bridgeClaim[key.replace('_claim', '')] = value
+ } else if (key.endsWith('_refund')) {
+ bridgeRefund[key.replace('_refund', '')] = value
+ } else if (key.endsWith('_dispute')) {
+ bridgeDispute[key.replace('_dispute', '')] = value
+ } else {
+ transactionFields[key] = value
+ }
+ }
+
+ const result: { [key: string]: any } = { Bridge: transactionFields }
+ if (Object.keys(bridgeRequest).length) {
+ result.BridgeRequest = bridgeRequest
+ }
+ if (Object.keys(bridgeRelay).length) {
+ result.BridgeRelay = bridgeRelay
+ }
+ if (Object.keys(bridgeProof).length) {
+ result.BridgeProof = bridgeProof
+ }
+ if (Object.keys(bridgeClaim).length) {
+ result.BridgeClaim = bridgeClaim
+ }
+ if (Object.keys(bridgeRefund).length) {
+ result.BridgeRefund = bridgeRefund
+ }
+ if (Object.keys(bridgeDispute).length) {
+ result.BridgeDispute = bridgeDispute
+ }
+ return result
+ })
+}
diff --git a/packages/rfq-indexer/api/src/utils/overrideJsonBigIntSerialization.ts b/packages/rfq-indexer/api/src/utils/overrideJsonBigIntSerialization.ts
new file mode 100644
index 0000000000..c6c4525ea9
--- /dev/null
+++ b/packages/rfq-indexer/api/src/utils/overrideJsonBigIntSerialization.ts
@@ -0,0 +1,32 @@
+export const overrideJsonBigIntSerialization = () => {
+ const originalJSONStringify = JSON.stringify
+
+ JSON.stringify = function (value: any, replacer, space: number): string {
+ const bigIntReplacer = (_key: string, value: any): any => {
+ if (typeof value === 'bigint') {
+ return parseInt(value.toString())
+ }
+ return value
+ }
+
+ const customReplacer = (key: string, value: any): any => {
+ if (Array.isArray(replacer) && !replacer.includes(key) && key !== '') {
+ return undefined
+ }
+
+ const modifiedValue = bigIntReplacer(key, value)
+
+ if (typeof replacer === 'function') {
+ return replacer(key, modifiedValue)
+ }
+
+ return modifiedValue
+ }
+
+ return originalJSONStringify(
+ value,
+ replacer != null ? customReplacer : bigIntReplacer,
+ space
+ )
+ }
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/.env.example b/packages/rfq-indexer/indexer/.env.example
new file mode 100644
index 0000000000..33ca569c2c
--- /dev/null
+++ b/packages/rfq-indexer/indexer/.env.example
@@ -0,0 +1,14 @@
+# RPC URLs used for fetching blockchain data. Alchemy is recommended.
+
+ETH_MAINNET_RPC=
+OPTIMISM_MAINNET_RPC=
+ARBITRUM_MAINNET_RPC=
+BASE_MAINNET_RPC=
+BLAST_MAINNET_RPC=
+SCROLL_MAINNET_RPC=
+LINEA_MAINNET_RPC=
+BNB_MAINNET_RPC=
+
+# (Optional) Postgres database URL. If not provided, SQLite will be used.
+DATABASE_URL=
+
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/.eslintrc.json b/packages/rfq-indexer/indexer/.eslintrc.json
new file mode 100644
index 0000000000..7a5d4a11fc
--- /dev/null
+++ b/packages/rfq-indexer/indexer/.eslintrc.json
@@ -0,0 +1,7 @@
+{
+ "extends": "ponder",
+ "ignorePatterns": ["ponder-env.d.ts"],
+ "rules": {
+ "@typescript-eslint/no-unused-vars": "off"
+ }
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/.gitignore b/packages/rfq-indexer/indexer/.gitignore
new file mode 100644
index 0000000000..cb25d48dfc
--- /dev/null
+++ b/packages/rfq-indexer/indexer/.gitignore
@@ -0,0 +1,20 @@
+# Dependencies
+/node_modules
+node_modules/
+
+# Debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# Misc
+.DS_Store
+
+# Env files
+.env
+.env*.local
+
+# Ponder
+/generated/
+/.ponder/
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/.nvmrc b/packages/rfq-indexer/indexer/.nvmrc
new file mode 100644
index 0000000000..39d00c0517
--- /dev/null
+++ b/packages/rfq-indexer/indexer/.nvmrc
@@ -0,0 +1 @@
+18.17.0
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/.prettier.json b/packages/rfq-indexer/indexer/.prettier.json
new file mode 100644
index 0000000000..54eb1fc7c6
--- /dev/null
+++ b/packages/rfq-indexer/indexer/.prettier.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "http://json.schemastore.org/prettierrc",
+ "trailingComma": "es5",
+ "tabWidth": 2,
+ "semi": false,
+ "singleQuote": true,
+ "arrowParens": "always"
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/CHANGELOG.md b/packages/rfq-indexer/indexer/CHANGELOG.md
new file mode 100644
index 0000000000..f00a5be39f
--- /dev/null
+++ b/packages/rfq-indexer/indexer/CHANGELOG.md
@@ -0,0 +1,16 @@
+# Change Log
+
+All notable changes to this project will be documented in this file.
+See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+
+## [0.0.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/rfq-indexer@0.0.2...@synapsecns/rfq-indexer@0.0.3) (2024-09-20)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer
+
+
+
+
+
+## 0.0.2 (2024-09-19)
+
+**Note:** Version bump only for package @synapsecns/rfq-indexer
diff --git a/packages/rfq-indexer/indexer/README.md b/packages/rfq-indexer/indexer/README.md
new file mode 100644
index 0000000000..f7e6ed12a3
--- /dev/null
+++ b/packages/rfq-indexer/indexer/README.md
@@ -0,0 +1,25 @@
+# RFQ Indexer
+
+This indexer captures and stores FastBridgeV2 events from various blockchain networks.
+
+## Important Scripts
+
+- `yarn dev:local`: Runs the indexer in development mode, clearing previous data
+- `yarn dev`: Runs the indexer in development mode
+- `yarn start`: Starts the indexer in production mode
+
+To run these scripts, use `yarn ` in the terminal from the indexer directory.
+
+## Main Files for Contributors
+
+1. ponder.schema.ts
+ - Description: Defines the database schema for indexed events
+2. ponder.config.ts
+ - Description: Configures the indexer, including network details and contract addresses
+3. src/index.ts
+ - Description: Contains the main indexing logic for different event types
+
+4. abis/FastBridgeV2.ts
+ - Description: Contains the ABI (Application Binary Interface) for the FastBridgeV2 contract
+
+When contributing, focus on these files for making changes to the indexing logic, adding new event types, or modifying the database schema.
diff --git a/packages/rfq-indexer/indexer/abis/FastBridgeV2.ts b/packages/rfq-indexer/indexer/abis/FastBridgeV2.ts
new file mode 100644
index 0000000000..5e355f84cc
--- /dev/null
+++ b/packages/rfq-indexer/indexer/abis/FastBridgeV2.ts
@@ -0,0 +1,643 @@
+export const FastBridgeV2Abi = [
+ {
+ inputs: [
+ { internalType: 'address', name: '_owner', type: 'address' },
+ ],
+ stateMutability: 'nonpayable',
+ type: 'constructor',
+ },
+ {
+ inputs: [],
+ name: 'AccessControlBadConfirmation',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'account', type: 'address' },
+ { internalType: 'bytes32', name: 'neededRole', type: 'bytes32' },
+ ],
+ name: 'AccessControlUnauthorizedAccount',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'target', type: 'address' },
+ ],
+ name: 'AddressEmptyCode',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'AddressInsufficientBalance',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'AmountIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'ChainIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineExceeded',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineNotExceeded',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DeadlineTooShort',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DisputePeriodNotPassed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'DisputePeriodPassed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'FailedInnerCall',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'MsgValueIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'token', type: 'address' },
+ ],
+ name: 'SafeERC20FailedOperation',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'SenderIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'StatusIncorrect',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'TokenNotContract',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'TransactionRelayed',
+ type: 'error',
+ },
+ {
+ inputs: [],
+ name: 'ZeroAddress',
+ type: 'error',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'relayer', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'token', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'BridgeDepositClaimed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'token', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'BridgeDepositRefunded',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'relayer', type: 'address' },
+ ],
+ name: 'BridgeProofDisputed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'relayer', type: 'address' },
+ { indexed: false, internalType: 'bytes32', name: 'transactionHash', type: 'bytes32' },
+ ],
+ name: 'BridgeProofProvided',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'relayer', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'to', type: 'address' },
+ { indexed: false, internalType: 'uint32', name: 'originChainId', type: 'uint32' },
+ { indexed: false, internalType: 'address', name: 'originToken', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'destToken', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'chainGasAmount', type: 'uint256' },
+ ],
+ name: 'BridgeRelayed',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ { indexed: false, internalType: 'bytes', name: 'request', type: 'bytes' },
+ { indexed: false, internalType: 'uint32', name: 'destChainId', type: 'uint32' },
+ { indexed: false, internalType: 'address', name: 'originToken', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'destToken', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { indexed: false, internalType: 'bool', name: 'sendChainGas', type: 'bool' },
+ ],
+ name: 'BridgeRequested',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'uint256', name: 'oldChainGasAmount', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'newChainGasAmount', type: 'uint256' },
+ ],
+ name: 'ChainGasAmountUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'uint256', name: 'oldFeeRate', type: 'uint256' },
+ { indexed: false, internalType: 'uint256', name: 'newFeeRate', type: 'uint256' },
+ ],
+ name: 'FeeRateUpdated',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: false, internalType: 'address', name: 'token', type: 'address' },
+ { indexed: false, internalType: 'address', name: 'recipient', type: 'address' },
+ { indexed: false, internalType: 'uint256', name: 'amount', type: 'uint256' },
+ ],
+ name: 'FeesSwept',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { indexed: true, internalType: 'bytes32', name: 'previousAdminRole', type: 'bytes32' },
+ { indexed: true, internalType: 'bytes32', name: 'newAdminRole', type: 'bytes32' },
+ ],
+ name: 'RoleAdminChanged',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'account', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ ],
+ name: 'RoleGranted',
+ type: 'event',
+ },
+ {
+ anonymous: false,
+ inputs: [
+ { indexed: true, internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { indexed: true, internalType: 'address', name: 'account', type: 'address' },
+ { indexed: true, internalType: 'address', name: 'sender', type: 'address' },
+ ],
+ name: 'RoleRevoked',
+ type: 'event',
+ },
+ {
+ inputs: [],
+ name: 'DEFAULT_ADMIN_ROLE',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'DISPUTE_PERIOD',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'FEE_BPS',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'FEE_RATE_MAX',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'GOVERNOR_ROLE',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'GUARD_ROLE',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'MIN_DEADLINE_PERIOD',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'REFUNDER_ROLE',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'REFUND_DELAY',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'RELAYER_ROLE',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ {
+ components: [
+ { internalType: 'uint32', name: 'dstChainId', type: 'uint32' },
+ { internalType: 'address', name: 'sender', type: 'address' },
+ { internalType: 'address', name: 'to', type: 'address' },
+ { internalType: 'address', name: 'originToken', type: 'address' },
+ { internalType: 'address', name: 'destToken', type: 'address' },
+ { internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { internalType: 'bool', name: 'sendChainGas', type: 'bool' },
+ { internalType: 'uint256', name: 'deadline', type: 'uint256' },
+ ],
+ internalType: 'struct IFastBridge.BridgeParams',
+ name: 'params',
+ type: 'tuple',
+ },
+ ],
+ name: 'bridge',
+ outputs: [],
+ stateMutability: 'payable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ name: 'bridgeProofs',
+ outputs: [
+ { internalType: 'uint96', name: 'timestamp', type: 'uint96' },
+ { internalType: 'address', name: 'relayer', type: 'address' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ name: 'bridgeRelays',
+ outputs: [
+ { internalType: 'bool', name: '', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ name: 'bridgeStatuses',
+ outputs: [
+ { internalType: 'enum FastBridge.BridgeStatus', name: '', type: 'uint8' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ { internalType: 'address', name: 'relayer', type: 'address' },
+ ],
+ name: 'canClaim',
+ outputs: [
+ { internalType: 'bool', name: '', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'chainGasAmount',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ { internalType: 'address', name: 'to', type: 'address' },
+ ],
+ name: 'claim',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'deployBlock',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'transactionId', type: 'bytes32' },
+ ],
+ name: 'dispute',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ ],
+ name: 'getBridgeTransaction',
+ outputs: [
+ {
+ components: [
+ { internalType: 'uint32', name: 'originChainId', type: 'uint32' },
+ { internalType: 'uint32', name: 'destChainId', type: 'uint32' },
+ { internalType: 'address', name: 'originSender', type: 'address' },
+ { internalType: 'address', name: 'destRecipient', type: 'address' },
+ { internalType: 'address', name: 'originToken', type: 'address' },
+ { internalType: 'address', name: 'destToken', type: 'address' },
+ { internalType: 'uint256', name: 'originAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'destAmount', type: 'uint256' },
+ { internalType: 'uint256', name: 'originFeeAmount', type: 'uint256' },
+ { internalType: 'bool', name: 'sendChainGas', type: 'bool' },
+ { internalType: 'uint256', name: 'deadline', type: 'uint256' },
+ { internalType: 'uint256', name: 'nonce', type: 'uint256' },
+ ],
+ internalType: 'struct IFastBridge.BridgeTransaction',
+ name: '',
+ type: 'tuple',
+ },
+ ],
+ stateMutability: 'pure',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ ],
+ name: 'getRoleAdmin',
+ outputs: [
+ { internalType: 'bytes32', name: '', type: 'bytes32' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'uint256', name: 'index', type: 'uint256' },
+ ],
+ name: 'getRoleMember',
+ outputs: [
+ { internalType: 'address', name: '', type: 'address' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ ],
+ name: 'getRoleMemberCount',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'grantRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'hasRole',
+ outputs: [
+ { internalType: 'bool', name: '', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'nonce',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [],
+ name: 'protocolFeeRate',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: '', type: 'address' },
+ ],
+ name: 'protocolFees',
+ outputs: [
+ { internalType: 'uint256', name: '', type: 'uint256' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ { internalType: 'bytes32', name: 'destTxHash', type: 'bytes32' },
+ ],
+ name: 'prove',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ ],
+ name: 'refund',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes', name: 'request', type: 'bytes' },
+ ],
+ name: 'relay',
+ outputs: [],
+ stateMutability: 'payable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'callerConfirmation', type: 'address' },
+ ],
+ name: 'renounceRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes32', name: 'role', type: 'bytes32' },
+ { internalType: 'address', name: 'account', type: 'address' },
+ ],
+ name: 'revokeRole',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'newChainGasAmount', type: 'uint256' },
+ ],
+ name: 'setChainGasAmount',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'uint256', name: 'newFeeRate', type: 'uint256' },
+ ],
+ name: 'setProtocolFeeRate',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'bytes4', name: 'interfaceId', type: 'bytes4' },
+ ],
+ name: 'supportsInterface',
+ outputs: [
+ { internalType: 'bool', name: '', type: 'bool' },
+ ],
+ stateMutability: 'view',
+ type: 'function',
+ },
+ {
+ inputs: [
+ { internalType: 'address', name: 'token', type: 'address' },
+ { internalType: 'address', name: 'recipient', type: 'address' },
+ ],
+ name: 'sweepProtocolFees',
+ outputs: [],
+ stateMutability: 'nonpayable',
+ type: 'function',
+ },
+ ] as const
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/package.json b/packages/rfq-indexer/indexer/package.json
new file mode 100644
index 0000000000..ed2987c772
--- /dev/null
+++ b/packages/rfq-indexer/indexer/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "@synapsecns/rfq-indexer",
+ "private": true,
+ "version": "0.0.3",
+ "type": "module",
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev:local": "rm -rf .ponder && APP_ENV=local ponder dev",
+ "dev": "ponder dev",
+ "start": "ponder start",
+ "codegen": "ponder codegen",
+ "lint": "eslint .",
+ "ci:lint": "eslint .",
+ "typecheck": "tsc",
+ "test:coverage": "echo 'No tests defined.'",
+ "build:go": " ",
+ "build": " ",
+ "build:slither": " "
+ },
+ "author": "",
+ "license": "ISC",
+ "dependencies": {
+ "@ponder/core": "^0.4.41",
+ "viem": "^1.19.3"
+ },
+ "engines": {
+ "node": ">=18.17.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.9.0",
+ "eslint": "^8.53.0",
+ "eslint-config-ponder": "^0.3.11",
+ "typescript": "^5.2.2"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/synapsecns/sanguine",
+ "directory": "packages/rfq-indexer/indexer"
+ }
+}
diff --git a/packages/rfq-indexer/indexer/ponder-env.d.ts b/packages/rfq-indexer/indexer/ponder-env.d.ts
new file mode 100644
index 0000000000..f8e7347cf6
--- /dev/null
+++ b/packages/rfq-indexer/indexer/ponder-env.d.ts
@@ -0,0 +1,27 @@
+// This file enables type checking and editor autocomplete for this Ponder project.
+// After upgrading, you may find that changes have been made to this file.
+// If this happens, please commit the changes. Do not manually edit this file.
+// See https://ponder.sh/docs/getting-started/installation#typescript for more information.
+
+declare module "@/generated" {
+ import type { Virtual } from "@ponder/core";
+
+ type config = typeof import("./ponder.config.ts").default;
+ type schema = typeof import("./ponder.schema.ts").default;
+
+ export const ponder: Virtual.Registry;
+
+ export type EventNames = Virtual.EventNames;
+ export type Event = Virtual.Event<
+ config,
+ name
+ >;
+ export type Context = Virtual.Context<
+ config,
+ schema,
+ name
+ >;
+ export type IndexingFunctionArgs =
+ Virtual.IndexingFunctionArgs;
+ export type Schema = Virtual.Schema;
+}
diff --git a/packages/rfq-indexer/indexer/ponder.config.ts b/packages/rfq-indexer/indexer/ponder.config.ts
new file mode 100644
index 0000000000..da8d6f8f57
--- /dev/null
+++ b/packages/rfq-indexer/indexer/ponder.config.ts
@@ -0,0 +1,226 @@
+import { createConfig } from '@ponder/core'
+import { http } from 'viem'
+
+import { FastBridgeV2Abi } from '@/abis/FastBridgeV2'
+import { AddressConfig } from '@/indexer/src/types'
+
+// Mainnets
+const ethereumChainId = 1
+const optimismChainId = 10
+const arbitrumChainId = 42161
+const baseChainId = 8453
+const blastChainId = 81457
+const scrollChainId = 534352
+const lineaChainId = 59144
+const bnbChainId = 56
+
+const configByChainId = {
+ [1]: {
+ transport: http(process.env.ETH_MAINNET_RPC),
+ chainName: 'ethereum',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ // FastBridgeV2StartBlock: 19420718, first block
+ FastBridgeV2StartBlock: 20426589,// new block
+ },
+ [10]: {
+ transport: http(process.env.OPTIMISM_MAINNET_RPC),
+ chainName: 'optimism',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ // FastBridgeV2StartBlock: 117334308, first block
+ FastBridgeV2StartBlock: 123416470, // new block
+ },
+ [42161]: {
+ transport: http(process.env.ARBITRUM_MAINNET_RPC),
+ chainName: 'arbitrum',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ // FastBridgeV2StartBlock: 189700328, first block
+ FastBridgeV2StartBlock: 237979967, // new block
+ },
+ [8453]: {
+ transport: http(process.env.BASE_MAINNET_RPC),
+ chainName: 'base',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ // FastBridgeV2StartBlock: 12478374, first block
+ FastBridgeV2StartBlock: 17821292, // new block
+ },
+ [81457]: {
+ transport: http(process.env.BLAST_MAINNET_RPC),
+ chainName: 'blast',
+ FastBridgeV2Address: '0x34F52752975222d5994C206cE08C1d5B329f24dD',
+ // FastBridgeV2StartBlock: 6378234, first block
+ FastBridgeV2StartBlock: 6811045, // new block
+ },
+ [534352]: {
+ transport: http(process.env.SCROLL_MAINNET_RPC),
+ chainName: 'scroll',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ // FastBridgeV2StartBlock: 5357000, first block
+ FastBridgeV2StartBlock: 7941653, // new block
+ },
+ [59144]: {
+ transport: http(process.env.LINEA_MAINNET_RPC),
+ chainName: 'linea',
+ FastBridgeV2Address: '0x34F52752975222d5994C206cE08C1d5B329f24dD',
+ FastBridgeV2StartBlock: 7124666, // first block and new block
+ },
+ [56]: {
+ transport: http(process.env.BNB_MAINNET_RPC),
+ chainName: 'bnb',
+ FastBridgeV2Address: '0x5523D3c98809DdDB82C686E152F5C58B1B0fB59E',
+ FastBridgeV2StartBlock: 40497843, // first block and new block
+ },
+ disableCache: true,
+}
+
+export const networkDetails = {
+ [ethereumChainId]: {
+ name: configByChainId[ethereumChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[ethereumChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[ethereumChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [optimismChainId]: {
+ name: configByChainId[optimismChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[optimismChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[optimismChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [arbitrumChainId]: {
+ name: configByChainId[arbitrumChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[arbitrumChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[arbitrumChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [baseChainId]: {
+ name: configByChainId[baseChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[baseChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[baseChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [blastChainId]: {
+ name: configByChainId[blastChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[blastChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[blastChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [scrollChainId]: {
+ name: configByChainId[scrollChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[scrollChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[scrollChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [lineaChainId]: {
+ name: configByChainId[lineaChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[lineaChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[lineaChainId].FastBridgeV2StartBlock,
+ },
+ },
+ [bnbChainId]: {
+ name: configByChainId[bnbChainId].chainName,
+ FastBridgeV2: {
+ address: configByChainId[bnbChainId].FastBridgeV2Address,
+ abi: FastBridgeV2Abi,
+ startBlock: configByChainId[bnbChainId].FastBridgeV2StartBlock,
+ },
+ },
+} as Record
+
+const config = createConfig({
+ networks: {
+ [configByChainId[ethereumChainId].chainName]: {
+ chainId: ethereumChainId,
+ transport: configByChainId[ethereumChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[optimismChainId].chainName]: {
+ chainId: optimismChainId,
+ transport: configByChainId[optimismChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[arbitrumChainId].chainName]: {
+ chainId: arbitrumChainId,
+ transport: configByChainId[arbitrumChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[baseChainId].chainName]: {
+ chainId: baseChainId,
+ transport: configByChainId[baseChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[blastChainId].chainName]: {
+ chainId: blastChainId,
+ transport: configByChainId[blastChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[scrollChainId].chainName]: {
+ chainId: scrollChainId,
+ transport: configByChainId[scrollChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[lineaChainId].chainName]: {
+ chainId: lineaChainId,
+ transport: configByChainId[lineaChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ [configByChainId[bnbChainId].chainName]: {
+ chainId: bnbChainId,
+ transport: configByChainId[bnbChainId].transport,
+ // disableCache: configByChainId.disableCache,
+ },
+ },
+ contracts: {
+ FastBridgeV2: {
+ network: {
+ [configByChainId[ethereumChainId].chainName]: {
+ address: networkDetails[ethereumChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[ethereumChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[optimismChainId].chainName]: {
+ address: networkDetails[optimismChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[optimismChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[arbitrumChainId].chainName]: {
+ address: networkDetails[arbitrumChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[arbitrumChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[baseChainId].chainName]: {
+ address: networkDetails[baseChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[baseChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[blastChainId].chainName]: {
+ address: networkDetails[blastChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[blastChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[scrollChainId].chainName]: {
+ address: networkDetails[scrollChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[scrollChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[lineaChainId].chainName]: {
+ address: networkDetails[lineaChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[lineaChainId]?.FastBridgeV2.startBlock,
+ },
+ [configByChainId[bnbChainId].chainName]: {
+ address: networkDetails[bnbChainId]?.FastBridgeV2.address,
+ startBlock: networkDetails[bnbChainId]?.FastBridgeV2.startBlock,
+ },
+ },
+ abi: FastBridgeV2Abi,
+ },
+ },
+})
+
+export default config
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/ponder.schema.ts b/packages/rfq-indexer/indexer/ponder.schema.ts
new file mode 100644
index 0000000000..67f34464d2
--- /dev/null
+++ b/packages/rfq-indexer/indexer/ponder.schema.ts
@@ -0,0 +1,83 @@
+import { createSchema } from '@ponder/core'
+
+export default createSchema((p) => ({
+ BridgeRequestEvents: p.createTable({
+ id: p.string(),
+ transactionId: p.string(),
+ sender: p.string(),
+ originToken: p.string(),
+ destToken: p.string(),
+ originAmount: p.bigint().optional(),
+ originAmountFormatted: p.string(),
+ destAmount: p.bigint(),
+ destAmountFormatted: p.string(),
+ originChainId: p.int(),
+ originChain: p.string(),
+ destChainId: p.int(),
+ destChain: p.string(),
+ sendChainGas: p.boolean(),
+ blockNumber: p.bigint(),
+ blockTimestamp: p.int(),
+ transactionHash: p.string(),
+ }),
+
+ BridgeRelayedEvents: p.createTable({
+ id: p.string(),
+ transactionId: p.string(),
+ relayer: p.string(),
+ to: p.string(),
+ originToken: p.string(),
+ destToken: p.string(),
+ originAmount: p.bigint(),
+ originAmountFormatted: p.string(),
+ destAmount: p.bigint(),
+ destAmountFormatted: p.string(),
+ originChainId: p.int(),
+ originChain: p.string(),
+ destChainId: p.int(),
+ destChain: p.string(),
+ blockNumber: p.bigint(),
+ blockTimestamp: p.int(),
+ transactionHash: p.string()
+ }),
+
+ BridgeProofProvidedEvents: p.createTable({
+ id: p.string(),
+ transactionId: p.string(),
+ relayer: p.string(),
+ originChainId: p.int(),
+ originChain: p.string(),
+ blockNumber: p.bigint(),
+ blockTimestamp: p.int(),
+ transactionHash: p.string(),
+ }),
+
+ BridgeDepositRefundedEvents: p.createTable({
+ id: p.string(),
+ transactionId: p.string(),
+ to: p.string(),
+ token: p.string(),
+ amount: p.bigint(),
+ amountFormatted: p.string(),
+ originChainId: p.int(),
+ originChain: p.string(),
+ blockNumber: p.bigint(),
+ blockTimestamp: p.int(),
+ transactionHash: p.string()
+ }),
+
+ BridgeDepositClaimedEvents: p.createTable({
+ id: p.string(),
+ transactionId: p.string(),
+ relayer: p.string(),
+ to: p.string(),
+ token: p.string(),
+ amount: p.bigint(),
+ amountFormatted: p.string(),
+ originChainId: p.int(),
+ originChain: p.string(),
+ blockNumber: p.bigint(),
+ blockTimestamp: p.int(),
+ transactionHash: p.string()
+ }),
+}))
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/index.ts b/packages/rfq-indexer/indexer/src/index.ts
new file mode 100644
index 0000000000..cd770c49d4
--- /dev/null
+++ b/packages/rfq-indexer/indexer/src/index.ts
@@ -0,0 +1,197 @@
+import { trim } from 'viem'
+
+
+import { ponder } from '@/generated'
+import { formatAmount } from './utils/formatAmount'
+import { getChainName } from './utils/chains'
+
+/* ORIGIN CHAIN EVENTS */
+
+ponder.on('FastBridgeV2:BridgeRequested', async ({ event, context }) => {
+ const {
+ db: { BridgeRequestEvents },
+ network: { chainId },
+ } = context
+
+ const {
+ args: {
+ transactionId,
+ sender,
+ destChainId,
+ originToken,
+ destToken,
+ originAmount,
+ destAmount,
+ sendChainGas,
+ },
+ block: { timestamp },
+ transaction: { hash },
+ log: { blockNumber },
+ } = event
+
+ await BridgeRequestEvents.create({
+ id: transactionId,
+ data: {
+ transactionId,
+ sender: trim(sender),
+ originChainId: Number(chainId),
+ originChain: getChainName(Number(chainId)),
+ destChainId: Number(destChainId),
+ destChain: getChainName(Number(destChainId)),
+ originToken: trim(originToken),
+ destToken: trim(destToken),
+ originAmount,
+ originAmountFormatted: formatAmount(originAmount, originToken),
+ destAmount,
+ destAmountFormatted: formatAmount(destAmount, destToken),
+ sendChainGas,
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ },
+ })
+})
+
+ponder.on('FastBridgeV2:BridgeDepositRefunded', async ({ event, context }) => {
+ const {
+ args: { transactionId, to, token, amount },
+ block: { timestamp },
+ transaction: { hash },
+ log: { blockNumber },
+ } = event
+
+ const {
+ db: { BridgeDepositRefundedEvents },
+ network: { chainId },
+ } = context
+
+ await BridgeDepositRefundedEvents.create({
+ id: transactionId,
+ data: {
+ transactionId,
+ to: trim(to),
+ token: trim(token),
+ amount,
+ amountFormatted: formatAmount(amount,token),
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ originChainId: chainId,
+ originChain: getChainName(Number(chainId))
+ },
+ })
+})
+
+ponder.on('FastBridgeV2:BridgeProofProvided', async ({ event, context }) => {
+ const {
+ args: { transactionId, relayer },
+ block: { timestamp },
+ transaction: { hash },
+ log: { address, blockNumber }, // may want to add address here eventually
+ } = event
+
+ const {
+ db: { BridgeProofProvidedEvents },
+ network: { chainId },
+ } = context
+
+ await BridgeProofProvidedEvents.upsert({
+ id: transactionId,
+ // Save the full data first time we index this event
+ create: {
+ transactionId,
+ relayer: trim(relayer),
+ originChainId: chainId,
+ originChain: getChainName(Number(chainId)),
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ },
+ // Update the data with the latest event data on subsequent indexes
+ update: {
+ relayer: trim(relayer),
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ }
+ })
+})
+
+ponder.on('FastBridgeV2:BridgeDepositClaimed', async ({ event, context }) => {
+ const {
+ args: { transactionId, relayer, to, token, amount },
+ block: { timestamp },
+ transaction: { hash },
+ log: { blockNumber },
+ } = event
+
+ const {
+ db: { BridgeDepositClaimedEvents },
+ network: { chainId },
+ } = context
+
+ await BridgeDepositClaimedEvents.create({
+ id: transactionId,
+ data: {
+ transactionId,
+ relayer: trim(relayer),
+ to: trim(to),
+ token: trim(token),
+ amount,
+ amountFormatted: formatAmount(amount, token),
+ originChainId: chainId,
+ originChain: getChainName(Number(chainId)),
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ },
+ })
+})
+
+/* DESTINATION CHAIN EVENTS */
+
+
+ponder.on('FastBridgeV2:BridgeRelayed', async ({ event, context }) => {
+ const {
+ args: {
+ transactionId,
+ relayer,
+ to,
+ originChainId,
+ originToken,
+ destToken,
+ originAmount,
+ destAmount,
+ },
+ block: { timestamp },
+ transaction: { hash },
+ log: { blockNumber },
+ } = event
+
+ const {
+ db: { BridgeRelayedEvents },
+ network: { chainId },
+ } = context
+
+ await BridgeRelayedEvents.create({
+ id: transactionId,
+ data: {
+ transactionId,
+ relayer: trim(relayer),
+ to: trim(to),
+ originChainId: Number(originChainId),
+ originChain: getChainName(Number(originChainId)),
+ destChainId: Number(chainId),
+ destChain: getChainName(Number(chainId)),
+ originToken: trim(originToken),
+ destToken: trim(destToken),
+ originAmount,
+ originAmountFormatted: formatAmount(originAmount, originToken),
+ destAmount,
+ destAmountFormatted: formatAmount(destAmount, destToken),
+ blockNumber: BigInt(blockNumber),
+ blockTimestamp: Number(timestamp),
+ transactionHash: hash,
+ },
+ })
+})
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/types.ts b/packages/rfq-indexer/indexer/src/types.ts
new file mode 100644
index 0000000000..673464d6ff
--- /dev/null
+++ b/packages/rfq-indexer/indexer/src/types.ts
@@ -0,0 +1,10 @@
+import { type Abi, type Address } from 'viem'
+
+export interface AddressConfig {
+ name: string
+ FastBridgeV2: {
+ address: Address
+ abi: Abi
+ startBlock: number
+ }
+}
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/utils/chains.ts b/packages/rfq-indexer/indexer/src/utils/chains.ts
new file mode 100644
index 0000000000..618d532684
--- /dev/null
+++ b/packages/rfq-indexer/indexer/src/utils/chains.ts
@@ -0,0 +1,14 @@
+export const chainIdToName: { [key: number]: string } = {
+ 1: 'ethereum',
+ 10: 'optimism',
+ 42161: 'arbitrum',
+ 8453: 'base',
+ 81457: 'blast',
+ 534352: 'scroll',
+ 59144: 'linea',
+ 56: 'bnb'
+ }
+
+ export const getChainName = (chainId: number): string => {
+ return chainIdToName[chainId] || 'unknown'
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/utils/formatAmount.ts b/packages/rfq-indexer/indexer/src/utils/formatAmount.ts
new file mode 100644
index 0000000000..9cbc1b2768
--- /dev/null
+++ b/packages/rfq-indexer/indexer/src/utils/formatAmount.ts
@@ -0,0 +1,8 @@
+import { formatUnits } from 'viem'
+
+const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'
+
+export function formatAmount(amount: bigint, tokenAddress: string): string {
+ const decimals = tokenAddress.toLowerCase() === ETH_ADDRESS.toLowerCase() ? 18 : 6
+ return formatUnits(amount, decimals)
+}
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/utils/generateEntryId.ts b/packages/rfq-indexer/indexer/src/utils/generateEntryId.ts
new file mode 100644
index 0000000000..e90c02131a
--- /dev/null
+++ b/packages/rfq-indexer/indexer/src/utils/generateEntryId.ts
@@ -0,0 +1,3 @@
+export const generateEntryId = (chainId: number, dbNonce: number) => {
+ return `${chainId}-${Number(dbNonce)}`
+ }
\ No newline at end of file
diff --git a/packages/rfq-indexer/indexer/src/utils/tokens.ts b/packages/rfq-indexer/indexer/src/utils/tokens.ts
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/rfq-indexer/indexer/tsconfig.json b/packages/rfq-indexer/indexer/tsconfig.json
new file mode 100644
index 0000000000..90f144357b
--- /dev/null
+++ b/packages/rfq-indexer/indexer/tsconfig.json
@@ -0,0 +1,35 @@
+{
+ "compilerOptions": {
+ // Type checking
+ "strict": true,
+ "noUncheckedIndexedAccess": true,
+
+ // Interop constraints
+ "verbatimModuleSyntax": false,
+ "esModuleInterop": true,
+ "isolatedModules": true,
+ "allowSyntheticDefaultImports": true,
+ "resolveJsonModule": true,
+
+ // Language and environment
+ "moduleResolution": "bundler",
+ "module": "ESNext",
+ "noEmit": true,
+ "lib": ["ES2022"],
+ "target": "ES2022",
+
+ // Skip type checking for node modules
+ "skipLibCheck": true,
+
+ // Paths
+ "paths": {
+ "@/*": ["./*"],
+ "@abis": ["./abis"],
+ "@abis/*": ["./abis/*"],
+ "@/utils/*": ["./src/utils/*"],
+ "@/types": ["./src/types.ts"]
+ }
+ },
+ "include": ["./**/*.ts"],
+ "exclude": ["node_modules"]
+ }
\ No newline at end of file
diff --git a/packages/sdk-router/CHANGELOG.md b/packages/sdk-router/CHANGELOG.md
index f66f3b5ab4..fff8579edf 100644
--- a/packages/sdk-router/CHANGELOG.md
+++ b/packages/sdk-router/CHANGELOG.md
@@ -3,6 +3,17 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.11.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.1...@synapsecns/sdk-router@0.11.2) (2024-09-26)
+
+
+### Bug Fixes
+
+* **sdk-router:** disable ARB airdrop tests ([#3195](https://github.com/synapsecns/sanguine/issues/3195)) ([fc6ddae](https://github.com/synapsecns/sanguine/commit/fc6ddaedf03f7769dab362f0bcdf81a3dd010516))
+
+
+
+
+
## [0.11.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/sdk-router@0.11.0...@synapsecns/sdk-router@0.11.1) (2024-09-04)
**Note:** Version bump only for package @synapsecns/sdk-router
diff --git a/packages/sdk-router/package.json b/packages/sdk-router/package.json
index 56f4c91bea..6042ce0125 100644
--- a/packages/sdk-router/package.json
+++ b/packages/sdk-router/package.json
@@ -1,7 +1,7 @@
{
"name": "@synapsecns/sdk-router",
"description": "An SDK for interacting with the Synapse Protocol",
- "version": "0.11.1",
+ "version": "0.11.2",
"license": "MIT",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
diff --git a/packages/sdk-router/src/sdk.test.ts b/packages/sdk-router/src/sdk.test.ts
index 4d49d9da45..fd79df50d2 100644
--- a/packages/sdk-router/src/sdk.test.ts
+++ b/packages/sdk-router/src/sdk.test.ts
@@ -54,7 +54,8 @@ global.fetch = jest.fn(() =>
const EXPECTED_GAS_DROP: { [chainId: number]: BigNumber } = {
[SupportedChainId.ETH]: BigNumber.from(0),
- [SupportedChainId.ARBITRUM]: parseFixed('0.0003', 18),
+ // TODO: reenable once both ARB airdrops are adjusted
+ // [SupportedChainId.ARBITRUM]: parseFixed('0.0003', 18),
[SupportedChainId.BSC]: parseFixed('0.002', 18),
[SupportedChainId.AVALANCHE]: parseFixed('0.025', 18),
}
@@ -296,9 +297,10 @@ describe('SynapseSDK', () => {
MEDIAN_TIME_BRIDGE[SupportedChainId.ETH]
)
expect(result.bridgeModuleName).toEqual('SynapseBridge')
- expect(result.gasDropAmount).toEqual(
- EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
- )
+ // TODO: reenable
+ // expect(result.gasDropAmount).toEqual(
+ // EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
+ // )
expect(result.originChainId).toEqual(SupportedChainId.ETH)
expect(result.destChainId).toEqual(SupportedChainId.ARBITRUM)
})
@@ -814,12 +816,13 @@ describe('SynapseSDK', () => {
allQuotes[0].bridgeModuleName === 'SynapseCCTP' ||
allQuotes[1].bridgeModuleName === 'SynapseCCTP'
).toBe(true)
- expect(allQuotes[0].gasDropAmount).toEqual(
- EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
- )
- expect(allQuotes[1].gasDropAmount).toEqual(
- EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
- )
+ // TODO: reenable
+ // expect(allQuotes[0].gasDropAmount).toEqual(
+ // EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
+ // )
+ // expect(allQuotes[1].gasDropAmount).toEqual(
+ // EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
+ // )
expect(allQuotes[0].originChainId).toEqual(SupportedChainId.ETH)
expect(allQuotes[0].destChainId).toEqual(SupportedChainId.ARBITRUM)
expect(allQuotes[1].originChainId).toEqual(SupportedChainId.ETH)
@@ -848,9 +851,10 @@ describe('SynapseSDK', () => {
expect(allQuotes.length).toEqual(1)
expectCorrectBridgeQuote(allQuotes[0])
expect(allQuotes[0].bridgeModuleName).toEqual('SynapseBridge')
- expect(allQuotes[0].gasDropAmount).toEqual(
- EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
- )
+ // TODO: reenable
+ // expect(allQuotes[0].gasDropAmount).toEqual(
+ // EXPECTED_GAS_DROP[SupportedChainId.ARBITRUM]
+ // )
})
})
diff --git a/packages/solidity-devops/.vscode/settings.json b/packages/solidity-devops/.vscode/settings.json
new file mode 100644
index 0000000000..9cab755557
--- /dev/null
+++ b/packages/solidity-devops/.vscode/settings.json
@@ -0,0 +1,7 @@
+{
+ "[solidity]": {
+ "editor.defaultFormatter": "JuanBlanco.solidity"
+ },
+ "solidity.formatter": "forge",
+ "solidity.monoRepoSupport": false
+}
diff --git a/packages/solidity-devops/CHANGELOG.md b/packages/solidity-devops/CHANGELOG.md
index 38e5edb4bf..262bd0a971 100644
--- a/packages/solidity-devops/CHANGELOG.md
+++ b/packages/solidity-devops/CHANGELOG.md
@@ -3,6 +3,25 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+## [0.4.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/solidity-devops@0.4.5...@synapsecns/solidity-devops@0.4.6) (2024-09-24)
+
+
+### Bug Fixes
+
+* **contracts-rfq:** CI workflows [SLT-245] ([#3178](https://github.com/synapsecns/sanguine/issues/3178)) ([74b620e](https://github.com/synapsecns/sanguine/commit/74b620e4c928be8d0dbb422708376d167db7848d))
+
+
+
+
+
+## [0.4.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/solidity-devops@0.4.4...@synapsecns/solidity-devops@0.4.5) (2024-09-23)
+
+**Note:** Version bump only for package @synapsecns/solidity-devops
+
+
+
+
+
## [0.4.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/solidity-devops@0.4.3...@synapsecns/solidity-devops@0.4.4) (2024-07-17)
diff --git a/packages/solidity-devops/package.json b/packages/solidity-devops/package.json
index 713845fae2..b69ffa6e81 100644
--- a/packages/solidity-devops/package.json
+++ b/packages/solidity-devops/package.json
@@ -1,6 +1,6 @@
{
"name": "@synapsecns/solidity-devops",
- "version": "0.4.4",
+ "version": "0.4.6",
"description": "A collection of utils to effortlessly test, deploy and maintain the smart contracts on EVM compatible blockchains",
"license": "MIT",
"repository": {
@@ -40,6 +40,6 @@
"vp": "js/verifyProxy.js"
},
"devDependencies": {
- "solhint": "^4.5.4"
+ "solhint": "5.0.3"
}
}
diff --git a/packages/synapse-interface/CHANGELOG.md b/packages/synapse-interface/CHANGELOG.md
index 6e75c970f7..8af7d86ed2 100644
--- a/packages/synapse-interface/CHANGELOG.md
+++ b/packages/synapse-interface/CHANGELOG.md
@@ -3,6 +3,95 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
+# [0.40.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.39.2...@synapsecns/synapse-interface@0.40.0) (2024-09-26)
+
+
+### Features
+
+* **synapse-interface:** refund RFQ transaction [SLT-272] ([#3197](https://github.com/synapsecns/sanguine/issues/3197)) ([f0b13bc](https://github.com/synapsecns/sanguine/commit/f0b13bc456620004a1787f62e87f404d95272356))
+
+
+
+
+
+## [0.39.2](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.39.1...@synapsecns/synapse-interface@0.39.2) (2024-09-26)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+## [0.39.1](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.39.0...@synapsecns/synapse-interface@0.39.1) (2024-09-26)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+# [0.39.0](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.9...@synapsecns/synapse-interface@0.39.0) (2024-09-23)
+
+
+### Features
+
+* **synapse-interface:** confirm new price [SLT-150] ([#3084](https://github.com/synapsecns/sanguine/issues/3084)) ([6f21b1a](https://github.com/synapsecns/sanguine/commit/6f21b1a7f6eb2ea3885582fcd678fa122f9f87e5))
+
+
+
+
+
+## [0.38.9](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.8...@synapsecns/synapse-interface@0.38.9) (2024-09-23)
+
+
+### Bug Fixes
+
+* **synapse-interface:** Additional checks on screen [SLT-166] ([#3152](https://github.com/synapsecns/sanguine/issues/3152)) ([9418b40](https://github.com/synapsecns/sanguine/commit/9418b40aa25a441d6a4460695962f7fbf41c4221))
+
+
+
+
+
+## [0.38.8](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.7...@synapsecns/synapse-interface@0.38.8) (2024-09-20)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+## [0.38.7](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.6...@synapsecns/synapse-interface@0.38.7) (2024-09-19)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+## [0.38.6](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.5...@synapsecns/synapse-interface@0.38.6) (2024-09-19)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+## [0.38.5](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.4...@synapsecns/synapse-interface@0.38.5) (2024-09-19)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
+## [0.38.4](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.3...@synapsecns/synapse-interface@0.38.4) (2024-09-18)
+
+**Note:** Version bump only for package @synapsecns/synapse-interface
+
+
+
+
+
## [0.38.3](https://github.com/synapsecns/sanguine/compare/@synapsecns/synapse-interface@0.38.2...@synapsecns/synapse-interface@0.38.3) (2024-09-10)
**Note:** Version bump only for package @synapsecns/synapse-interface
diff --git a/packages/synapse-interface/README.md b/packages/synapse-interface/README.md
index 4023fc2c54..ca91c23a00 100644
--- a/packages/synapse-interface/README.md
+++ b/packages/synapse-interface/README.md
@@ -2,7 +2,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next
## Getting Started
-First, run the development server:
+First, run development server:
```bash
yarn dev
diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeQuoteResetTimer.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeQuoteResetTimer.tsx
new file mode 100644
index 0000000000..86717650ba
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/BridgeQuoteResetTimer.tsx
@@ -0,0 +1,113 @@
+import { useState, useEffect, useMemo } from 'react'
+
+import { BridgeQuote } from '@/utils/types'
+import { convertMsToSeconds } from '@/utils/time'
+
+export const BridgeQuoteResetTimer = ({
+ bridgeQuote,
+ isLoading,
+ isActive,
+ duration, // in ms
+}: {
+ bridgeQuote: BridgeQuote
+ isLoading: boolean
+ isActive: boolean
+ duration: number
+}) => {
+ const memoizedTimer = useMemo(() => {
+ if (!isActive) return null
+
+ if (isLoading) {
+ return
+ } else {
+ return (
+
+ )
+ }
+ }, [bridgeQuote, duration, isActive])
+
+ return memoizedTimer
+}
+
+const AnimatedLoadingCircle = () => {
+ return (
+
+ )
+}
+
+const AnimatedProgressCircle = ({
+ animateKey,
+ duration,
+}: {
+ animateKey: string
+ duration: number
+}) => {
+ const [animationKey, setAnimationKey] = useState(0)
+
+ useEffect(() => {
+ setAnimationKey((prevKey) => prevKey + 1)
+ }, [animateKey])
+
+ return (
+
+ )
+}
diff --git a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
index 44aa9a3fb5..86f6006650 100644
--- a/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
+++ b/packages/synapse-interface/components/StateManagedBridge/BridgeTransactionButton.tsx
@@ -12,6 +12,7 @@ import { useBridgeDisplayState, useBridgeState } from '@/slices/bridge/hooks'
import { TransactionButton } from '@/components/buttons/TransactionButton'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { segmentAnalyticsEvent } from '@/contexts/SegmentAnalyticsProvider'
+import { useConfirmNewBridgePrice } from './hooks/useConfirmNewBridgePrice'
export const BridgeTransactionButton = ({
approveTxn,
@@ -19,6 +20,7 @@ export const BridgeTransactionButton = ({
isApproved,
isBridgePaused,
isTyping,
+ isQuoteStale,
}) => {
const dispatch = useAppDispatch()
const { openConnectModal } = useConnectModal()
@@ -48,6 +50,8 @@ export const BridgeTransactionButton = ({
debouncedFromValue,
} = useBridgeState()
const { bridgeQuote, isLoading } = useBridgeQuoteState()
+ const { isPendingConfirmChange, onUserAcceptChange } =
+ useConfirmNewBridgePrice()
const { isWalletPending } = useWalletState()
const { showDestinationWarning, isDestinationWarningAccepted } =
@@ -73,6 +77,7 @@ export const BridgeTransactionButton = ({
isBridgeQuoteAmountGreaterThanInputForRfq ||
(isConnected && !hasValidQuote) ||
(isConnected && !hasSufficientBalance) ||
+ (isConnected && isQuoteStale) ||
(destinationAddress && !isAddress(destinationAddress))
let buttonProperties
@@ -97,6 +102,26 @@ export const BridgeTransactionButton = ({
label: t('Please select an Origin token'),
onClick: null,
}
+ } else if (isConnected && !hasSufficientBalance) {
+ buttonProperties = {
+ label: t('Insufficient balance'),
+ onClick: null,
+ }
+ } else if (isLoading && hasValidQuote) {
+ buttonProperties = {
+ label: isPendingConfirmChange
+ ? t('Confirm new quote')
+ : t('Bridge {symbol}', { symbol: fromToken?.symbol }),
+ pendingLabel: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
+ onClick: null,
+ className: `
+ ${
+ isPendingConfirmChange
+ ? '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight'
+ : '!bg-gradient-to-r !from-fuchsia-500 !to-purple-500 dark:!to-purple-600'
+ }
+ !opacity-100`,
+ }
} else if (isLoading) {
buttonProperties = {
label: t('Bridge {symbol}', { symbol: fromToken?.symbol }),
@@ -144,11 +169,6 @@ export const BridgeTransactionButton = ({
label: t('Invalid bridge quote'),
onClick: null,
}
- } else if (!isLoading && isConnected && !hasSufficientBalance) {
- buttonProperties = {
- label: t('Insufficient balance'),
- onClick: null,
- }
} else if (destinationAddress && !isAddress(destinationAddress)) {
buttonProperties = {
label: t('Invalid Destination address'),
@@ -167,6 +187,13 @@ export const BridgeTransactionButton = ({
onClick: () => switchChain({ chainId: fromChainId }),
pendingLabel: t('Switching chains'),
}
+ } else if (isApproved && hasValidQuote && isPendingConfirmChange) {
+ buttonProperties = {
+ label: t('Confirm new quote'),
+ onClick: () => onUserAcceptChange(),
+ className:
+ '!outline !outline-1 !outline-synapsePurple !outline-offset-[-1px] !from-bgLight !to-bgLight',
+ }
} else if (!isApproved && hasValidInput && hasValidQuote) {
buttonProperties = {
onClick: approveTxn,
diff --git a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
index c891391fcd..5af733cb91 100644
--- a/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
+++ b/packages/synapse-interface/components/StateManagedBridge/OutputContainer.tsx
@@ -17,7 +17,11 @@ import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
import { useBridgeValidations } from './hooks/useBridgeValidations'
import { useTranslations } from 'next-intl'
-export const OutputContainer = () => {
+interface OutputContainerProps {
+ isQuoteStale: boolean
+}
+
+export const OutputContainer = ({ isQuoteStale }: OutputContainerProps) => {
const { address } = useAccount()
const { bridgeQuote, isLoading } = useBridgeQuoteState()
const { showDestinationAddress } = useBridgeDisplayState()
@@ -33,6 +37,8 @@ export const OutputContainer = () => {
}
}, [bridgeQuote, hasValidInput, hasValidQuote])
+ const inputClassName = isQuoteStale ? 'opacity-50' : undefined
+
return (
@@ -48,6 +54,7 @@ export const OutputContainer = () => {
disabled={true}
showValue={showValue}
isLoading={isLoading}
+ className={inputClassName}
/>
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts
index e64ac72587..b3f31ab0f6 100644
--- a/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts
+++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useBridgeValidations.ts
@@ -111,7 +111,7 @@ export const useBridgeValidations = () => {
}
}
-const constructStringifiedBridgeSelections = (
+export const constructStringifiedBridgeSelections = (
originAmount,
originChainId,
originToken,
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useConfirmNewBridgePrice.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useConfirmNewBridgePrice.ts
new file mode 100644
index 0000000000..2dbead7f24
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useConfirmNewBridgePrice.ts
@@ -0,0 +1,125 @@
+import { useState, useEffect, useMemo, useRef } from 'react'
+
+import { useBridgeState } from '@/slices/bridge/hooks'
+import { useBridgeQuoteState } from '@/slices/bridgeQuote/hooks'
+import { constructStringifiedBridgeSelections } from './useBridgeValidations'
+import { BridgeQuote } from '@/utils/types'
+
+export const useConfirmNewBridgePrice = () => {
+ const triggerQuoteRef = useRef
(null)
+ const bpsThreshold = 0.0001 // 1bps
+
+ const [hasQuoteOutputChanged, setHasQuoteOutputChanged] =
+ useState(false)
+ const [hasUserConfirmedChange, setHasUserConfirmedChange] =
+ useState(false)
+
+ const { bridgeQuote, previousBridgeQuote } = useBridgeQuoteState()
+ const { debouncedFromValue, fromToken, toToken, fromChainId, toChainId } =
+ useBridgeState()
+
+ const currentBridgeQuoteSelections = useMemo(
+ () =>
+ constructStringifiedBridgeSelections(
+ debouncedFromValue,
+ fromChainId,
+ fromToken,
+ toChainId,
+ toToken
+ ),
+ [debouncedFromValue, fromChainId, fromToken, toChainId, toToken]
+ )
+
+ const previousBridgeQuoteSelections = useMemo(
+ () =>
+ constructStringifiedBridgeSelections(
+ previousBridgeQuote?.inputAmountForQuote,
+ previousBridgeQuote?.originChainId,
+ previousBridgeQuote?.originTokenForQuote,
+ previousBridgeQuote?.destChainId,
+ previousBridgeQuote?.destTokenForQuote
+ ),
+ [previousBridgeQuote]
+ )
+
+ const hasSameSelectionsAsPreviousQuote = useMemo(
+ () => currentBridgeQuoteSelections === previousBridgeQuoteSelections,
+ [currentBridgeQuoteSelections, previousBridgeQuoteSelections]
+ )
+
+ const isPendingConfirmChange =
+ hasQuoteOutputChanged &&
+ hasSameSelectionsAsPreviousQuote &&
+ !hasUserConfirmedChange
+
+ useEffect(() => {
+ const validQuotes =
+ bridgeQuote?.outputAmount && previousBridgeQuote?.outputAmount
+
+ const hasBridgeModuleChanged =
+ bridgeQuote?.bridgeModuleName !==
+ (triggerQuoteRef.current?.bridgeModuleName ??
+ previousBridgeQuote?.bridgeModuleName)
+
+ const outputAmountDiffMoreThanThreshold = validQuotes
+ ? calculateOutputRelativeDifference(
+ bridgeQuote,
+ triggerQuoteRef.current ?? previousBridgeQuote
+ ) > bpsThreshold
+ : false
+
+ if (
+ validQuotes &&
+ hasSameSelectionsAsPreviousQuote &&
+ (outputAmountDiffMoreThanThreshold || hasBridgeModuleChanged)
+ ) {
+ requestUserConfirmChange(previousBridgeQuote)
+ } else {
+ resetConfirm()
+ }
+ }, [bridgeQuote, previousBridgeQuote, hasSameSelectionsAsPreviousQuote])
+
+ const requestUserConfirmChange = (previousQuote: BridgeQuote) => {
+ if (!hasQuoteOutputChanged && !hasUserConfirmedChange) {
+ triggerQuoteRef.current = previousQuote
+ setHasQuoteOutputChanged(true)
+ }
+ setHasUserConfirmedChange(false)
+ }
+
+ const resetConfirm = () => {
+ if (hasUserConfirmedChange) {
+ triggerQuoteRef.current = null
+ setHasQuoteOutputChanged(false)
+ setHasUserConfirmedChange(false)
+ }
+ }
+
+ const onUserAcceptChange = () => {
+ triggerQuoteRef.current = null
+ setHasUserConfirmedChange(true)
+ }
+
+ return {
+ isPendingConfirmChange,
+ onUserAcceptChange,
+ }
+}
+
+const calculateOutputRelativeDifference = (
+ currentQuote?: BridgeQuote,
+ previousQuote?: BridgeQuote
+) => {
+ if (!currentQuote?.outputAmountString || !previousQuote?.outputAmountString) {
+ return null
+ }
+
+ const currentOutput = parseFloat(currentQuote.outputAmountString)
+ const previousOutput = parseFloat(previousQuote.outputAmountString)
+
+ if (previousOutput === 0) {
+ return null
+ }
+
+ return (previousOutput - currentOutput) / previousOutput
+}
diff --git a/packages/synapse-interface/components/StateManagedBridge/hooks/useStaleQuoteUpdater.ts b/packages/synapse-interface/components/StateManagedBridge/hooks/useStaleQuoteUpdater.ts
new file mode 100644
index 0000000000..9a9cbf1444
--- /dev/null
+++ b/packages/synapse-interface/components/StateManagedBridge/hooks/useStaleQuoteUpdater.ts
@@ -0,0 +1,114 @@
+import { useEffect, useRef, useState } from 'react'
+
+import { BridgeQuote } from '@/utils/types'
+import { useIntervalTimer } from '@/utils/hooks/useIntervalTimer'
+
+export const useStaleQuoteUpdater = (
+ quote: BridgeQuote,
+ refreshQuoteCallback: () => Promise,
+ enabled: boolean,
+ staleTimeout: number = 15000, // in ms
+ autoRefreshDuration: number = 30000 // in ms
+) => {
+ const [isStale, setIsStale] = useState(false)
+ const autoRefreshIntervalRef = useRef(null)
+ const autoRefreshStartTimeRef = useRef(null)
+ const mouseMoveListenerRef = useRef void)>(null)
+ const manualRefreshRef = useRef(null)
+
+ useIntervalTimer(staleTimeout, !enabled)
+
+ const [mouseMoved, resetMouseMove] = useTrackMouseMove()
+
+ const clearManualRefreshTimeout = () => {
+ if (manualRefreshRef.current) {
+ clearTimeout(manualRefreshRef.current)
+ }
+ }
+
+ const clearAutoRefreshInterval = () => {
+ if (autoRefreshIntervalRef.current) {
+ clearInterval(autoRefreshIntervalRef.current)
+ }
+ }
+
+ const clearMouseMoveListener = () => {
+ if (mouseMoveListenerRef.current) {
+ mouseMoveListenerRef.current = null
+ }
+ }
+
+ useEffect(() => {
+ if (mouseMoved && autoRefreshStartTimeRef.current) {
+ autoRefreshStartTimeRef.current = null
+ resetMouseMove()
+ }
+ }, [quote])
+
+ // Start auto-refresh logic for ${autoRefreshDuration}ms seconds
+ useEffect(() => {
+ if (enabled) {
+ // If auto-refresh has not started yet, initialize the start time
+ if (autoRefreshStartTimeRef.current === null) {
+ autoRefreshStartTimeRef.current = Date.now()
+ }
+
+ const elapsedTime = Date.now() - autoRefreshStartTimeRef.current
+
+ // If ${autoRefreshDuration}ms hasn't passed, keep auto-refreshing
+ if (elapsedTime < autoRefreshDuration) {
+ clearManualRefreshTimeout()
+ clearAutoRefreshInterval()
+
+ autoRefreshIntervalRef.current = setInterval(() => {
+ refreshQuoteCallback()
+ }, staleTimeout)
+ } else {
+ // If more than ${autoRefreshDuration}ms have passed, stop auto-refreshing and switch to mousemove logic
+ clearAutoRefreshInterval()
+
+ manualRefreshRef.current = setTimeout(() => {
+ clearMouseMoveListener()
+ setIsStale(true)
+
+ const handleMouseMove = () => {
+ refreshQuoteCallback()
+ clearMouseMoveListener()
+ setIsStale(false)
+ }
+
+ document.addEventListener('mousemove', handleMouseMove, {
+ once: true,
+ })
+
+ mouseMoveListenerRef.current = handleMouseMove
+ }, staleTimeout)
+ }
+ }
+
+ return () => {
+ clearManualRefreshTimeout()
+ clearAutoRefreshInterval()
+ setIsStale(false)
+ }
+ }, [quote, enabled])
+
+ return isStale
+}
+
+export const useTrackMouseMove = (): [boolean, () => void] => {
+ const [moved, setMoved] = useState(false)
+
+ const onMove = () => setMoved(true)
+ const onReset = () => setMoved(false)
+
+ useEffect(() => {
+ document.addEventListener('mousemove', onMove)
+
+ return () => {
+ document.removeEventListener('mousemove', onMove)
+ }
+ }, [])
+
+ return [moved, onReset]
+}
diff --git a/packages/synapse-interface/components/_Transaction/_Transaction.tsx b/packages/synapse-interface/components/_Transaction/_Transaction.tsx
index 1a1158825d..1aea90352b 100644
--- a/packages/synapse-interface/components/_Transaction/_Transaction.tsx
+++ b/packages/synapse-interface/components/_Transaction/_Transaction.tsx
@@ -19,6 +19,7 @@ import { TransactionSupport } from './components/TransactionSupport'
import { RightArrow } from '@/components/icons/RightArrow'
import { Address } from 'viem'
import { useIsTxReverted } from './helpers/useIsTxReverted'
+import { useTxRefundStatus } from './helpers/useTxRefundStatus'
interface _TransactionProps {
connectedAddress: string
@@ -30,11 +31,12 @@ interface _TransactionProps {
destinationToken: Token
originTxHash: string
bridgeModuleName: string
+ routerAddress: string
estimatedTime: number // in seconds
timestamp: number
currentTime: number
kappa?: string
- status: 'pending' | 'completed' | 'reverted'
+ status: 'pending' | 'completed' | 'reverted' | 'refunded'
disabled: boolean
}
@@ -49,6 +51,7 @@ export const _Transaction = ({
destinationToken,
originTxHash,
bridgeModuleName,
+ routerAddress,
estimatedTime,
timestamp,
currentTime,
@@ -80,6 +83,7 @@ export const _Transaction = ({
isEstimatedTimeReached,
isCheckTxStatus,
isCheckTxForRevert,
+ isCheckTxForRefund,
} = calculateEstimatedTimeStatus(currentTime, timestamp, estimatedTime)
const [isTxCompleted, _kappa] = useBridgeTxStatus({
@@ -98,18 +102,29 @@ export const _Transaction = ({
isCheckTxForRevert && status === 'pending'
)
+ const isTxRefunded = useTxRefundStatus(
+ kappa,
+ routerAddress as Address,
+ originChain,
+ isCheckTxForRefund &&
+ status === 'pending' &&
+ bridgeModuleName === 'SynapseRFQ'
+ )
+
useBridgeTxUpdater(
connectedAddress,
destinationChain,
_kappa,
originTxHash,
isTxCompleted,
- isTxReverted
+ isTxReverted,
+ isTxRefunded
)
// Show transaction support if the transaction is delayed by more than 5 minutes and not finalized or reverted
const showTransactionSupport =
status === 'reverted' ||
+ status === 'refunded' ||
(status === 'pending' && delayedTimeInMin && delayedTimeInMin <= -5)
return (
@@ -184,7 +199,7 @@ export const _Transaction = ({
{status !== 'pending' && (