Skip to content

Commit

Permalink
feat: add ethereum confirmed blocks property (#2533)
Browse files Browse the repository at this point in the history
* feat: enable default evm chains

* feat: add ethereum block confirmation poll

* chore: type fix

* chore: extract constants

* feat: add confirmations to local evm transaction type

* feat: persist confirmations

* fix linter

* feat: use inclusion state for EVM transactions

* chore: fetch confirmations on hover

* feat: confirmationPill -> pending pill

---------

Co-authored-by: Mark Nardi <[email protected]>
  • Loading branch information
Tuditi and MarkNerdi authored May 28, 2024
1 parent ce3ff9b commit 6820cd6
Show file tree
Hide file tree
Showing 25 changed files with 217 additions and 93 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,21 @@
import { EvmActivityType } from '@core/activity/enums/evm'
import { NftStandard } from '@core/nfts/enums'
import { convertCamelCaseToPhrase } from '@core/utils/string'
import { ConfirmationPill } from '@ui'
export let activity: Activity
let typePill = ''
let standardPill = ''
$: activity, setPills()
function setPills() {
function setPills(): void {
if (activity.namespace === NetworkNamespace.Stardust) {
if (activity.type === StardustActivityType.Basic) {
if (activity.tokenTransfer && activity.tokenTransfer?.tokenId !== BASE_TOKEN_ID) {
const token = getTokenFromActivity(activity)
typePill = 'token'
standardPill = token.standard
standardPill = token?.standard ?? ''
} else {
typePill = 'baseCoin'
standardPill = ''
Expand Down Expand Up @@ -80,5 +81,8 @@
{standardPill}
</Pill>
{/if}
{#if activity.namespace === NetworkNamespace.Evm}
<ConfirmationPill {activity} />
{/if}
</div>
{/if}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
<GovernanceAvatar governanceAction={activity.governanceAction} size="lg" />
{:else if token}
<TokenAvatar {token} hideNetworkBadge size="lg" />
{:else if showNft}
{:else if showNft && nft}
<NftAvatar {nft} size="lg" shape="square" />
{:else if activity.type === EvmActivityType.ContractCall}
<Avatar
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,7 @@
<Tabs bind:selectedTab {tabs} />
{/if}
{#if selectedTab.key === PopupTab.Transaction}
<EvmGenericInformation
time={activity.time}
sourceNetworkId={activity.sourceNetworkId}
destinationNetworkId={activity.destinationNetworkId}
maxGasFee={activity.maxGasFee}
transactionFee={activity.transactionFee}
/>
<EvmGenericInformation {activity} />
{:else if selectedTab.key === PopupTab.NftMetadata && nft}
<NftMetadataTable {nft} />
{:else if selectedTab.key === PopupTab.SmartContract && (activity.type === EvmActivityType.ContractCall || activity.type === EvmActivityType.TokenTransfer || activity.type === EvmActivityType.TokenMinting)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
<script lang="ts">
import { Table } from '@bloomwalletio/ui'
import { EvmActivity, InclusionState } from '@core/activity'
import { getFormattedTimeStamp, localize } from '@core/i18n'
import { NetworkId, getNetwork } from '@core/network'
import { getNetwork } from '@core/network'
import { formatTokenAmount } from '@core/token'
import { NetworkLabel } from '@ui'
export let time: Date
export let sourceNetworkId: NetworkId
export let destinationNetworkId: NetworkId
export let maxGasFee: bigint | undefined = undefined
export let transactionFee: bigint | undefined = undefined
export let activity: EvmActivity
$: formattedTransactionTime = getFormattedTimeStamp(time)
$: formattedTransactionTime = getFormattedTimeStamp(activity.time)
$: formattedMaxGasFee = formatAmount(maxGasFee)
$: formattedTransactionFee = formatAmount(transactionFee)
$: formattedMaxGasFee = formatAmount(activity.maxGasFee)
$: formattedTransactionFee = formatAmount(activity.transactionFee)
function formatAmount(amount: bigint | undefined): string | undefined {
return amount ? formatTokenAmount(amount, getNetwork(sourceNetworkId)?.baseToken) : undefined
return amount ? formatTokenAmount(amount, getNetwork(activity.sourceNetworkId)?.baseToken) : undefined
}
</script>

Expand All @@ -28,7 +25,7 @@
slot: {
component: NetworkLabel,
props: {
networkId: destinationNetworkId,
networkId: activity.destinationNetworkId,
},
},
},
Expand All @@ -44,5 +41,12 @@
key: localize('general.transactionFee'),
value: formattedTransactionFee,
},
{
key: localize('general.status'),
value:
activity.inclusionState === InclusionState.Confirmed
? localize('general.confirmed')
: localize('general.pending'),
},
]}
/>
31 changes: 31 additions & 0 deletions packages/shared/src/components/pills/ConfirmationPill.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<script lang="ts">
import { Pill } from '@bloomwalletio/ui'
import { Activity, InclusionState } from '@core/activity'
import { localize } from '@core/i18n'
import { getEvmNetwork } from '@core/network/stores'
import { getPersistedTransaction } from '@core/transactions/stores'
export let activity: Activity
let tooltip = ''
async function onHover(): Promise<void> {
tooltip = 'loading...'
const transaction = getPersistedTransaction(activity.transactionId)
const network = getEvmNetwork(activity?.sourceNetworkId)
const blockHeight = await network?.provider.eth.getBlockNumber()
const confirmations = BigInt(blockHeight ?? 0) - BigInt(transaction?.local?.blockNumber ?? 0)
tooltip = `
${localize('general.confirmed')}: ${confirmations}\n
${localize('general.required')}: ${network?.blocksUntilConfirmed}
`
}
</script>

{#if activity.inclusionState === InclusionState.Pending}
<div role="contentinfo" on:focus={onHover} on:mouseover={onHover}>
<Pill color="warning" compact tooltipEvent="hover" {tooltip}>
{localize('general.pending')}
</Pill>
</div>
{/if}
1 change: 1 addition & 0 deletions packages/shared/src/components/pills/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { default as AsyncStatusPill } from './AsyncStatusPill.svelte'
export { default as NetworkStatusPill } from './NetworkStatusPill.svelte'
export { default as NetworkTypePill } from './NetworkTypePill.svelte'
export { default as NftStandardPill } from './NftStandardPill.svelte'
export { default as ConfirmationPill } from './ConfirmationPill.svelte'

This file was deleted.

1 change: 0 additions & 1 deletion packages/shared/src/lib/core/activity/enums/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
export * from './activity-action.enum'
export * from './activity-direction.enum'
export * from './activity-status.enum'
export * from './activity-type-filter-option.enum'
export * from './inclusion-state.enum'

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { get, writable } from 'svelte/store'
import { AsyncData, BaseStardustActivity, Activity } from '../types'
import { AsyncData, BaseStardustActivity, Activity, EvmActivity, BaseEvmActivity } from '../types'
import { NetworkNamespace } from '@core/network'

export const allAccountActivities = writable<{ [accountIndex: number]: Activity[] }>({})
Expand Down Expand Up @@ -35,7 +35,13 @@ export function setAccountActivities(accountIndex: number, accountActivities: Ac
})
}

export function getActivityByTransactionId(accountIndex: number, transactionId: string): Activity | undefined {
export function getActivityByTransactionId(
accountIndex: number,
transactionId: string | undefined
): Activity | undefined {
if (!transactionId) {
return
}
return get(allAccountActivities)?.[accountIndex]?.find((_activity) => _activity?.transactionId === transactionId)
}

Expand Down Expand Up @@ -97,6 +103,25 @@ export function updateAsyncDataByTransactionId(
})
}

export function updateEvmActivity(
accountIndex: number,
transactionHash: string,
partialActivity: Partial<BaseEvmActivity>
): void {
allAccountActivities.update((state) => {
const activity = state[accountIndex]?.find(
(_activity) => _activity.namespace === NetworkNamespace.Evm && _activity?.transactionId === transactionHash
) as EvmActivity

if (!activity) {
return state
}

Object.assign(activity, partialActivity)
return state
})
}

export function clearAccountActivities(): void {
allAccountActivities.set({})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,10 @@ import { MILLISECONDS_PER_SECOND } from '@core/utils/constants'
import { getSubjectFromAddress, isSubjectInternal } from '@core/wallet'
import { ActivityAction, ActivityDirection, InclusionState } from '../../enums'
import { BaseEvmActivity } from '../../types'
import type { BigIntLike } from '@ethereumjs/util'
import { LocalEvmTransaction } from '@core/transactions'

export async function generateBaseEvmActivity(
transaction: {
transactionHash: string
from: string
recipient: string
gasUsed: number
blockNumber: number
estimatedGas?: bigint
gasPrice?: BigIntLike
timestamp?: number
},
transaction: LocalEvmTransaction,
evmNetwork: IEvmNetwork,
account: IAccountState
): Promise<BaseEvmActivity> {
Expand All @@ -28,7 +19,7 @@ export async function generateBaseEvmActivity(
: ActivityDirection.Outgoing

const sender = getSubjectFromAddress(transaction.from, networkId)
const recipient = getSubjectFromAddress(transaction.recipient, networkId)
const recipient = getSubjectFromAddress(transaction.recipient ?? transaction.to, networkId)

const subject = direction === ActivityDirection.Outgoing ? recipient : sender
const isInternal = isSubjectInternal(recipient)
Expand All @@ -38,6 +29,10 @@ export async function generateBaseEvmActivity(
// https://discord.com/channels/397872799483428865/930642258427019354/1168854453005332490
const gasUsed = transaction.gasUsed || transaction.estimatedGas
const transactionFee = transaction.gasPrice ? calculateGasFee(gasUsed, transaction.gasPrice) : undefined
const inclusionState =
Number(transaction.confirmations) >= evmNetwork.blocksUntilConfirmed
? InclusionState.Confirmed
: InclusionState.Pending

return {
namespace: NetworkNamespace.Evm,
Expand All @@ -50,7 +45,7 @@ export async function generateBaseEvmActivity(
// transaction information
transactionId: transaction.transactionHash,
time: new Date(timestamp),
inclusionState: InclusionState.Confirmed,
inclusionState,

// sender / recipient information
sourceNetworkId: networkId,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { IAccountState } from '@core/account'
import { getEvmNetworks } from '@core/network'
import { FAILED_CONFIRMATION, getEvmNetworks, IEvmNetwork } from '@core/network'
import { getPersistedTransactionsForChain } from '@core/transactions/stores'
import { EvmActivity } from '../../types'
import { generateEvmActivityFromPersistedTransaction } from './generateEvmActivityFromPersistedTransaction'
import { LocalEvmTransaction } from '@core/transactions'
import { startEvmConfirmationPoll } from '@core/wallet'

export async function generateEvmActivitiesFromEvmChains(
profileId: string,
Expand All @@ -13,6 +15,11 @@ export async function generateEvmActivitiesFromEvmChains(
for (const evmNetwork of getEvmNetworks()) {
const persistedTransactions = getPersistedTransactionsForChain(profileId, account.index, evmNetwork)
for (const persistedTransaction of persistedTransactions) {
const { local } = persistedTransaction
if (local) {
updateConfirmationsForEvmTransactions(evmNetwork, local, account.index, profileId)
}

try {
const activity = await generateEvmActivityFromPersistedTransaction(
persistedTransaction,
Expand All @@ -30,3 +37,22 @@ export async function generateEvmActivitiesFromEvmChains(

return activities
}

function updateConfirmationsForEvmTransactions(
evmNetwork: IEvmNetwork,
transaction: LocalEvmTransaction,
accountIndex: number,
profileId: string
): void {
if (transaction.confirmations === FAILED_CONFIRMATION) {
return
}

if (!transaction?.confirmations || transaction.confirmations < evmNetwork.blocksUntilConfirmed) {
try {
startEvmConfirmationPoll(transaction, evmNetwork, accountIndex, profileId)
} catch (error) {
console.error(error)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,20 @@ async function generateBaseEvmActivityFromBlockscoutTransaction(
evmNetwork: IEvmNetwork,
account: IAccountState
): Promise<BaseEvmActivity> {
const baseActivity = await generateBaseEvmActivity(
{
recipient: blockscoutTransaction.to.hash.toLowerCase(),
from: blockscoutTransaction.from.hash.toLowerCase(),
gasUsed: Number(blockscoutTransaction.gas_used),
estimatedGas: localTransaction?.estimatedGas ? BigInt(localTransaction.estimatedGas) : undefined,
gasPrice: blockscoutTransaction.gas_price,
transactionHash: blockscoutTransaction.hash,
timestamp: new Date(blockscoutTransaction.timestamp).getTime(),
blockNumber: blockscoutTransaction.block,
},
evmNetwork,
account
)
const newLocalTransaction: LocalEvmTransaction = {
recipient: blockscoutTransaction.to.hash.toLowerCase(),
from: blockscoutTransaction.from.hash.toLowerCase(),
gasUsed: Number(blockscoutTransaction.gas_used),
gasPrice: blockscoutTransaction.gas_price,
transactionHash: blockscoutTransaction.hash,
timestamp: new Date(blockscoutTransaction.timestamp).getTime(),
blockNumber: blockscoutTransaction.block,
confirmations: blockscoutTransaction.confirmations,
status: localTransaction?.status ?? false,
transactionIndex: localTransaction?.transactionIndex ?? 0,
to: localTransaction?.to ?? blockscoutTransaction.to.hash.toLowerCase(),
}
const baseActivity = await generateBaseEvmActivity(newLocalTransaction, evmNetwork, account)

if (blockscoutTransaction.to.is_contract) {
baseActivity.recipient = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,8 @@ export async function generateEvmActivityFromLocalEvmTransaction(
account: IAccountState
): Promise<EvmActivity | undefined> {
if (!transaction.data) {
const { to, from, gasUsed, estimatedGas, gasPrice, transactionHash, timestamp, blockNumber } = transaction
// i.e must be a coin transfer
const baseActivity = await generateBaseEvmActivity(
{
recipient: to?.toString().toLowerCase(),
from: from?.toString().toLowerCase(),
gasUsed: Number(gasUsed),
estimatedGas: estimatedGas ? BigInt(estimatedGas) : undefined,
gasPrice: gasPrice ?? undefined,
transactionHash,
timestamp,
blockNumber,
},
evmNetwork,
account
)
const baseActivity = await generateBaseEvmActivity(transaction, evmNetwork, account)

return {
...baseActivity,
Expand All @@ -57,21 +43,8 @@ export async function generateEvmActivityFromLocalEvmTransaction(
return
}

const { to, from, gasUsed, estimatedGas, gasPrice, transactionHash, timestamp, blockNumber } = transaction
let baseActivity = await generateBaseEvmActivity(
{
recipient: parsedData.recipientAddress ?? to?.toString().toLowerCase(),
from: from?.toString().toLowerCase(),
gasUsed: Number(gasUsed),
estimatedGas: estimatedGas ? BigInt(estimatedGas) : undefined,
gasPrice: gasPrice ?? undefined,
transactionHash,
timestamp,
blockNumber,
},
evmNetwork,
account
)
transaction.recipient = parsedData.recipientAddress ?? transaction.to?.toString().toLowerCase()
let baseActivity = await generateBaseEvmActivity(transaction, evmNetwork, account)

baseActivity = {
...baseActivity,
Expand All @@ -81,7 +54,7 @@ export async function generateEvmActivityFromLocalEvmTransaction(
rawData: String(transaction.data ?? ''),
contract: {
type: SubjectType.SmartContract,
address: to?.toString().toLowerCase(),
address: transaction.to?.toString().toLowerCase(),
name: '',
verified: false,
},
Expand Down
Loading

0 comments on commit 6820cd6

Please sign in to comment.