From 81c3acb2f82a280ef9390ff7ba2397f82dc3b00a Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 21 Oct 2024 20:19:58 +0200 Subject: [PATCH 1/6] Add default in memory stores --- .../src/in-memory/abi-store.ts | 86 +++++++++++++++++++ .../src/in-memory/contract-meta-store.ts | 42 +++++++++ .../src/in-memory/index.ts | 2 + 3 files changed, 130 insertions(+) create mode 100644 packages/transaction-decoder/src/in-memory/abi-store.ts create mode 100644 packages/transaction-decoder/src/in-memory/contract-meta-store.ts create mode 100644 packages/transaction-decoder/src/in-memory/index.ts diff --git a/packages/transaction-decoder/src/in-memory/abi-store.ts b/packages/transaction-decoder/src/in-memory/abi-store.ts new file mode 100644 index 00000000..f6af78d3 --- /dev/null +++ b/packages/transaction-decoder/src/in-memory/abi-store.ts @@ -0,0 +1,86 @@ +import { + EtherscanStrategyResolver, + FourByteStrategyResolver, + ContractABI, + AbiStore, + SourcifyStrategyResolver, + OpenchainStrategyResolver, +} from '../effect.js' +import { Config, Effect, Layer } from 'effect' + +const abiCache = new Map() + +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, + } + }), + }) + }), +) diff --git a/packages/transaction-decoder/src/in-memory/contract-meta-store.ts b/packages/transaction-decoder/src/in-memory/contract-meta-store.ts new file mode 100644 index 00000000..f7850dc9 --- /dev/null +++ b/packages/transaction-decoder/src/in-memory/contract-meta-store.ts @@ -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() + +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) + } + }), + }) + }), +) diff --git a/packages/transaction-decoder/src/in-memory/index.ts b/packages/transaction-decoder/src/in-memory/index.ts new file mode 100644 index 00000000..574c4a60 --- /dev/null +++ b/packages/transaction-decoder/src/in-memory/index.ts @@ -0,0 +1,2 @@ +export * from './abi-store.js' +export * from './contract-meta-store.js' From bfddb7f645438c43fb74081620666453ad0da9bd Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 21 Oct 2024 20:20:42 +0200 Subject: [PATCH 2/6] Allow vanilla abi to accept effect layers for stores --- packages/transaction-decoder/src/vanilla.ts | 52 +++++++++++++-------- 1 file changed, 33 insertions(+), 19 deletions(-) diff --git a/packages/transaction-decoder/src/vanilla.ts b/packages/transaction-decoder/src/vanilla.ts index c406c116..5b6c145a 100644 --- a/packages/transaction-decoder/src/vanilla.ts +++ b/packages/transaction-decoder/src/vanilla.ts @@ -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> + contractMetaStore: + | VanillaContractMetaStore + | Layer.Layer> logLevel?: LogLevel.Literal } @@ -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> - const contractMetaStrategies = contractMetaStore.strategies?.map((strategy) => strategy(PublicClientLive)) + if (Layer.isLayer(abiStore)) { + AbiStoreLive = abiStore as Layer.Layer> + } 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> + + if (Layer.isLayer(contractMetaStore)) { + MetaStoreLive = contractMetaStore as Layer.Layer> + } 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( From cea2292dc99bbe57c2e010b99410d1314d460b73 Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 21 Oct 2024 20:49:52 +0200 Subject: [PATCH 3/6] Add package configuration and documentation --- .../docs/guides/decode-transaction.mdx | 18 ++---- apps/web/src/lib/interpreter.ts | 2 +- packages/transaction-decoder/package.json | 16 ++++-- .../transaction-decoder/tsconfig.build.json | 3 +- packages/transaction-decoder/tsup.config.ts | 13 ++++- pnpm-lock.yaml | 55 ++++++++++++++++++- 6 files changed, 82 insertions(+), 25 deletions(-) diff --git a/apps/docs/src/content/docs/guides/decode-transaction.mdx b/apps/docs/src/content/docs/guides/decode-transaction.mdx index d868d70e..b61d0631 100644 --- a/apps/docs/src/content/docs/guides/decode-transaction.mdx +++ b/apps/docs/src/content/docs/guides/decode-transaction.mdx @@ -5,8 +5,6 @@ sidebar: order: 1 --- -import { Content as MemoryAbiLoader } from '../../components/memory-abi-loader.md' -import { Content as MemoryContractLoader } from '../../components/memory-contract-loader.md' import { Content as RpcProvider } from '../../components/rpc-provider.md' In this guide, we will go through the process of decoding an Ethereum transaction using Loop Decoder. For the simplicity of the example, we assume that that contract ABIs involved in the transaction are verified on Etherscan. @@ -53,27 +51,23 @@ We will start by creating a function which will return an object with PublicClie ### ABI loader -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. - -Create a cache for contract ABI and add your free Etherscan API key instead of the placeholder `YourApiKeyToken`: - - +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. ### Contract Metadata loader -Create an in-memory cache for contract meta-information. Using `ERC20RPCStrategyResolver` we will automatically retrieve token meta information from the contract such as token name, decimals, symbol, etc. - - +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' const decoder = new TransactionDecoder({ getPublicClient: getPublicClient, - abiStore: abiStore, - contractMetaStore: contractMetaStore, + abiStore: InMemoryAbiStoreLive, + contractMetaStore: InMemoryContractMetaStoreLive, }) ``` diff --git a/apps/web/src/lib/interpreter.ts b/apps/web/src/lib/interpreter.ts index 23f41d93..4a34b36b 100644 --- a/apps/web/src/lib/interpreter.ts +++ b/apps/web/src/lib/interpreter.ts @@ -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, diff --git a/packages/transaction-decoder/package.json b/packages/transaction-decoder/package.json index 43d54763..a9a4463d 100644 --- a/packages/transaction-decoder/package.json +++ b/packages/transaction-decoder/package.json @@ -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": { @@ -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": { @@ -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", diff --git a/packages/transaction-decoder/tsconfig.build.json b/packages/transaction-decoder/tsconfig.build.json index 06d02928..3d2736e1 100644 --- a/packages/transaction-decoder/tsconfig.build.json +++ b/packages/transaction-decoder/tsconfig.build.json @@ -11,7 +11,8 @@ "module": "commonjs", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, - "skipLibCheck": true + "skipLibCheck": true, + "noEmit": true }, "include": ["src"], "exclude": ["dist", "example", "node_modules"] diff --git a/packages/transaction-decoder/tsup.config.ts b/packages/transaction-decoder/tsup.config.ts index 97f94050..2adaf7e9 100644 --- a/packages/transaction-decoder/tsup.config.ts +++ b/packages/transaction-decoder/tsup.config.ts @@ -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, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 469ec911..4a30ae5b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -232,6 +232,9 @@ importers: eslint-config-prettier: specifier: ^8.10.0 version: 8.10.0(eslint@8.57.0) + glob: + specifier: ^11.0.0 + version: 11.0.0 prettier: specifier: ^2.8.8 version: 2.8.8 @@ -3391,6 +3394,7 @@ packages: eslint@8.57.0: resolution: {integrity: sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true espree@9.6.1: @@ -3646,6 +3650,11 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@11.0.0: + resolution: {integrity: sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==} + engines: {node: 20 || >=22} + hasBin: true + glob@7.1.7: resolution: {integrity: sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==} deprecated: Glob versions prior to v9 are no longer supported @@ -4127,6 +4136,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jackspeak@4.0.2: + resolution: {integrity: sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==} + engines: {node: 20 || >=22} + jiti@1.21.6: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true @@ -4308,6 +4321,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@11.0.1: + resolution: {integrity: sha512-CgeuL5uom6j/ZVrg7G/+1IXqRY8JXX4Hghfy5YE0EhoYQWvndP1kufu58cmZLNIDKnRhZrXfdS9urVWx98AipQ==} + engines: {node: 20 || >=22} + lru-cache@4.1.5: resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} @@ -4658,6 +4675,10 @@ packages: engines: {node: '>=16.13'} hasBin: true + minimatch@10.0.1: + resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} + engines: {node: 20 || >=22} + minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -4964,6 +4985,10 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-scurry@2.0.0: + resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==} + engines: {node: 20 || >=22} + path-to-regexp@6.2.2: resolution: {integrity: sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==} @@ -9644,7 +9669,7 @@ snapshots: eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0) - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) eslint-plugin-jsx-a11y: 6.9.0(eslint@8.57.0) eslint-plugin-react: 7.35.0(eslint@8.57.0) eslint-plugin-react-hooks: 4.6.2(eslint@8.57.0) @@ -9692,7 +9717,7 @@ snapshots: is-bun-module: 1.1.0 is-glob: 4.0.3 optionalDependencies: - eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0) + eslint-plugin-import: 2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0) transitivePeerDependencies: - '@typescript-eslint/parser' - eslint-import-resolver-node @@ -9779,7 +9804,7 @@ snapshots: eslint: 8.57.0 ignore: 5.2.4 - eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-node@0.3.9)(eslint-plugin-import@2.29.1(eslint@8.57.0))(eslint@8.57.0))(eslint@8.57.0): + eslint-plugin-import@2.29.1(@typescript-eslint/parser@5.62.0(eslint@8.57.0)(typescript@5.1.3))(eslint-import-resolver-typescript@3.6.3)(eslint@8.57.0): dependencies: array-includes: 3.1.8 array.prototype.findlastindex: 1.2.5 @@ -10306,6 +10331,15 @@ snapshots: package-json-from-dist: 1.0.0 path-scurry: 1.11.1 + glob@11.0.0: + dependencies: + foreground-child: 3.3.0 + jackspeak: 4.0.2 + minimatch: 10.0.1 + minipass: 7.1.2 + package-json-from-dist: 1.0.0 + path-scurry: 2.0.0 + glob@7.1.7: dependencies: fs.realpath: 1.0.0 @@ -10903,6 +10937,10 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jackspeak@4.0.2: + dependencies: + '@isaacs/cliui': 8.0.2 + jiti@1.21.6: {} jju@1.4.0: {} @@ -11045,6 +11083,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.0.1: {} + lru-cache@4.1.5: dependencies: pseudomap: 1.0.2 @@ -11896,6 +11936,10 @@ snapshots: - supports-color - utf-8-validate + minimatch@10.0.1: + dependencies: + brace-expansion: 2.0.1 + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 @@ -12231,6 +12275,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.0: + dependencies: + lru-cache: 11.0.1 + minipass: 7.1.2 + path-to-regexp@6.2.2: {} path-type@4.0.0: {} From 17667f191705628d2993918e863ab01d2ea6ca67 Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 21 Oct 2024 20:51:14 +0200 Subject: [PATCH 4/6] Changeset --- .changeset/forty-cheetahs-push.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/forty-cheetahs-push.md diff --git a/.changeset/forty-cheetahs-push.md b/.changeset/forty-cheetahs-push.md new file mode 100644 index 00000000..0d7691ae --- /dev/null +++ b/.changeset/forty-cheetahs-push.md @@ -0,0 +1,5 @@ +--- +'@3loop/transaction-decoder': patch +--- + +Add default in-memory stores for contract and abi From 1947412e2143d1fa62d748182ae5edc75c8c942a Mon Sep 17 00:00:00 2001 From: Gheorghe Pinzaru Date: Mon, 21 Oct 2024 22:14:37 +0200 Subject: [PATCH 5/6] Fix linting issues --- packages/transaction-decoder/src/in-memory/abi-store.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transaction-decoder/src/in-memory/abi-store.ts b/packages/transaction-decoder/src/in-memory/abi-store.ts index f6af78d3..4b674b63 100644 --- a/packages/transaction-decoder/src/in-memory/abi-store.ts +++ b/packages/transaction-decoder/src/in-memory/abi-store.ts @@ -27,9 +27,9 @@ export const InMemoryAbiStoreLive = Layer.effect( const etherscanStrategy = etherscanEndpoint && etherscanApiKey ? EtherscanStrategyResolver({ - apikey: etherscanApiKey, - endpoint: etherscanEndpoint, - }) + apikey: etherscanApiKey, + endpoint: etherscanEndpoint, + }) : undefined return AbiStore.of({ From 1d5d03616e395dbd4ab2548c13431f2bc62d6a56 Mon Sep 17 00:00:00 2001 From: Anastasia Rodionova Date: Wed, 23 Oct 2024 13:05:55 +0200 Subject: [PATCH 6/6] Upd doc --- .../docs/guides/decode-transaction.mdx | 18 ++++++--- .../content/docs/welcome/getting-started.mdx | 37 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/apps/docs/src/content/docs/guides/decode-transaction.mdx b/apps/docs/src/content/docs/guides/decode-transaction.mdx index b61d0631..d868d70e 100644 --- a/apps/docs/src/content/docs/guides/decode-transaction.mdx +++ b/apps/docs/src/content/docs/guides/decode-transaction.mdx @@ -5,6 +5,8 @@ sidebar: order: 1 --- +import { Content as MemoryAbiLoader } from '../../components/memory-abi-loader.md' +import { Content as MemoryContractLoader } from '../../components/memory-contract-loader.md' import { Content as RpcProvider } from '../../components/rpc-provider.md' In this guide, we will go through the process of decoding an Ethereum transaction using Loop Decoder. For the simplicity of the example, we assume that that contract ABIs involved in the transaction are verified on Etherscan. @@ -51,23 +53,27 @@ We will start by creating a function which will return an object with PublicClie ### ABI loader -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. +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. + +Create a cache for contract ABI and add your free Etherscan API key instead of the placeholder `YourApiKeyToken`: + + ### Contract Metadata loader -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. +Create an in-memory cache for contract meta-information. Using `ERC20RPCStrategyResolver` we will automatically retrieve 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' const decoder = new TransactionDecoder({ getPublicClient: getPublicClient, - abiStore: InMemoryAbiStoreLive, - contractMetaStore: InMemoryContractMetaStoreLive, + abiStore: abiStore, + contractMetaStore: contractMetaStore, }) ``` diff --git a/apps/docs/src/content/docs/welcome/getting-started.mdx b/apps/docs/src/content/docs/welcome/getting-started.mdx index e3d26939..50b34cee 100644 --- a/apps/docs/src/content/docs/welcome/getting-started.mdx +++ b/apps/docs/src/content/docs/welcome/getting-started.mdx @@ -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. - - +- 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/). - +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. + -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. - +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, }) ```