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

feat: persist evm blockscout transactions #2008

Merged
merged 5 commits into from
Feb 28, 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
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
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './fetchAndPersistTransactionsForAccounts'
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './transactions.store'
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
}
Loading