Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use TaggedRequest for request to allow for equality check in cache #158

Merged
merged 2 commits into from
Nov 29, 2024
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
5 changes: 5 additions & 0 deletions .changeset/green-elephants-guess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': patch
---

Use TaggedRequest for request to allow for equality check for in-memory caching of requests
5 changes: 5 additions & 0 deletions .changeset/wet-mirrors-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': minor
---

Fix sql stores syntax
38 changes: 30 additions & 8 deletions packages/transaction-decoder/src/abi-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import { Context, Effect, Either, RequestResolver, Request, Array, pipe, Data } from 'effect'
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 { Abi } from 'viem'

Expand Down Expand Up @@ -63,12 +75,22 @@ export class EmptyCalldataError extends Data.TaggedError('DecodeError')<
}
}

export interface AbiLoader extends Request.Request<Abi, MissingABIError>, LoadParameters {
_tag: 'AbiLoader'
class SchemaAbi extends Schema.make<Abi>(SchemaAST.objectKeyword) {}
class AbiLoader extends Schema.TaggedRequest<AbiLoader>()('AbiLoader', {
failure: Schema.instanceOf(MissingABIError),
success: SchemaAbi, // Abi
payload: {
chainID: Schema.Number,
address: Schema.String,
event: Schema.optional(Schema.String),
signature: Schema.optional(Schema.String),
},
}) {
[PrimaryKey.symbol]() {
return `abi::${this.chainID}:${this.address}:${this.event}:${this.signature}`
}
}

const AbiLoader = Request.tagged<AbiLoader>('AbiLoader')

function makeRequestKey(key: AbiLoader) {
return `abi::${key.chainID}:${key.address}:${key.event}:${key.signature}`
}
Expand Down Expand Up @@ -191,7 +213,7 @@ 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 = GetContractABIStrategy({
const strategyRequest = new GetContractABIStrategy({
address: req.address,
chainID: req.chainID,
})
Expand All @@ -216,7 +238,7 @@ const AbiLoaderRequestResolver: Effect.Effect<

// NOTE: Secondly we request strategies to fetch fragments
const fragmentStrategyResults = yield* Effect.forEach(notFound, ({ chainID, address, event, signature }) => {
const strategyRequest = GetContractABIStrategy({
const strategyRequest = new GetContractABIStrategy({
address,
chainID,
event,
Expand Down Expand Up @@ -271,7 +293,7 @@ export const getAndCacheAbi = (params: AbiParams) =>
return yield* Effect.fail(new EmptyCalldataError(params))
}

return yield* Effect.request(AbiLoader(params), AbiLoaderRequestResolver)
return yield* Effect.request(new AbiLoader(params), AbiLoaderRequestResolver)
}).pipe(
Effect.withSpan('AbiLoader.GetAndCacheAbi', {
attributes: {
Expand Down
27 changes: 17 additions & 10 deletions packages/transaction-decoder/src/abi-strategy/request-model.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Request, RequestResolver } from 'effect'
import { PrimaryKey, RequestResolver, Schema, SchemaAST } from 'effect'

export interface FetchABIParams {
readonly chainID: number
Expand Down Expand Up @@ -41,16 +41,23 @@ interface AddressABI {

export type ContractABI = FunctionFragmentABI | EventFragmentABI | AddressABI

// NOTE: We might want to return a list of ABIs, this might be helpful when fetching for signature
export interface GetContractABIStrategy
extends Request.Request<ContractABI[], ResolveStrategyABIError>,
FetchABIParams {
readonly _tag: 'GetContractABIStrategy'
}

export const GetContractABIStrategy = Request.tagged<GetContractABIStrategy>('GetContractABIStrategy')

export interface ContractAbiResolverStrategy {
type: 'address' | 'fragment'
resolver: RequestResolver.RequestResolver<GetContractABIStrategy, never>
}

class SchemaContractAbi extends Schema.make<ContractABI>(SchemaAST.objectKeyword) {}
export class GetContractABIStrategy extends Schema.TaggedRequest<GetContractABIStrategy>()('GetContractABIStrategy', {
failure: Schema.instanceOf(ResolveStrategyABIError),
success: Schema.Array(SchemaContractAbi),
payload: {
chainID: Schema.Number,
address: Schema.String,
event: Schema.optional(Schema.String),
signature: Schema.optional(Schema.String),
},
}) {
[PrimaryKey.symbol]() {
return `abi-strategy::${this.chainID}:${this.address}:${this.event}:${this.signature}`
}
}
26 changes: 17 additions & 9 deletions packages/transaction-decoder/src/contract-meta-loader.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Context, Effect, RequestResolver, Request, Array, Either, pipe } from 'effect'
import { Context, Effect, RequestResolver, Request, Array, Either, pipe, Schema, PrimaryKey, SchemaAST } from 'effect'
import { ContractData } from './types.js'
import { GetContractMetaStrategy } from './meta-strategy/request-model.js'
import { Address } from 'viem'
Expand Down Expand Up @@ -50,14 +50,22 @@ export interface ContractMetaStore<Key = ContractMetaParams, Value = ContractMet

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

export interface ContractMetaLoader extends Request.Request<ContractData | null, never> {
_tag: 'ContractMetaLoader'
address: Address
chainID: number
class SchemaContractData extends Schema.make<ContractData>(SchemaAST.objectKeyword) {}
class SchemaAddress extends Schema.make<Address>(SchemaAST.stringKeyword) {}

class ContractMetaLoader extends Schema.TaggedRequest<ContractMetaLoader>()('ContractMetaLoader', {
failure: Schema.Never,
success: Schema.NullOr(SchemaContractData),
payload: {
address: SchemaAddress,
chainID: Schema.Number,
},
}) {
[PrimaryKey.symbol]() {
return `contract-meta::${this.chainID}:${this.address}`
}
}

const ContractMetaLoader = Request.tagged<ContractMetaLoader>('ContractMetaLoader')

function makeKey(key: ContractMetaLoader) {
return `contract-meta::${key.chainID}:${key.address}`
}
Expand Down Expand Up @@ -148,7 +156,7 @@ const ContractMetaLoaderRequestResolver = RequestResolver.makeBatched((requests:

// Fetch ContractMeta from the strategies
const strategyResults = yield* Effect.forEach(remaining, ({ chainID, address }) => {
const strategyRequest = GetContractMetaStrategy({
const strategyRequest = new GetContractMetaStrategy({
address,
chainID,
})
Expand Down Expand Up @@ -186,7 +194,7 @@ export const getAndCacheContractMeta = ({
readonly address: Address
}) => {
return Effect.withSpan(
Effect.request(ContractMetaLoader({ chainID, address }), ContractMetaLoaderRequestResolver),
Effect.request(new ContractMetaLoader({ chainID, address }), ContractMetaLoaderRequestResolver),
'GetAndCacheContractMeta',
{ attributes: { chainID, address } },
)
Expand Down
25 changes: 17 additions & 8 deletions packages/transaction-decoder/src/meta-strategy/request-model.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { UnknownNetwork } from '../public-client.js'
import { ContractData } from '../types.js'
import { Request } from 'effect'
import { PrimaryKey, Schema, SchemaAST } from 'effect'
import { Address } from 'viem'

export interface FetchMetaParams {
Expand All @@ -17,11 +17,20 @@ export class ResolveStrategyMetaError {
) {}
}

// TODO: Remove UnknownNetwork
export interface GetContractMetaStrategy
extends Request.Request<ContractData, ResolveStrategyMetaError | UnknownNetwork>,
FetchMetaParams {
readonly _tag: 'GetContractMetaStrategy'
class SchemaAddress extends Schema.make<Address>(SchemaAST.stringKeyword) {}
class SchemaContractData extends Schema.make<ContractData>(SchemaAST.objectKeyword) {}
export class GetContractMetaStrategy extends Schema.TaggedRequest<GetContractMetaStrategy>()(
'GetContractMetaStrategy',
{
failure: Schema.Union(Schema.instanceOf(ResolveStrategyMetaError), Schema.instanceOf(UnknownNetwork)),
success: SchemaContractData,
payload: {
chainID: Schema.Number,
address: SchemaAddress,
},
},
) {
[PrimaryKey.symbol]() {
return `contract-meta-strategy::${this.chainID}:${this.address}`
}
}

export const GetContractMetaStrategy = Request.tagged<GetContractMetaStrategy>('GetContractMetaStrategy')
77 changes: 60 additions & 17 deletions packages/transaction-decoder/src/sql/abi-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@ export const make = (strategies: AbiStore['strategies']) =>
Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient

const table = sql('loop_decoder_contract_abi__')

// TODO; add timestamp to the table
yield* sql`
CREATE TABLE IF NOT EXISTS contractAbi (
CREATE TABLE IF NOT EXISTS ${table} (
type TEXT NOT NULL,
address TEXT,
event TEXT,
Expand All @@ -18,7 +21,10 @@ export const make = (strategies: AbiStore['strategies']) =>
abi TEXT,
status TEXT NOT NULL
)
`.pipe(Effect.catchAll(() => Effect.dieMessage('Failed to create contractAbi table')))
`.pipe(
Effect.tapError(Effect.logError),
Effect.catchAll(() => Effect.dieMessage('Failed to create contractAbi table')),
)

return AbiStore.of({
strategies,
Expand All @@ -28,33 +34,70 @@ export const make = (strategies: AbiStore['strategies']) =>
if (value.status === 'success' && value.result.type === 'address') {
const result = value.result
yield* sql`
INSERT INTO contractAbi (type, address, chain, abi, status)
VALUES (${result.type}, ${normalizedAddress}, ${result.chainID}, ${result.abi}, "success")
INSERT INTO ${table}
${sql.insert([
{
type: result.type,
address: normalizedAddress,
chain: key.chainID,
abi: result.abi,
status: 'success',
},
])}
`
} else if (value.status === 'success') {
const result = value.result
yield* sql`
INSERT INTO contractAbi (type, event, signature, abi, status)
VALUES (${result.type}, ${'event' in result ? result.event : null}, ${
'signature' in result ? result.signature : null
}, ${result.abi}, "success")
INSERT INTO ${table}
${sql.insert([
{
type: result.type,
event: 'event' in result ? result.event : null,
signature: 'signature' in result ? result.signature : null,
abi: result.abi,
status: 'success',
},
])}
`
} else {
yield* sql`
INSERT INTO contractAbi (type, address, chain, status)
VALUES ("address", ${normalizedAddress}, ${key.chainID}, "not-found")
INSERT INTO ${table}
${sql.insert([
{
type: 'address',
address: normalizedAddress,
chain: key.chainID,
status: 'not-found',
},
])}
`
}
}).pipe(Effect.catchAll(() => Effect.succeed(null))),
}).pipe(
Effect.tapError(Effect.logError),
Effect.catchAll(() => {
return Effect.succeed(null)
}),
),

get: ({ address, signature, event, chainID }) =>
Effect.gen(function* () {
const items = yield* sql`
SELECT * FROM contractAbi
WHERE (address = ${address.toLowerCase()} AND chain = ${chainID} AND type = "address")
${signature ? `OR (signature = ${signature} AND type = "func")` : ''}
${event ? `OR (event = ${event} AND type = "event")` : ''}
`.pipe(Effect.catchAll(() => Effect.succeed([])))
const addressQuery = sql.and([
sql`address = ${address.toLowerCase()}`,
sql`chain = ${chainID}`,
sql`type = 'address'`,
])

const signatureQuery = signature ? sql.and([sql`signature = ${signature}`, sql`type = 'func'`]) : undefined
const eventQuery = event ? sql.and([sql`event = ${event}`, sql`type = 'event'`]) : undefined
const query =
signature == null && event == null
? addressQuery
: sql.or([addressQuery, signatureQuery, eventQuery].filter(Boolean))

const items = yield* sql` SELECT * FROM ${table} WHERE ${query}`.pipe(
Effect.tapError(Effect.logError),
Effect.catchAll(() => Effect.succeed([])),
)

const item =
items.find((item) => {
Expand Down
Loading
Loading