Skip to content

Commit

Permalink
feat: implement function to generate csv object from activities (#2208)
Browse files Browse the repository at this point in the history
* convert activities to csv string

* add type to activity row

* implement asset fields

* improve csv consistency

Co-authored-by: Nicole O'Brien <[email protected]>

* add csv export to export popup

* add storage deposit unlock condition

* convert fee and storage deposit to SMR

* fixes

* use correct enum

---------

Co-authored-by: Nicole O'Brien <[email protected]>
  • Loading branch information
MarkNerdi and nicole-obrien authored Mar 27, 2024
1 parent c497a06 commit faf6b47
Show file tree
Hide file tree
Showing 7 changed files with 349 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
import { localize } from '@core/i18n'
import { closePopup } from '@desktop/auxiliary/popup'
import PopupTemplate from '../PopupTemplate.svelte'
import { convertActvitiesToCsv } from '@core/activity/utils'
import { allAccountActivities } from '@core/activity'
import { activeAccounts } from '@core/profile/stores'
const busy = false
Expand All @@ -10,7 +13,9 @@
}
function onExportClick(): void {
// TODO: implement CSV export
// TODO: Write string to users device
convertActvitiesToCsv($activeAccounts, $allAccountActivities)
closePopup()
}
</script>

Expand Down
299 changes: 299 additions & 0 deletions packages/shared/src/lib/core/activity/utils/convertActvitiesToCsv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
import { NetworkNamespace, getNameFromNetworkId } from '@core/network'
import {
Activity,
EvmActivity,
EvmBalanceChangeActivity,
EvmCoinTransferActivity,
EvmTokenTransferActivity,
StardustActivity,
StardustNftActivity,
StardustTransactionActivity,
} from '../types'
import { IAccountState } from '@core/account'
import { getNameFromSubject } from './helper'
import { StardustActivityAsyncStatus, StardustActivityType } from '../enums'
import { EvmActivityType } from '../enums/evm'
import { getPersistedToken } from '@core/token/stores'
import {
BASE_TOKEN_ID,
IBaseToken,
IErc20Metadata,
IIrc30Metadata,
TokenStandard,
formatTokenAmountBestMatch,
} from '@core/token'
import { NftStandard } from '@core/nfts'
import { getNftByIdFromAllAccountNfts } from '@core/nfts/actions'

const CSV_KEYS = [
'associatedAccount',
'transactionId',
'transactionTag',
'transactionDate',
'transactionType',
'direction',
'fromNetworkId',
'fromNetworkName',
'fromAddress',
'fromAddressAlias',
'toNetworkId',
'toNetworkName',
'toAddress',
'toAddressAlias',
'assetId',
'assetType',
'assetStandard',
'assetName',
'assetTicker',
'amount',
'feeInSMR',
'storageDepositInSMR',
'sdruc',
'sdrucStatus',
'expirationDate',
'expirationStatus',
'timelockDate',
'timelockStatus',
]

type ActivityCsvRow = {
[key in (typeof CSV_KEYS)[number]]: string | undefined
}

export function convertActvitiesToCsv(account: IAccountState[], activities: Activity[][]): string {
const activityRows = account.flatMap((account) => {
return activities[account.index]
.map((activity) => {
if (activity.namespace === NetworkNamespace.Stardust && shouldStardustActivityBeInCsv(activity)) {
const activityRow = getRowForStardustActivity(account, activity)
const values = CSV_KEYS.map((key) => escapeValue(activityRow[key] ?? ''))
return values.join(',')
} else if (activity.namespace === NetworkNamespace.Evm && shouldEvmActivityBeInCsv(activity)) {
const activityRow = getRowForEvmActivity(account, activity)
const values = CSV_KEYS.map((key) => escapeValue(activityRow[key] ?? ''))
return values.join(',')
} else {
return ''
}
})
.filter(Boolean)
})

const header = CSV_KEYS.join(',')
const table = activityRows.join('\n')

return `${header}\n${table}`
}

function getRowForStardustActivity(
account: IAccountState,
activity: StardustTransactionActivity | StardustNftActivity
): ActivityCsvRow {
let assetId: string | undefined
let assetType: string | undefined
let assetStandard: string | undefined
let assetName: string | undefined
let assetTicker: string | undefined
let amount: string | undefined

const baseCoinMetadata = getPersistedToken(BASE_TOKEN_ID)?.metadata as IBaseToken
if (activity.type === StardustActivityType.Basic) {
if (activity.tokenTransfer) {
const tokenId = activity.tokenTransfer.tokenId
const metadata = getPersistedToken(tokenId)?.metadata as IErc20Metadata | IIrc30Metadata
amount = metadata
? formatTokenAmountBestMatch(activity.tokenTransfer.rawAmount, metadata, {
round: false,
withUnit: false,
})
: ''

assetId = tokenId
assetType = 'TOKEN'
assetStandard = metadata?.standard
assetName = metadata?.name
assetTicker = metadata?.symbol
} else {
amount = baseCoinMetadata
? formatTokenAmountBestMatch(activity.baseTokenTransfer.rawAmount, baseCoinMetadata, {
round: false,
withUnit: false,
})
: ''

assetId = BASE_TOKEN_ID
assetType = 'TOKEN'
assetStandard = baseCoinMetadata?.standard
assetName = baseCoinMetadata?.name
assetTicker = baseCoinMetadata?.tickerSymbol
}
} else if (activity.type === StardustActivityType.Nft) {
const nft = getNftByIdFromAllAccountNfts(account.index, activity.nftId)

assetId = activity.nftId
assetType = 'NFT'
assetStandard = nft?.standard
assetName = nft?.metadata?.name
assetTicker = ''
amount = '1'
}

const transactionType = assetId === BASE_TOKEN_ID ? EvmActivityType.CoinTransfer : EvmActivityType.TokenTransfer

const storageDepositInSMR = activity.storageDeposit
? formatTokenAmountBestMatch(activity.storageDeposit, baseCoinMetadata, {
round: false,
withUnit: false,
})
: undefined
const feeInSMR = activity.transactionFee
? formatTokenAmountBestMatch(activity.transactionFee, baseCoinMetadata, {
round: false,
withUnit: false,
})
: undefined

return {
associatedAccount: account.name,
transactionId: activity.transactionId,
transactionTag: activity.tag,
transactionDate: activity.time.toString(),
transactionType,
direction: activity.direction,
fromNetworkId: activity.sourceNetworkId,
fromNetworkName: getNameFromNetworkId(activity.sourceNetworkId),
fromAddress: activity.sender?.address,
fromAddressAlias: getNameFromSubject(activity.sender),
toNetworkId: activity.destinationNetworkId,
toNetworkName: getNameFromNetworkId(activity.destinationNetworkId),
toAddress: activity.recipient?.address,
toAddressAlias: getNameFromSubject(activity.recipient),
assetId,
assetType,
assetStandard,
assetName,
assetTicker,
amount,
feeInSMR,
storageDepositInSMR,
sdruc: activity.storageDeposit ? 'True' : 'False',
sdrucStatus: activity.storageDeposit ? activity.asyncData?.asyncStatus : undefined,
expirationDate: activity.asyncData?.expirationDate?.toString(),
expirationStatus: activity.asyncData?.expirationDate ? activity.asyncData?.asyncStatus : undefined, // TODO: Improve this
timelockDate: activity.asyncData?.timelockDate?.toString(),
timelockStatus: activity.asyncData?.timelockDate
? activity.asyncData?.timelockDate > new Date()
? StardustActivityAsyncStatus.Timelocked
: undefined
: undefined, // TODO: Improve this
}
}

function getRowForEvmActivity(
account: IAccountState,
activity: EvmCoinTransferActivity | EvmTokenTransferActivity | EvmBalanceChangeActivity
): ActivityCsvRow {
let assetId: string | undefined
let assetType: string | undefined
let assetStandard: string | undefined
let assetName: string | undefined
let assetTicker: string | undefined
let amount: string | undefined
const baseCoinMetadata = getPersistedToken(BASE_TOKEN_ID)?.metadata as IBaseToken

if (activity.type === EvmActivityType.CoinTransfer) {
amount = baseCoinMetadata
? formatTokenAmountBestMatch(activity.baseTokenTransfer.rawAmount, baseCoinMetadata, {
round: false,
withUnit: false,
})
: ''

assetId = BASE_TOKEN_ID
assetType = 'TOKEN'
assetStandard = baseCoinMetadata?.standard
assetName = baseCoinMetadata?.name
assetTicker = baseCoinMetadata?.tickerSymbol
} else if (activity.type === EvmActivityType.TokenTransfer || activity.type === EvmActivityType.BalanceChange) {
const { standard, tokenId, rawAmount } = activity.tokenTransfer
if (standard === TokenStandard.Erc20 || standard === TokenStandard.Irc30) {
const metadata = getPersistedToken(tokenId)?.metadata as IErc20Metadata | IIrc30Metadata
amount = metadata ? formatTokenAmountBestMatch(rawAmount, metadata, { round: false, withUnit: false }) : ''

assetId = tokenId
assetType = 'TOKEN'
assetStandard = standard
assetName = metadata?.name
assetTicker = metadata?.symbol
} else if (standard === NftStandard.Erc721 || standard === NftStandard.Irc27) {
const nft = getNftByIdFromAllAccountNfts(account.index, tokenId)

assetId = tokenId
assetType = 'NFT'
assetStandard = standard
assetName = nft?.metadata?.name
assetTicker = ''
amount = '1'
}
}

const feeInSMR = activity.transactionFee
? formatTokenAmountBestMatch(activity.transactionFee, baseCoinMetadata, {
round: false,
withUnit: false,
})
: undefined

return {
associatedAccount: account.name,
transactionId: activity.transactionId,
transactionTag: undefined,
transactionDate: activity.time.toString(),
transactionType: activity.type,
direction: activity.direction,
fromNetworkId: activity.sourceNetworkId,
fromNetworkName: getNameFromNetworkId(activity.sourceNetworkId),
fromAddress: activity.sender?.address,
fromAddressAlias: getNameFromSubject(activity.sender),
toNetworkId: activity.destinationNetworkId,
toNetworkName: getNameFromNetworkId(activity.destinationNetworkId),
toAddress: activity.recipient?.address,
toAddressAlias: getNameFromSubject(activity.recipient),
assetId,
assetType,
assetStandard,
assetName,
assetTicker,
amount,
feeInSMR,
storageDepositInSMR: undefined,
sdruc: undefined,
sdrucStatus: undefined,
expirationDate: undefined,
expirationStatus: undefined,
timelockDate: undefined,
timelockStatus: undefined,
}
}

function escapeValue(value: string): string {
const valuesToEscape = ['"', ',']

return valuesToEscape.some((valueToEscape) => value?.includes(valueToEscape)) ? `"${value}"` : value
}

function shouldStardustActivityBeInCsv(
activity: StardustActivity
): activity is StardustTransactionActivity | StardustNftActivity {
return activity.type === StardustActivityType.Basic || activity.type === StardustActivityType.Nft
}

function shouldEvmActivityBeInCsv(
activity: EvmActivity
): activity is EvmCoinTransferActivity | EvmTokenTransferActivity | EvmBalanceChangeActivity {
return (
activity.type === EvmActivityType.CoinTransfer ||
activity.type === EvmActivityType.TokenTransfer ||
activity.type === EvmActivityType.BalanceChange
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { ActivityAction, StardustActivityType } from '../enums'
import { Activity } from '../types'
import { getVotingEvent } from '@contexts/governance/actions'
import { truncateString } from '@core/utils'
import { getSubjectLocaleFromActivity } from './helper'
import { getNameFromSubject } from './helper'
import { NetworkNamespace } from '@core/network/enums'
import { EvmActivityType } from '../enums/evm'

Expand Down Expand Up @@ -32,12 +32,16 @@ export async function getActivityDetailsTitle(activity: Activity): Promise<strin
const key = `${localizationPrefix}.${(activity.isInternal ? 'internal.' : 'external.') + activity.direction}.${
activity.inclusionState
}`
const displayedSubject = getSubjectLocaleFromActivity(activity)
const displayedSubject = getNameFromSubject(
activity.subject,
true,
activity.type === StardustActivityType.Basic && activity?.isShimmerClaiming
)

return localize(key, { subject: displayedSubject })
} else if (activity.action === ActivityAction.Mint || activity.action === ActivityAction.Burn) {
const key = `${localizationPrefix}.${activity.action}.${activity.inclusionState}`
const displayedSubject = getSubjectLocaleFromActivity(activity)
const displayedSubject = getNameFromSubject(activity.subject, true)

return localize(key, { subject: displayedSubject })
} else {
Expand All @@ -52,11 +56,11 @@ export async function getActivityDetailsTitle(activity: Activity): Promise<strin
const key = `${localizationPrefix}.${(activity.isInternal ? 'internal.' : 'external.') + activity.direction}.${
activity.inclusionState
}`
const displayedSubject = getSubjectLocaleFromActivity(activity)
const displayedSubject = getNameFromSubject(activity.subject, true)

return localize(key, { subject: displayedSubject })
} else if (activity.type === EvmActivityType.ContractCall) {
const displayedSubject = getSubjectLocaleFromActivity(activity)
const displayedSubject = getNameFromSubject(activity.subject, true)

return localize('general.contractCall') + ` - ${displayedSubject}`
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { localize } from '@core/i18n'
import { truncateString } from '@core/utils'
import { Subject } from '@core/wallet'
import { SubjectType } from '@core/wallet/enums'

export function getNameFromSubject(
subject: Subject | undefined,
truncate: boolean = false,
isShimmerGenesis?: boolean
): string {
let name = ''
if (isShimmerGenesis) {
return localize('general.shimmerGenesis')
} else if (subject?.type === SubjectType.Account) {
name = subject.account?.name
} else if (subject?.type === SubjectType.Contact) {
name = subject.contact?.name
} else if (subject?.type === SubjectType.SmartContract) {
name = subject.name
} else if (subject?.type === SubjectType.Network) {
name = subject.name
} else if (subject?.type === SubjectType.Address) {
name = subject.address
} else {
return localize('general.unknownAddress')
}

if (!truncate) {
return name
}

return subject?.type === SubjectType.Address ? truncateString(name, 6, 6) : truncateString(name, 13, 0)
}
Loading

0 comments on commit faf6b47

Please sign in to comment.