Skip to content

Commit

Permalink
feat: persist evm blockscout transactions (#2008)
Browse files Browse the repository at this point in the history
* refactor: improve blockscout transaction interface

Co-authored-by: Mark Nardi <[email protected]>

* feat: add new persisted transactions store

* feat: fetch and persist transactions from blockscout on login

* feat: add exit function to recursive blockscout api calls

* interupt recursion if error occured

---------

Co-authored-by: Mark Nardi <[email protected]>
  • Loading branch information
nicole-obrien and MarkNerdi authored Feb 28, 2024
1 parent f8879a9 commit 482db21
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 9 deletions.
27 changes: 21 additions & 6 deletions packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ interface INextPageParams {
index: number
items_count: number
}

interface IPaginationResponse<T> {
items: T[]
next_page_params: INextPageParams | null
}

export type BlockscoutExitFunction<T> = (items: T[]) => boolean

export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
constructor(networkId: NetworkId) {
const explorerUrl = DEFAULT_EXPLORER_URLS[networkId as SupportedNetworkId]
Expand All @@ -28,21 +29,26 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
path: string,
queryParameters?: QueryParameters,
items: T[] = [],
nextPageParameters?: INextPageParams | null
nextPageParameters?: INextPageParams | null,
exitFunction?: BlockscoutExitFunction<T>
): Promise<T[]> {
if (nextPageParameters === null) {
return Promise.resolve(items)
}
if (exitFunction && exitFunction(items)) {
return Promise.resolve(items)
}
return this.get<IPaginationResponse<T>>(path, { ...queryParameters, ...nextPageParameters }).then(
(response) => {
if (!response) {
if (!response?.items) {
return Promise.resolve(items)
}
return this.makePaginatedGetRequest(
path,
queryParameters,
items.concat(response.items),
response.next_page_params
response.next_page_params,
exitFunction
)
}
)
Expand Down Expand Up @@ -76,9 +82,18 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi {
}
}

async getTransactionsForAddress(address: string): Promise<IBlockscoutTransaction[]> {
async getTransactionsForAddress(
address: string,
exitFunction?: BlockscoutExitFunction<IBlockscoutTransaction>
): Promise<IBlockscoutTransaction[]> {
const path = `addresses/${address}/transactions`
const items = await this.makePaginatedGetRequest<IBlockscoutTransaction>(path)
const items = await this.makePaginatedGetRequest<IBlockscoutTransaction>(
path,
undefined,
[],
undefined,
exitFunction
)
return items
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface'

interface IFee {
type: string
type: 'maximum' | 'actual'
value: string
}

Expand Down Expand Up @@ -43,12 +43,25 @@ interface ITokenTransfer {
token: IBlockscoutAssetMetadata
}

enum BlockscoutTransactionType {
TokenTransfer = 'token_transfer',
ContractCreation = 'contract_creation',
ContractCall = 'contract_call',
TokenCreation = 'token_creation',
CoinTransfer = 'coin_transfer',
}

enum BlockscoutTransactionStatus {
Ok = 'ok',
Error = 'error',
}

export interface IBlockscoutTransaction {
timestamp: string
fee: IFee
gas_limit: number
block: number
status: string // e.g ok | error
status: BlockscoutTransactionStatus
method: string // e.g transferFrom
confirmations: number
type: number
Expand All @@ -63,7 +76,7 @@ export interface IBlockscoutTransaction {
base_fee_per_gas: string
from: IAddressParam
token_transfers: ITokenTransfer[]
tx_types: string[]
tx_types: BlockscoutTransactionType[]
gas_used: string
created_contract: IAddressParam
position: number
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { get } from 'svelte/store'
import { ProfileType } from '../../enums'
import { ILoginOptions } from '../../interfaces'
import {
activeAccounts,
activeProfile,
getLastLoggedInProfileId,
incrementLoginProgress,
Expand All @@ -42,6 +43,7 @@ import { subscribeToWalletApiEventsForActiveProfile } from './subscribeToWalletA
import { disconnectAllDapps } from '@auxiliary/wallet-connect/utils'
import { initializeWalletConnect } from '@auxiliary/wallet-connect/actions'
import { cleanupOnboarding } from '@contexts/onboarding'
import { fetchAndPersistTransactionsForAccounts } from '@core/transactions/actions'

export async function login(loginOptions?: ILoginOptions): Promise<void> {
const loginRouter = get(routerManager).getRouterForAppContext(AppContext.Login)
Expand Down Expand Up @@ -105,6 +107,7 @@ export async function login(loginOptions?: ILoginOptions): Promise<void> {
subscribeToWalletApiEventsForActiveProfile()
await startBackgroundSync({ syncIncomingTransactions: true })
fetchL2BalanceForAllAccounts()
void fetchAndPersistTransactionsForAccounts(_activeProfile.id, get(activeAccounts))

// Step 8: finish login
incrementLoginProgress()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces'
import { IAccountState, getAddressFromAccountForNetwork } from '@core/account'
import { addBlockscoutTransactionToPersistedTransactions, isBlockscoutTransactionPersisted } from '../stores'
import { BlockscoutApi } from '@auxiliary/blockscout/api'
import { EvmNetworkId, getNetwork } from '@core/network'

export async function fetchAndPersistTransactionsForAccounts(
profileId: string,
accounts: IAccountState[]
): Promise<void> {
const chains = getNetwork()?.getChains() ?? []
for (const chain of chains) {
const networkId = chain.getConfiguration().id as EvmNetworkId
for (const account of accounts) {
try {
const blockscoutTransactions = await fetchBlockscoutTransactionsForAccount(
profileId,
account,
networkId
)
blockscoutTransactions &&
addBlockscoutTransactionToPersistedTransactions(
profileId,
account.index,
networkId,
blockscoutTransactions
)
} catch (err) {
console.error(err)
}
}
}
}

function getTransactionsExitFunction(
items: IBlockscoutTransaction[],
profileId: string,
accountIndex: number,
networkId: EvmNetworkId
): boolean {
const lastItem = items[items.length - 1]
return lastItem ? isBlockscoutTransactionPersisted(profileId, accountIndex, networkId, lastItem.hash) : false
}

async function fetchBlockscoutTransactionsForAccount(
profileId: string,
account: IAccountState,
networkId: EvmNetworkId
): Promise<IBlockscoutTransaction[] | undefined> {
const address = getAddressFromAccountForNetwork(account, networkId)
if (!address) {
return undefined
}
const blockscoutApi = new BlockscoutApi(networkId)
const transactions = await blockscoutApi.getTransactionsForAddress(address, (items: IBlockscoutTransaction[]) =>
getTransactionsExitFunction(items, profileId, account.index, networkId)
)
return transactions
}
1 change: 1 addition & 0 deletions packages/shared/src/lib/core/transactions/actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fetchAndPersistTransactionsForAccounts'
1 change: 1 addition & 0 deletions packages/shared/src/lib/core/transactions/stores/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './transactions.store'
126 changes: 126 additions & 0 deletions packages/shared/src/lib/core/transactions/stores/transactions.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces'
import { PersistedEvmTransaction } from '@core/activity'
import { EvmNetworkId } from '@core/network'
import { IChain } from '@core/network/interfaces'
import { activeProfileId } from '@core/profile/stores'
import { persistent } from '@core/utils/store'
import { get } from 'svelte/store'

type PersistedTransaction =
| {
blockscout: IBlockscoutTransaction
local: PersistedEvmTransaction
}
| {
blockscout?: IBlockscoutTransaction
local: PersistedEvmTransaction
}
| {
blockscout: IBlockscoutTransaction
local?: PersistedEvmTransaction
}

type PersistedTransactions = {
[profileId: string]: {
[accountId: string]: {
[networkId in EvmNetworkId]?: {
[transactionHash: string]: PersistedTransaction
}
}
}
}

export const persistedTransactions = persistent<PersistedTransactions>('transactions', {})

export function getPersistedTransactionsForChain(
profileId: string,
accountIndex: number,
chain: IChain
): PersistedTransaction[] {
const networkId = chain.getConfiguration().id as EvmNetworkId
return Object.values(get(persistedTransactions)?.[profileId]?.[accountIndex]?.[networkId] ?? {}) ?? []
}

export function addLocalTransactionToPersistedTransaction(
profileId: string,
accountIndex: number,
networkId: EvmNetworkId,
newTransactions: PersistedEvmTransaction[]
): void {
persistedTransactions.update((state) => {
if (!state[profileId]) {
state[profileId] = {}
}
if (!state[profileId][accountIndex]) {
state[profileId][accountIndex] = {
[networkId]: {},
}
}
if (!state[profileId][accountIndex][networkId]) {
state[profileId][accountIndex][networkId] = {}
}

const _transactions = state[get(activeProfileId)][accountIndex][networkId] ?? {}
for (const transaction of newTransactions) {
const existingTransaction = _transactions?.[transaction.transactionHash.toLowerCase()]
const updatedTransaction: PersistedTransaction = {
...existingTransaction,
local: transaction,
}
_transactions[transaction.transactionHash.toLowerCase()] = updatedTransaction
}
state[get(activeProfileId)][accountIndex][networkId] = _transactions

return state
})
}

export function addBlockscoutTransactionToPersistedTransactions(
profileId: string,
accountIndex: number,
networkId: EvmNetworkId,
newTransactions: IBlockscoutTransaction[]
): void {
persistedTransactions.update((state) => {
if (!state[profileId]) {
state[profileId] = {}
}
if (!state[profileId][accountIndex]) {
state[profileId][accountIndex] = {
[networkId]: {},
}
}
if (!state[profileId][accountIndex][networkId]) {
state[profileId][accountIndex][networkId] = {}
}

const _transactions = state[get(activeProfileId)][accountIndex][networkId] ?? {}
for (const transaction of newTransactions) {
const existingTransaction = _transactions?.[transaction.hash.toLowerCase()]
const updatedTransaction: PersistedTransaction = {
...existingTransaction,
blockscout: transaction,
}
_transactions[transaction.hash.toLowerCase()] = updatedTransaction
}
state[get(activeProfileId)][accountIndex][networkId] = _transactions

return state
})
}

export function removePersistedTransactionsForProfile(profileId: string): void {
persistedTransactions.update((state) => {
delete state[profileId]
return state
})
}

export function isBlockscoutTransactionPersisted(
profileId: string,
accountIndex: number,
networkId: EvmNetworkId,
transactionHash: string
): boolean {
return !!get(persistedTransactions)?.[profileId]?.[accountIndex]?.[networkId]?.[transactionHash]?.blockscout
}

0 comments on commit 482db21

Please sign in to comment.