Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .changeset/fresh-candles-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
'@3loop/transaction-decoder': minor
---

BREAKING CHANGE: Changing the interface we use to declare ABI strategies and adding rate limiter per strategy. The Breaking Change will only affect you if you are declaring the store layers manually, if you are using the default stores you will not be affected.

Example usage:

```ts
EtherscanV2StrategyResolver({
apikey: apikey,
rateLimit: { limit: 5, interval: '2 seconds', algorithm: 'fixed-window' },
})
```
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
```ts
export interface AbiStore<Key = AbiParams, Value = ContractAbiResult> {
export interface AbiStore {
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
readonly set: (key: Key, value: Value) => Effect.Effect<void, never>
readonly get: (arg: Key) => Effect.Effect<Value, never>
readonly getMany?: (arg: Array<Key>) => Effect.Effect<Array<Value>, never>
readonly set: (key: AbiParams, value: ContractAbiResult) => Effect.Effect<void, never>
readonly get: (arg: AbiParams) => Effect.Effect<ContractAbiResult, never>
readonly getMany?: (arg: Array<AbiParams>) => Effect.Effect<Array<ContractAbiResult>, never>
}
```
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
```ts
export interface ContractMetaStore<Key = ContractMetaParams, Value = ContractMetaResult> {
export interface ContractMetaStore {
readonly strategies: Record<ChainOrDefault, readonly ContractMetaResolverStrategy[]>
readonly set: (arg: Key, value: Value) => Effect.Effect<void, never>
readonly get: (arg: Key) => Effect.Effect<Value, never>
readonly getMany?: (arg: Array<Key>) => Effect.Effect<Array<Value>, never>
readonly set: (arg: ContractMetaParams, value: ContractMetaResult) => Effect.Effect<void, never>
readonly get: (arg: ContractMetaParams) => Effect.Effect<ContractMetaResult, never>
readonly getMany?: (arg: Array<ContractMetaParams>) => Effect.Effect<Array<ContractMetaResult>, never>
}
```
20 changes: 1 addition & 19 deletions apps/web/src/lib/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ import {
FourByteStrategyResolver,
OpenchainStrategyResolver,
SourcifyStrategyResolver,
AbiStore,
AbiParams,
ContractAbiResult,
ContractMetaStore,
ContractMetaParams,
ContractMetaResult,
PublicClient,
ERC20RPCStrategyResolver,
NFTRPCStrategyResolver,
Expand All @@ -24,10 +18,6 @@ import {
import { SqlAbiStore, SqlContractMetaStore } from '@3loop/transaction-decoder/sql'
import { Hex } from 'viem'
import { DatabaseLive } from './database'
import { PgClient } from '@effect/sql-pg/PgClient'
import { SqlClient } from '@effect/sql/SqlClient'
import { ConfigError } from 'effect/ConfigError'
import { SqlError } from '@effect/sql/SqlError'

const AbiStoreLive = Layer.unwrapEffect(
Effect.gen(function* () {
Expand Down Expand Up @@ -60,15 +50,7 @@ const CacheLayer = Layer.setRequestCache(Request.makeCache({ capacity: 100, time
const DataLayer = Layer.mergeAll(RPCProviderLive, DatabaseLive)
const LoadersLayer = Layer.mergeAll(AbiStoreLive, MetaStoreLive)

const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer) as Layer.Layer<
| AbiStore<AbiParams, ContractAbiResult>
| ContractMetaStore<ContractMetaParams, ContractMetaResult>
| PublicClient
| PgClient
| SqlClient,
ConfigError | SqlError,
never
>
const MainLayer = Layer.provideMerge(LoadersLayer, DataLayer)

const runtime = ManagedRuntime.make(Layer.provide(MainLayer, CacheLayer))

Expand Down
83 changes: 41 additions & 42 deletions packages/eslint-config-custom/library.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,46 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
env: {
browser: true,
es6: true,
node: true,
},
extends: [
"turbo",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:sort-export-all/recommended",
"plugin:prettier/recommended",
env: {
browser: true,
es6: true,
node: true,
},
extends: [
"turbo",
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:sort-export-all/recommended",
],
parser: "@typescript-eslint/parser",
plugins: [
"@typescript-eslint",
"prettier",
"sort-export-all",
],
settings: {
"import/resolver": { typescript: true, node: true },
"import/parsers": { "@typescript-eslint/parser": [".ts", ".tsx"] },
},
rules: {
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
'@typescript-eslint/strict-boolean-expressions': [
'error',
{
allowString: false,
allowNullableObject: false,
allowNumber: false,
allowNullableBoolean: true,
},
],
parser: "@typescript-eslint/parser",
plugins: [
"@typescript-eslint",
"prettier",
"sort-export-all",
"@typescript-eslint/strict-boolean-expressions": "off",
"object-shorthand": ["warn", "always"],
"eqeqeq": [
'error',
'always',
{
null: 'never',
},
],
settings: {
"import/resolver": { typescript: true, node: true },
"import/parsers": { "@typescript-eslint/parser": [".ts", ".tsx"] },
},
rules: {
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-explicit-any": "off",
'@typescript-eslint/strict-boolean-expressions': [
'error',
{
allowString: false,
allowNullableObject: false,
allowNumber: false,
allowNullableBoolean: true,
},
],
"@typescript-eslint/strict-boolean-expressions": "off",
"object-shorthand": ["warn", "always"],
"eqeqeq": [
'error',
'always',
{
null: 'never',
},
],
},
},
};
92 changes: 16 additions & 76 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,7 @@
import {
Context,
Effect,
Either,
RequestResolver,
Request,
Array,
pipe,
Data,
PrimaryKey,
Schema,
SchemaAST,
} from 'effect'
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategy } from './abi-strategy/request-model.js'
import { Effect, Either, RequestResolver, Request, Array, pipe, Data, PrimaryKey, Schema, SchemaAST } from 'effect'
import { ContractABI } from './abi-strategy/request-model.js'
import { Abi } from 'viem'

export interface AbiParams {
chainID: number
address: string
event?: string | undefined
signature?: string | undefined
}

export interface ContractAbiSuccess {
status: 'success'
result: ContractABI
}

export interface ContractAbiNotFound {
status: 'not-found'
result: null
}

export interface ContractAbiEmpty {
status: 'empty'
result: null
}

export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty

type ChainOrDefault = number | 'default'

export interface AbiStore<Key = AbiParams, Value = ContractAbiResult> {
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
readonly set: (key: Key, value: Value) => Effect.Effect<void, never>
readonly get: (arg: Key) => Effect.Effect<Value, never>
readonly getMany?: (arg: Array<Key>) => Effect.Effect<Array<Value>, never>
}

export const AbiStore = Context.GenericTag<AbiStore>('@3loop-decoder/AbiStore')
import { AbiParams, AbiStore } from './abi-store.js'

interface LoadParameters {
readonly chainID: number
Expand Down Expand Up @@ -174,7 +128,7 @@ const getBestMatch = (abi: ContractABI | null) => {
const AbiLoaderRequestResolver: Effect.Effect<
RequestResolver.RequestResolver<AbiLoader, never>,
never,
AbiStore<AbiParams, ContractAbiResult>
AbiStore
> = RequestResolver.makeBatched((requests: Array<AbiLoader>) =>
Effect.gen(function* () {
if (requests.length === 0) return
Expand Down Expand Up @@ -218,27 +172,19 @@ const AbiLoaderRequestResolver: Effect.Effect<
const allAvailableStrategies = Array.prependAll(strategies.default, strategies[req.chainID] ?? []).filter(
(strategy) => strategy.type === 'address',
)

return Effect.validateFirst(allAvailableStrategies, (strategy) => {
return pipe(
Effect.request(
new GetContractABIStrategy({
address: req.address,
chainId: req.chainID,
strategyId: strategy.id,
}),
strategy.resolver,
),
Effect.withRequestCaching(true),
)
return strategy.resolver({
address: req.address,
chainId: req.chainID,
strategyId: strategy.id,
})
}).pipe(
Effect.map(Either.left),
Effect.orElseSucceed(() => Either.right(req)),
)
},
{
concurrency: 'unbounded',
batching: true,
},
)

Expand All @@ -254,19 +200,13 @@ 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(
new GetContractABIStrategy({
address,
chainId: chainID,
event,
signature,
strategyId: strategy.id,
}),
strategy.resolver,
),
Effect.withRequestCaching(true),
),
strategy.resolver({
address,
chainId: chainID,
event,
signature,
strategyId: strategy.id,
}),
).pipe(Effect.orElseSucceed(() => null))
},
{
Expand Down
69 changes: 69 additions & 0 deletions packages/transaction-decoder/src/abi-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Context, Effect, RateLimiter, Function, Layer } from 'effect'
import { ContractABI, ContractAbiResolverStrategy, GetContractABIStrategyParams } from './abi-strategy/request-model.js'

export interface AbiParams {
chainID: number
address: string
event?: string | undefined
signature?: string | undefined
}

export interface ContractAbiSuccess {
status: 'success'
result: ContractABI
}

export interface ContractAbiNotFound {
status: 'not-found'
result: null
}

export interface ContractAbiEmpty {
status: 'empty'
result: null
}

export type ContractAbiResult = ContractAbiSuccess | ContractAbiNotFound | ContractAbiEmpty

type ChainOrDefault = number | 'default'

export interface AbiStore {
readonly strategies: Record<ChainOrDefault, readonly ContractAbiResolverStrategy[]>
readonly set: (key: AbiParams, value: ContractAbiResult) => Effect.Effect<void, never>
readonly get: (arg: AbiParams) => Effect.Effect<ContractAbiResult, never>
readonly getMany?: (arg: Array<AbiParams>) => Effect.Effect<Array<ContractAbiResult>, never>
}

export const AbiStore = Context.GenericTag<AbiStore>('@3loop-decoder/AbiStore')

export const make = ({ strategies: strategiesWithoutRateLimit, ...rest }: AbiStore) =>
Effect.gen(function* () {
const strategies = yield* Effect.reduce(
Object.entries(strategiesWithoutRateLimit),
{} as Record<ChainOrDefault, ContractAbiResolverStrategy[]>,
(acc, [chainID, specific]) =>
Effect.gen(function* () {
const all = yield* Effect.forEach(specific, (strategy) =>
Effect.gen(function* () {
const rateLimit = strategy.rateLimit ? yield* RateLimiter.make(strategy.rateLimit) : Function.identity

return yield* Effect.succeed({
...strategy,
resolver: (params: GetContractABIStrategyParams) => strategy.resolver(params).pipe(rateLimit),
})
}),
)

return {
...acc,
[chainID]: all,
}
}),
)
return {
strategies,
...rest,
}
})

export const layer = (args: AbiStore) => Layer.scoped(AbiStore, make(args))
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Effect, RequestResolver } from 'effect'
import { Effect } from 'effect'
import * as RequestModel from './request-model.js'

async function fetchContractABI(
{ address, chainId }: RequestModel.GetContractABIStrategy,
{ address, chainId }: RequestModel.GetContractABIStrategyParams,
config: { apikey?: string; endpoint: string },
): Promise<RequestModel.ContractABI[]> {
const endpoint = config.endpoint
Expand Down Expand Up @@ -43,7 +43,7 @@ export const BlockscoutStrategyResolver = (config: {
return {
id: 'blockscout-strategy',
type: 'address',
resolver: RequestResolver.fromEffect((req: RequestModel.GetContractABIStrategy) =>
resolver: (req: RequestModel.GetContractABIStrategyParams) =>
Effect.withSpan(
Effect.tryPromise({
try: () => fetchContractABI(req, config),
Expand All @@ -52,6 +52,5 @@ export const BlockscoutStrategyResolver = (config: {
'AbiStrategy.BlockscoutStrategyResolver',
{ attributes: { chainId: req.chainId, address: req.address } },
),
),
}
}
Loading
Loading