Skip to content

Commit

Permalink
Add default in memory stores (#133)
Browse files Browse the repository at this point in the history
* Add default in memory stores

* Allow vanilla abi to accept effect layers for stores

* Add package configuration and documentation

* Changeset

* Fix linting issues

* Upd doc

---------

Co-authored-by: Anastasia Rodionova <argali96@gmail.com>
  • Loading branch information
Ferossgp and anastasiarods authored Oct 23, 2024
1 parent c3cd218 commit f8dbcbd
Show file tree
Hide file tree
Showing 11 changed files with 269 additions and 44 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-cheetahs-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@3loop/transaction-decoder': patch
---

Add default in-memory stores for contract and abi
37 changes: 25 additions & 12 deletions apps/docs/src/content/docs/welcome/getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -26,36 +26,49 @@ import { Content as RpcProvider } from '../../components/rpc-provider.md'
To get started, install the package from npm, along with its peer dependencies:

```sh
npm i @3loop/transaction-decoder
npm i @3loop/transaction-decoder viem effect
```

### Quick Start

To begin using the Loop Decoder, you need to create an instance of the LoopDecoder class. At a minimum, you must provide three data loaders:

1. `getPublicClient`: This function returns an object with [Viem](https://viem.sh/) `PublicClient` based on the chain ID.

<RpcProvider />
- RPC Provider
- ABI Loader
- Contract Meta Information Loader

2. `contractMetaStore`: This object has two required properties, `get` and `set`, which return and cache contract meta-information. Optionally, you can provide a list of `strategies` that will resolve data when it is missing in the cache. See the `ContractData` type for the required properties of the contract meta-information.
Loop Decoder has default in-memory implementations for ABI and contract meta-information loaders: `InMemoryAbiStoreLive` and `InMemoryContractMetaStoreLive`. If you need more customization for a storage, see our guide on [How To Decode Transaction](/guides/decode-transaction/).

<MemoryContractLoader />
1. `getPublicClient`: This function returns an object with [Viem](https://viem.sh/) `PublicClient` based on the chain ID.

1. `abiStore`: Similarly, this object has two required properties, `get` and `set`, which return and cache the contract or fragment ABI based on the chain ID, address, function, or event signature. Additionally, it includes strategies to resolve the data from third parties when it is missing in the cache.
<RpcProvider />

In the following example we will cache all types of ABIs into the same Map.
2. `abiStore`: To avoid making unecessary calls to third-party APIs, Loop Decoder uses an API that allows cache. For this example, we will keep it simple and use an in-memory cache.
We will also use some strategies to download contract ABIs from Etherscan and 4byte.directory. You can find more information about the strategies in the [Strategies](/reference/data-loaders/) reference.

<MemoryAbiLoader />
3. `contractMetaStore`: Create an in-memory cache for contract meta-information. We will automatically retrieve ERC20, ERC721, and ERC1155 token meta information from the contract such as token name, decimals, symbol, etc.

Finally, you can create a new instance of the LoopDecoder class:

```ts
import { TransactionDecoder } from '@3loop/transaction-decoder'
import { InMemoryAbiStoreLive, InMemoryContractMetaStoreLive } from '@3loop/transaction-decoder/in-memory'
import { ConfigProvider, Layer } from 'effect'

// Create a config for the ABI loader
const ABILoaderConfig = ConfigProvider.fromMap(
new Map([
['ETHERSCAN_API_KEY', 'YourApiKeyToken'],
['ETHERSCAN_ENDPOINT', 'https://api.etherscan.io/api'],
]),
)

const ConfigLayer = Layer.setConfigProvider(ABILoaderConfig)

const decoded = new TransactionDecoder({
const decoder = new TransactionDecoder({
getPublicClient: getPublicClient,
abiStore: abiStore,
contractMetaStore: contractMetaStore,
abiStore: InMemoryAbiStoreLive.pipe(Layer.provide(ConfigLayer)),
contractMetaStore: InMemoryContractMetaStoreLive.pipe,
})
```

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/lib/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ import {
QuickjsConfig,
TransactionInterpreter,
fallbackInterpreter,
getInterpreter,
} from '@3loop/transaction-interpreter'
import { Effect, Layer } from 'effect'
import variant from '@jitl/quickjs-singlefile-browser-release-sync'
import { getInterpreter } from '@3loop/transaction-interpreter'

const config = Layer.succeed(QuickjsConfig, {
variant: variant,
Expand Down
16 changes: 10 additions & 6 deletions packages/transaction-decoder/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"description": "A library for decoding Ethereum transactions",
"types": "dist/index.d.ts",
"main": "dist/index.cjs",
"module": "dist/index.js",
"license": "GPL-3.0-only",
"type": "module",
"exports": {
Expand All @@ -12,15 +13,17 @@
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
},
"./*": {
"./in-memory": {
"import": {
"types": "./dist/*.d.ts",
"default": "./dist/*.js"
"types": "./dist/in-memory/index.d.ts",
"default": "./dist/in-memory/index.js"
},
"require": {
"types": "./dist/*.d.cts",
"default": "./dist/*.cjs"
}
"types": "./dist/in-memory/index.d.cts",
"default": "./dist/in-memory/index.cjs"
},
"types": "./dist/in-memory/index.d.ts",
"default": "./dist/in-memory/index.js"
}
},
"scripts": {
Expand Down Expand Up @@ -56,6 +59,7 @@
"eslint": "^8.57.0",
"eslint-config-custom": "workspace:*",
"eslint-config-prettier": "^8.10.0",
"glob": "^11.0.0",
"prettier": "^2.8.8",
"quickjs-emscripten": "^0.29.2",
"ts-node": "^10.9.2",
Expand Down
86 changes: 86 additions & 0 deletions packages/transaction-decoder/src/in-memory/abi-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {
EtherscanStrategyResolver,
FourByteStrategyResolver,
ContractABI,
AbiStore,
SourcifyStrategyResolver,
OpenchainStrategyResolver,
} from '../effect.js'
import { Config, Effect, Layer } from 'effect'

const abiCache = new Map<string, ContractABI>()

export const InMemoryAbiStoreLive = Layer.effect(
AbiStore,
Effect.gen(function* () {
const etherscanApiKey = yield* Config.string('ETHERSCAN_API_KEY').pipe(
Effect.catchTag('ConfigError', () => {
return Effect.succeed(undefined)
}),
)
const etherscanEndpoint = yield* Config.string('ETHERSCAN_ENDPOINT').pipe(
Effect.catchTag('ConfigError', () => {
return Effect.succeed(undefined)
}),
)

const etherscanStrategy =
etherscanEndpoint && etherscanApiKey
? EtherscanStrategyResolver({
apikey: etherscanApiKey,
endpoint: etherscanEndpoint,
})
: undefined

return AbiStore.of({
strategies: {
default: [
etherscanStrategy,
SourcifyStrategyResolver(),
OpenchainStrategyResolver(),
FourByteStrategyResolver(),
].filter(Boolean),
},
set: (_key, value) =>
Effect.sync(() => {
if (value.status === 'success') {
if (value.result.type === 'address') {
abiCache.set(value.result.address, value.result)
} else if (value.result.type === 'event') {
abiCache.set(value.result.event, value.result)
} else if (value.result.type === 'func') {
abiCache.set(value.result.signature, value.result)
}
}
}),
get: (key) =>
Effect.sync(() => {
if (abiCache.has(key.address)) {
return {
status: 'success',
result: abiCache.get(key.address)!,
}
}

if (key.event && abiCache.has(key.event)) {
return {
status: 'success',
result: abiCache.get(key.event)!,
}
}

if (key.signature && abiCache.has(key.signature)) {
return {
status: 'success',
result: abiCache.get(key.signature)!,
}
}

return {
status: 'empty',
result: null,
}
}),
})
}),
)
42 changes: 42 additions & 0 deletions packages/transaction-decoder/src/in-memory/contract-meta-store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { ContractData } from '../types.js'
import { ContractMetaStore, ERC20RPCStrategyResolver, NFTRPCStrategyResolver, PublicClient } from '../effect.js'
import { Effect, Layer } from 'effect'

const contractMetaCache = new Map<string, ContractData>()

export const InMemoryContractMetaStoreLive = Layer.effect(
ContractMetaStore,
Effect.gen(function* () {
const publicClient = yield* PublicClient
const erc20Loader = ERC20RPCStrategyResolver(publicClient)
const nftLoader = NFTRPCStrategyResolver(publicClient)
return ContractMetaStore.of({
strategies: { default: [erc20Loader, nftLoader] },
get: ({ address, chainID }) =>
Effect.sync(() => {
const key = `${address}-${chainID}`.toLowerCase()
const value = contractMetaCache.get(key)

if (value) {
return {
status: 'success',
result: value,
}
}

return {
status: 'empty',
result: null,
}
}),
set: ({ address, chainID }, result) =>
Effect.sync(() => {
const key = `${address}-${chainID}`.toLowerCase()

if (result.status === 'success') {
contractMetaCache.set(key, result.result)
}
}),
})
}),
)
2 changes: 2 additions & 0 deletions packages/transaction-decoder/src/in-memory/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './abi-store.js'
export * from './contract-meta-store.js'
52 changes: 33 additions & 19 deletions packages/transaction-decoder/src/vanilla.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import type { GetContractMetaStrategy } from './meta-strategy/request-model.js'

export interface TransactionDecoderOptions {
getPublicClient: (chainID: number) => PublicClientObject | undefined
abiStore: VanillaAbiStore
contractMetaStore: VanillaContractMetaStore
abiStore: VanillaAbiStore | Layer.Layer<EffectAbiStore<AbiParams, ContractAbiResult>>
contractMetaStore:
| VanillaContractMetaStore
| Layer.Layer<EffectContractMetaStore<ContractMetaParams, ContractMetaResult>>
logLevel?: LogLevel.Literal
}

Expand Down Expand Up @@ -58,25 +60,37 @@ export class TransactionDecoder {
},
})

const AbiStoreLive = Layer.succeed(
EffectAbiStore,
EffectAbiStore.of({
strategies: { default: abiStore.strategies ?? [] },
get: (key) => Effect.promise(() => abiStore.get(key)),
set: (key, val) => Effect.promise(() => abiStore.set(key, val)),
}),
)
let AbiStoreLive: Layer.Layer<EffectAbiStore<AbiParams, ContractAbiResult>>

const contractMetaStrategies = contractMetaStore.strategies?.map((strategy) => strategy(PublicClientLive))
if (Layer.isLayer(abiStore)) {
AbiStoreLive = abiStore as Layer.Layer<EffectAbiStore<AbiParams, ContractAbiResult>>
} else {
const store = abiStore as VanillaAbiStore
AbiStoreLive = Layer.succeed(
EffectAbiStore,
EffectAbiStore.of({
strategies: { default: store.strategies ?? [] },
get: (key) => Effect.promise(() => store.get(key)),
set: (key, val) => Effect.promise(() => store.set(key, val)),
}),
)
}

const MetaStoreLive = Layer.succeed(
EffectContractMetaStore,
EffectContractMetaStore.of({
strategies: { default: contractMetaStrategies ?? [] },
get: (key) => Effect.promise(() => contractMetaStore.get(key)),
set: (key, val) => Effect.promise(() => contractMetaStore.set(key, val)),
}),
)
let MetaStoreLive: Layer.Layer<EffectContractMetaStore<ContractMetaParams, ContractMetaResult>>

if (Layer.isLayer(contractMetaStore)) {
MetaStoreLive = contractMetaStore as Layer.Layer<EffectContractMetaStore<ContractMetaParams, ContractMetaResult>>
} else {
const store = contractMetaStore as VanillaContractMetaStore
MetaStoreLive = Layer.succeed(
EffectContractMetaStore,
EffectContractMetaStore.of({
strategies: { default: (store.strategies ?? [])?.map((strategy) => strategy(PublicClientLive)) },
get: (key) => Effect.promise(() => store.get(key)),
set: (key, val) => Effect.promise(() => store.set(key, val)),
}),
)
}

const LoadersLayer = Layer.provideMerge(AbiStoreLive, MetaStoreLive)
const MainLayer = Layer.provideMerge(Layer.succeed(PublicClient, PublicClientLive), LoadersLayer).pipe(
Expand Down
3 changes: 2 additions & 1 deletion packages/transaction-decoder/tsconfig.build.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
"skipLibCheck": true,
"noEmit": true
},
"include": ["src"],
"exclude": ["dist", "example", "node_modules"]
Expand Down
13 changes: 11 additions & 2 deletions packages/transaction-decoder/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import path from 'path'
import { globSync } from 'glob'
import { defineConfig } from 'tsup'

const entries = globSync('src/**/*.ts')

export default defineConfig({
dts: true,
bundle: false,
splitting: false,
treeshake: true,
target: 'node16',
sourcemap: true,
format: ['esm', 'cjs'],
entry: ['src/**/*.ts'],
entry: entries,
outExtension({ format }) {
return {
js: format === 'cjs' ? '.cjs' : '.js',
}
},
tsconfig: path.resolve(__dirname, './tsconfig.build.json'),
outDir: 'dist',
clean: true,
Expand Down
Loading

0 comments on commit f8dbcbd

Please sign in to comment.