From d5ab3f40bab7df0e257e3b4516fe9947f7d83084 Mon Sep 17 00:00:00 2001 From: Jean Ribeiro Date: Tue, 27 Feb 2024 10:13:43 -0300 Subject: [PATCH 1/3] feat: adds votes percentage projection toggle (#1999) * feat: adds votes percentage projection toggle * fix: adds Mark suggestion Co-authored-by: Mark Nardi --------- Co-authored-by: Mark Nardi --- .../components/ProposalQuestion.svelte | 11 ++++- .../ProposalQuestionListPane.svelte | 16 +++++-- .../utils/getProjectedPercentages.ts | 44 +++++++++++++++++++ .../lib/contexts/governance/utils/index.ts | 1 + packages/shared/src/locales/en.json | 6 ++- 5 files changed, 72 insertions(+), 6 deletions(-) create mode 100644 packages/shared/src/lib/contexts/governance/utils/getProjectedPercentages.ts diff --git a/packages/desktop/views/dashboard/governance/components/ProposalQuestion.svelte b/packages/desktop/views/dashboard/governance/components/ProposalQuestion.svelte index cacb6edc86..ca4874b36a 100644 --- a/packages/desktop/views/dashboard/governance/components/ProposalQuestion.svelte +++ b/packages/desktop/views/dashboard/governance/components/ProposalQuestion.svelte @@ -5,7 +5,11 @@ import { Icon, IconName, Text, TooltipIcon } from '@bloomwalletio/ui' import { ABSTAIN_VOTE_VALUE } from '@contexts/governance/constants' - import { getPercentagesFromAnswerStatuses, IProposalAnswerPercentages } from '@contexts/governance' + import { + getPercentagesFromAnswerStatuses, + getProjectedPercentages, + IProposalAnswerPercentages, + } from '@contexts/governance' import { selectedProposal } from '@contexts/governance/stores' export let onQuestionClick: (questionIndex: number) => void @@ -18,12 +22,15 @@ export let selectedAnswerValue: number = undefined export let votedAnswerValue: number = undefined export let isLoading: boolean = false + export let projected: boolean = false let percentages: IProposalAnswerPercentages = {} let winnerAnswerIndex: number $: answers = [...(question?.answers ?? []), { value: 0, text: 'Abstain', additionalInfo: '' }] - $: percentages = getPercentagesFromAnswerStatuses(answerStatuses) + $: percentages = projected + ? getProjectedPercentages(answerStatuses) + : getPercentagesFromAnswerStatuses(answerStatuses) $: disabled = $selectedProposal?.status === EventStatus.Upcoming || $selectedProposal?.status === EventStatus.Ended || diff --git a/packages/desktop/views/dashboard/governance/components/proposal-details/ProposalQuestionListPane.svelte b/packages/desktop/views/dashboard/governance/components/proposal-details/ProposalQuestionListPane.svelte index b56fdc68d4..a9db06ea26 100644 --- a/packages/desktop/views/dashboard/governance/components/proposal-details/ProposalQuestionListPane.svelte +++ b/packages/desktop/views/dashboard/governance/components/proposal-details/ProposalQuestionListPane.svelte @@ -5,7 +5,7 @@ VotingEventPayload, TrackedParticipationOverview, } from '@iota/sdk/out/types' - import { Alert, Button } from '@bloomwalletio/ui' + import { Alert, Button, Text, Toggle, TooltipIcon } from '@bloomwalletio/ui' import { getVotingEvent } from '@contexts/governance/actions' import { ABSTAIN_VOTE_VALUE } from '@contexts/governance/constants' import { @@ -37,6 +37,7 @@ let openedQuestionIndex: number = -1 let isUpdatingVotedAnswerValues: boolean = false let lastAction: 'vote' | 'stopVote' + let projected: boolean = false $: selectedProposalOverview = $participationOverviewForSelectedAccount?.participations?.[$selectedProposal?.id] $: trackedParticipations = Object.values(selectedProposalOverview ?? {}) @@ -179,7 +180,15 @@ }) - + + {@const isVotable = [EventStatus.Commencing, EventStatus.Holding].includes($selectedProposal?.status)} + {#if isVotable} +
+ + {localize('views.governance.details.projection.label')} + +
+ {/if} {/each} {/if} {#if $selectedProposal?.status === EventStatus.Upcoming} - {:else if [EventStatus.Commencing, EventStatus.Holding].includes($selectedProposal?.status)} + {:else if isVotable} {@const isLoaded = questions && overviewLoaded && statusLoaded} {@const isStoppingVote = lastAction === 'stopVote' && hasGovernanceTransactionInProgress} {@const isStopVotingDisabled = !isLoaded || !isVotingForProposal || isUpdatingVotedAnswerValues} diff --git a/packages/shared/src/lib/contexts/governance/utils/getProjectedPercentages.ts b/packages/shared/src/lib/contexts/governance/utils/getProjectedPercentages.ts new file mode 100644 index 0000000000..07e4e46543 --- /dev/null +++ b/packages/shared/src/lib/contexts/governance/utils/getProjectedPercentages.ts @@ -0,0 +1,44 @@ +import { get } from 'svelte/store' +import { IProposal, IProposalAnswerPercentages, selectedProposal } from '..' +import { AnswerStatus } from '@iota/sdk' +import { networkStatus } from '@core/network/stores/network-status.store' +import { round } from '@core/utils/number' + +export function getProjectedPercentages( + answerStatuses: AnswerStatus[], + proposal: IProposal = get(selectedProposal) +): IProposalAnswerPercentages { + if (!proposal) { + return {} + } + + const answerStatusesWithProjection = answerStatuses.map((answerStatus) => { + return { ...answerStatus, projected: getProjectedVotesFromAnswerStatus(answerStatus, proposal) } + }) + + const totalVotes = answerStatusesWithProjection?.reduce((acc, answerStatus) => acc + answerStatus.projected, 0) ?? 0 + if (totalVotes === 0 || Number.isNaN(totalVotes)) { + return {} + } + + let percentages: IProposalAnswerPercentages = {} + answerStatusesWithProjection.forEach((answerStatus) => { + if (answerStatus.value !== undefined) { + const divisionResult = answerStatus.projected / totalVotes + percentages = { + ...percentages, + [answerStatus.value]: Number.isNaN(divisionResult) ? '0%' : `${round(divisionResult * 100, 1)}%`, + } + } + }) + + return percentages +} + +function getProjectedVotesFromAnswerStatus(answerStatus: AnswerStatus, proposal: IProposal): number { + const { accumulated, current } = answerStatus + const endingMilestone = proposal.milestones?.ended ?? 0 + const currentMilestone = get(networkStatus)?.currentMilestone ?? 0 + + return Math.max(accumulated, accumulated + current * (endingMilestone - currentMilestone)) +} diff --git a/packages/shared/src/lib/contexts/governance/utils/index.ts b/packages/shared/src/lib/contexts/governance/utils/index.ts index 0f869782e9..24ce2398a1 100644 --- a/packages/shared/src/lib/contexts/governance/utils/index.ts +++ b/packages/shared/src/lib/contexts/governance/utils/index.ts @@ -9,6 +9,7 @@ export * from './getNumberOfVotedProposals' export * from './getNumberOfVotingProposals' export * from './getParticipationsForProposal' export * from './getPercentagesFromAnswerStatuses' +export * from './getProjectedPercentages' export * from './getProposalStatusForMilestone' export * from './isAccountVoting' export * from './isParticipationOutput' diff --git a/packages/shared/src/locales/en.json b/packages/shared/src/locales/en.json index b7e92aefbe..d92055e4b2 100644 --- a/packages/shared/src/locales/en.json +++ b/packages/shared/src/locales/en.json @@ -688,7 +688,11 @@ "nodeUrl": "Node URL" }, "fetching": "Fetching proposal data", - "hintVote": "You can not vote on a proposal that is in the announcement phase, voting will open in {time}." + "hintVote": "You can not vote on a proposal that is in the announcement phase, voting will open in {time}.", + "projection": { + "label": "Projected votes", + "tooltip": "The projection is based on current voting weight and remaining milestones." + } } }, "updateStronghold": { From 5ec84d68e5fd935296da091c32c8310b2283b187 Mon Sep 17 00:00:00 2001 From: Mark Nardi Date: Tue, 27 Feb 2024 15:28:30 +0100 Subject: [PATCH 2/3] ui: improve governance activity rows (#2005) * Update governance icons Co-authored-by: Jean Ribeiro * update governance action titles + cleanup action color * get proposal name * add fiat value + fix colors --------- Co-authored-by: Jean Ribeiro --- .../row-sections/ActivityAmountSection.svelte | 2 +- .../row-sections/ActivityAssetSection.svelte | 23 ++++++---- .../row-sections/getActivityActionPill.ts | 42 ------------------- packages/shared/package.json | 2 +- .../avatars/GovernanceAvatar.svelte | 24 +++++++++++ .../shared/src/components/avatars/index.ts | 1 + .../activity/utils/getActivityActionColor.ts | 39 ----------------- .../utils/getActivityActionTextColor.ts} | 20 +++++---- .../activity/utils/getActivityTileAction.ts | 2 +- .../activity/utils/getActivityTileAsset.ts | 12 +++++- .../src/lib/core/activity/utils/index.ts | 2 +- packages/shared/src/locales/en.json | 12 +++--- yarn.lock | 8 ++-- 13 files changed, 78 insertions(+), 111 deletions(-) delete mode 100644 packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionPill.ts create mode 100644 packages/shared/src/components/avatars/GovernanceAvatar.svelte delete mode 100644 packages/shared/src/lib/core/activity/utils/getActivityActionColor.ts rename packages/{desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionColor.ts => shared/src/lib/core/activity/utils/getActivityActionTextColor.ts} (68%) diff --git a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAmountSection.svelte b/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAmountSection.svelte index 8c4384ed9e..29f767853e 100644 --- a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAmountSection.svelte +++ b/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAmountSection.svelte @@ -35,7 +35,7 @@ } function getFormattedMarketPrice(_activity: Activity): string | undefined { - if ((_activity.type === ActivityType.Basic || _activity.type === ActivityType.Foundry) && token) { + if ([ActivityType.Basic, ActivityType.Governance, ActivityType.Foundry].includes(_activity.type) && token) { const amount = _activity.tokenTransfer?.rawAmount ?? _activity.baseTokenTransfer.rawAmount const marketPrice = getFiatValueFromTokenAmount(amount, token) diff --git a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAssetSection.svelte b/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAssetSection.svelte index da454b52a8..ff05f4a23a 100644 --- a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAssetSection.svelte +++ b/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/ActivityAssetSection.svelte @@ -1,9 +1,16 @@
- {#if token} + {#if activity.type === ActivityType.Governance} + + {:else if token} {:else if activity.type === ActivityType.Nft} @@ -47,21 +56,21 @@ {:else if activity.type === ActivityType.Alias} {/if}
- {localize(getActivityTileAction(activity))} + {localize(getActivityTileAction(activity))} {getActivityTileAsset(activity, $selectedAccountIndex)}
diff --git a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionPill.ts b/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionPill.ts deleted file mode 100644 index 30b635eecb..0000000000 --- a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionPill.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Activity, ActivityAsyncStatus, ActivityDirection } from '@core/activity' -import { getTimeDifference } from '@core/utils/time' - -export function getActivityActionPill( - activity: Activity, - currentDate: Date -): { type: 'timelock' | 'unclaimed' | 'expired'; timeDiff?: string } | undefined { - if (!activity?.asyncData?.asyncStatus) { - return undefined - } - - const { asyncStatus, expirationDate, timelockDate } = activity.asyncData - - switch (asyncStatus) { - case ActivityAsyncStatus.Claimed: { - return undefined - } - case ActivityAsyncStatus.Timelocked: { - if (activity.direction === ActivityDirection.Outgoing) { - if (expirationDate) { - const timeDiff = getTimeDifference(expirationDate, currentDate) - return { type: 'unclaimed', timeDiff } - } else { - return undefined - } - } else { - const timeDiff = getTimeDifference(timelockDate, currentDate) - return { type: 'timelock', timeDiff } - } - } - case ActivityAsyncStatus.Unclaimed: { - const timeDiff = expirationDate ? getTimeDifference(expirationDate, currentDate) : undefined - return { type: 'unclaimed', timeDiff } - } - case ActivityAsyncStatus.Expired: { - return { type: 'expired' } - } - default: { - return undefined - } - } -} diff --git a/packages/shared/package.json b/packages/shared/package.json index 5c275dc3cc..d23d2d135b 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -5,7 +5,7 @@ "author": "Bloom Labs Ltd ", "license": "Apache-2.0", "dependencies": { - "@bloomwalletio/ui": "0.20.6", + "@bloomwalletio/ui": "0.20.8", "@ethereumjs/rlp": "4.0.1", "@ethereumjs/tx": "5.2.1", "@ethereumjs/util": "9.0.2", diff --git a/packages/shared/src/components/avatars/GovernanceAvatar.svelte b/packages/shared/src/components/avatars/GovernanceAvatar.svelte new file mode 100644 index 0000000000..984c05b829 --- /dev/null +++ b/packages/shared/src/components/avatars/GovernanceAvatar.svelte @@ -0,0 +1,24 @@ + + + diff --git a/packages/shared/src/components/avatars/index.ts b/packages/shared/src/components/avatars/index.ts index 9022cceba8..a3ded1d46f 100644 --- a/packages/shared/src/components/avatars/index.ts +++ b/packages/shared/src/components/avatars/index.ts @@ -1,4 +1,5 @@ export { default as ContactAvatar } from './ContactAvatar.svelte' +export { default as GovernanceAvatar } from './GovernanceAvatar.svelte' export { default as NftAvatar } from './NftAvatar.svelte' export { default as NetworkAvatar } from './NetworkAvatar.svelte' export { default as ProfileAvatar } from './ProfileAvatar.svelte' diff --git a/packages/shared/src/lib/core/activity/utils/getActivityActionColor.ts b/packages/shared/src/lib/core/activity/utils/getActivityActionColor.ts deleted file mode 100644 index 61449349d4..0000000000 --- a/packages/shared/src/lib/core/activity/utils/getActivityActionColor.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Activity, ActivityAction, ActivityDirection, ActivityType } from '@core/activity' - -export function getActivityActionColor(activity: Activity, darkMode: boolean): string { - const { type, direction, action } = activity - const neutralColor = darkMode ? 'neutral-1' : 'neutral-7' - - if (type === ActivityType.Basic && activity.isShimmerClaiming) { - return 'info' - } - if (type === ActivityType.Governance) { - return neutralColor - } else if (type === ActivityType.Consolidation) { - return neutralColor - } else if (type === ActivityType.SmartContract) { - return neutralColor - } else if (action === ActivityAction.Mint) { - return 'success' - } else if (action === ActivityAction.Burn) { - return 'danger' - } else if (action === ActivityAction.InitialBalance) { - return neutralColor - } else if (action === ActivityAction.Send || action === ActivityAction.BalanceChange) { - const isReceived = [ - ActivityDirection.Incoming, - ActivityDirection.SelfTransaction, - ActivityDirection.Genesis, - ].includes(direction) - - if (direction === ActivityDirection.Outgoing) { - return 'brand' - } else if (isReceived) { - return 'info' - } else { - return neutralColor - } - } else { - return neutralColor - } -} diff --git a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionColor.ts b/packages/shared/src/lib/core/activity/utils/getActivityActionTextColor.ts similarity index 68% rename from packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionColor.ts rename to packages/shared/src/lib/core/activity/utils/getActivityActionTextColor.ts index e976b14bc7..f9861983a1 100644 --- a/packages/desktop/views/dashboard/wallet/tab-section/activity/components/row-sections/getActivityActionColor.ts +++ b/packages/shared/src/lib/core/activity/utils/getActivityActionTextColor.ts @@ -1,23 +1,27 @@ -import { Activity, ActivityAction, ActivityDirection, ActivityType } from '@core/activity' +import { TextColor } from '@bloomwalletio/ui' +import { Activity, ActivityAction, ActivityDirection, ActivityType, GovernanceAction } from '@core/activity' -export function getActivityActionColor(activity: Activity, darkMode: boolean): string { +export function getActivityActionTextColor(activity: Activity): TextColor { const { type, direction, action } = activity if (type === ActivityType.Basic && activity.isShimmerClaiming) { return 'info' } if (type === ActivityType.Governance) { - return darkMode ? 'neutral-1' : 'neutral-7' + if ([GovernanceAction.StartVoting, GovernanceAction.StopVoting].includes(activity.governanceAction)) { + return 'secondary' + } + return 'primary' } else if (type === ActivityType.Consolidation) { - return darkMode ? 'neutral-1' : 'neutral-7' + return 'primary' } else if (type === ActivityType.SmartContract) { - return 'brand' + return 'primary' } else if (action === ActivityAction.Mint) { return 'success' } else if (action === ActivityAction.Burn) { return 'danger' } else if (action === ActivityAction.InitialBalance) { - return darkMode ? 'neutral-1' : 'neutral-7' + return 'primary' } else if (action === ActivityAction.Send || action === ActivityAction.BalanceChange) { const isReceived = [ ActivityDirection.Incoming, @@ -30,9 +34,9 @@ export function getActivityActionColor(activity: Activity, darkMode: boolean): s } else if (isReceived) { return 'info' } else { - return darkMode ? 'neutral-1' : 'neutral-7' + return 'primary' } } else { - return darkMode ? 'neutral-1' : 'neutral-7' + return 'primary' } } diff --git a/packages/shared/src/lib/core/activity/utils/getActivityTileAction.ts b/packages/shared/src/lib/core/activity/utils/getActivityTileAction.ts index 26468ff2c3..0c6de07927 100644 --- a/packages/shared/src/lib/core/activity/utils/getActivityTileAction.ts +++ b/packages/shared/src/lib/core/activity/utils/getActivityTileAction.ts @@ -17,7 +17,7 @@ export function getActivityTileAction(activity: Activity): string | undefined { } else if (activity.governanceAction === GovernanceAction.DecreaseVotingPower) { return isConfirmed ? 'general.decreased' : 'general.decreasing' } else if (activity.governanceAction === GovernanceAction.StartVoting) { - return isConfirmed ? 'general.voted' : 'general.voting' + return isConfirmed ? 'general.voteStarted' : 'general.voteStarting' } else if (activity.governanceAction === GovernanceAction.StopVoting) { return isConfirmed ? 'general.unvoted' : 'general.unvoting' } else if (activity.governanceAction === GovernanceAction.ChangedVote) { diff --git a/packages/shared/src/lib/core/activity/utils/getActivityTileAsset.ts b/packages/shared/src/lib/core/activity/utils/getActivityTileAsset.ts index d381304c19..6e05e972d4 100644 --- a/packages/shared/src/lib/core/activity/utils/getActivityTileAsset.ts +++ b/packages/shared/src/lib/core/activity/utils/getActivityTileAsset.ts @@ -1,7 +1,9 @@ -import { ActivityType } from '../enums' +import { ActivityType, GovernanceAction } from '../enums' import { Activity } from '../types' import { getNftByIdFromAllAccountNfts } from '@core/nfts/actions' import { getTokenFromActivity } from './getTokenFromActivity' +import { get } from 'svelte/store' +import { registeredProposalsForSelectedAccount } from '@contexts/governance' export function getActivityTileAsset(activity: Activity, accountIndex: number): string | undefined { if (activity.type === ActivityType.Basic || activity.type === ActivityType.Foundry) { @@ -19,6 +21,14 @@ export function getActivityTileAsset(activity: Activity, accountIndex: number): } else if (activity.type === ActivityType.Consolidation) { return '' } else if (activity.type === ActivityType.Governance) { + if ([GovernanceAction.StartVoting, GovernanceAction.StopVoting].includes(activity.governanceAction)) { + if (activity?.participation?.eventId) { + const proposal = get(registeredProposalsForSelectedAccount)?.[activity.participation.eventId] + return proposal.title + } else { + return activity.participation?.eventId + } + } return '' } else { return '' diff --git a/packages/shared/src/lib/core/activity/utils/index.ts b/packages/shared/src/lib/core/activity/utils/index.ts index 7d8ec3f0d4..f0c50eb7c9 100644 --- a/packages/shared/src/lib/core/activity/utils/index.ts +++ b/packages/shared/src/lib/core/activity/utils/index.ts @@ -14,7 +14,7 @@ export * from './generateSingleFoundryActivity' export * from './generateSingleGovernanceActivity' export * from './generateSingleNftActivity' export * from './getActivityDetailsTitle' -export * from './getActivityActionColor' +export * from './getActivityActionTextColor' export * from './getActivityActionPill' export * from './getActivityTileAction' export * from './getActivityTileAsset' diff --git a/packages/shared/src/locales/en.json b/packages/shared/src/locales/en.json index d92055e4b2..ec696f6156 100644 --- a/packages/shared/src/locales/en.json +++ b/packages/shared/src/locales/en.json @@ -1396,18 +1396,18 @@ "receiving": "Receiving", "newVotingPower": "New voting power", "votingPower": "Voting power", - "increased": "Voting power increased", - "increasing": "Increasing voting power", - "decreased": "Voting power decreased", - "decreasing": "Decreasing voting power", + "increased": "Voting increased", + "increasing": "Increasing voting", + "decreased": "Voting decreased", + "decreasing": "Decreasing voting", "voted": "Voted", "voting": "Voting", "changedVote": "Changed vote", "changingVote": "Changing vote", "revoted": "Revoted", "revoting": "Revoting", - "unvoted": "Unvoted", - "unvoting": "Unvoting", + "unvoted": "Stopped voting", + "unvoting": "Stopping voting", "transferred": "Transferred", "transferring": "Transferring", "shimmerClaimed": "Claimed", diff --git a/yarn.lock b/yarn.lock index 78e86cfe65..bcd8b0ea03 100644 --- a/yarn.lock +++ b/yarn.lock @@ -343,10 +343,10 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@bloomwalletio/ui@0.20.6": - version "0.20.6" - resolved "https://npm.pkg.github.com/download/@bloomwalletio/ui/0.20.6/566636d21ecf9553142868a95d7bf2c64e2a3007#566636d21ecf9553142868a95d7bf2c64e2a3007" - integrity sha512-MqU+umZ5IaODS0/Zi4zJdzbeVta2wbBmBz9hbSK7JEKCqFiZ9ocgmZXw5IZmb5nbWLUQUzdVUT8/C6m4pY+pCg== +"@bloomwalletio/ui@0.20.8": + version "0.20.8" + resolved "https://npm.pkg.github.com/download/@bloomwalletio/ui/0.20.8/c315895a962b23263da7fe5e91ddff0eb61fb9db#c315895a962b23263da7fe5e91ddff0eb61fb9db" + integrity sha512-jwailq9C48f4R+ID3wnzzC+A7KKSMI6TUM/HAX7St/qwhruK583wof05ci6JAkEnJKkj4Y6pGE5i4eHB+s48aQ== dependencies: "@floating-ui/dom" "1.4.3" "@popperjs/core" "2.11.8" From 70a7506eeb89d989151f7562ab74156e3949427c Mon Sep 17 00:00:00 2001 From: Nicole O'Brien Date: Tue, 27 Feb 2024 16:39:34 +0000 Subject: [PATCH 3/3] feat: explorer api pagination (#2002) * refactor: query parameters on base api add: make paginated request on explorer api Co-authored-by: Tuditi * fix: paginated request logic * refactor: move evm explorer to blockscout module * fix: imports * feat: add blockscout transaction interface * fix: correct queryParameters --------- Co-authored-by: Tuditi Co-authored-by: Tuditi <45079109+Tuditi@users.noreply.github.com> Co-authored-by: Tuditi --- .../blockscout/api/blockscout.api.ts | 84 +++++++++++++++++++ .../src/lib/auxiliary/blockscout/api/index.ts | 1 + .../interfaces/blockscout-api.interface.ts | 14 ++++ .../blockscout-asset-metadata.interface.ts} | 2 +- .../interfaces/blockscout-asset.interface.ts | 9 ++ .../blockscout-transaction.interface.ts | 81 ++++++++++++++++++ .../auxiliary/blockscout/interfaces/index.ts | 4 + .../actions/checkForUntrackedTokens.ts | 7 +- .../network/classes/evm-explorer-api.class.ts | 43 ---------- .../src/lib/core/network/classes/index.ts | 1 - .../interfaces/explorer-api.interface.ts | 12 --- .../interfaces/explorer-asset.interface.ts | 9 -- .../src/lib/core/network/interfaces/index.ts | 3 - .../nfts/actions/checkForUntrackedNfts.ts | 11 +-- .../shared/src/lib/core/tide/apis/tide.api.ts | 8 +- packages/shared/src/lib/core/utils/api.ts | 31 +++++-- .../src/lib/core/utils/types/api.types.ts | 1 + .../shared/src/lib/core/utils/types/index.ts | 1 + packages/shared/src/lib/core/utils/url.ts | 3 +- 19 files changed, 237 insertions(+), 88 deletions(-) create mode 100644 packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts create mode 100644 packages/shared/src/lib/auxiliary/blockscout/api/index.ts create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts rename packages/shared/src/lib/{core/network/interfaces/explorer-asset-metadata.interface.ts => auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts} (85%) create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts create mode 100644 packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts delete mode 100644 packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts delete mode 100644 packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts delete mode 100644 packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts create mode 100644 packages/shared/src/lib/core/utils/types/api.types.ts diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts new file mode 100644 index 0000000000..29781e54e3 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/api/blockscout.api.ts @@ -0,0 +1,84 @@ +import { NftStandard } from '@core/nfts/enums' +import { TokenStandard } from '@core/token/enums' +import { QueryParameters } from '@core/utils' +import { BaseApi } from '@core/utils/api' +import { DEFAULT_EXPLORER_URLS } from '@core/network/constants' +import { SupportedNetworkId } from '@core/network/enums' +import { IBlockscoutApi, IBlockscoutAsset, IBlockscoutAssetMetadata, IBlockscoutTransaction } from '../interfaces' +import { NetworkId } from '@core/network/types' + +interface INextPageParams { + block_number: number + index: number + items_count: number +} + +interface IPaginationResponse { + items: T[] + next_page_params: INextPageParams | null +} + +export class BlockscoutApi extends BaseApi implements IBlockscoutApi { + constructor(networkId: NetworkId) { + const explorerUrl = DEFAULT_EXPLORER_URLS[networkId as SupportedNetworkId] + super(`${explorerUrl}/api/v2`) + } + + private async makePaginatedGetRequest( + path: string, + queryParameters?: QueryParameters, + items: T[] = [], + nextPageParameters?: INextPageParams | null + ): Promise { + if (nextPageParameters === null) { + return Promise.resolve(items) + } + return this.get>(path, { ...queryParameters, ...nextPageParameters }).then( + (response) => { + if (!response) { + return Promise.resolve(items) + } + return this.makePaginatedGetRequest( + path, + queryParameters, + items.concat(response.items), + response.next_page_params + ) + } + ) + } + + async getAssetMetadata(assetAddress: string): Promise { + const response = await this.get(`tokens/${assetAddress}`) + if (response) { + response.type = response.type.replace('-', '') as TokenStandard.Erc20 | NftStandard.Erc721 + return response + } + } + + async getAssetsForAddress( + address: string, + standard: TokenStandard.Erc20 | NftStandard.Erc721 = TokenStandard.Erc20 + ): Promise { + const tokenType = standard.replace('ERC', 'ERC-') + const path = `addresses/${address}/tokens` + const response = await this.get>(path, { type: tokenType }) + if (response) { + return (response?.items ?? []).map((asset) => ({ + ...asset, + token: { + ...asset.token, + type: asset.token.type.replace('-', ''), + }, + })) + } else { + return [] + } + } + + async getTransactionsForAddress(address: string): Promise { + const path = `addresses/${address}/transactions` + const items = await this.makePaginatedGetRequest(path) + return items + } +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/api/index.ts b/packages/shared/src/lib/auxiliary/blockscout/api/index.ts new file mode 100644 index 0000000000..9c3b613282 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/api/index.ts @@ -0,0 +1 @@ +export * from './blockscout.api' diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts new file mode 100644 index 0000000000..46a5610528 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-api.interface.ts @@ -0,0 +1,14 @@ +import { NftStandard } from '@core/nfts/enums' +import { TokenStandard } from '@core/token/enums' +import { IBlockscoutAsset } from './blockscout-asset.interface' +import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' +import { IBlockscoutTransaction } from './blockscout-transaction.interface' + +export interface IBlockscoutApi { + getAssetMetadata(assetAddress: string): Promise + getAssetsForAddress( + address: string, + tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721 + ): Promise + getTransactionsForAddress(address: string): Promise +} diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts similarity index 85% rename from packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts rename to packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts index 1952408e7b..6c67bc5929 100644 --- a/packages/shared/src/lib/core/network/interfaces/explorer-asset-metadata.interface.ts +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset-metadata.interface.ts @@ -1,5 +1,5 @@ // snake_case returned by the API -export interface IExplorerAssetMetadata { +export interface IBlockscoutAssetMetadata { address: string circulating_market_cap: string decimals: number diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts new file mode 100644 index 0000000000..5ebb40c5fe --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-asset.interface.ts @@ -0,0 +1,9 @@ +import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' + +// snake_case returned by the API +export interface IBlockscoutAsset { + token: IBlockscoutAssetMetadata + token_id: string + token_instance: unknown + value: string +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts new file mode 100644 index 0000000000..fb26b6845a --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/blockscout-transaction.interface.ts @@ -0,0 +1,81 @@ +import { IBlockscoutAssetMetadata } from './blockscout-asset-metadata.interface' + +interface IFee { + type: string + value: string +} + +interface IAddressTag { + address_hash: string + display_name: string + label: string +} + +interface IWatchlistName { + display_name: string + label: string +} + +interface IAddressParam { + hash: string + implementation_name: string + name: string + is_contract: boolean + private_tags: IAddressTag[] + watchlist_names: IWatchlistName[] + public_tags: IAddressTag[] + is_verified: boolean +} + +interface IDecodedInput { + method_call: string + method_id: string + parameters: Record // IDecodedInputParameters +} + +interface ITokenTransfer { + block_hash: string + from: IAddressParam + log_index: string + method: string + timestamp: string + to: IAddressParam + token: IBlockscoutAssetMetadata +} + +export interface IBlockscoutTransaction { + timestamp: string + fee: IFee + gas_limit: number + block: number + status: string // e.g ok | error + method: string // e.g transferFrom + confirmations: number + type: number + exchange_rate: string + to: IAddressParam + tx_burnt_fee: string + max_fee_per_gas: string + result: string + hash: string + gas_price: string + priority_fee: string + base_fee_per_gas: string + from: IAddressParam + token_transfers: ITokenTransfer[] + tx_types: string[] + gas_used: string + created_contract: IAddressParam + position: number + nonce: number + has_error_in_internal_txs: boolean + actions: unknown // TransactionAction + decoded_input: IDecodedInput + token_transfers_overflow: boolean + raw_input: string + value: string + max_priority_fee_per_gas: string + revert_reason: string + confirmation_duration: string + tx_tag: string +} diff --git a/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts new file mode 100644 index 0000000000..3a8f009cd6 --- /dev/null +++ b/packages/shared/src/lib/auxiliary/blockscout/interfaces/index.ts @@ -0,0 +1,4 @@ +export * from './blockscout-api.interface' +export * from './blockscout-asset.interface' +export * from './blockscout-asset-metadata.interface' +export * from './blockscout-transaction.interface' diff --git a/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts b/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts index 20106ade3f..a0b7ef96de 100644 --- a/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts +++ b/packages/shared/src/lib/core/layer-2/actions/checkForUntrackedTokens.ts @@ -1,9 +1,10 @@ import { IAccountState } from '@core/account/interfaces' -import { EvmExplorerApi, EvmNetworkId } from '@core/network' +import { EvmNetworkId } from '@core/network' import { getNetwork } from '@core/network/stores' import { TokenStandard, TokenTrackingStatus } from '@core/token' import { addNewTrackedTokenToActiveProfile, hasTokenBeenUntracked } from '@core/wallet/actions' import { BASE_TOKEN_CONTRACT_ADDRESS } from '../constants' +import { BlockscoutApi } from '@auxiliary/blockscout/api' export function checkForUntrackedTokens(account: IAccountState, addPreviouslyUntracked?: boolean): void { const chains = getNetwork()?.getChains() @@ -14,9 +15,9 @@ export function checkForUntrackedTokens(account: IAccountState, addPreviouslyUnt return } const networkId = chain.getConfiguration().id - const explorerApi = new EvmExplorerApi(networkId) + const blockscoutApi = new BlockscoutApi(networkId) - const tokens = await explorerApi.getAssetsForAddress(evmAddress) + const tokens = await blockscoutApi.getAssetsForAddress(evmAddress) const untrackedTokensToTrack = tokens.filter( ({ token }) => addPreviouslyUntracked || !hasTokenBeenUntracked(token.address.toLowerCase(), networkId) ) diff --git a/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts b/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts deleted file mode 100644 index 9b06c043f5..0000000000 --- a/packages/shared/src/lib/core/network/classes/evm-explorer-api.class.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NftStandard } from '@core/nfts/enums' -import { TokenStandard } from '@core/token/enums' -import { BaseApi } from '@core/utils/api' - -import { DEFAULT_EXPLORER_URLS } from '../constants' -import { IExplorerApi, IExplorerAsset, IExplorerAssetMetadata } from '../interfaces' -import { NetworkId } from '../types' - -export class EvmExplorerApi extends BaseApi implements IExplorerApi { - constructor(networkId: NetworkId) { - const explorerUrl = DEFAULT_EXPLORER_URLS[networkId] - super(`${explorerUrl}/api/v2`) - } - - async getAssetMetadata(assetAddress: string): Promise { - const response = await this.get(`tokens/${assetAddress}`) - if (response) { - response.type = response.type.replace('-', '') as TokenStandard.Erc20 | NftStandard.Erc721 - return response - } - } - - async getAssetsForAddress( - address: string, - standard: TokenStandard.Erc20 | NftStandard.Erc721 = TokenStandard.Erc20 - ): Promise { - const tokenType = standard.replace('ERC', 'ERC-') - const response = await this.get<{ items: IExplorerAsset[]; next_page_params: unknown }>( - `addresses/${address}/tokens?type=${tokenType}` - ) - if (response) { - return (response?.items ?? []).map((asset) => ({ - ...asset, - token: { - ...asset.token, - type: asset.token.type.replace('-', ''), - }, - })) - } else { - return [] - } - } -} diff --git a/packages/shared/src/lib/core/network/classes/index.ts b/packages/shared/src/lib/core/network/classes/index.ts index bbca6f250e..0710e6657d 100644 --- a/packages/shared/src/lib/core/network/classes/index.ts +++ b/packages/shared/src/lib/core/network/classes/index.ts @@ -1,3 +1,2 @@ -export * from './evm-explorer-api.class' export * from './iscp-chain.class' export * from './stardust-network.class' diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts b/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts deleted file mode 100644 index b219a55a0a..0000000000 --- a/packages/shared/src/lib/core/network/interfaces/explorer-api.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NftStandard } from '@core/nfts/enums' -import { TokenStandard } from '@core/token/enums' -import { IExplorerAsset } from './explorer-asset.interface' -import { IExplorerAssetMetadata } from './explorer-asset-metadata.interface' - -export interface IExplorerApi { - getAssetMetadata(assetAddress: string): Promise - getAssetsForAddress( - address: string, - tokenStandard?: TokenStandard.Erc20 | NftStandard.Erc721 - ): Promise -} diff --git a/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts b/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts deleted file mode 100644 index 9a7c8e10c9..0000000000 --- a/packages/shared/src/lib/core/network/interfaces/explorer-asset.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IExplorerAssetMetadata } from './explorer-asset-metadata.interface' - -// snake_case returned by the API -export interface IExplorerAsset { - token: IExplorerAssetMetadata - token_id: string - token_instance: unknown - value: string -} diff --git a/packages/shared/src/lib/core/network/interfaces/index.ts b/packages/shared/src/lib/core/network/interfaces/index.ts index 71e0401773..6ff8524eba 100644 --- a/packages/shared/src/lib/core/network/interfaces/index.ts +++ b/packages/shared/src/lib/core/network/interfaces/index.ts @@ -6,9 +6,6 @@ export * from './chain.interface' export * from './client-options.interface' export * from './connected-chain.interface' export * from './evm-addresses.interface' -export * from './explorer-api.interface' -export * from './explorer-asset-metadata.interface' -export * from './explorer-asset.interface' export * from './gas-fee-policy.interface' export * from './gas-limits.interface' export * from './network-status.interface' diff --git a/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts b/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts index 9471b0399b..19fe0c48d5 100644 --- a/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts +++ b/packages/shared/src/lib/core/nfts/actions/checkForUntrackedNfts.ts @@ -1,8 +1,7 @@ import { IAccountState } from '@core/account/interfaces' import { ContractType } from '@core/layer-2/enums' -import { EvmExplorerApi } from '@core/network/classes' import { getNetwork } from '@core/network/stores' -import { IChain, IExplorerAsset } from '@core/network/interfaces' +import { IChain } from '@core/network/interfaces' import features from '@features/features' import { NftStandard } from '../enums' @@ -13,6 +12,8 @@ import { addNftsToDownloadQueue } from './addNftsToDownloadQueue' import { Nft } from '../interfaces' import { addNewTrackedNftToActiveProfile } from './addNewTrackedNftToActiveProfile' import { TokenTrackingStatus } from '@core/token' +import { IBlockscoutAsset } from '@auxiliary/blockscout/interfaces' +import { BlockscoutApi } from '@auxiliary/blockscout/api' export async function checkForUntrackedNfts(account: IAccountState): Promise { if (!features?.collectibles?.erc721?.enabled) { @@ -28,9 +29,9 @@ export async function checkForUntrackedNfts(account: IAccountState): Promise { const { token, value } = asset diff --git a/packages/shared/src/lib/core/tide/apis/tide.api.ts b/packages/shared/src/lib/core/tide/apis/tide.api.ts index 780a88bee4..0312139097 100644 --- a/packages/shared/src/lib/core/tide/apis/tide.api.ts +++ b/packages/shared/src/lib/core/tide/apis/tide.api.ts @@ -1,5 +1,5 @@ import { INftAttribute } from '@core/nfts' -import { BaseApi, buildQueryParametersFromObject } from '@core/utils' +import { BaseApi } from '@core/utils' import { TIDE_API_BASE_URL } from '../constants' import { TideApiEndpoint } from '../enums' import { ITideLeaderboardItem, ITideUserPosition } from '../interfaces' @@ -82,10 +82,8 @@ export class TideApi extends BaseApi { projectId: number, queryParams?: ProjectLeaderboardQueryParams ): Promise { - const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard?${ - queryParams ? buildQueryParametersFromObject(queryParams) : '' - }` - const response = await this.get(path) + const path = `${TideApiEndpoint.Project}/${projectId}/leaderboard` + const response = await this.get(path, queryParams) return response } diff --git a/packages/shared/src/lib/core/utils/api.ts b/packages/shared/src/lib/core/utils/api.ts index bfffcaf1a8..823c0afca7 100644 --- a/packages/shared/src/lib/core/utils/api.ts +++ b/packages/shared/src/lib/core/utils/api.ts @@ -1,3 +1,6 @@ +import { QueryParameters } from './types' +import { buildQueryParametersFromObject } from './url' + interface IApiRequestOptions { disableCors?: boolean } @@ -9,15 +12,29 @@ export class BaseApi { this._baseUrl = baseUrl } - protected get(path: string, options?: IApiRequestOptions): Promise { - return this.makeRequest(path, '', options) + protected get( + path: string, + queryParameters?: QueryParameters, + options?: IApiRequestOptions + ): Promise { + return this.makeRequest(path, queryParameters, undefined, options) } - protected post(path: string, body: string, options?: IApiRequestOptions): Promise { - return this.makeRequest(path, body, options) + protected post( + path: string, + queryParameters?: QueryParameters, + body?: string, + options?: IApiRequestOptions + ): Promise { + return this.makeRequest(path, queryParameters, body, options) } - private async makeRequest(path: string, body?: string, options?: IApiRequestOptions): Promise { + private async makeRequest( + path: string, + queryParameters?: QueryParameters, + body?: string, + options?: IApiRequestOptions + ): Promise { try { const requestInit: RequestInit = { method: body ? 'POST' : 'GET', @@ -28,6 +45,10 @@ export class BaseApi { ...(body && { body }), ...(options?.disableCors && { mode: 'no-cors' }), } + if (queryParameters && Object.keys(queryParameters).length) { + const queryParametersString = buildQueryParametersFromObject(queryParameters) + path = `${path}?${queryParametersString}` + } const response = await fetch(`${this._baseUrl}/${path}`, requestInit) return (await response.json()) as T } catch (err) { diff --git a/packages/shared/src/lib/core/utils/types/api.types.ts b/packages/shared/src/lib/core/utils/types/api.types.ts new file mode 100644 index 0000000000..7fb05ab28b --- /dev/null +++ b/packages/shared/src/lib/core/utils/types/api.types.ts @@ -0,0 +1 @@ +export type QueryParameters = Record diff --git a/packages/shared/src/lib/core/utils/types/index.ts b/packages/shared/src/lib/core/utils/types/index.ts index ccaa2d5c5f..deda6f81da 100644 --- a/packages/shared/src/lib/core/utils/types/index.ts +++ b/packages/shared/src/lib/core/utils/types/index.ts @@ -1,3 +1,4 @@ +export * from './api.types' export * from './currencies.type' export * from './duration.type' export * from './exchange-rates.type' diff --git a/packages/shared/src/lib/core/utils/url.ts b/packages/shared/src/lib/core/utils/url.ts index 10dd8a7455..a0a4cf8ee7 100644 --- a/packages/shared/src/lib/core/utils/url.ts +++ b/packages/shared/src/lib/core/utils/url.ts @@ -1,4 +1,5 @@ import { stripSpaces, stripTrailingSlash } from './string' +import { QueryParameters } from './types' export function cleanUrl( url: string, @@ -23,7 +24,7 @@ export function cleanUrl( return cleanedUrl } -export function buildQueryParametersFromObject(obj: Record): string { +export function buildQueryParametersFromObject(obj: QueryParameters): string { return Object.keys(obj) .map( (key) =>