Skip to content

Data Store

Loop decoder relays on two data stores for caching the ABI and contract metadata. The user of the library is free to choose which data layer to use depending on their environment and requirements.

If your application is designed to decode a fixed subset of contracts, you can provide a hardcoded map of ABIs and contract metadata. For more flexible applications, you can use any persistent database. When running in a browser, you can implement a REST API to fetch the ABIs and contract metadata from a server.

A store also requires a set of strategies that we cover separately in the Data Loaders section.

AbiStore

ABI Store Interface requires 2 methods set and get to store and retrieve the ABI of a contract. Optionally you can provide a batch get method getMany for further optimization. Because our API supports ABI fragments, the get method will receive both the contract address and the fragment signature.

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

The set method will receive a key of the type AbiParams and and ContractAbiResult. You can choose to store the data in the best format that fits your database.

interface FunctionFragmentABI {
type: 'func'
abi: string
address: string
chainID: number
signature: string
}
interface EventFragmentABI {
type: 'event'
abi: string
address: string
chainID: number
event: string
}
interface AddressABI {
type: 'address'
abi: string
address: string
chainID: number
}
export type ContractABI = FunctionFragmentABI | EventFragmentABI | AddressABI
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

The full interface looks as follows:

export interface AbiStore {
readonly strategies: Record<ChainOrDefault, readonly RequestResolver.RequestResolver<GetContractABIStrategy>[]>
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>
}

ContractMetadataStore

Similar to the ABI Store, the Contract Metadata Store Interface requires 2 methods set, get, and optionally getMany to store and retrieve the contract metadata.

The get method will receive the contract address and the chain ID as input.

interface ContractMetaParams {
address: string
chainID: number
}

And, the set method will be called with 2 pramaters, the key in the same format as the get method, and the metadata in a format of ContractMetaResult.

interface ContractMetaSuccess {
status: 'success'
result: ContractData
}
interface ContractMetaNotFound {
status: 'not-found'
result: null
}
interface ContractMetaEmpty {
status: 'empty'
result: null
}
export type ContractMetaResult = ContractMetaSuccess | ContractMetaNotFound | ContractMetaEmpty

Contract metadata is a map of the following interface:

export interface ContractData {
address: string
contractName: string
contractAddress: string
tokenSymbol: string
decimals?: number
type: ContractType
chainID: number
}

The full interface looks as follows:

interface ContractMetaStore {
readonly strategies: Record<ChainOrDefault, readonly RequestResolver.RequestResolver<GetContractMetaStrategy>[]>
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>
}

You can notice that the AbiStore and ContractMetadataStore interfaces are very similar, and both have a status for the set and get methods. Both stores can return three states:

  1. success - The requested data is found in the store.
  2. not-found - The requested data is found in the store, but is missing a value. This means that we have tried to resolve it from a third party, but it was missing there.
  3. empty - The requested data is not found in the store, which means that it has never been requested before.

We have two states that can return an empty result. We want to be able to skip the meta strategy in cases where we know it’s not available, as this can significantly reduce the number of requests to the strategies and improve performance.

Some strategies may be able to add the data later. Therefore, we encourage storing a timestamp and removing the “not-found” state to be able to check again.

Default Stores

By defulat loop decoder provides two stores that can be used out of the box:

  1. In-memory store - located at @3loop/transaction-decoder/in-memory
  • InMemoryAbiStore - caches the resolved abi in-memory
  • InMemoryContractMetaStore - caches the resolved contract metadata in-memory

You will most likely use these stores for testing and development purposes. A persistent store will significantly improve the performance of the decoder over time.

  1. Sqlite store - located at @3loop/transaction-decoder/sql
  • SqlAbiStore - caches the resolved abi in any sql database supported by @effect/sql
  • SqlContractMetaStore - caches the resolved contract metadata in any sql database supported by @effect/sql