From b8d1fa3e8c9e1e92725c255e0f5e9239bff66708 Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Wed, 28 Feb 2024 10:04:00 +0000 Subject: [PATCH 1/5] refactor: improve blockscout transaction interface Co-authored-by: Mark Nardi --- .../blockscout-transaction.interface.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) 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 From 9c296b64e570afb54290120e59c9c526bd1394b8 Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Wed, 28 Feb 2024 10:06:39 +0000 Subject: [PATCH 2/5] feat: add new persisted transactions store --- .../src/lib/core/transactions/stores/index.ts | 1 + .../transactions/stores/transactions.store.ts | 107 ++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 packages/shared/src/lib/core/transactions/stores/index.ts create mode 100644 packages/shared/src/lib/core/transactions/stores/transactions.store.ts 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..1a1093b684 --- /dev/null +++ b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts @@ -0,0 +1,107 @@ +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) { + _transactions[transaction.transactionHash.toLowerCase()].local = transaction + } + 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) { + _transactions[transaction.hash.toLowerCase()].blockscout = transaction + } + state[get(activeProfileId)][accountIndex][networkId] = _transactions + + return state + }) +} + +export function removePersistedTransactionsForProfile(profileId: string): void { + persistedTransactions.update((state) => { + delete state[profileId] + return state + }) +} From 5734de82ae63072c3294974e10915cf0d4660e8a Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Wed, 28 Feb 2024 10:34:16 +0000 Subject: [PATCH 3/5] feat: fetch and persist transactions from blockscout on login --- .../profile/actions/active-profile/login.ts | 3 ++ .../fetchAndPersistTransactionsForAccounts.ts | 42 +++++++++++++++++++ .../lib/core/transactions/actions/index.ts | 1 + .../transactions/stores/transactions.store.ts | 18 ++++++-- 4 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts create mode 100644 packages/shared/src/lib/core/transactions/actions/index.ts 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..fe3d83c55c --- /dev/null +++ b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts @@ -0,0 +1,42 @@ +import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' +import { IAccountState, getAddressFromAccountForNetwork } from '@core/account' +import { addBlockscoutTransactionToPersistedTransactions } 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(account, networkId) + blockscoutTransactions && + addBlockscoutTransactionToPersistedTransactions( + profileId, + account.index, + networkId, + blockscoutTransactions + ) + } catch (err) { + console.error(err) + } + } + } +} + +async function fetchBlockscoutTransactionsForAccount( + 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) + 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/transactions.store.ts b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts index 1a1093b684..fd903bf319 100644 --- a/packages/shared/src/lib/core/transactions/stores/transactions.store.ts +++ b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts @@ -53,7 +53,7 @@ export function addLocalTransactionToPersistedTransaction( } if (!state[profileId][accountIndex]) { state[profileId][accountIndex] = { - [networkId]: [], + [networkId]: {}, } } if (!state[profileId][accountIndex][networkId]) { @@ -62,7 +62,12 @@ export function addLocalTransactionToPersistedTransaction( const _transactions = state[get(activeProfileId)][accountIndex][networkId] ?? {} for (const transaction of newTransactions) { - _transactions[transaction.transactionHash.toLowerCase()].local = transaction + const existingTransaction = _transactions?.[transaction.transactionHash.toLowerCase()] + const updatedTransaction: PersistedTransaction = { + ...existingTransaction, + local: transaction, + } + _transactions[transaction.transactionHash.toLowerCase()] = updatedTransaction } state[get(activeProfileId)][accountIndex][networkId] = _transactions @@ -82,7 +87,7 @@ export function addBlockscoutTransactionToPersistedTransactions( } if (!state[profileId][accountIndex]) { state[profileId][accountIndex] = { - [networkId]: [], + [networkId]: {}, } } if (!state[profileId][accountIndex][networkId]) { @@ -91,7 +96,12 @@ export function addBlockscoutTransactionToPersistedTransactions( const _transactions = state[get(activeProfileId)][accountIndex][networkId] ?? {} for (const transaction of newTransactions) { - _transactions[transaction.hash.toLowerCase()].blockscout = transaction + const existingTransaction = _transactions?.[transaction.hash.toLowerCase()] + const updatedTransaction: PersistedTransaction = { + ...existingTransaction, + blockscout: transaction, + } + _transactions[transaction.hash.toLowerCase()] = updatedTransaction } state[get(activeProfileId)][accountIndex][networkId] = _transactions From 0ad9dd92973456154a2e9efb3b49d1ada0a4e24f Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Wed, 28 Feb 2024 11:20:33 +0000 Subject: [PATCH 4/5] feat: add exit function to recursive blockscout api calls --- .../blockscout/api/blockscout.api.ts | 25 +++++++++++++++---- .../fetchAndPersistTransactionsForAccounts.ts | 23 ++++++++++++++--- .../transactions/stores/transactions.store.ts | 9 +++++++ 3 files changed, 49 insertions(+), 8 deletions(-) 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..43938cbff6 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,11 +29,15 @@ 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) { @@ -42,7 +47,8 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi { 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/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts index fe3d83c55c..a3adfb9976 100644 --- a/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts +++ b/packages/shared/src/lib/core/transactions/actions/fetchAndPersistTransactionsForAccounts.ts @@ -1,6 +1,6 @@ import { IBlockscoutTransaction } from '@auxiliary/blockscout/interfaces' import { IAccountState, getAddressFromAccountForNetwork } from '@core/account' -import { addBlockscoutTransactionToPersistedTransactions } from '../stores' +import { addBlockscoutTransactionToPersistedTransactions, isBlockscoutTransactionPersisted } from '../stores' import { BlockscoutApi } from '@auxiliary/blockscout/api' import { EvmNetworkId, getNetwork } from '@core/network' @@ -13,7 +13,11 @@ export async function fetchAndPersistTransactionsForAccounts( const networkId = chain.getConfiguration().id as EvmNetworkId for (const account of accounts) { try { - const blockscoutTransactions = await fetchBlockscoutTransactionsForAccount(account, networkId) + const blockscoutTransactions = await fetchBlockscoutTransactionsForAccount( + profileId, + account, + networkId + ) blockscoutTransactions && addBlockscoutTransactionToPersistedTransactions( profileId, @@ -28,7 +32,18 @@ export async function fetchAndPersistTransactionsForAccounts( } } +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 { @@ -37,6 +52,8 @@ async function fetchBlockscoutTransactionsForAccount( return undefined } const blockscoutApi = new BlockscoutApi(networkId) - const transactions = await blockscoutApi.getTransactionsForAddress(address) + 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/stores/transactions.store.ts b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts index fd903bf319..9e93583963 100644 --- a/packages/shared/src/lib/core/transactions/stores/transactions.store.ts +++ b/packages/shared/src/lib/core/transactions/stores/transactions.store.ts @@ -115,3 +115,12 @@ export function removePersistedTransactionsForProfile(profileId: string): void { return state }) } + +export function isBlockscoutTransactionPersisted( + profileId: string, + accountIndex: number, + networkId: EvmNetworkId, + transactionHash: string +): boolean { + return !!get(persistedTransactions)?.[profileId]?.[accountIndex]?.[networkId]?.[transactionHash]?.blockscout +} From c7fae7a10143f3b50acb3bbeac03c2ff1032de5f Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Wed, 28 Feb 2024 14:01:53 +0100 Subject: [PATCH 5/5] interupt recursion if error occured --- .../shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 43938cbff6..b408a9f773 100644 --- a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts @@ -40,7 +40,7 @@ export class BlockscoutApi extends BaseApi implements IBlockscoutApi { } return this.get>(path, { ...queryParameters, ...nextPageParameters }).then( (response) => { - if (!response) { + if (!response?.items) { return Promise.resolve(items) } return this.makePaginatedGetRequest(