Skip to content

Commit

Permalink
Rework transaction contract info strategy
Browse files Browse the repository at this point in the history
The new strategy is in the wrong place, but has the right general
approach of adding a new field with contract interaction information.

This commit also adds asset transfer and contract deployment/interaction
detection, which is then rendered (poorly) in the transaction list.
  • Loading branch information
Shadowfiend committed Dec 9, 2021
1 parent 50c1a5b commit 6b69209
Show file tree
Hide file tree
Showing 8 changed files with 202 additions and 86 deletions.
22 changes: 13 additions & 9 deletions background/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import {
signed,
} from "./redux-slices/transaction-construction"
import { allAliases } from "./redux-slices/utils"
import { determineToken } from "./redux-slices/utils/activity-utils"
import { enrichTransactionWithContractInfo } from "./redux-slices/utils/activity-utils"
import BaseService from "./services/base"
import InternalEthereumProviderService from "./services/internal-ethereum-provider"
import ProviderBridgeService from "./services/provider-bridge"
Expand Down Expand Up @@ -287,15 +287,19 @@ export default class Main extends BaseService<never> {
})
this.chainService.emitter.on("transaction", async (payload) => {
const { transaction } = payload
const enrichedPayload = {
...payload,
transaction: {
...transaction,
token: await determineToken(transaction),
},
}

this.store.dispatch(activityEncountered(enrichedPayload))
const enrichedTransaction = enrichTransactionWithContractInfo(
this.store.getState().assets,
transaction,
2 /* TODO desiredDecimals should be configurable */
)

this.store.dispatch(
activityEncountered({
...payload,
transaction: enrichedTransaction,
})
)
})
this.chainService.emitter.on("block", (block) => {
this.store.dispatch(blockSeen(block))
Expand Down
7 changes: 4 additions & 3 deletions background/redux-slices/activities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
keysMap,
adaptForUI,
ActivityItem,
determineActivityDecimalValue,
ContractInfo,
} from "./utils/activity-utils"
import { AnyEVMTransaction } from "../networks"

Expand Down Expand Up @@ -46,7 +46,9 @@ const activitiesSlice = createSlice({
payload: { transaction, forAccounts },
}: {
payload: {
transaction: AnyEVMTransaction
transaction: AnyEVMTransaction & {
contractInfo?: ContractInfo | undefined
}
forAccounts: string[]
}
}
Expand All @@ -65,7 +67,6 @@ const activitiesSlice = createSlice({
infoRows,
fromTruncated: truncateAddress(transaction.from),
toTruncated: truncateAddress(transaction.to ?? ""),
tokenDecimalValue: determineActivityDecimalValue(transaction),
})
})
},
Expand Down
23 changes: 15 additions & 8 deletions background/redux-slices/selectors/activitiesSelectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,22 @@ export const selectCurrentAccountActivitiesWithTimestamps = createSelector(
ui: UIState
activities: ActivitiesState
account: AccountState
}) => state,
({ activities, ui, account }) => {
const currentAccountActivities = ui.currentAccount
? activities[ui.currentAccount.address]
: undefined
}) => ({
currentAccountAddress: state.ui.currentAccount?.address,
currentAccountActivities:
typeof state.ui.currentAccount !== "undefined"
? state.activities[state.ui.currentAccount?.address]
: undefined,
blocks: state.account.blocks,
}),
({ currentAccountAddress, currentAccountActivities, blocks }) => {
return currentAccountActivities?.ids.map((id: EntityId): ActivityItem => {
const activityItem = currentAccountActivities.entities[id] as ActivityItem
const isSent =
activityItem.from.toLowerCase() === ui.currentAccount?.address
// Guaranteed by the fact that we got the id from the ids collection.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const activityItem = currentAccountActivities.entities[id]!

const isSent = activityItem.from.toLowerCase() === currentAccountAddress

return {
...activityItem,
timestamp:
Expand Down
153 changes: 105 additions & 48 deletions background/redux-slices/utils/activity-utils.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,17 @@
import dayjs from "dayjs"
import { getNetwork } from "@ethersproject/networks"
import { AlchemyProvider } from "@ethersproject/providers"
import logger from "../../lib/logger"
import { ETH } from "../../constants/currencies"
import { SmartContractFungibleAsset, FungibleAsset } from "../../assets"
import { getTokenMetadata } from "../../lib/alchemy"
import { convertToEth, getEthereumNetwork } from "../../lib/utils"
import {
SmartContractFungibleAsset,
AnyAsset,
isSmartContractFungibleAsset,
AnyAssetAmount,
} from "../../assets"
import { convertToEth } from "../../lib/utils"
import { AnyEVMTransaction } from "../../networks"
import { AssetDecimalAmount } from "./asset-utils"

const pollingProviders = {
ethereum: new AlchemyProvider(
getNetwork(Number(getEthereumNetwork().chainID)),
process.env.ALCHEMY_KEY
),
}
import {
AssetDecimalAmount,
enrichAssetAmountWithDecimalValues,
} from "./asset-utils"
import { HexString } from "../../types"

function ethTransformer(
value: string | number | bigint | null | undefined
Expand All @@ -35,21 +32,43 @@ export type UIAdaptationMap<T> = {
[P in keyof T]?: FieldAdapter<T[P]>
}

export type BaseContractInfo = {
contractLogoURL?: string | undefined
}

export type ContractDeployment = BaseContractInfo & {
type: "contract-deployment"
}

export type ContractInteraction = BaseContractInfo & {
type: "contract-interaction"
}

export type AssetTransfer = BaseContractInfo & {
type: "asset-transfer"
assetAmount: AnyAssetAmount & AssetDecimalAmount
}

export type ContractInfo =
| ContractDeployment
| ContractInteraction
| AssetTransfer
| undefined

export type ActivityItem = AnyEVMTransaction & {
contractInfo?: ContractInfo | undefined
timestamp?: number
isSent?: boolean
blockHeight: number | null
fromTruncated: string
toTruncated: string
infoRows: {
[name: string]: {
label: string
value: unknown
value: string
valueDetail: string
}
}
token: FungibleAsset
tokenDecimalValue: AssetDecimalAmount["decimalAmount"]
fromTruncated: string
toTruncated: string
}

/**
Expand Down Expand Up @@ -103,7 +122,8 @@ export function adaptForUI<T>(
export const keysMap: UIAdaptationMap<ActivityItem> = {
blockHeight: {
readableName: "Block Height",
transformer: (item: number) => item.toString(),
transformer: (height: number | null) =>
height === null ? "(pending)" : height.toString(),
detailTransformer: () => {
return ""
},
Expand Down Expand Up @@ -142,39 +162,76 @@ export const keysMap: UIAdaptationMap<ActivityItem> = {
},
}

export async function determineToken(
result: AnyEVMTransaction
): Promise<FungibleAsset | SmartContractFungibleAsset | null> {
const { input } = result
let asset = ETH
if (input) {
try {
let meta: SmartContractFungibleAsset | null = null
if (result?.to) {
meta = await getTokenMetadata(pollingProviders.ethereum, result.to)
}
if (meta) {
asset = meta
}
} catch (err) {
logger.error(`Error getting token metadata`, err)
function resolveContractInfo(
assets: AnyAsset[],
contractAddress: HexString | undefined,
contractInput: HexString,
desiredDecimals: number
): ContractInfo | undefined {
// A missing recipient means a contract deployment.
if (typeof contractAddress === "undefined") {
return {
type: "contract-deployment",
}
}

return asset
}
// See if the address matches a fungible asset.
const matchingFungibleAsset = assets.find(
(asset): asset is SmartContractFungibleAsset =>
isSmartContractFungibleAsset(asset) &&
asset.contractAddress.toLowerCase() === contractAddress.toLowerCase()
)

export function determineActivityDecimalValue(
activityItem: ActivityItem
): number {
const { token } = activityItem
let { value } = activityItem
const contractLogoURL = matchingFungibleAsset?.metadata?.logoURL

// Derive value from transaction transfer if not sending ETH
if (value === BigInt(0) && activityItem.input) {
value = BigInt(`0x${activityItem.input.slice(10).slice(0, 64)}`)
// FIXME Move to ERC20 parsing using ethers.
if (
typeof matchingFungibleAsset !== "undefined" &&
contractInput.length >= 74 &&
contractInput.startsWith("0xa9059cbb") // transfer selector
) {
return {
type: "asset-transfer",
contractLogoURL,
assetAmount: enrichAssetAmountWithDecimalValues(
{
asset: matchingFungibleAsset,
amount: BigInt(`0x${contractInput.slice(10, 10 + 64)}`),
},
desiredDecimals
),
}
}

const decimalValue = Number(value) / 10 ** token.decimals
return decimalValue
// Fall back on a standard contract interaction.
return {
type: "contract-interaction",
contractLogoURL,
}
}

export function enrichTransactionWithContractInfo(
assets: AnyAsset[],
transaction: AnyEVMTransaction,
desiredDecimals: number
): AnyEVMTransaction & { contractInfo?: ContractInfo | undefined } {
if (transaction.input === null || transaction.input === "0x") {
// This is _almost certainly_ not a contract interaction, move on. Note that
// a simple ETH send to a contract address can still effectively be a
// contract interaction (because it calls the fallback function on the
// contract), but for now we deliberately ignore that scenario when
// categorizing activities.
return transaction
}

return {
...transaction,
contractInfo: resolveContractInfo(
assets,
transaction.to,
transaction.input,
desiredDecimals
),
}
}
3 changes: 2 additions & 1 deletion background/services/indexing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,10 +227,11 @@ export default class IndexingService extends BaseService<Events> {
const otherActiveAssets = cachedAssets
.filter(isSmartContractFungibleAsset)
.filter(
(a: SmartContractFungibleAsset) =>
(a) =>
a.homeNetwork.chainID === getEthereumNetwork().chainID &&
!checkedContractAddresses.has(a.contractAddress)
)

await this.retrieveTokenBalances(
addressNetwork,
otherActiveAssets.map((a) => a.contractAddress)
Expand Down
68 changes: 53 additions & 15 deletions ui/components/Wallet/WalletActivityListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,58 @@ export default function WalletActivityListItem(props: Props): ReactElement {
if (typeof activity.value === "undefined" || activity.value === BigInt(0))
return <></>

let activityContent = (
<div className="left">
<div className="token_icon_wrap">
<SharedAssetIcon symbol={activity.asset.symbol} size="small" />
</div>
<div className="amount">
<span className="bold_amount_count">{activity.value ?? ""}</span>
{activity.asset.symbol}
</div>
</div>
)

switch (activity.contractInfo?.type) {
case "asset-transfer":
activityContent = (
<div className="left">
<div className="token_icon_wrap">
<SharedAssetIcon
logoURL={activity.contractInfo.contractLogoURL}
symbol={activity.contractInfo.assetAmount.asset.symbol}
size="small"
/>
</div>
<div className="amount">
<span className="bold_amount_count">
{activity.contractInfo.assetAmount.localizedDecimalAmount}
</span>
{activity.contractInfo.assetAmount.asset.symbol}
</div>
</div>
)
break
case "contract-deployment":
case "contract-interaction":
activityContent = (
<div className="left">
<div className="token_icon_wrap">
<SharedAssetIcon
logoURL={activity.contractInfo.contractLogoURL}
symbol={activity.asset.symbol}
size="small"
/>
</div>
<div className="amount">
<span className="bold_amount_count">Contract Interaction</span>
</div>
</div>
)
break
default:
}

return (
<li>
<button type="button" className="standard_width" onClick={onClick}>
Expand All @@ -33,21 +85,7 @@ export default function WalletActivityListItem(props: Props): ReactElement {
</div>
</div>
<div className="bottom">
<div className="left">
<div className="token_icon_wrap">
<SharedAssetIcon
logoURL={activity?.token?.metadata?.logoURL}
symbol={activity.token?.symbol}
size="small"
/>
</div>
<div className="amount">
<span className="bold_amount_count">
{`${activity.tokenDecimalValue}`.substring(0, 6)}
</span>
{activity.token.symbol}
</div>
</div>
{activityContent}
<div className="right">
{activity.isSent ? (
<div className="outcome">
Expand Down
4 changes: 4 additions & 0 deletions ui/components/Wallet/WalletAssetListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ export default function WalletAssetListItem(props: Props): ReactElement {
pathname: "/singleAsset",
state: {
symbol: assetAmount.asset.symbol,
contractAddress:
"contractAddress" in assetAmount.asset
? assetAmount.asset.contractAddress
: undefined,
},
}}
>
Expand Down
Loading

0 comments on commit 6b69209

Please sign in to comment.