Skip to content

Commit

Permalink
Handle includeInactive in ViewService#auctions (#1047)
Browse files Browse the repository at this point in the history
* Store auction state in the database

* Rename state -> seqNum

* Add test re: upserting seqNum to be safe

* Store the sequence number in the block processor

* Support includeInactive

* Refactor

* Handle Dutch auction withdrawals in the block processor

* Bump IDB version

* Support `queryLatestState` in `ViewService#auctions` (#1035)

* Support queryLatestState

* Simplify

* Remove unused method

* Change method signature

* Rework a bit

* Remove unused import

* Account for the fullnode returning a DutchAuction, not a DutchAuctionState

* Fix type name check

* UI for ending auctions (#1060)

* Build UI to end an auction

* Fix layout issue

* Tweak symbols

* Reload auctions after scheduling or ending an auction

* Save auction metadata when ending an auction

* Rename helper
  • Loading branch information
jessepinho authored May 8, 2024
1 parent cb9894d commit fea6900
Show file tree
Hide file tree
Showing 27 changed files with 460 additions and 133 deletions.
2 changes: 1 addition & 1 deletion apps/extension/.env.testnet
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CHAIN_ID=penumbra-testnet-deimos-7
IDB_VERSION=37
IDB_VERSION=39
MINIFRONT_URL=https://app.testnet.penumbra.zone
PRAX=lkpmkhpnhknhmibgnmmhdhgdilepfghe
25 changes: 17 additions & 8 deletions apps/minifront/src/components/swap/dutch-auction/auctions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ const getMetadata = (metadataByAssetId: Record<string, Metadata>, assetId?: Asse
};

const auctionsSelector = (state: AllSlices) => ({
auctions: state.dutchAuction.auctions,
auctionInfos: state.dutchAuction.auctionInfos,
metadataByAssetId: state.dutchAuction.metadataByAssetId,
endAuction: state.dutchAuction.endAuction,
});

export const Auctions = () => {
const { auctions, metadataByAssetId } = useStoreShallow(auctionsSelector);
const { auctionInfos, metadataByAssetId, endAuction } = useStoreShallow(auctionsSelector);

return (
<>
Expand All @@ -32,17 +33,25 @@ export const Auctions = () => {
</p>

<div className='flex flex-col gap-2'>
{!auctions.length && "You don't currently have any auctions running."}
{!auctionInfos.length && "You don't currently have any auctions running."}

{auctions.map(auction => (
{auctionInfos.map(auctionInfo => (
<ViewBox
key={auction.description?.nonce.toString()}
key={auctionInfo.auction.description?.nonce.toString()}
label='Dutch Auction'
visibleContent={
<DutchAuctionComponent
dutchAuction={auction}
inputMetadata={getMetadata(metadataByAssetId, auction.description?.input?.assetId)}
outputMetadata={getMetadata(metadataByAssetId, auction.description?.outputId)}
dutchAuction={auctionInfo.auction}
inputMetadata={getMetadata(
metadataByAssetId,
auctionInfo.auction.description?.input?.assetId,
)}
outputMetadata={getMetadata(
metadataByAssetId,
auctionInfo.auction.description?.outputId,
)}
showEndButton
onClickEndButton={() => void endAuction(auctionInfo.id)}
/>
}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export const DutchAuctionLoader = async () => {
await throwIfPraxNotConnectedTimeout();

// Load into state, but don't block rendering.
void useStore.getState().dutchAuction.loadAuctions();
void useStore.getState().dutchAuction.loadAuctionInfos();

const [assets, balancesResponses] = await Promise.all([
getAllAssets(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it, vi } from 'vitest';
import { assembleRequest } from './assemble-request';
import { assembleScheduleRequest } from './assemble-schedule-request';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { BLOCKS_PER_MINUTE, DURATION_IN_BLOCKS } from './constants';
Expand Down Expand Up @@ -42,7 +42,7 @@ const balancesResponse = new BalancesResponse({
},
});

const ARGS: Parameters<typeof assembleRequest>[0] = {
const ARGS: Parameters<typeof assembleScheduleRequest>[0] = {
amount: '123',
duration: '10min',
minOutput: '1',
Expand All @@ -51,9 +51,9 @@ const ARGS: Parameters<typeof assembleRequest>[0] = {
assetOut: metadata,
};

describe('assembleRequest()', () => {
describe('assembleScheduleRequest()', () => {
it('correctly converts durations to block heights', async () => {
const req = await assembleRequest({ ...ARGS, duration: '10min' });
const req = await assembleScheduleRequest({ ...ARGS, duration: '10min' });

expect(req.dutchAuctionScheduleActions[0]!.description!.startHeight).toBe(
MOCK_START_HEIGHT + BLOCKS_PER_MINUTE,
Expand All @@ -62,7 +62,7 @@ describe('assembleRequest()', () => {
MOCK_START_HEIGHT + BLOCKS_PER_MINUTE + DURATION_IN_BLOCKS['10min'],
);

const req2 = await assembleRequest({ ...ARGS, duration: '48h' });
const req2 = await assembleScheduleRequest({ ...ARGS, duration: '48h' });

expect(req2.dutchAuctionScheduleActions[0]!.description!.startHeight).toBe(
MOCK_START_HEIGHT + BLOCKS_PER_MINUTE,
Expand All @@ -73,21 +73,21 @@ describe('assembleRequest()', () => {
});

it('uses a step count of 120', async () => {
const req = await assembleRequest(ARGS);
const req = await assembleScheduleRequest(ARGS);

expect(req.dutchAuctionScheduleActions[0]!.description!.stepCount).toBe(120n);
});

it('correctly parses the input based on the display denom exponent', async () => {
const req = await assembleRequest(ARGS);
const req = await assembleScheduleRequest(ARGS);

expect(req.dutchAuctionScheduleActions[0]!.description!.input?.amount).toEqual(
new Amount({ hi: 0n, lo: 123_000_000n }),
);
});

it('correctly parses the min/max outputs based on the display denom exponent', async () => {
const req = await assembleRequest(ARGS);
const req = await assembleScheduleRequest(ARGS);

expect(req.dutchAuctionScheduleActions[0]!.description!.minOutput).toEqual(
new Amount({ hi: 0n, lo: 1_000_000n }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { fromString } from '@penumbra-zone/types/amount';
*/
const getStartHeight = (fullSyncHeight: bigint) => fullSyncHeight + BLOCKS_PER_MINUTE;

export const assembleRequest = async ({
export const assembleScheduleRequest = async ({
amount: amountAsString,
assetIn,
assetOut,
Expand Down
53 changes: 41 additions & 12 deletions apps/minifront/src/state/dutch-auction/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import {
BalancesResponse,
TransactionPlannerRequest,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb';
import { SliceCreator } from '..';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { planBuildBroadcast } from '../helpers';
import { assembleRequest } from './assemble-request';
import { assembleScheduleRequest } from './assemble-schedule-request';
import { DurationOption } from './constants';
import { DutchAuction } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1alpha1/auction_pb';
import {
AuctionId,
DutchAuction,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/auction/v1alpha1/auction_pb';
import { viewClient } from '../../clients';
import { bech32mAssetId } from '@penumbra-zone/bech32m/passet';

interface AuctionInfo {
id: AuctionId;
auction: DutchAuction;
}

export interface DutchAuctionSlice {
balancesResponses: BalancesResponse[];
setBalancesResponses: (balancesResponses: BalancesResponse[]) => void;
Expand All @@ -28,10 +39,11 @@ export interface DutchAuctionSlice {
setMaxOutput: (maxOutput: string) => void;
onSubmit: () => Promise<void>;
txInProgress: boolean;
auctions: DutchAuction[];
loadAuctions: () => Promise<void>;
auctionInfos: AuctionInfo[];
loadAuctionInfos: () => Promise<void>;
loadMetadata: (assetId?: AssetId) => Promise<void>;
metadataByAssetId: Record<string, Metadata>;
endAuction: (auctionId: AuctionId) => Promise<void>;
}

export const createDutchAuctionSlice = (): SliceCreator<DutchAuctionSlice> => (set, get) => ({
Expand Down Expand Up @@ -89,10 +101,11 @@ export const createDutchAuctionSlice = (): SliceCreator<DutchAuctionSlice> => (s
});

try {
const req = await assembleRequest(get().dutchAuction);
const req = await assembleScheduleRequest(get().dutchAuction);
await planBuildBroadcast('dutchAuctionSchedule', req);

get().dutchAuction.setAmount('');
void get().dutchAuction.loadAuctionInfos();
} finally {
set(state => {
state.dutchAuction.txInProgress = false;
Expand All @@ -102,22 +115,22 @@ export const createDutchAuctionSlice = (): SliceCreator<DutchAuctionSlice> => (s

txInProgress: false,

auctions: [],
loadAuctions: async () => {
auctionInfos: [],
loadAuctionInfos: async () => {
set(state => {
state.dutchAuction.auctions = [];
state.dutchAuction.auctionInfos = [];
});

for await (const response of viewClient.auctions({})) {
if (!response.auction) continue;
if (!response.auction || !response.id) continue;
const auction = DutchAuction.fromBinary(response.auction.value);
const auctions = [...get().dutchAuction.auctions, auction];
const auctions = [...get().dutchAuction.auctionInfos, { id: response.id, auction }];

void get().dutchAuction.loadMetadata(auction.description?.input?.assetId);
void get().dutchAuction.loadMetadata(auction.description?.outputId);

set(state => {
state.dutchAuction.auctions = auctions;
state.dutchAuction.auctionInfos = auctions;
});
}
},
Expand All @@ -135,4 +148,20 @@ export const createDutchAuctionSlice = (): SliceCreator<DutchAuctionSlice> => (s
},

metadataByAssetId: {},

endAuction: async auctionId => {
set(state => {
state.dutchAuction.txInProgress = true;
});

try {
const req = new TransactionPlannerRequest({ dutchAuctionEndActions: [{ auctionId }] });
await planBuildBroadcast('dutchAuctionEnd', req);
void get().dutchAuction.loadAuctionInfos();
} finally {
set(state => {
state.dutchAuction.txInProgress = false;
});
}
},
});
6 changes: 5 additions & 1 deletion packages/perspective/transaction/classification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ export type TransactionClassification =
/** The transaction contains an `ics20Withdrawal` action. */
| 'ics20Withdrawal'
/** The transaction contains an `actionDutchAuctionSchedule` action. */
| 'dutchAuctionSchedule';
| 'dutchAuctionSchedule'
/** The transaction contains an `actionDutchAuctionEnd` action. */
| 'dutchAuctionEnd'
/** The transaction contains an `actionDutchAuctionWithdraw` action. */
| 'dutchAuctionWithdraw';
41 changes: 21 additions & 20 deletions packages/perspective/transaction/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,32 +320,33 @@ describe('classifyTransaction()', () => {
it('returns `dutchAuctionSchedule` for transactions with an `actionDutchAuctionSchedule` action', () => {
const transactionView = new TransactionView({
bodyView: {
actionViews: [
{
actionView: {
case: 'actionDutchAuctionSchedule',
value: {},
},
},
{
actionView: {
case: 'spend',
value: {},
},
},
{
actionView: {
case: 'output',
value: {},
},
},
],
actionViews: [{ actionView: { case: 'actionDutchAuctionSchedule', value: {} } }],
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionSchedule');
});

it('returns `dutchAuctionEnd` for transactions with an `actionDutchAuctionEnd` action', () => {
const transactionView = new TransactionView({
bodyView: {
actionViews: [{ actionView: { case: 'actionDutchAuctionEnd', value: {} } }],
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionEnd');
});

it('returns `dutchAuctionWithdraw` for transactions with an `actionDutchAuctionWithdraw` action', () => {
const transactionView = new TransactionView({
bodyView: {
actionViews: [{ actionView: { case: 'actionDutchAuctionWithdraw', value: {} } }],
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionWithdraw');
});

it("returns `unknown` for transactions that don't fit the above categories", () => {
const transactionView = new TransactionView({
bodyView: {
Expand Down
4 changes: 4 additions & 0 deletions packages/perspective/transaction/classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export const classifyTransaction = (txv?: TransactionView): TransactionClassific
if (allActionCases.has('undelegateClaim')) return 'undelegateClaim';
if (allActionCases.has('ics20Withdrawal')) return 'ics20Withdrawal';
if (allActionCases.has('actionDutchAuctionSchedule')) return 'dutchAuctionSchedule';
if (allActionCases.has('actionDutchAuctionEnd')) return 'dutchAuctionEnd';
if (allActionCases.has('actionDutchAuctionWithdraw')) return 'dutchAuctionWithdraw';

const hasOpaqueSpend = txv.bodyView?.actionViews.some(
a => a.actionView.case === 'spend' && a.actionView.value.spendView.case === 'opaque',
Expand Down Expand Up @@ -90,6 +92,8 @@ export const TRANSACTION_LABEL_BY_CLASSIFICATION: Record<TransactionClassificati
undelegateClaim: 'Undelegate Claim',
ics20Withdrawal: 'Ics20 Withdrawal',
dutchAuctionSchedule: 'Dutch Auction Schedule',
dutchAuctionEnd: 'Dutch Auction End',
dutchAuctionWithdraw: 'Dutch Auction Withdraw',
};

export const getTransactionClassificationLabel = (txv?: TransactionView): string =>
Expand Down
43 changes: 28 additions & 15 deletions packages/query/src/block-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,29 +419,42 @@ export class BlockProcessor implements BlockProcessorInterface {
private async identifyAuctionNfts(action: Action['action']) {
if (action.case === 'actionDutchAuctionSchedule' && action.value.description) {
const auctionId = getAuctionId(action.value.description);
const metadata = getAuctionNftMetadata(
auctionId,
// Always a sequence number of 0 when starting a Dutch auction
0n,
);

// Always a sequence number of 0 when starting a Dutch auction
const seqNum = 0n;

const metadata = getAuctionNftMetadata(auctionId, seqNum);

await Promise.all([
this.indexedDb.saveAssetsMetadata(metadata),
this.indexedDb.upsertAuction(auctionId, {
auction: action.value.description,
seqNum,
}),
]);
} else if (action.case === 'actionDutchAuctionEnd' && action.value.auctionId) {
const metadata = getAuctionNftMetadata(
action.value.auctionId,
// Always a sequence number of 1 when ending a Dutch auction
1n,
);
await this.indexedDb.saveAssetsMetadata(metadata);
// Always a sequence number of 1 when ending a Dutch auction
const seqNum = 1n;

const metadata = getAuctionNftMetadata(action.value.auctionId, seqNum);

await Promise.all([
this.indexedDb.saveAssetsMetadata(metadata),
this.indexedDb.upsertAuction(action.value.auctionId, { seqNum }),
]);
} else if (action.case === 'actionDutchAuctionWithdraw') {
const auctionId = action.value.auctionId;
if (!auctionId) return;

const metadata = getAuctionNftMetadata(auctionId, action.value.seq);

await Promise.all([
this.indexedDb.saveAssetsMetadata(metadata),
this.indexedDb.upsertAuction(auctionId, {
seqNum: action.value.seq,
}),
]);
}
/**
* @todo Handle `actionDutchAuctionWithdraw`, and figure out how to
* determine the sequence number if there have been multiple withdrawals.
*/
}

/**
Expand Down
Loading

0 comments on commit fea6900

Please sign in to comment.