diff --git a/.changeset/short-parents-punch.md b/.changeset/short-parents-punch.md new file mode 100644 index 0000000000..2e7f238480 --- /dev/null +++ b/.changeset/short-parents-punch.md @@ -0,0 +1,5 @@ +--- +'minifront': minor +--- + +Add a "claim all" button to the auctions list diff --git a/apps/minifront/src/components/swap/auction-list/end-or-withdraw-all-button.tsx b/apps/minifront/src/components/swap/auction-list/end-or-withdraw-all-button.tsx new file mode 100644 index 0000000000..fb0b4bf07e --- /dev/null +++ b/apps/minifront/src/components/swap/auction-list/end-or-withdraw-all-button.tsx @@ -0,0 +1,117 @@ +import { useAuctionInfos } from '../../../state/swap/dutch-auction'; +import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; +import { Button } from '@repo/ui/components/ui/button'; +import { AllSlices } from '../../../state'; +import { useStoreShallow } from '../../../utils/use-store-shallow.ts'; +import { filterWithLimit } from './helpers.ts'; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from '@repo/ui/components/ui/tooltip'; +import { AuctionInfo } from '../../../fetchers/auction-infos.ts'; + +const endOrWithdrawAllButtonSelector = (state: AllSlices) => ({ + endAllAuctions: state.swap.dutchAuction.endAllAuctions, + withdrawAllAuctions: state.swap.dutchAuction.withdrawAllAuctions, +}); + +export interface AuctionsBatch { + auctions: AuctionInfo[]; + source: AddressIndex; +} + +// Assemble batch auctions for end or withdrawal. +// All auctions in the batch will have the same 'AddressIndex' +export const assembleAuctionBatch = ( + auctions: AuctionInfo[], + filteredSeqNumber: bigint, + batchLimit: number, +): AuctionsBatch => { + const filteredBySeqAuctions: AuctionInfo[] = auctions.filter( + a => a.localSeqNum === filteredSeqNumber, + ); + // Get the address index of the first auction in the list and filter other auctions with this address index + const firstFoundAddressIndex = filteredBySeqAuctions[0]?.addressIndex; + + const filteredBySeqAndAddressIndexAuctions = filterWithLimit( + filteredBySeqAuctions, + a => a.addressIndex.equals(firstFoundAddressIndex), + batchLimit, + ); + return { auctions: filteredBySeqAndAddressIndexAuctions, source: firstFoundAddressIndex! }; +}; + +export const EndOrWithdrawAllButton = () => { + const { endAllAuctions, withdrawAllAuctions } = useStoreShallow(endOrWithdrawAllButtonSelector); + const { data } = useAuctionInfos(); + + if (!data?.length) { + return null; + } + if (data.some(a => a.localSeqNum === 0n)) { + return ( + + + { + // Chain has a transaction size limit, so we can add at most a batch of 48 auctions in a single transaction + // see https://github.com/penumbra-zone/web/issues/1166#issuecomment-2263550249 + const auctionBatch = assembleAuctionBatch(data, 0n, 48); + + void endAllAuctions( + auctionBatch.auctions, + // TODO Should use the index of the selected account after the account selector for the auction is implemented + auctionBatch.source, + ); + }} + aria-label='End all open auctions, with a limit of 48 auctions per transaction' + > +
+ +
+
+ + End all open auctions, with a limit of 48 auctions per transaction + +
+
+ ); + } + + if (data.some(a => a.localSeqNum === 1n)) { + return ( + + + { + // Chain has a transaction size limit, so we can add at most a batch of 48 auctions in a single transaction + // see https://github.com/penumbra-zone/web/issues/1166#issuecomment-2263550249 + const auctionBatch = assembleAuctionBatch(data, 1n, 48); + void withdrawAllAuctions( + auctionBatch.auctions, + // TODO Should use the index of the selected account after the account selector for the auction is implemented + auctionBatch.source, + ); + }} + aria-label='Withdraw all ended auctions, with a limit of 48 auctions per transaction' + > +
+ +
+
+ + Withdraw all ended auctions, with a limit of 48 auctions per transaction + +
+
+ ); + } + + return null; +}; diff --git a/apps/minifront/src/components/swap/auction-list/helpers.ts b/apps/minifront/src/components/swap/auction-list/helpers.ts index 1cd9ce912c..babf9ea98b 100644 --- a/apps/minifront/src/components/swap/auction-list/helpers.ts +++ b/apps/minifront/src/components/swap/auction-list/helpers.ts @@ -1,5 +1,25 @@ import { AuctionInfo } from '../../../fetchers/auction-infos'; +export const filterWithLimit = ( + array: T[], + predicate: (value: T) => boolean, + limit: number, +): T[] => { + const result: T[] = []; + let count = 0; + + for (const item of array) { + if (predicate(item)) { + result.push(item); + count++; + if (count >= limit) { + break; + } + } + } + return result; +}; + export const byStartHeightAscending = (a: AuctionInfo, b: AuctionInfo) => { if (!a.auction.description?.startHeight || !b.auction.description?.startHeight) { return 0; diff --git a/apps/minifront/src/components/swap/auction-list/index.tsx b/apps/minifront/src/components/swap/auction-list/index.tsx index 5c89a6a25c..66c4a6fdd6 100644 --- a/apps/minifront/src/components/swap/auction-list/index.tsx +++ b/apps/minifront/src/components/swap/auction-list/index.tsx @@ -14,6 +14,7 @@ import { useStatus } from '../../../state/status'; import { byStartHeightAscending } from './helpers'; import { Filters } from './filters'; import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js'; +import { EndOrWithdrawAllButton } from './end-or-withdraw-all-button.tsx'; const auctionListSelector = (state: AllSlices) => ({ endAuction: state.swap.dutchAuction.endAuction, @@ -66,6 +67,7 @@ export const AuctionList = () => { {!!auctionInfos.data?.length && } + diff --git a/apps/minifront/src/state/swap/dutch-auction/index.ts b/apps/minifront/src/state/swap/dutch-auction/index.ts index 24303a7e86..89f8c7c032 100644 --- a/apps/minifront/src/state/swap/dutch-auction/index.ts +++ b/apps/minifront/src/state/swap/dutch-auction/index.ts @@ -1,4 +1,8 @@ -import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb.js'; +import { + TransactionPlannerRequest, + TransactionPlannerRequest_ActionDutchAuctionWithdraw, + TransactionPlannerRequest_ActionDutchAuctionEnd, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb.js'; import { AllSlices, SliceCreator, useStore } from '../..'; import { planBuildBroadcast } from '../../helpers'; import { assembleScheduleRequest } from './assemble-schedule-request'; @@ -40,6 +44,8 @@ interface Actions { currentSeqNum: bigint, addressIndex: AddressIndex, ) => Promise; + endAllAuctions: (auctions: AuctionInfo[], addressIndex: AddressIndex) => Promise; + withdrawAllAuctions: (auctions: AuctionInfo[], addressIndex: AddressIndex) => Promise; reset: VoidFunction; setFilter: (filter: Filter) => void; estimate: () => Promise; @@ -196,7 +202,30 @@ export const createDutchAuctionSlice = (): SliceCreator => (s await planBuildBroadcast('dutchAuctionWithdraw', req); get().swap.dutchAuction.auctionInfos.revalidate(); }, - + endAllAuctions: async (auctions: AuctionInfo[], addressIndex: AddressIndex) => { + const req = new TransactionPlannerRequest({ + dutchAuctionEndActions: auctions.map( + au => new TransactionPlannerRequest_ActionDutchAuctionEnd({ auctionId: au.id }), + ), + source: addressIndex, + }); + await planBuildBroadcast('dutchAuctionEnd', req); + get().swap.dutchAuction.auctionInfos.revalidate(); + }, + withdrawAllAuctions: async (auctions: AuctionInfo[], addressIndex: AddressIndex) => { + const req = new TransactionPlannerRequest({ + dutchAuctionWithdrawActions: auctions.map( + au => + new TransactionPlannerRequest_ActionDutchAuctionWithdraw({ + auctionId: au.id, + seq: au.localSeqNum + 1n, + }), + ), + source: addressIndex, + }); + await planBuildBroadcast('dutchAuctionWithdraw', req); + get().swap.dutchAuction.auctionInfos.revalidate(); + }, reset: () => set(({ swap }) => { swap.dutchAuction = {