diff --git a/.changeset/lucky-crabs-float.md b/.changeset/lucky-crabs-float.md new file mode 100644 index 00000000..21bf8e09 --- /dev/null +++ b/.changeset/lucky-crabs-float.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': minor +--- + +Add expermiental erc20 abi strategy and change how sql strategy is defined diff --git a/.changeset/thick-bottles-cry.md b/.changeset/thick-bottles-cry.md new file mode 100644 index 00000000..e817c845 --- /dev/null +++ b/.changeset/thick-bottles-cry.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Fix timeout on strategy would stop fetching all strategies diff --git a/apps/web/src/lib/decode.ts b/apps/web/src/lib/decode.ts index 809f0e7d..e52ae9d8 100644 --- a/apps/web/src/lib/decode.ts +++ b/apps/web/src/lib/decode.ts @@ -1,18 +1,15 @@ import { getProvider, RPCProviderLive } from './rpc-provider' -import { Effect, Layer, ManagedRuntime } from 'effect' +import { Config, Effect, Layer, ManagedRuntime } from 'effect' import { DecodedTransaction, DecodeResult, decodeCalldata as calldataDecoder, decodeTransactionByHash, EtherscanV2StrategyResolver, - FetchTransactionError, + ExperimentalErc20AbiStrategyResolver, FourByteStrategyResolver, OpenchainStrategyResolver, - RPCFetchError, SourcifyStrategyResolver, - UnknownNetwork, - UnsupportedEvent, AbiStore, AbiParams, ContractAbiResult, @@ -20,6 +17,9 @@ import { ContractMetaParams, ContractMetaResult, PublicClient, + ERC20RPCStrategyResolver, + NFTRPCStrategyResolver, + ProxyRPCStrategyResolver, } from '@3loop/transaction-decoder' import { SqlAbiStore, SqlContractMetaStore } from '@3loop/transaction-decoder/sql' import { Hex } from 'viem' @@ -29,19 +29,33 @@ import { SqlClient } from '@effect/sql/SqlClient' import { ConfigError } from 'effect/ConfigError' import { SqlError } from '@effect/sql/SqlError' -const AbiStoreLive = SqlAbiStore.make({ - default: [ - EtherscanV2StrategyResolver({ - apikey: process.env.ETHERSCAN_API_KEY, - }), - SourcifyStrategyResolver(), - OpenchainStrategyResolver(), - FourByteStrategyResolver(), - ], -}) +const AbiStoreLive = Layer.unwrapEffect( + Effect.gen(function* () { + const service = yield* PublicClient + const apikey = yield* Config.withDefault(Config.string('ETHERSCAN_API_KEY'), undefined) + return SqlAbiStore.make({ + default: [ + EtherscanV2StrategyResolver({ + apikey: apikey, + }), + ExperimentalErc20AbiStrategyResolver(service), + OpenchainStrategyResolver(), + SourcifyStrategyResolver(), + FourByteStrategyResolver(), + ], + }) + }), +) -const MetaStoreLive = SqlContractMetaStore.make() +const MetaStoreLive = Layer.unwrapEffect( + Effect.gen(function* () { + const service = yield* PublicClient + return SqlContractMetaStore.make({ + default: [ERC20RPCStrategyResolver(service), NFTRPCStrategyResolver(service), ProxyRPCStrategyResolver(service)], + }) + }), +) const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive) const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive) const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer< diff --git a/packages/transaction-decoder/src/abi-loader.ts b/packages/transaction-decoder/src/abi-loader.ts index d1eeb44d..f43d0ab9 100644 --- a/packages/transaction-decoder/src/abi-loader.ts +++ b/packages/transaction-decoder/src/abi-loader.ts @@ -14,7 +14,6 @@ import { import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js' import { Abi } from 'viem' -const STRATEGY_TIMEOUT = 5000 export interface AbiParams { chainID: number address: string @@ -212,27 +211,30 @@ const AbiLoaderRequestResolver: Effect.Effect< ) // NOTE: Firstly we batch strategies by address because in a transaction most of events and traces are from the same abi - const response = yield* Effect.forEach(remaining, (req) => { - const strategyRequest = new GetContractABIStrategy({ - address: req.address, - chainID: req.chainID, - }) - - const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter( - (strategy) => strategy.type === 'address', - ) + const response = yield* Effect.forEach( + remaining, + (req) => { + const strategyRequest = new GetContractABIStrategy({ + address: req.address, + chainID: req.chainID, + }) + + const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter( + (strategy) => strategy.type === 'address', + ) - return Effect.validateFirst(allAvailableStrategies, (strategy) => - pipe( - Effect.request(strategyRequest, strategy.resolver), - Effect.withRequestCaching(true), - Effect.timeout(STRATEGY_TIMEOUT), - ), - ).pipe( - Effect.map(Either.left), - Effect.orElseSucceed(() => Either.right(req)), - ) - }) + return Effect.validateFirst(allAvailableStrategies, (strategy) => { + return pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true)) + }).pipe( + Effect.map(Either.left), + Effect.orElseSucceed(() => Either.right(req)), + ) + }, + { + concurrency: 'unbounded', + batching: true, + }, + ) const [addressStrategyResults, notFound] = Array.partitionMap(response, (res) => res) @@ -251,11 +253,7 @@ const AbiLoaderRequestResolver: Effect.Effect< // TODO: Distinct the errors and missing data, so we can retry on errors return Effect.validateFirst(allAvailableStrategies, (strategy) => - pipe( - Effect.request(strategyRequest, strategy.resolver), - Effect.withRequestCaching(true), - Effect.timeout(STRATEGY_TIMEOUT), - ), + pipe(Effect.request(strategyRequest, strategy.resolver), Effect.withRequestCaching(true)), ).pipe(Effect.orElseSucceed(() => null)) }) @@ -276,7 +274,11 @@ const AbiLoaderRequestResolver: Effect.Effect< Effect.forEach(group, (req) => Request.completeEffect(req, result), { discard: true }), ) }, - { discard: true }, + { + discard: true, + concurrency: 'unbounded', + batching: true, + }, ) }), ).pipe(RequestResolver.contextFromServices(AbiStore), Effect.withRequestCaching(true)) diff --git a/packages/transaction-decoder/src/abi-strategy/experimental-erc20.ts b/packages/transaction-decoder/src/abi-strategy/experimental-erc20.ts new file mode 100644 index 00000000..4d4156b4 --- /dev/null +++ b/packages/transaction-decoder/src/abi-strategy/experimental-erc20.ts @@ -0,0 +1,52 @@ +import * as RequestModel from './request-model.js' +import { Effect, RequestResolver } from 'effect' +import { PublicClient } from '../public-client.js' +import { erc20Abi, getAddress, getContract } from 'viem' + +const getLocalFragments = (service: PublicClient, { address, chainID }: RequestModel.GetContractABIStrategy) => + Effect.gen(function* () { + const client = yield* service + .getPublicClient(chainID) + .pipe( + Effect.catchAll(() => + Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID)), + ), + ) + + const inst = getContract({ + abi: erc20Abi, + address: getAddress(address), + client: client.client, + }) + + const decimals = yield* Effect.tryPromise({ + try: () => inst.read.decimals(), + catch: () => new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID), + }) + + if (decimals != null) { + return [ + { + type: 'address', + address, + chainID, + abi: JSON.stringify(erc20Abi), + }, + ] as RequestModel.ContractABI[] + } + + return yield* Effect.fail(new RequestModel.ResolveStrategyABIError('local-strategy', address, chainID)) + }) + +export const ExperimentalErc20AbiStrategyResolver = ( + service: PublicClient, +): RequestModel.ContractAbiResolverStrategy => { + return { + type: 'address', + resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) => + Effect.withSpan(getLocalFragments(service, req), 'AbiStrategy.ExperimentalErc20AbiStrategyResolver', { + attributes: { chainID: req.chainID, address: req.address }, + }), + ), + } +} diff --git a/packages/transaction-decoder/src/abi-strategy/index.ts b/packages/transaction-decoder/src/abi-strategy/index.ts index d0bac1e7..e45b0beb 100644 --- a/packages/transaction-decoder/src/abi-strategy/index.ts +++ b/packages/transaction-decoder/src/abi-strategy/index.ts @@ -1,6 +1,7 @@ export * from './blockscout-abi.js' export * from './etherscan-abi.js' export * from './etherscanv2-abi.js' +export * from './experimental-erc20.js' export * from './fourbyte-abi.js' export * from './openchain-abi.js' export * from './request-model.js' diff --git a/packages/transaction-decoder/src/contract-meta-loader.ts b/packages/transaction-decoder/src/contract-meta-loader.ts index f6dfbfc1..538ddd1c 100644 --- a/packages/transaction-decoder/src/contract-meta-loader.ts +++ b/packages/transaction-decoder/src/contract-meta-loader.ts @@ -3,8 +3,6 @@ import { ContractData } from './types.js' import { GetContractMetaStrategy } from './meta-strategy/request-model.js' import { Address } from 'viem' -const STRATEGY_TIMEOUT = 5000 - export interface ContractMetaParams { address: string chainID: number @@ -166,10 +164,9 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests: const allAvailableStrategies = Array.prependAll(strategies.default, strategies[chainID] ?? []) // TODO: Distinct the errors and missing data, so we can retry on errors - return Effect.validateFirst(allAvailableStrategies, (strategy) => Effect.request(strategyRequest, strategy)).pipe( - Effect.timeout(STRATEGY_TIMEOUT), - Effect.orElseSucceed(() => null), - ) + return Effect.validateFirst(allAvailableStrategies, (strategy) => + pipe(Effect.request(strategyRequest, strategy), Effect.withRequestCaching(true)), + ).pipe(Effect.orElseSucceed(() => null)) }) // Store results and resolve pending requests diff --git a/packages/transaction-decoder/src/sql/contract-meta-store.ts b/packages/transaction-decoder/src/sql/contract-meta-store.ts index e78e642e..93df6058 100644 --- a/packages/transaction-decoder/src/sql/contract-meta-store.ts +++ b/packages/transaction-decoder/src/sql/contract-meta-store.ts @@ -1,24 +1,14 @@ import { SqlClient } from '@effect/sql' import { Effect, Layer } from 'effect' -import { - ContractData, - ContractMetaStore, - ERC20RPCStrategyResolver, - NFTRPCStrategyResolver, - ProxyRPCStrategyResolver, - PublicClient, -} from '../effect.js' +import { ContractData, ContractMetaStore } from '../effect.js' -export const make = () => +export const make = (strategies: ContractMetaStore['strategies']) => Layer.effect( ContractMetaStore, Effect.gen(function* () { const sql = yield* SqlClient.SqlClient - const publicClient = yield* PublicClient - const table = sql('_loop_decoder_contract_meta_') - // TODO; add timestamp to the table yield* sql` CREATE TABLE IF NOT EXISTS ${table} ( address TEXT NOT NULL, @@ -37,13 +27,7 @@ export const make = () => ) return ContractMetaStore.of({ - strategies: { - default: [ - ERC20RPCStrategyResolver(publicClient), - NFTRPCStrategyResolver(publicClient), - ProxyRPCStrategyResolver(publicClient), - ], - }, + strategies, set: (key, value) => Effect.gen(function* () { if (value.status === 'success') {