diff --git a/docs/specifications/activities/class-diagram.md b/docs/specifications/activities/class-diagram.md index 94af714ae76..1b098ed1134 100644 --- a/docs/specifications/activities/class-diagram.md +++ b/docs/specifications/activities/class-diagram.md @@ -42,7 +42,6 @@ classDiagram - type: ActivityType.Basic - rawAmount: number - assetId: string - - publicNote: string - isShimmerClaiming: boolean } class Nft Activity { diff --git a/packages/shared/lib/contexts/onboarding/stores/shimmer-claiming-transactions.store.ts b/packages/shared/lib/contexts/onboarding/stores/shimmer-claiming-transactions.store.ts index ed0f4f8f70f..a98809758a0 100644 --- a/packages/shared/lib/contexts/onboarding/stores/shimmer-claiming-transactions.store.ts +++ b/packages/shared/lib/contexts/onboarding/stores/shimmer-claiming-transactions.store.ts @@ -12,7 +12,7 @@ export const shimmerClaimingTransactions: Writable { state[get(selectedWalletId)].forEach((_activity) => { - if (_activity.type === ActivityType.Basic || _activity.type === ActivityType.Foundry) { + if (_activity.type === ActivityType.Transaction || _activity.type === ActivityType.Foundry) { const isAssetHidden = !assets[_activity.assetId] || assets[_activity.assetId]?.hidden updateActivityFromPartialActivity(_activity, { isAssetHidden }) } diff --git a/packages/shared/lib/core/wallet/actions/activities/linkTransactionsWithClaimingTransactions.ts b/packages/shared/lib/core/wallet/actions/activities/linkTransactionsWithClaimingTransactions.ts index d55edcb912e..58c4c786362 100644 --- a/packages/shared/lib/core/wallet/actions/activities/linkTransactionsWithClaimingTransactions.ts +++ b/packages/shared/lib/core/wallet/actions/activities/linkTransactionsWithClaimingTransactions.ts @@ -5,6 +5,8 @@ import { isOutputAsync } from '@core/wallet/utils/outputs/isOutputAsync' import { get } from 'svelte/store' import { addClaimedActivity, claimedActivities } from '../../stores' +// TODO: Refactor and clean this + /** * It takes a list of transactions and links the transactions that are claiming async transactions * @param {IProcessedTransaction[]} transactions - IProcessedTransaction[] diff --git a/packages/shared/lib/core/wallet/actions/activities/loadAssetsForAllWallets.ts b/packages/shared/lib/core/wallet/actions/activities/loadAssetsForAllWallets.ts index 19afb3ad237..cbc4a263202 100644 --- a/packages/shared/lib/core/wallet/actions/activities/loadAssetsForAllWallets.ts +++ b/packages/shared/lib/core/wallet/actions/activities/loadAssetsForAllWallets.ts @@ -11,7 +11,7 @@ export async function loadAssetsForAllWallets(wallet: IWalletState): Promise { + const input = wrappedInput.output + if (input.type === OutputType.Nft) { + const nftInput = input as NftOutput + const nftId = getNftId(nftInput.nftId, wrappedInput.outputId) + + const isIncludedInOutputs = this.outputs.some((output) => { + if (output.output.type === OutputType.Nft) { + const nftOutput = output.output as NftOutput + return getNftId(nftOutput.nftId, output.outputId) === nftId + } else { + return false + } + }) + + return !isIncludedInOutputs + } else { + return false + } + }) + } + + getBurnedNativeTokens(): { assetId: string; amount: number } | undefined { + // If the transaction is unblanced and there is a surplus of native tokens on the + // input side of the transaction: the transaction destroys tokens. + if (this.direction !== ActivityDirection.SelfTransaction) { + return + } + + const inputNativeTokens: { [key: string]: number } = ProcessedTransaction.getAllNativeTokensFromOutputs( + this.wrappedInputs + ) + // No burned native tokens if input doesn't contain any native tokens + if (Object.keys(inputNativeTokens).length === 0) { + return + } + + const outputNativeTokens: { [key: string]: number } = ProcessedTransaction.getAllNativeTokensFromOutputs( + this.outputs + ) + // Find missing native tokens in outputNativeTokens (ex. input native tokens count === 3, output native tokens count === 2) + // TO DO: adjust UI to account for burining entire amounts of multiple native tokens in one transaction. + // We assume here that transaction burns entire amount of only one token. + // There may be transactions created outside of FF that burn entire amount for multiple tokens from the input side + // (ex.input native tokens count === 3, output native tokens count === 0) + let burnedTokenKeys: string[] = Object.keys(inputNativeTokens).filter((key) => !(key in outputNativeTokens)) + if (Object.keys(burnedTokenKeys).length > 0) { + return { assetId: burnedTokenKeys[0], amount: inputNativeTokens[burnedTokenKeys[0]] } + } + // Check if the amount of output native token was larger on the input side (partially burned native tokens) + burnedTokenKeys = Object.keys(outputNativeTokens).filter( + (key) => outputNativeTokens[key] < inputNativeTokens[key] + ) + if (Object.keys(burnedTokenKeys).length > 0) { + const burnedAmount = inputNativeTokens[burnedTokenKeys[0]] - Number(outputNativeTokens[burnedTokenKeys[0]]) + return { assetId: burnedTokenKeys[0], amount: burnedAmount } + } + } + + static getAllNativeTokensFromOutputs(outputs: IWrappedOutput[]): { [key: string]: number } { + const nativeTokens: { [key: string]: number } = {} + for (const output of outputs) { + if (output.output.type === OutputType.Foundry || output.output.type === OutputType.Basic) { + const commonOutput = output.output as CommonOutput + const nativeToken = getNativeTokenFromOutput(commonOutput) + if (nativeToken) { + if (!nativeTokens[nativeToken.id]) { + nativeTokens[nativeToken.id] = 0 + } + + nativeTokens[nativeToken.id] += Number(nativeToken.amount) + } + } + } + return nativeTokens + } +} diff --git a/packages/shared/lib/core/wallet/stores/selected-wallet-activities.store.ts b/packages/shared/lib/core/wallet/stores/selected-wallet-activities.store.ts index 6ad7af83d0c..7e82abd4440 100644 --- a/packages/shared/lib/core/wallet/stores/selected-wallet-activities.store.ts +++ b/packages/shared/lib/core/wallet/stores/selected-wallet-activities.store.ts @@ -28,14 +28,15 @@ export const activitySearchTerm: Writable = writable('') export const queriedActivities: Readable = derived( [selectedWalletActivities, activitySearchTerm, activityFilter], ([$selectedWalletActivities, $activitySearchTerm]) => { + // TODO: Refactor this an clean up. let activityList = $selectedWalletActivities.filter((_activity) => { - const containsAssets = _activity.type === ActivityType.Basic || _activity.type === ActivityType.Foundry + const containsAssets = _activity.type === ActivityType.Transaction || _activity.type === ActivityType.Foundry if (!_activity.isHidden && !containsAssets) { return true } const asset = - _activity.type === ActivityType.Basic || _activity.type === ActivityType.Foundry + _activity.type === ActivityType.Transaction || _activity.type === ActivityType.Foundry ? getAssetFromPersistedAssets(_activity.assetId) : undefined const hasValidAsset = asset?.metadata && isValidIrc30Token(asset.metadata) @@ -64,7 +65,7 @@ function getFieldsToSearchFromActivity(activity: Activity): string[] { fieldsToSearch.push(activity.transactionId) } - if ((activity.type === ActivityType.Basic || activity.type === ActivityType.Foundry) && activity.assetId) { + if ((activity.type === ActivityType.Transaction || activity.type === ActivityType.Foundry) && activity.assetId) { fieldsToSearch.push(activity.assetId) const assetName = getAssetFromPersistedAssets(activity.assetId)?.metadata?.name @@ -73,7 +74,7 @@ function getFieldsToSearchFromActivity(activity: Activity): string[] { } } - if ((activity.type === ActivityType.Basic || activity.type === ActivityType.Foundry) && activity.rawAmount) { + if ((activity.type === ActivityType.Transaction || activity.type === ActivityType.Foundry) && activity.rawAmount) { fieldsToSearch.push(activity.rawAmount?.toString()) fieldsToSearch.push(getFormattedAmountFromActivity(activity, false)?.toLowerCase()) } diff --git a/packages/shared/lib/core/wallet/types/activities/account-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/account-activity.type.ts index 410ea7842a7..7e6f3d16fb0 100644 --- a/packages/shared/lib/core/wallet/types/activities/account-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/account-activity.type.ts @@ -1,8 +1,129 @@ -import { ActivityType } from '@core/wallet/enums' -import { BaseActivity } from './base-activity.type' +import { ActivityAction, ActivityType } from '@core/wallet/enums' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { + ActivityGenerationParameters, + IActivityGenerationParameters, + IProcessedTransaction, + IWalletState, + ProcessedTransaction, +} from '../../interfaces' +import { AccountOutput, InclusionState, OutputType } from '@iota/sdk/out/types' +import { api } from 'shared/lib/core/api' +import { getNetworkHrp } from 'shared/lib/core/profile' +import { + getStorageDepositFromOutput, + getAmountFromOutput, + getMetadataFromOutput, + getTagFromOutput, + getAsyncDataFromOutput, + getSendingInformation, +} from '../../utils' +import { Activity } from '../activity.type' +import { EMPTY_HEX_ID } from '../../constants' export type AccountActivity = BaseActivity & { type: ActivityType.Account accountAddress: string accountId: string } + +interface ActivityAccountOptions extends ActivityBaseOptions { + accountAddress: string + accountId: string +} + +export class ActivityAccount extends ActivityBase { + constructor(private accountOptions: ActivityAccountOptions) { + super(accountOptions) + } + + accountId(): string { + return this.accountOptions.accountId + } + + accountAddress(): string { + return this.accountOptions.accountAddress + } + + tileTitle(): string { + const isConfirmed = this.inclusionState() === InclusionState.Confirmed + //if (action === ActivityAction.Mint) { + // return isConfirmed ? 'general.accountCreated' : 'general.creatingAnAccount' + //} + return isConfirmed ? 'general.minted' : 'general.minting' + } + + static async fromOutputs( + processedTransaction: ProcessedTransaction, + wallet: IWalletState + ): Promise { + const outputs = processedTransaction.outputs + const activities: ActivityAccount[] = [] + + const accountOutputs = outputs.filter((output) => output.output.type === OutputType.Account) + for (const accountOutput of accountOutputs) { + const output = accountOutput.output as AccountOutput + const activity = await ActivityAccount.fromProcessedTransaction(wallet, { + // TODO: Check if an account is created or minted and set the action accordingly + action: output.accountId === EMPTY_HEX_ID ? ActivityAction.Send : ActivityAction.Mint, + processedTransaction, + wrappedOutput: accountOutput, + }) + activities.push(activity) + } + return activities + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ) { + const { transactionId, claimingData, direction, time, inclusionState } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const output = wrappedOutput.output as AccountOutput + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + + const { storageDeposit: _storageDeposit, giftedStorageDeposit } = await getStorageDepositFromOutput(output) + const storageDeposit = getAmountFromOutput(output) + _storageDeposit + const accountId = getAccountId(output, outputId) + const accountAddress = api.accountIdToBech32(accountId, getNetworkHrp()) + + const isHidden = false + const isAssetHidden = false + const containsValue = true + + const metadata = getMetadataFromOutput(output) + const tag = getTagFromOutput(output) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + + return new ActivityAccount({ + isHidden, + id, + inclusionState, + specialStatus, + accountAddress, + accountId, + time, + action, + direction, + isAssetHidden, + containsValue, + metadata, + tag, + asyncData, + outputId, + transactionId, + storageDeposit, + giftedStorageDeposit, + ...sendingInfo + }) + } +} + +function getAccountId(output: AccountOutput, outputId: string): string { + const isNewAccount = output.accountId === EMPTY_HEX_ID + return isNewAccount ? api.computeAccountId(outputId) : output.accountId +} diff --git a/packages/shared/lib/core/wallet/types/activities/anchor-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/anchor-activity.type.ts index ab77b06f452..f5171775f9a 100644 --- a/packages/shared/lib/core/wallet/types/activities/anchor-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/anchor-activity.type.ts @@ -1,8 +1,105 @@ -import { ActivityType } from '@core/wallet/enums' -import { BaseActivity } from './base-activity.type' +import { ActivityAction, ActivityType } from '@core/wallet/enums' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { + ActivityGenerationParameters, + IWalletState, + ProcessedTransaction, +} from '../../interfaces' +import { AnchorOutput, OutputType } from '@iota/sdk/out/types' +import { EMPTY_HEX_ID } from '../../constants' +import { getAmountFromOutput, getAsyncDataFromOutput, getGovernorAddressFromAnchorOutput, getMetadataFromOutput, getStateControllerAddressFromAnchorOutput, getStorageDepositFromOutput, getTagFromOutput } from '../../utils' export type AnchorActivity = BaseActivity & { type: ActivityType.Anchor governorAddress: string stateControllerAddress: string } + +interface ActivityAnchorOptions extends ActivityBaseOptions { + governorAddress: string + stateControllerAddress: string +} + +export class ActivityAnchor extends ActivityBase { + constructor(private anchorOptions: ActivityAnchorOptions) { + super(anchorOptions) + } + + governorAddress(): string { + return this.anchorOptions.governorAddress + } + + stateControllerAddress(): string { + return this.anchorOptions.stateControllerAddress + } + + static async fromOutputs( + processedTransaction: ProcessedTransaction, + wallet: IWalletState + ): Promise { + const outputs = processedTransaction.outputs + const activities = [] + + const anchorOutputs = outputs.filter((output) => output.output.type === OutputType.Anchor) + for (const anchorOutput of anchorOutputs) { + const output = anchorOutput.output as AnchorOutput + const activity = await ActivityAnchor.fromProcessedTransaction(wallet, { + action: output.anchorId === EMPTY_HEX_ID ? ActivityAction.Mint : ActivityAction.Send, + processedTransaction, + wrappedOutput: anchorOutput, + }) + activities.push(activity) + } + return activities + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ): Promise { + const { transactionId, claimingData, direction, time, inclusionState } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const output = wrappedOutput.output as AnchorOutput + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + + const { storageDeposit: _storageDeposit, giftedStorageDeposit } = await getStorageDepositFromOutput( + output + ) + const storageDeposit = getAmountFromOutput(output) + _storageDeposit + const governorAddress = getGovernorAddressFromAnchorOutput(output) + const stateControllerAddress = getStateControllerAddressFromAnchorOutput(output) + + const isHidden = false + const isAssetHidden = false + const containsValue = true + + const metadata = getMetadataFromOutput(output) + const tag = getTagFromOutput(output) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + + return new ActivityAnchor({ + isHidden, + id, + inclusionState, + specialStatus, + time, + transactionId, + direction, + action, + governorAddress, + stateControllerAddress, + isAssetHidden, + containsValue, + outputId, + storageDeposit, + giftedStorageDeposit, + metadata, + tag, + asyncData, + ...sendingInfo, + }) + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/base-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/base-activity.type.ts index 99de5e6d85e..996d653a38d 100644 --- a/packages/shared/lib/core/wallet/types/activities/base-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/base-activity.type.ts @@ -1,6 +1,14 @@ -import { ActivityAsyncStatus, ActivityDirection, InclusionState, ActivityAction } from '../../enums' +import { NftOutput, OutputType, InclusionState } from '@iota/sdk/out/types' +import { ActivityAsyncStatus, ActivityDirection, ActivityAction, ActivityType, SubjectType } from '../../enums' +import { IProcessedTransaction, IWalletState, IWrappedOutput, ProcessedTransaction } from '../../interfaces' import { Subject } from '../subject.type' -import { Layer2Metadata } from '@core/layer-2' +import { Layer2Metadata, getLayer2NetworkFromAddress } from '@core/layer-2' +import { isParticipationOutput } from '@contexts/governance' +import { getActivityTypeFromOutput, getNftId, getNonRemainderBasicOutputsFromTransaction } from '../../utils' +import { addOrUpdateNftInAllWalletNfts, buildNftFromNftOutput } from '@core/nfts' // TODO: Fix imports +import * as Activities from './' +import { localize } from 'shared/lib/core/i18n' +import { truncateString } from 'shared/lib/core/utils' export type BaseActivity = { id: string @@ -10,7 +18,7 @@ export type BaseActivity = { inclusionState: InclusionState isHidden?: boolean containsValue: boolean - isAssetHidden: boolean + isAssetHidden: boolean // TODO: Is `isAssetHidden` even used? direction: ActivityDirection action: ActivityAction isInternal: boolean @@ -20,7 +28,7 @@ export type BaseActivity = { subject: Subject | undefined metadata?: string tag?: string - asyncData: AsyncData + asyncData?: AsyncData destinationNetwork?: string parsedLayer2Metadata?: Partial } @@ -34,3 +42,306 @@ export type AsyncData = { claimingTransactionId: string claimedDate: Date } + +// TODO: Move somewhere else. +export enum SpecialStatus { + Unclaimed = 'Unclaimed', + Claimed = 'Claimed', + Expired = 'Expired', + TimeLocked = 'TimeLocked', +} + +export interface ActivityBaseOptions { + id: string + inclusionState: InclusionState + specialStatus: SpecialStatus + time: Date // Should this be number, slot index? + //from: string[] + //to: string[] + + isHidden: boolean + action: ActivityAction + direction: ActivityDirection + isInternal?: boolean + + outputId: string + transactionId: string + containsValue: boolean + isAssetHidden: boolean // TODO: Is `isAssetHidden` even used? + storageDeposit: number + giftedStorageDeposit: number + surplus?: number + subject: Subject | undefined + metadata?: string + tag?: string + asyncData?: AsyncData + destinationNetwork?: string + parsedLayer2Metadata?: Partial | null +} + +abstract class ActivityUtils { + abstract tileTitle(): string + + abstract subjectLocale(): string +} + +export class ActivityBase implements ActivityUtils { + constructor(private options: ActivityBaseOptions) {} + + isIncoming(): boolean { + return [ActivityDirection.Incoming, ActivityDirection.Incoming].includes(this.direction()) + } + + subject(): Subject | undefined { + const subject = this.options.subject; + if (this.parsedLayer2Metadata() && subject) { + return { + ...subject, + ...(subject?.type === SubjectType.Address && { + address: this.parsedLayer2Metadata()?.ethereumAddress, + }), + } + } else if (subject?.type === SubjectType.Address) { + const network = getLayer2NetworkFromAddress(subject.address) + return { ...subject, address: network ?? subject.address } + } else { + return this.subject() + } + } + + subjectLocale(): string { + const subject = this.subject(); + + if (subject?.type === SubjectType.Wallet) { + return truncateString(subject?.wallet?.name, 13, 0) + } else if (subject?.type === SubjectType.Address) { + const address = this.parsedLayer2Metadata()?.ethereumAddress ?? subject?.address + const network = getLayer2NetworkFromAddress(address) + + return network ?? truncateString(address, 6, 6) + } else { + return localize('general.unknownAddress') + } + } + + id() { + return this.id + } + + inclusionState() { + return this.options.inclusionState + } + + specialStatus() { + return this.options.specialStatus + } + + time() { + return this.options.time + } + + isHidden() { + return this.options.isHidden + } + + action() { + return this.options.action + } + + direction() { + return this.options.direction + } + + isInternal(): boolean { + return this.options.isInternal ?? false + } + + outputId() { + return this.options.outputId + } + + transactionId() { + return this.options.transactionId + } + + containsValue() { + return this.options.containsValue + } + + isAssetHidden() { + return this.options.isAssetHidden + } + + storageDeposit() { + return this.options.storageDeposit + } + + giftedStorageDeposit() { + return this.options.giftedStorageDeposit + } + + surplus() { + return this.options.surplus + } + + metadata() { + return this.options.metadata + } + + tag() { + return this.options.tag + } + + asyncData(): AsyncData | undefined { + return this.options.asyncData + } + + destinationNetwork() { + return this.options.destinationNetwork + } + + parsedLayer2Metadata() { + return this.options.parsedLayer2Metadata + } + + tileTitle(): string { + const isConfirmed = this.inclusionState() === InclusionState.Confirmed + if (this.action() === ActivityAction.Burn) { + return isConfirmed ? 'general.burned' : 'general.burning' + } else if (this.action() === ActivityAction.Send) { + if (this.isInternal()) { + return isConfirmed ? 'general.transfer' : 'general.transferring' + } + if ( + this.direction() === ActivityDirection.Incoming || + this.direction() === ActivityDirection.SelfTransaction + ) { + return isConfirmed ? 'general.received' : 'general.receiving' + } + if (this.direction() === ActivityDirection.Outgoing) { + return isConfirmed ? 'general.sent' : 'general.sending' + } + } else { + return 'general.unknown' + } + + return '' + } + + /** + * Generate a group of activies given a processed transaction + * @returns ActivityBase[] + */ + static generateActivitiesFromProcessedTransaction( + wallet: IWalletState, + processedTransaction: ProcessedTransaction + ): Promise> { + if (processedTransaction.wrappedInputs?.length > 0) { + return this.generateActivitiesFromProcessedTransactionsWithInputs(wallet, processedTransaction) + } else { + return this.generateActivitiesFromProcessedTransactionsWithoutInputs(wallet, processedTransaction) + } + } + + static async generateActivitiesFromProcessedTransactionsWithInputs( + wallet: IWalletState, + processedTransaction: ProcessedTransaction + ): Promise> { + let activities: Array = [] + + const { wrappedInputs, outputs } = processedTransaction + + const containsFoundryActivity = outputs.some((output) => output.output.type === OutputType.Foundry) + if (containsFoundryActivity) { + const foundryActivities = await Activities.ActivityFoundry.fromOutputs(processedTransaction, wallet) + activities.push(...foundryActivities) + } + + const containsNftActivity = outputs.some((output) => output.output.type === OutputType.Nft) + if (containsNftActivity) { + const nftActivities = await Activities.ActivityNft.fromOutputs(processedTransaction, wallet) + activities.push(...nftActivities) + } + + const containsAccountActivity = + outputs.some((output) => output.output.type === OutputType.Account) && !containsFoundryActivity + if (containsAccountActivity) { + const accountActivities = await Activities.ActivityAccount.fromOutputs(processedTransaction, wallet) + activities.push(...accountActivities) + } + + const hasParticipationInputs = wrappedInputs?.some((input) => isParticipationOutput(input.output)) + const governanceOutput = hasParticipationInputs + ? outputs[0] + : outputs.find((output) => isParticipationOutput(output.output)) + if (governanceOutput) { + const governanceActivity = await Activities.ActivityGovernance.fromProcessedTransaction(wallet, { + processedTransaction, + wrappedOutput: governanceOutput, + action: ActivityAction.Unknown, // TODO: Maybe this should be optional? + }) + activities.push(governanceActivity) + } + + const containsAnchorActivity = outputs.some((output) => output.output.type === OutputType.Anchor) + if (containsAnchorActivity) { + const anchorActivities = await Activities.ActivityAnchor.fromOutputs(processedTransaction, wallet) + activities.push(...anchorActivities) + } + + if (!containsFoundryActivity && !containsNftActivity && !containsAccountActivity && !governanceOutput) { + const basicActivities = await Activities.ActivityTransaction.fromOutputs(processedTransaction, wallet) + activities.push(...basicActivities) + } + + return activities + } + + static async generateActivitiesFromProcessedTransactionsWithoutInputs( + wallet: IWalletState, + processedTransaction: ProcessedTransaction + ): Promise> { + const nonRemainderOutputs = processedTransaction.outputs.filter((wrappedOutput) => !wrappedOutput.remainder) + const activities = await Promise.all( + nonRemainderOutputs.map(async (wrappedOutput) => { + const params = { + action: ActivityAction.Unknown, + processedTransaction, + wrappedOutput, + } + const activityType = getActivityTypeFromOutput(wrappedOutput) + switch (activityType) { + case ActivityType.Transaction: + return Activities.ActivityTransaction.fromProcessedTransaction(wallet, params) + case ActivityType.Governance: + return Activities.ActivityGovernance.fromProcessedTransaction(wallet, params) + case ActivityType.Foundry: + return Activities.ActivityFoundry.fromProcessedTransaction(wallet, params) + case ActivityType.Account: + return Activities.ActivityAccount.fromProcessedTransaction(wallet, params) + case ActivityType.Nft: + return Activities.ActivityNft.fromProcessedTransaction(wallet, params) + case ActivityType.Vesting: + return Activities.ActivityVesting.fromProcessedTransaction(wallet, params) + case ActivityType.Anchor: + return Activities.ActivityAnchor.fromProcessedTransaction(wallet, params) + default: + throw new Error(`Unknown activity type: ${activityType}`) + } + }) + ) + return activities + } + + static isConsolidation(output: IWrappedOutput, processedTransaction: IProcessedTransaction): boolean { + const allBasicInputs = processedTransaction.wrappedInputs.every( + (input) => input.output.type === OutputType.Basic + ) + const isSelfTransaction = processedTransaction.direction === ActivityDirection.SelfTransaction + const isSameAmount = + processedTransaction.wrappedInputs.reduce((sum, input) => sum + Number(input.output.amount), 0) === + Number(output.output.amount) + + return allBasicInputs && isSelfTransaction && isSameAmount + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/consolidation-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/consolidation-activity.type.ts index ea36de6aa22..9ff77f6b199 100644 --- a/packages/shared/lib/core/wallet/types/activities/consolidation-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/consolidation-activity.type.ts @@ -1,7 +1,80 @@ import { ActivityType } from '@core/wallet/enums' -import { BaseActivity } from './base-activity.type' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { ActivityGenerationParameters, IActivityGenerationParameters, IWalletState, IWrappedOutput } from '../../interfaces' +import { BasicOutput, InclusionState, OutputType } from '@iota/sdk/out/types' +import { activityOutputContainsValue, getAsyncDataFromOutput, getMetadataFromOutput, getSendingInformation, getStorageDepositFromOutput, getTagFromOutput } from '../../utils' export type ConsolidationActivity = BaseActivity & { type: ActivityType.Consolidation amountConsolidatedInputs: number } + +interface ActivityConsolidationOptions extends ActivityBaseOptions { + amountConsolidatedInputs: number +} + +export class ActivityConsolidation extends ActivityBase { + constructor(private consolidationOptions: ActivityConsolidationOptions) { + super(consolidationOptions) + } + + amountConsolidatedInputs(): number { + return this.consolidationOptions.amountConsolidatedInputs + } + + tileTitle(): string { + const isConfirmed = this.inclusionState() === InclusionState.Confirmed + return isConfirmed ? 'general.consolidated' : 'general.consolidating' + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ) { + const { transactionId, direction, claimingData, time, inclusionState, wrappedInputs } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const isHidden = false + const isAssetHidden = false + const containsValue = await activityOutputContainsValue(wallet, wrappedOutput) + + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + + const output = wrappedOutput.output as BasicOutput + + const amountConsolidatedInputs = getAmountOfConsolidationInputs(wrappedInputs) + + const tag = getTagFromOutput(output) + const metadata = getMetadataFromOutput(output) + + const sendingInfo = getSendingInformation(processedTransaction, output, wallet) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + + const { storageDeposit, giftedStorageDeposit } = await getStorageDepositFromOutput(output) + return new ActivityConsolidation({ + specialStatus, + isHidden, + id, + transactionId, + time, + direction, + action, + isAssetHidden, + inclusionState, + containsValue, + outputId, + storageDeposit, + giftedStorageDeposit, + metadata, + tag, + asyncData, + amountConsolidatedInputs, + ...sendingInfo, + }) + } +} + +function getAmountOfConsolidationInputs(inputs: IWrappedOutput[]): number { + return inputs.filter((input) => input.output.type === OutputType.Basic).length +} \ No newline at end of file diff --git a/packages/shared/lib/core/wallet/types/activities/foundry-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/foundry-activity.type.ts index 7c1dc02aeab..eed60f3b675 100644 --- a/packages/shared/lib/core/wallet/types/activities/foundry-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/foundry-activity.type.ts @@ -1,5 +1,10 @@ -import { ActivityType } from '@core/wallet/enums' -import { BaseActivity } from './base-activity.type' +import { ActivityAction, ActivityType } from '@core/wallet/enums' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { ActivityGenerationParameters, IWalletState, ProcessedTransaction } from '../../interfaces' +import { AccountAddress, FoundryOutput, ImmutableAccountAddressUnlockCondition, OutputType, SimpleTokenScheme, UnlockConditionType } from '@iota/sdk/out/types' +import { getAmountFromOutput, getAsyncDataFromOutput, getMetadataFromOutput, getNativeTokenFromOutput, getTagFromOutput } from '../../utils' +import { api } from 'shared/lib/core/api' +import { getCoinType, getNetworkHrp } from 'shared/lib/core/profile' export type FoundryActivity = BaseActivity & { type: ActivityType.Foundry @@ -10,3 +15,102 @@ export type FoundryActivity = BaseActivity & { meltedTokens: string maximumSupply: string } + +interface ActivityFoundryOptions extends ActivityBaseOptions { + rawAmount: number + assetId: string + accountAddress: string + mintedTokens: string + meltedTokens: string + maximumSupply: string +} + +export class ActivityFoundry extends ActivityBase { + constructor(private foundryOptions: ActivityFoundryOptions) { + super(foundryOptions) + } + + static async fromOutputs( + processedTransaction: ProcessedTransaction, + wallet: IWalletState + ): Promise { + const outputs = processedTransaction.outputs + const activities = [] + + const foundryOutputs = outputs.filter((output) => output.output.type === OutputType.Foundry) + for (const foundryOutput of foundryOutputs) { + activities.push( + await ActivityFoundry.fromProcessedTransaction(wallet, { + action: ActivityAction.Mint, + processedTransaction, + wrappedOutput: foundryOutput, + }) + ) + } + return activities + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ) { + const { transactionId, claimingData, time, direction, inclusionState } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const output = wrappedOutput.output as FoundryOutput + const outputId = wrappedOutput.outputId + const tokenScheme = output.tokenScheme as SimpleTokenScheme + const mintedTokens = tokenScheme.mintedTokens.toString() + const meltedTokens = tokenScheme.meltedTokens.toString() + const maximumSupply = tokenScheme.maximumSupply.toString() + + const addressUnlockCondition = output.unlockConditions.find( + (unlockCondition) => unlockCondition.type === UnlockConditionType.ImmutableAccountAddress + ) as ImmutableAccountAddressUnlockCondition + const accountId = (addressUnlockCondition?.address as AccountAddress)?.accountId + // TODO: Research whether this address should be optional or not. + const accountAddress = accountId ? api.accountIdToBech32(accountId, getNetworkHrp()) : undefined + + const isHidden = false + const isAssetHidden = false + const containsValue = true + + const id = outputId || transactionId + const nativeToken = getNativeTokenFromOutput(output) + const assetId = nativeToken?.id ?? getCoinType() + + const storageDeposit = getAmountFromOutput(output) + const giftedStorageDeposit = 0 + const rawAmount = Number(nativeToken?.amount ?? 0) + const metadata = getMetadataFromOutput(output) + const tag = getTagFromOutput(output) + + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + return new ActivityFoundry({ + specialStatus, + isHidden, + id, + outputId, + transactionId, + direction, + action, + assetId, + accountAddress, + mintedTokens, + meltedTokens, + maximumSupply, + storageDeposit, + giftedStorageDeposit, + rawAmount, + time, + inclusionState, + containsValue, + isAssetHidden, + metadata, + tag, + asyncData, + ...sendingInfo, + }) + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/governance-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/governance-activity.type.ts index f2c4f81dee0..ecac4c3c559 100644 --- a/packages/shared/lib/core/wallet/types/activities/governance-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/governance-activity.type.ts @@ -1,6 +1,12 @@ import { ActivityType, GovernanceAction } from '@core/wallet/enums' -import { IParticipation } from '@core/wallet/interfaces' -import { BaseActivity } from './base-activity.type' +import { + ActivityGenerationParameters, + IParticipation, + IWalletState, +} from '@core/wallet/interfaces' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { BasicOutput, InclusionState } from '@iota/sdk/out/types' +import { activityOutputContainsValue, getGovernanceInfo, getMetadataFromOutput, getStorageDepositFromOutput, getTagFromOutput } from '../../utils' export type GovernanceActivity = BaseActivity & { type: ActivityType.Governance @@ -9,3 +15,86 @@ export type GovernanceActivity = BaseActivity & { participation?: IParticipation votingPowerDifference?: number } + +interface ActivityGovernanceOptions extends ActivityBaseOptions { + governanceAction: GovernanceAction + votingPower: number + participation?: IParticipation + votingPowerDifference?: number +} + +export class ActivityGovernance extends ActivityBase { + constructor(private governanceOptions: ActivityGovernanceOptions) { + super(governanceOptions) + } + + governanceAction(){ + return this.governanceOptions.governanceAction + } + + tileTitle(): string { + const isConfirmed = this.inclusionState() === InclusionState.Confirmed; + switch(this.governanceAction()) { + case GovernanceAction.IncreaseVotingPower: + return isConfirmed ? 'general.increased' : 'general.increasing' + case GovernanceAction.DecreaseVotingPower: + return isConfirmed ? 'general.decreased' : 'general.decreasing' + case GovernanceAction.StartVoting: + return isConfirmed ? 'general.voted' : 'general.voting' + case GovernanceAction.StopVoting: + return isConfirmed ? 'general.unvoted' : 'general.unvoting' + case GovernanceAction.ChangedVote: + return isConfirmed ? 'general.changedVote' : 'general.changingVote' + case GovernanceAction.Revote: + return isConfirmed ? 'general.revoted' : 'general.revoting' + default: + return super.tileTitle() + } + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ): Promise { + const { transactionId, direction, time, inclusionState, wrappedInputs } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const giftedStorageDeposit = 0 + const isHidden = false + const isAssetHidden = false + const containsValue = await activityOutputContainsValue(wallet, wrappedOutput) + + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + + const output = wrappedOutput.output as BasicOutput + + const tag = getTagFromOutput(output) + const metadata = getMetadataFromOutput(output) + + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + + const { storageDeposit } = await getStorageDepositFromOutput(output) + const governanceInfo = getGovernanceInfo(output, wrappedInputs, metadata) + + return new ActivityGovernance({ + isHidden, + id, + inclusionState, + specialStatus, + time, + transactionId, + direction, + action, + isAssetHidden, + containsValue, + outputId, + storageDeposit, + giftedStorageDeposit, + metadata, + tag, + ...governanceInfo, + ...sendingInfo, + }) + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/index.ts b/packages/shared/lib/core/wallet/types/activities/index.ts index befe1e74963..11716f037a8 100644 --- a/packages/shared/lib/core/wallet/types/activities/index.ts +++ b/packages/shared/lib/core/wallet/types/activities/index.ts @@ -1,9 +1,9 @@ export * from './account-activity.type' export * from './anchor-activity.type' -export * from './base-activity.type' +export * from './transaction-activity.type' export * from './consolidation-activity.type' export * from './foundry-activity.type' export * from './governance-activity.type' export * from './nft-activity.type' -export * from './transaction-activity.type' export * from './vesting-activity.type' +export * from './base-activity.type' diff --git a/packages/shared/lib/core/wallet/types/activities/nft-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/nft-activity.type.ts index d336ccd157c..c913df75306 100644 --- a/packages/shared/lib/core/wallet/types/activities/nft-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/nft-activity.type.ts @@ -1,7 +1,128 @@ -import { ActivityType } from '@core/wallet' -import { BaseActivity } from './base-activity.type' +import { + ActivityAction, + ActivityGenerationParameters, + ActivityType, + EMPTY_HEX_ID, + IWalletState, + ProcessedTransaction, + getAsyncDataFromOutput, + getClient, + getLayer2ActivityInformation, + getMetadataFromOutput, + getNftId, + getStorageDepositFromOutput, + getTagFromOutput, +} from '@core/wallet' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { NftOutput, OutputType } from '@iota/sdk/out/types' +import { handleError } from '@core/error/handlers' export type NftActivity = BaseActivity & { type: ActivityType.Nft nftId: string } + +interface ActivityNftOptions extends ActivityBaseOptions { + nftId: string +} + +export class ActivityNft extends ActivityBase { + constructor(private nftOptions: ActivityNftOptions) { + super(nftOptions) + } + + nftId(): string { + return this.nftOptions.nftId + } + + static async fromOutputs( + processedTransaction: ProcessedTransaction, + wallet: IWalletState + ): Promise { + const outputs = processedTransaction.outputs + const activities = [] + + const nftOutputs = outputs.filter((output) => output.output.type === OutputType.Nft) + for (const nftOutput of nftOutputs) { + const output = nftOutput.output as NftOutput + const activity = await ActivityNft.fromProcessedTransaction(wallet, { + action: output.nftId === EMPTY_HEX_ID ? ActivityAction.Mint : ActivityAction.Send, + processedTransaction, + wrappedOutput: nftOutput, + }) + activities.push(activity) + } + return activities + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters, + nftIdFromInput?: string + ): Promise { + const { claimingData, time, inclusionState, transactionId, direction } = processedTransaction + const outputId = wrappedOutput.outputId + const output = wrappedOutput.output as NftOutput + const id = outputId || transactionId + + const isHidden = false + const isAssetHidden = false + const containsValue = true + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + + const nftId = nftIdFromInput ? nftIdFromInput : getNftId(output.nftId, outputId) + const metadata = getMetadataFromOutput(output) + const tag = getTagFromOutput(output) + + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + const { subject, isInternal } = sendingInfo + + const { parsedLayer2Metadata, destinationNetwork } = getLayer2ActivityInformation(metadata, sendingInfo) + const gasBudget = Number(parsedLayer2Metadata?.gasBudget ?? '0') + + const storageDepositData = await getStorageDepositFromOutput(output) + const { storageDeposit } = storageDepositData + let { giftedStorageDeposit } = storageDepositData + giftedStorageDeposit = action === ActivityAction.Burn ? 0 : giftedStorageDeposit + giftedStorageDeposit = gasBudget === 0 ? giftedStorageDeposit : 0 + + let surplus: number | undefined = undefined + try { + const client = await getClient() + const minimumRequiredStorageDeposit = await client.computeMinimumOutputAmount(output) + surplus = Number(output.amount) - Number(minimumRequiredStorageDeposit) + if (surplus && !storageDeposit) { + giftedStorageDeposit = Number(minimumRequiredStorageDeposit) + } + } catch (err) { + handleError(err) + } + + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + + return new ActivityNft({ + specialStatus, + id, + transactionId, + outputId, + nftId, + time, + isHidden, + action, + giftedStorageDeposit, + surplus, + isAssetHidden, + containsValue, + inclusionState, + storageDeposit, + metadata, + tag, + asyncData, + subject, + isInternal, + direction, + destinationNetwork, + parsedLayer2Metadata, + }) + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/transaction-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/transaction-activity.type.ts index 792acfa5922..b66e236b75e 100644 --- a/packages/shared/lib/core/wallet/types/activities/transaction-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/transaction-activity.type.ts @@ -1,10 +1,221 @@ +import { BasicOutput, InclusionState, NftOutput } from '@iota/sdk/out/types' +import { ActivityAction, ActivityDirection } from '../../enums' +import { + ActivityGenerationParameters, + IWalletState, + ProcessedTransaction, +} from '../../interfaces' +import { + activityOutputContainsValue, + getAmountFromOutput, + getAsyncDataFromOutput, + getLayer2ActivityInformation, + getMetadataFromOutput, + getNativeTokenFromOutput, + getNftId, + getNonRemainderBasicOutputsFromTransaction, + getStorageDepositFromOutput, + getTagFromOutput, +} from '../../utils' +import { ActivityBase, ActivityBaseOptions, SpecialStatus } from './base-activity.type' +import { isShimmerClaimingTransaction } from '@contexts/onboarding' +import { activeProfileId, getCoinType } from '@core/profile' +import { get } from 'svelte/store' +import { ActivityNft } from './nft-activity.type' +import { addOrUpdateNftInAllWalletNfts, buildNftFromNftOutput } from '@core/nfts' +import { ActivityConsolidation } from './consolidation-activity.type' +import { localize } from '@core/i18n' import { ActivityType } from '@core/wallet/enums' import { BaseActivity } from './base-activity.type' export type TransactionActivity = BaseActivity & { - type: ActivityType.Basic + type: ActivityType.Transaction rawAmount: number assetId: string - publicNote: string isShimmerClaiming: boolean } + + +interface ActivityTransactionOptions extends ActivityBaseOptions { + rawAmount: number + assetId: string + isShimmerClaiming: boolean +} + +export class ActivityTransaction extends ActivityBase { + constructor(private basicOptions: ActivityTransactionOptions) { + super(basicOptions) + } + + subjectLocale(): string { + if (this.isShimmerClaiming()) { + return localize('general.shimmerGenesis') + } else { + return super.subjectLocale() + } + } + + isShimmerClaiming(){ + return this.basicOptions.isShimmerClaiming + } + + tileTitle(): string { + const isConfirmed = this.inclusionState() === InclusionState.Confirmed + if (this.isShimmerClaiming()) { + return isConfirmed ? 'general.shimmerClaimed' : 'general.shimmerClaiming' + } else { + return super.tileTitle() + } + } + + static async fromOutputs( + processedTransaction: ProcessedTransaction, + wallet: IWalletState + ): Promise { + const activities = [] + + const basicOutputs = getNonRemainderBasicOutputsFromTransaction( + processedTransaction.outputs, + wallet.depositAddress, + processedTransaction.direction + ) + const burnedNftInputs = processedTransaction.getBurnedNftInputs() + for (const basicOutput of basicOutputs) { + let activity: ActivityBase + + const isSelfTransaction = processedTransaction.direction === ActivityDirection.SelfTransaction + const burnedNftInputIndex = burnedNftInputs.findIndex( + (input) => input.output.amount === basicOutput.output.amount + ) + const burnedNativeToken = burnedNftInputIndex < 0 ? processedTransaction.getBurnedNativeTokens() : undefined + + // NFT Activity + if (isSelfTransaction && burnedNftInputIndex >= 0) { + const wrappedInput = burnedNftInputs[burnedNftInputIndex] + const nftInput = wrappedInput.output as NftOutput + + activity = await ActivityNft.fromProcessedTransaction( + wallet, + { + action: ActivityAction.Burn, + processedTransaction, + wrappedOutput: basicOutput, + }, + getNftId(nftInput.nftId, wrappedInput.outputId) + ) + const nft = buildNftFromNftOutput(wrappedInput, wallet.depositAddress, false) + addOrUpdateNftInAllWalletNfts(wallet.id, nft) + + burnedNftInputs.splice(burnedNftInputIndex, 1) + } + // Burn Activity + else if (isSelfTransaction && burnedNativeToken) { + activity = await ActivityTransaction.fromProcessedTransaction(wallet, { + action: ActivityAction.Burn, + processedTransaction, + wrappedOutput: basicOutput, + }) + } + // Consolidation Activity + else if (isSelfTransaction && ActivityBase.isConsolidation(basicOutput, processedTransaction)) { + activity = await ActivityConsolidation.fromProcessedTransaction(wallet, { + action: ActivityAction.Send, + processedTransaction, + wrappedOutput: basicOutput, + }) + } + // Send Activity + else { + activity = await ActivityTransaction.fromProcessedTransaction(wallet, { + action: ActivityAction.Send, + processedTransaction, + wrappedOutput: basicOutput, + }) + } + activities.push(activity) + } + return activities + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters, + fallbackAssetId?: string, + fallbackAmount?: number + ): Promise { + const { transactionId, direction, claimingData, time, inclusionState } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const isHidden = false + const isAssetHidden = false + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + const output = wrappedOutput.output as BasicOutput + const containsValue = await activityOutputContainsValue(wallet, wrappedOutput) + const amount = getAmountFromOutput(output) + const isShimmerClaiming = isShimmerClaimingTransaction(transactionId, get(activeProfileId)) + const tag = getTagFromOutput(output) + const metadata = getMetadataFromOutput(output) + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + const { parsedLayer2Metadata, destinationNetwork } = getLayer2ActivityInformation(metadata, sendingInfo) + const layer2Allowance = Number(parsedLayer2Metadata?.baseTokens ?? '0') + const gasBudget = Number(parsedLayer2Metadata?.gasBudget ?? '0') + const gasFee = layer2Allowance > 0 ? amount - layer2Allowance : 0 + + let { storageDeposit, giftedStorageDeposit } = await getStorageDepositFromOutput(output) + giftedStorageDeposit = action === ActivityAction.Burn ? 0 : giftedStorageDeposit + giftedStorageDeposit = gasBudget === 0 ? giftedStorageDeposit : 0 + + const baseTokenAmount = amount - storageDeposit - gasFee + const nativeToken = getNativeTokenFromOutput(output) + const assetId = fallbackAssetId ?? nativeToken?.id ?? getCoinType() + + let surplus: number | undefined = undefined + if (nativeToken) { + const storageDepositToDeduct = (storageDeposit > 0 ? storageDeposit : giftedStorageDeposit) ?? 0 + surplus = Number(output.amount) - storageDepositToDeduct + } + + let rawAmount: number + if (fallbackAmount === undefined) { + rawAmount = nativeToken ? Number(nativeToken?.amount) : baseTokenAmount + } else { + rawAmount = fallbackAmount + } + + // Note: we update the displayed storage deposit so it matches what was displayed in the send confirmation flow + // set the storage deposit to zero if the amount is greater than the storage deposit + // to improve the UX so the user doesnt think they need to pay the storage deposit + if (!nativeToken && !storageDeposit && rawAmount >= giftedStorageDeposit) { + storageDeposit = giftedStorageDeposit = 0 + } + + return new ActivityTransaction({ + isHidden, + id, + inclusionState, + specialStatus, + time, + transactionId, + direction, + action, + isAssetHidden, + containsValue, + outputId, + storageDeposit, + giftedStorageDeposit, + surplus, + isShimmerClaiming, + metadata, + tag, + asyncData, + destinationNetwork, + parsedLayer2Metadata, + isInternal: sendingInfo.isInternal, + subject: sendingInfo.subject, + rawAmount, + assetId + }) + } +} diff --git a/packages/shared/lib/core/wallet/types/activities/vesting-activity.type.ts b/packages/shared/lib/core/wallet/types/activities/vesting-activity.type.ts index 5b6aaafa803..4511b6cfc62 100644 --- a/packages/shared/lib/core/wallet/types/activities/vesting-activity.type.ts +++ b/packages/shared/lib/core/wallet/types/activities/vesting-activity.type.ts @@ -1,8 +1,86 @@ import { ActivityType } from '@core/wallet/enums' -import { BaseActivity } from './base-activity.type' +import { ActivityBase, ActivityBaseOptions, BaseActivity, SpecialStatus } from './base-activity.type' +import { activityOutputContainsValue, getAmountFromOutput, getAsyncDataFromOutput, getLayer2ActivityInformation, getMetadataFromOutput, getStorageDepositFromOutput, getTagFromOutput } from '../../utils' +import { ActivityGenerationParameters, IActivityGenerationParameters, IWalletState } from '../../interfaces' +import { BasicOutput } from '@iota/sdk/out/types' +import { localize } from 'shared/lib/core/i18n' +import { getCoinType } from 'shared/lib/core/profile' export type VestingActivity = BaseActivity & { type: ActivityType.Vesting rawAmount: number assetId: string } + +interface ActivityVestingOptions extends ActivityBaseOptions { + rawAmount: number + assetId: string +} + +export class ActivityVesting extends ActivityBase { + constructor(options: ActivityVestingOptions) { + super(options) + } + + subjectLocale(): string { + return localize('general.stardustGenesis') + } + + tileTitle(): string { + return 'general.vestingReward' + } + + static async fromProcessedTransaction( + wallet: IWalletState, + { action, processedTransaction, wrappedOutput }: ActivityGenerationParameters + ): Promise { + const { transactionId, direction, claimingData, time, inclusionState } = processedTransaction + + const specialStatus = SpecialStatus.Unclaimed // TODO: Fix this + const isHidden = false + const isAssetHidden = false + const containsValue = await activityOutputContainsValue(wallet, wrappedOutput) + + const outputId = wrappedOutput.outputId + const id = outputId || transactionId + + const output = wrappedOutput.output as BasicOutput + + const tag = getTagFromOutput(output) + const metadata = getMetadataFromOutput(output) + + const sendingInfo = processedTransaction.getSendingInformation(wallet, output) + const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) + + const { parsedLayer2Metadata, destinationNetwork } = getLayer2ActivityInformation(metadata, sendingInfo) + + const { storageDeposit, giftedStorageDeposit } = await getStorageDepositFromOutput(output) + const rawAmount = getAmountFromOutput(output) - storageDeposit + + const assetId = getCoinType() + + return new ActivityVesting({ + isHidden, + id, + inclusionState, + specialStatus, + time, + transactionId, + direction, + action, + isAssetHidden, + containsValue, + outputId, + assetId, + storageDeposit, + giftedStorageDeposit, + rawAmount, + metadata, + tag, + asyncData, + destinationNetwork, + ...sendingInfo, + ...parsedLayer2Metadata, + }) + } +} diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/generateActivities.ts b/packages/shared/lib/core/wallet/utils/generateActivity/generateActivities.ts index c6904ab32c2..70cfaddd2f2 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/generateActivities.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/generateActivities.ts @@ -98,7 +98,7 @@ async function generateActivitiesFromProcessedTransactionsWithoutInputs( wrappedOutput, } switch (params.type) { - case ActivityType.Basic: + case ActivityType.Transaction: return generateSingleBasicActivity(wallet, params) case ActivityType.Governance: return generateSingleGovernanceActivity(wallet, params) diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/generateActivitiesFromBasicOutputs.ts b/packages/shared/lib/core/wallet/utils/generateActivity/generateActivitiesFromBasicOutputs.ts index d08cd5a7067..a32e4f0e2c5 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/generateActivitiesFromBasicOutputs.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/generateActivitiesFromBasicOutputs.ts @@ -157,7 +157,7 @@ function getAllNativeTokensFromOutputs(outputs: IWrappedOutput[]): { [key: strin return nativeTokens } -function isConsolidation(output: IWrappedOutput, processedTransaction: IProcessedTransaction): boolean { +export function isConsolidation(output: IWrappedOutput, processedTransaction: IProcessedTransaction): boolean { const allBasicInputs = processedTransaction.wrappedInputs.every((input) => input.output.type === OutputType.Basic) const isSelfTransaction = processedTransaction.direction === ActivityDirection.SelfTransaction const isSameAmount = diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/generateSingleBasicActivity.ts b/packages/shared/lib/core/wallet/utils/generateActivity/generateSingleBasicActivity.ts index 40eeee55d6b..2a8e267f40b 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/generateSingleBasicActivity.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/generateSingleBasicActivity.ts @@ -2,7 +2,7 @@ import { isShimmerClaimingTransaction } from '@contexts/onboarding/stores' import { IWalletState } from '@core/wallet/interfaces' import { activeProfileId, getCoinType } from '@core/profile' import { IActivityGenerationParameters } from '@core/wallet/interfaces' -import { TransactionActivity } from '@core/wallet/types' +import { BaseActivity } from '@core/wallet/types' import { get } from 'svelte/store' import { activityOutputContainsValue, getNativeTokenFromOutput } from '..' import { ActivityAction, ActivityType } from '../../enums' @@ -22,7 +22,7 @@ export async function generateSingleBasicActivity( { action, processedTransaction, wrappedOutput }: IActivityGenerationParameters, fallbackAssetId?: string, fallbackAmount?: number -): Promise { +): Promise { const { transactionId, direction, claimingData, time, inclusionState } = processedTransaction const isHidden = false @@ -39,8 +39,6 @@ export async function generateSingleBasicActivity( const tag = getTagFromOutput(output) const metadata = getMetadataFromOutput(output) - const publicNote = '' - const sendingInfo = getSendingInformation(processedTransaction, output, wallet) const asyncData = await getAsyncDataFromOutput(output, outputId, claimingData, wallet) @@ -79,7 +77,7 @@ export async function generateSingleBasicActivity( } return { - type: ActivityType.Basic, + type: ActivityType.Transaction, isHidden, id, transactionId, @@ -95,7 +93,6 @@ export async function generateSingleBasicActivity( surplus, rawAmount, isShimmerClaiming, - publicNote, metadata, tag, assetId, diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getActivityTypeFromOutput.ts b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getActivityTypeFromOutput.ts index ecc9c9c937d..3d74aabdee7 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getActivityTypeFromOutput.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getActivityTypeFromOutput.ts @@ -17,8 +17,8 @@ export function getActivityTypeFromOutput(output: IWrappedOutput): ActivityType return ActivityType.Governance } else if (isVestingOutputId(output.outputId)) { return ActivityType.Vesting - } else { - return ActivityType.Basic } } + + return ActivityType.Transaction } diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getAsyncDataFromOutput.ts b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getAsyncDataFromOutput.ts index 2b835d5a121..71eaeee304e 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getAsyncDataFromOutput.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getAsyncDataFromOutput.ts @@ -6,10 +6,11 @@ import { getAsyncStatus } from './getAsyncStatus' import { getStorageDepositFromOutput } from './getStorageDepositFromOutput' import { CommonOutput, Output } from '@iota/sdk/out/types' +// TODO: REFACTOR THIS export async function getAsyncDataFromOutput( output: Output, outputId: string, - claimingData: IClaimData, + claimingData: IClaimData | undefined, wallet: IWalletState ): Promise { const isAsync = isOutputAsync(output) diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getGovernanceInfo.ts b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getGovernanceInfo.ts index 26b28e054e2..27f33c2a032 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getGovernanceInfo.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getGovernanceInfo.ts @@ -13,6 +13,7 @@ interface IGovernanceInfo { participation?: IParticipation } +// TODO: Think about moving this to GovernanceActivity export function getGovernanceInfo(output: Output, inputs: IWrappedOutput[], metadata: string): IGovernanceInfo { /** * NOTE: If the output is NOT a participation output, then it doesn't have any voting power. diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getSubjectFromActivity.ts b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getSubjectFromActivity.ts index 0fab008e8dd..f94fa846e81 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/helper/getSubjectFromActivity.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/helper/getSubjectFromActivity.ts @@ -4,6 +4,7 @@ import { truncateString } from '@core/utils' import { ActivityType, SubjectType } from '@core/wallet/enums' import type { Activity, Subject } from '@core/wallet/types' +// TODO: Remove this export function getSubjectFromActivity(activity: Activity): Subject { if (activity.parsedLayer2Metadata) { return { @@ -23,7 +24,7 @@ export function getSubjectFromActivity(activity: Activity): Subject { export function getSubjectLocaleFromActivity(activity: Activity): string { const { subject } = activity - if (activity.type === ActivityType.Basic && activity?.isShimmerClaiming) { + if (activity.type === ActivityType.Transaction && activity?.isShimmerClaiming) { return localize('general.shimmerGenesis') } else if (activity.type === ActivityType.Vesting) { return localize('general.stardustGenesis') diff --git a/packages/shared/lib/core/wallet/utils/generateActivity/helper/updateActivityFromPartialActivity.ts b/packages/shared/lib/core/wallet/utils/generateActivity/helper/updateActivityFromPartialActivity.ts index 501c57dd498..e4bf2c3fcf6 100644 --- a/packages/shared/lib/core/wallet/utils/generateActivity/helper/updateActivityFromPartialActivity.ts +++ b/packages/shared/lib/core/wallet/utils/generateActivity/helper/updateActivityFromPartialActivity.ts @@ -2,7 +2,7 @@ import { ActivityType } from '@core/wallet/enums' import { Activity } from '@core/wallet/types' export function updateActivityFromPartialActivity(activity: Activity, partialData: Partial): void { - if (partialData.type === ActivityType.Basic && activity.type === ActivityType.Basic) { + if (partialData.type === ActivityType.Transaction && activity.type === ActivityType.Transaction) { Object.assign(activity, partialData) } else if (partialData.type === ActivityType.Foundry && activity.type === ActivityType.Foundry) { Object.assign(activity, partialData) diff --git a/packages/shared/lib/core/wallet/utils/getActivityTileTitle.ts b/packages/shared/lib/core/wallet/utils/getActivityTileTitle.ts index e1ce4e31372..9f99ea28308 100644 --- a/packages/shared/lib/core/wallet/utils/getActivityTileTitle.ts +++ b/packages/shared/lib/core/wallet/utils/getActivityTileTitle.ts @@ -5,7 +5,7 @@ export function getActivityTileTitle(activity: Activity): string { const { type, isInternal, direction, inclusionState, action } = activity const isConfirmed = inclusionState === InclusionState.Confirmed - if (activity.type === ActivityType.Basic && activity.isShimmerClaiming) { + if (activity.type === ActivityType.Transaction && activity.isShimmerClaiming) { return isConfirmed ? 'general.shimmerClaimed' : 'general.shimmerClaiming' } if (activity.type === ActivityType.Vesting) { diff --git a/packages/shared/lib/core/wallet/utils/isVisibleActivity.ts b/packages/shared/lib/core/wallet/utils/isVisibleActivity.ts index 5081f99e778..71b316b7065 100644 --- a/packages/shared/lib/core/wallet/utils/isVisibleActivity.ts +++ b/packages/shared/lib/core/wallet/utils/isVisibleActivity.ts @@ -16,6 +16,8 @@ import { StatusFilterOption, } from '@core/utils/enums/filters' +// TODO: Refactor this an clean up. + // Filters activities based on activity properties. If none of the conditionals are valid, then activity is shown. export function isVisibleActivity(activity: Activity): boolean { const filter = get(activityFilter) @@ -86,7 +88,7 @@ function isVisibleWithActiveRejectedFilter(activity: Activity, filter: ActivityF function isVisibleWithActiveAssetFilter(activity: Activity, filter: ActivityFilter): boolean { if (filter.asset.active && filter.asset.selected) { - if (activity.type !== ActivityType.Basic && activity.type !== ActivityType.Foundry) { + if (activity.type !== ActivityType.Transaction && activity.type !== ActivityType.Foundry) { return false } if (filter.asset.selected && activity.assetId !== filter.asset.selected) { @@ -98,7 +100,7 @@ function isVisibleWithActiveAssetFilter(activity: Activity, filter: ActivityFilt function isVisibleWithActiveAmountFilter(activity: Activity, filter: ActivityFilter): boolean { if (filter.amount.active) { - if (activity.type !== ActivityType.Basic && activity.type !== ActivityType.Foundry) return false + if (activity.type !== ActivityType.Transaction && activity.type !== ActivityType.Foundry) return false const asset = getAssetFromPersistedAssets(activity.assetId) if (!asset?.metadata) { return false @@ -250,14 +252,14 @@ function isVisibleWithActiveStatusFilter(activity: Activity, filter: ActivityFil } if ( filter.status.selected === StatusFilterOption.Claimed && - (activity.type === ActivityType.Basic || activity.type === ActivityType.Nft) && + (activity.type === ActivityType.Transaction || activity.type === ActivityType.Nft) && activity.asyncData?.asyncStatus === ActivityAsyncStatus.Claimed ) { return true } if ( filter.status.selected === StatusFilterOption.Unclaimed && - (activity.type === ActivityType.Basic || activity.type === ActivityType.Nft) && + (activity.type === ActivityType.Transaction || activity.type === ActivityType.Nft) && activity.asyncData?.asyncStatus === ActivityAsyncStatus.Unclaimed ) { return true diff --git a/packages/shared/lib/core/wallet/utils/outputs/getFormattedAmountFromActivity.ts b/packages/shared/lib/core/wallet/utils/outputs/getFormattedAmountFromActivity.ts index 6c947115c19..04c1d931280 100644 --- a/packages/shared/lib/core/wallet/utils/outputs/getFormattedAmountFromActivity.ts +++ b/packages/shared/lib/core/wallet/utils/outputs/getFormattedAmountFromActivity.ts @@ -10,7 +10,7 @@ export function getFormattedAmountFromActivity( if (!activity) return '' const metadata = getAssetFromPersistedAssets(activity.assetId)?.metadata const amount = formatTokenAmountBestMatch(activity.rawAmount, metadata) - if (activity.type === ActivityType.Basic) { + if (activity.type === ActivityType.Transaction) { return `${ (activity.direction === ActivityDirection.Outgoing || activity.action === ActivityAction.Burn) && signed ? '- ' diff --git a/packages/shared/lib/core/wallet/utils/send/sendUtils.ts b/packages/shared/lib/core/wallet/utils/send/sendUtils.ts index 8f0070bcbbc..569cf432a73 100644 --- a/packages/shared/lib/core/wallet/utils/send/sendUtils.ts +++ b/packages/shared/lib/core/wallet/utils/send/sendUtils.ts @@ -1,4 +1,4 @@ -import { NewTransactionDetails, NftActivity, Subject, TransactionActivity, VestingActivity } from '@core/wallet/types' +import { NewTransactionDetails, NftActivity, Subject, BaseActivity, VestingActivity } from '@core/wallet/types' import { NewTransactionType } from '@core/wallet/stores' import { ActivityAction, ActivityDirection, ActivityType, InclusionState } from '@core/wallet/enums' import { TimePeriod } from '@core/utils' @@ -44,9 +44,9 @@ export function rebuildActivity( visibleSurplus: number, isInternal: boolean, layer2Parameters: ILayer2Parameters -): Partial { +): Partial { return { - ...(transactionDetails as unknown as TransactionActivity | VestingActivity | NftActivity), + ...(transactionDetails as unknown as BaseActivity | VestingActivity | NftActivity), id: undefined, outputId: undefined, transactionId: undefined, @@ -61,7 +61,7 @@ export function rebuildActivity( isAssetHidden: false, giftedStorageDeposit: 0, surplus: visibleSurplus, - type: ActivityType.Basic, + type: ActivityType.Transaction, direction: ActivityDirection.Outgoing, inclusionState: InclusionState.Pending, action: ActivityAction.Send, diff --git a/packages/shared/lib/core/wallet/utils/transactions/activityOutputContainsValue.ts b/packages/shared/lib/core/wallet/utils/transactions/activityOutputContainsValue.ts index 4280572f83e..82aeb9b804e 100644 --- a/packages/shared/lib/core/wallet/utils/transactions/activityOutputContainsValue.ts +++ b/packages/shared/lib/core/wallet/utils/transactions/activityOutputContainsValue.ts @@ -6,11 +6,11 @@ import { getActivityTypeFromOutput, getAmountFromOutput, getStorageDepositFromOu import { BasicOutput } from '@iota/sdk/out/types' export async function activityOutputContainsValue( - wallet: IWalletState, + wallet: IWalletState, // TODO: Remove this parameter wrappedOutput: IWrappedOutput ): Promise { const type = getActivityTypeFromOutput(wrappedOutput) - const typesToCheck = [ActivityType.Basic] + const typesToCheck = [ActivityType.Transaction] if (typesToCheck.includes(type)) { const output = wrappedOutput.output as BasicOutput