0xtrails
Version:
SDK for Trails
514 lines (459 loc) • 12.2 kB
text/typescript
import { createPublicClient, http } from "viem"
import { useQuery } from "@tanstack/react-query"
import { getChainInfo } from "./chains.js"
import {
getSequenceProjectAccessKey,
getSequenceEnv,
getSequenceApiUrl,
} from "./config.js"
import type { GuestModuleEvent, TrailsTokenSweeperEvent } from "./decoders.js"
import {
arbitrum,
base,
baseSepolia,
optimism,
polygon,
mainnet,
apeChain,
arbitrumNova,
avalanche,
b3,
blast,
gnosis,
soneium,
xai,
bsc,
etherlink,
katana,
} from "viem/chains"
import { logger } from "./logger.js"
import { bigintReplacer } from "./utils.js"
import { getExplorerUrl } from "./explorer.js"
export type TransactionStateStatus =
| "pending"
| "failed"
| "confirmed"
| "aborted"
export type TransactionState = {
transactionHash: string
explorerUrl: string
blockNumber?: number
chainId: number
state: TransactionStateStatus
label: string
decodedTrailsTokenSweeperEvents?: TrailsTokenSweeperEvent[]
decodedGuestModuleEvents?: GuestModuleEvent[]
refunded?: boolean
}
export type TransferType = "SEND" | "RECEIVE"
export type ContractType = "ERC20" | "ERC721" | "ERC1155" | "NATIVE"
export type ContractInfo = {
chainId: number
address: string
source: string
name: string
type: ContractType
symbol: string
decimals: number
logoURI: string
deployed: boolean
bytecodeHash: string
extensions: {
link: string
description: string
ogImage: string
ogName: string
originChainId: number
originAddress: string
verified: boolean
verifiedBy: string
}
updatedAt: string
queuedAt: string
status: string
}
export type Transfer = {
transferType: TransferType
contractAddress: string
contractType: ContractType
from: string
to: string
tokenIds: string[]
amounts: string[]
logIndex: number
contractInfo: ContractInfo
}
export type TransactionHistoryItemFromAPI = {
txnHash: string
blockNumber: number
blockHash: string
chainId: number
metaTxnID: string | null
transfers: Transfer[]
timestamp: string
}
export type TransactionHistoryItem = TransactionHistoryItemFromAPI & {
explorerUrl?: string
chainName?: string
}
export type TransactionHistoryResponse = {
page: {
column: string
pageSize: number
more: boolean
}
transactions: TransactionHistoryItem[]
}
export type GetAccountTransactionHistoryParams = {
chainId: number
accountAddress: string
pageSize?: number
includeMetadata?: boolean
page?: number
}
// Standalone function to calculate time difference between two transactions
export async function getTxTimeDiff(
firstTx?: TransactionState,
lastTx?: TransactionState,
): Promise<number> {
if (!firstTx?.chainId || !lastTx?.chainId || !firstTx || !lastTx) {
return 0
}
if (
!(firstTx.blockNumber || firstTx.transactionHash) ||
!(lastTx.blockNumber || lastTx.transactionHash)
) {
return 0
}
if (
firstTx.blockNumber === lastTx.blockNumber &&
firstTx.transactionHash === lastTx.transactionHash
) {
return 0
}
const firstChainInfo = getChainInfo(firstTx.chainId)
const lastChainInfo = getChainInfo(lastTx.chainId)
if (!firstChainInfo || !lastChainInfo) return 0
const firstClient = createPublicClient({
chain: firstChainInfo,
transport: http(),
})
const lastClient = createPublicClient({
chain: lastChainInfo,
transport: http(),
})
async function getBlockNumber(
client: ReturnType<typeof createPublicClient>,
tx: TransactionState,
) {
if (tx.blockNumber) return BigInt(tx.blockNumber)
const receipt = await client.getTransactionReceipt({
hash: tx.transactionHash as `0x${string}`,
})
return receipt.blockNumber
}
async function getTimestamp(
client: ReturnType<typeof createPublicClient>,
blockNumber: bigint,
) {
const block = await client.getBlock({ blockNumber })
return typeof block.timestamp === "bigint"
? Number(block.timestamp)
: block.timestamp
}
try {
const [firstBlockNumber, lastBlockNumber] = await Promise.all([
getBlockNumber(firstClient, firstTx),
getBlockNumber(lastClient, lastTx),
])
const [firstTs, lastTs] = await Promise.all([
getTimestamp(firstClient, firstBlockNumber),
getTimestamp(lastClient, lastBlockNumber),
])
const diff = lastTs - firstTs
if (diff < 1) {
return 1 // round up to 1 second
}
return diff
} catch (error) {
logger.console.error(
"[trails-sdk] Error calculating transaction time difference:",
error,
)
return 0
}
}
export function getIndexerUrlFromChainSlug(chainSlug: string) {
const env = getSequenceEnv()
let envPrefix = ""
if (env === "dev") {
envPrefix = "dev-"
}
return `https://${envPrefix}${chainSlug}-indexer.sequence.app`
}
const chainIdToIndexerUrl = {
[arbitrum.id]: getIndexerUrlFromChainSlug("arbitrum"),
[base.id]: getIndexerUrlFromChainSlug("base"),
[baseSepolia.id]: getIndexerUrlFromChainSlug("base-sepolia"),
[optimism.id]: getIndexerUrlFromChainSlug("optimism"),
[polygon.id]: getIndexerUrlFromChainSlug("polygon"),
[mainnet.id]: getIndexerUrlFromChainSlug("mainnet"),
[apeChain.id]: getIndexerUrlFromChainSlug("apechain"),
[arbitrumNova.id]: getIndexerUrlFromChainSlug("arbitrum-nova"),
[avalanche.id]: getIndexerUrlFromChainSlug("avalanche"),
[b3.id]: getIndexerUrlFromChainSlug("b3"),
[blast.id]: getIndexerUrlFromChainSlug("blast"),
[gnosis.id]: getIndexerUrlFromChainSlug("gnosis"),
[soneium.id]: getIndexerUrlFromChainSlug("soneium"),
[xai.id]: getIndexerUrlFromChainSlug("xai"),
[bsc.id]: getIndexerUrlFromChainSlug("bsc"),
421613: getIndexerUrlFromChainSlug("arbitrum-nova-sepolia"),
[etherlink.id]: getIndexerUrlFromChainSlug("etherlink"),
[katana.id]: getIndexerUrlFromChainSlug("katana"),
}
export async function getAccountTransactionHistory({
chainId,
accountAddress,
pageSize = 10,
page = 1,
includeMetadata = true,
}: GetAccountTransactionHistoryParams): Promise<TransactionHistoryResponse> {
const accessKey = getSequenceProjectAccessKey()
if (!accessKey) {
throw new Error("Sequence project access key is required")
}
// Get the chain-specific indexer URL
const chainIndexerUrl =
chainIdToIndexerUrl[chainId as keyof typeof chainIdToIndexerUrl]
if (!chainIndexerUrl) {
throw new Error(`Unsupported chain ID: ${chainId}`)
}
const endpoint = `${chainIndexerUrl}/rpc/Indexer/GetTransactionHistory`
const requestBody = {
filter: {
accountAddress: accountAddress.toLowerCase(),
},
includeMetadata,
page: {
page,
pageSize,
},
}
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Access-Key": accessKey,
},
body: JSON.stringify(requestBody, bigintReplacer, 2),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data: TransactionHistoryResponse = await response.json()
const transactions = data.transactions.map((tx) => ({
...tx,
explorerUrl: getExplorerUrl({
txHash: tx.txnHash,
chainId: tx.chainId,
}),
chainName: getChainInfo(tx.chainId)?.name,
}))
return {
...data,
transactions,
}
} catch (error) {
logger.console.error(
"[trails-sdk] Error fetching transaction history:",
error,
)
throw error
}
}
export type IntentTransaction = {
originIntentAddress: string
destinationIntentAddress: string
mainSigner: string
metaTxnId?: string
txnHash?: string
executionStatus?: string
originChainId?: number
destinationChainId?: number
originTokenAddress?: string
originTokenAmount?: string
destinationTokenAddress?: string
destinationTokenAmount?: string
destinationToAddress?: string
createdAt?: string
// Enriched token information
originToken?: {
symbol: string
name: string
decimals: number
imageUrl: string
chainId: number
}
destinationToken?: {
symbol: string
name: string
decimals: number
imageUrl: string
chainId: number
}
}
export type IntentTransactionHistoryResponse = {
page?: {
page?: number
pageSize?: number
more?: boolean
}
transactions: IntentTransaction[]
}
export type GetIntentTransactionHistoryParams = {
accountAddress: string
pageSize?: number
page?: number
}
export async function getIntentTransactionHistory({
accountAddress,
pageSize = 10,
page = 1,
}: GetIntentTransactionHistoryParams): Promise<IntentTransactionHistoryResponse> {
const accessKey = getSequenceProjectAccessKey()
if (!accessKey) {
throw new Error("Sequence project access key is required")
}
const apiUrl = getSequenceApiUrl()
const endpoint = `${apiUrl}/rpc/API/GetIntentTransactionHistory`
const requestBody = {
accountAddress: accountAddress.toLowerCase(),
page: {
page,
pageSize,
sort: [
{
column: "created_at",
order: "DESC",
},
],
},
}
try {
const response = await fetch(endpoint, {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Access-Key": accessKey,
},
body: JSON.stringify(requestBody, bigintReplacer, 2),
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const data: IntentTransactionHistoryResponse = await response.json()
return data
} catch (error) {
logger.console.error(
"[trails-sdk] Error fetching intent transaction history:",
error,
)
throw error
}
}
export type UseAccountTransactionHistoryParams = {
chainId?: number | null
accountAddress?: string | null
pageSize?: number
page?: number
includeMetadata?: boolean
}
export type UseAccountTransactionHistoryReturn = {
data: TransactionHistoryResponse | undefined
isLoading: boolean
error: Error | null
}
export function useAccountTransactionHistory({
chainId,
accountAddress,
pageSize = 10,
page = 1,
includeMetadata = true,
}: UseAccountTransactionHistoryParams): UseAccountTransactionHistoryReturn {
const { data, isLoading, error } = useQuery({
queryKey: [
"accountTransactionHistory",
chainId,
accountAddress,
pageSize,
page,
includeMetadata,
],
queryFn: async () => {
if (!chainId || !accountAddress) {
return undefined
}
return await getAccountTransactionHistory({
chainId,
accountAddress,
pageSize,
page,
includeMetadata,
})
},
enabled: Boolean(chainId && accountAddress),
staleTime: 30 * 1000, // Consider data fresh for 30 seconds
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: false,
retry: 2,
refetchOnReconnect: true,
})
return {
data,
isLoading,
error: error as Error | null,
}
}
export type UseIntentTransactionHistoryParams = {
accountAddress?: string | null
pageSize?: number
page?: number
}
export type UseIntentTransactionHistoryReturn = {
data: IntentTransactionHistoryResponse | undefined
isLoading: boolean
error: Error | null
}
export function useIntentTransactionHistory({
accountAddress,
pageSize = 10,
page = 1,
}: UseIntentTransactionHistoryParams): UseIntentTransactionHistoryReturn {
const { data, isLoading, error } = useQuery({
queryKey: ["intentTransactionHistory", accountAddress, pageSize, page],
queryFn: async () => {
if (!accountAddress) {
return undefined
}
return await getIntentTransactionHistory({
accountAddress,
pageSize,
page,
})
},
enabled: Boolean(accountAddress),
staleTime: 30 * 1000, // Consider data fresh for 30 seconds
refetchOnWindowFocus: false,
refetchOnMount: false,
refetchInterval: false,
retry: 2,
refetchOnReconnect: true,
})
return {
data,
isLoading,
error: error as Error | null,
}
}