diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts index 29781e54e3..b408a9f773 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts @@ -12,12 +12,13 @@ interface INextPageParams { index: number items_count: number } - interface IPaginationResponse { items: T[] next_page_params: INextPageParams | null } +export type BlockscoutExitFunction = (items: T[]) => boolean + export class BlockscoutApi extends BaseApi implements IBlockscoutApi { constructor(networkId: NetworkId) { const explorerUrl = DEFAULT_EXPLORER_URLS[networkId as SupportedNetworkId] @@ -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 ): Promise { if (nextPageParameters === null) { return Promise.resolve(items) } + if (exitFunction && exitFunction(items)) { + return Promise.resolve(items) + } return this.get>(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 ) } ) @@ -76,9 +82,18 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi { } } - async getTransactionsForAddress(address: string): Promise { + async getTransactionsForAddress( + address: string, + exitFunction?: BlockscoutExitFunction + ): Promise { const path = `addresses/${address}/transactions` - const items = await this.makePaginatedGetRequest(path) + const items = await this.makePaginatedGetRequest( + path, + undefined, + [], + undefined, + exitFunction + ) return items } } diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts index fb26b6845a..8bcd877195 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts @@ -1,7 +1,7 @@ import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' interface IFee { - type: string + type: 'maximum' | 'actual' value: string } @@ -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 @@ -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 diff --git a/packages/shared/src/lib/core/profile/actions/active-profile/login.ts b/packages/shared/src/lib/core/profile/actions/active-profile/login.ts index 447fd6430e..24dc6d6d8f 100644 --- a/packages/shared/src/lib/core/profile/actions/active-profile/login.ts +++ b/packages/shared/src/lib/core/profile/actions/active-profile/login.ts @@ -25,6 +25,7 @@ import { get } from 'svelte/store' import { ProfileType } from '../../enums' import { ILoginOptions } from '../../interfaces' import { + activeAccounts, activeProfile, getLastLoggedInProfileId, incrementLoginProgress, @@ -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 { const loginRouter = get(routerManager).getRouterForAppContext(AppContext.Login) @@ -105,6 +107,7 @@ export async function login(loginOptions?: ILoginOptions): Promise { subscribeToWalletApiEventsForActiveProfile() await startBackgroundSync({ syncIncomingTransactions: true }) fetchL2BalanceForAllAccounts() + void fetchAndPersistTransactionsForAccounts(_activeProfile.id, get(activeAccounts)) // Step 8: finish login incrementLoginProgress() diff --git a/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts new file mode 100644 index 0000000000..a3adfb9976 --- /dev/null +++ b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts @@ -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 { + 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 { + 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 +} diff --git a/packages/shared/src/lib/core/transactions/actions/index.ts b/packages/shared/src/lib/core/transactions/actions/index.ts new file mode 100644 index 0000000000..aa79746517 --- /dev/null +++ b/packages/shared/src/lib/core/transactions/actions/index.ts @@ -0,0 +1 @@ +export * from './fetchAndPersistTransactionsForAccounts' diff --git a/packages/shared/src/lib/core/transactions/stores/index.ts b/packages/shared/src/lib/core/transactions/stores/index.ts new file mode 100644 index 0000000000..07282a2660 --- /dev/null +++ b/packages/shared/src/lib/core/transactions/stores/index.ts @@ -0,0 +1 @@ +export * from './transactions.store' diff --git a/packages/shared/src/lib/core/transactions/stores/transactions.store.ts b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts new file mode 100644 index 0000000000..9e93583963 --- /dev/null +++ b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts @@ -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('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 +}