Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

web: LQT integration #2041

Merged
merged 27 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
39d87d6
view service: LQT scaffolding (#2035)
TalDerei Feb 12, 2025
2d60fe4
wasm: extending planner with lqt voting action (#2037)
TalDerei Feb 12, 2025
cc0b9c1
indexedDB: LQT historical votes table (#2038)
TalDerei Feb 12, 2025
8159bb1
temporary measure for compilation purposes
TalDerei Feb 12, 2025
80ef6e3
linting
TalDerei Feb 12, 2025
44faa76
continue flushing out LQT integration
TalDerei Feb 13, 2025
698b706
wasm: direct all available voting power to a single asset
TalDerei Feb 13, 2025
cbbb9c6
wasm: informative expect messages
TalDerei Feb 13, 2025
e2a6e78
changeset
TalDerei Feb 13, 2025
d04fba2
wasm: wasm bindgen tests
TalDerei Feb 13, 2025
22a6a8c
view server: expand vitest suite for new view service methods
TalDerei Feb 13, 2025
12ead28
wasm: clippy
TalDerei Feb 13, 2025
f11857e
protobuf: revert temporary workaround and consume fixed protos
TalDerei Feb 13, 2025
cb13f59
Merge branch 'main' into protocol/lqt_branch
TalDerei Feb 13, 2025
febf46d
action view: liquidity tournament visible and opaque views and transl…
TalDerei Feb 17, 2025
98ebc0e
address feedback
TalDerei Feb 17, 2025
1afddaf
linting
TalDerei Feb 17, 2025
aec79e2
nit naming
TalDerei Feb 17, 2025
e898dcf
refactor: account for LQT votes per delegation token
TalDerei Feb 19, 2025
2097f09
action views: fix opaque action view
TalDerei Feb 19, 2025
30db7d3
more feedback
TalDerei Feb 19, 2025
cf88451
attempt to pass ci
TalDerei Feb 19, 2025
f133c63
Merge branch 'main' into protocol/lqt_branch
TalDerei Feb 19, 2025
89299da
satisfy linter
TalDerei Feb 19, 2025
561ab8c
changeset
TalDerei Feb 19, 2025
aab25ae
delete outdated changeset
TalDerei Feb 19, 2025
3ac2a91
stale ref; satisfy linter
TalDerei Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/late-foxes-care.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@penumbra-zone/protobuf': major
'@penumbra-zone/services': major
'@penumbra-zone/storage': major
'@penumbra-zone/types': major
'@penumbra-zone/ui-deprecated': minor
'@penumbra-zone/perspective': minor
'@penumbra-zone/wasm': minor
---

LQT integration in web packages
9 changes: 9 additions & 0 deletions packages/perspective/src/plan/view-action-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,15 @@ export const viewActionPlan =
});
}

case 'actionLiquidityTournamentVote': {
return new ActionView({
actionView: {
case: 'actionLiquidityTournamentVote',
value: {},
},
});
}

case undefined:
return new ActionView({
actionView: actionPlan.action,
Expand Down
3 changes: 2 additions & 1 deletion packages/perspective/src/transaction/classification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,5 @@ export type TransactionClassification =
| 'positionRewardClaim'
| 'communityPoolSpend'
| 'communityPoolOutput'
| 'communityPoolDeposit';
| 'communityPoolDeposit'
| 'liquidityTournamentVote';
4 changes: 4 additions & 0 deletions packages/perspective/src/transaction/classify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ export const classifyTransaction = (txv?: TransactionView): TransactionClassific
if (allActionCases.has('communityPoolOutput')) {
return 'communityPoolOutput';
}
if (allActionCases.has('actionLiquidityTournamentVote')) {
return 'liquidityTournamentVote';
}

const hasOpaqueSpend = txv.bodyView?.actionViews.some(
a => a.actionView.case === 'spend' && a.actionView.value.spendView.case === 'opaque',
Expand Down Expand Up @@ -167,6 +170,7 @@ export const TRANSACTION_LABEL_BY_CLASSIFICATION: Record<TransactionClassificati
proposalSubmit: 'Proposal Submit',
proposalWithdraw: 'Proposal Withdraw',
validatorDefinition: 'Validator Definition',
liquidityTournamentVote: 'Liquidity Tournament Vote',
};

export const getTransactionClassificationLabel = (txv?: TransactionView): string =>
Expand Down
9 changes: 9 additions & 0 deletions packages/perspective/src/translators/action-view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { asOpaqueSwapView } from './swap-view.js';
import { asOpaqueSwapClaimView } from './swap-claim-view.js';
import { asOpaqueDelegatorVoteView } from './delegator-vote-view.js';
import { asOpaqueLiquidityTournamentVoteView } from './liquidity-tournament-vote-view.js';

export const asPublicActionView: Translator<ActionView> = actionView => {
switch (actionView?.actionView.case) {
Expand Down Expand Up @@ -49,6 +50,14 @@ export const asPublicActionView: Translator<ActionView> = actionView => {
},
});

case 'actionLiquidityTournamentVote':
return new ActionView({
actionView: {
case: 'actionLiquidityTournamentVote',
value: asOpaqueLiquidityTournamentVoteView(actionView.actionView.value),
},
});

// Currently defaulting to displaying that all data is public as it's better
// to err on communicating private data as public than the other way around
// TODO: Do proper audit of what data for each action is public
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {
ActionLiquidityTournamentVoteView,
ActionLiquidityTournamentVoteView_Opaque,
} from '@penumbra-zone/protobuf/penumbra/core/component/funding/v1/funding_pb';
import { Translator } from './types.js';

export const asOpaqueLiquidityTournamentVoteView: Translator<
ActionLiquidityTournamentVoteView
> = liquidityTournamentVoteView => {
if (liquidityTournamentVoteView?.liquidityTournamentVote.case === 'opaque') {
return liquidityTournamentVoteView;
}

return new ActionLiquidityTournamentVoteView({
liquidityTournamentVote: {
case: 'opaque',
value: new ActionLiquidityTournamentVoteView_Opaque({
vote: liquidityTournamentVoteView?.liquidityTournamentVote.value?.vote,
}),
},
});
};
2 changes: 1 addition & 1 deletion packages/protobuf/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"gen:ibc": "buf generate buf.build/cosmos/ibc:7ab44ae956a0488ea04e04511efa5f70",
"gen:ics23": "buf generate buf.build/cosmos/ics23:55085f7c710a45f58fa09947208eb70b",
"gen:noble": "buf generate buf.build/noble-assets/forwarding:5a8609a6772d417584a9c60cd8b80881",
"gen:penumbra": "buf generate buf.build/penumbra-zone/penumbra:adb116eefae84c1abd53a1594b895360",
"gen:penumbra": "buf generate buf.build/penumbra-zone/penumbra:ae2300bce202a7d429727f1340e7412d3b9f810c",
"lint": "eslint src",
"lint:fix": "eslint src --fix",
"lint:strict": "tsc --noEmit && eslint src --max-warnings 0",
Expand Down
1 change: 1 addition & 0 deletions packages/protobuf/src/services/penumbra-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export { QueryService as GovernanceService } from '../../gen/penumbra/core/compo
export { QueryService as SctService } from '../../gen/penumbra/core/component/sct/v1/sct_connect.js';
export { QueryService as ShieldedPoolService } from '../../gen/penumbra/core/component/shielded_pool/v1/shielded_pool_connect.js';
export { QueryService as StakeService } from '../../gen/penumbra/core/component/stake/v1/stake_connect.js';
export { FundingService } from '../../gen/penumbra/core/component/funding/v1/funding_connect.js';
4 changes: 3 additions & 1 deletion packages/protobuf/src/web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import type { CustodyService } from './services/penumbra-custody.js';
import type { ViewService } from './services/penumbra-view.js';
import type {
FundingService,
AppService,
AuctionService,
CommunityPoolService,
Expand Down Expand Up @@ -43,4 +44,5 @@ export type PenumbraService =
| typeof SimulationService
| typeof StakeService
| typeof TendermintProxyService
| typeof ViewService;
| typeof ViewService
| typeof FundingService;
8 changes: 8 additions & 0 deletions packages/services/src/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export interface IndexedDbMock {
saveGasPrices?: Mock;
saveTransactionInfo?: Mock;
getTransactionInfo?: Mock;
getBlockHeightByEpoch?: Mock;
saveLQTHistoricalVote?: Mock;
getLQTHistoricalVote?: Mock;
}

export interface AuctionMock {
Expand All @@ -62,6 +65,11 @@ export interface MockQuerier {
sct?: SctMock;
shieldedPool?: ShieldedPoolMock;
stake?: StakeMock;
funding?: FundingMock;
}

export interface FundingMock {
lqtCheckNullifier?: Mock;
}

export interface SctMock {
Expand Down
4 changes: 4 additions & 0 deletions packages/services/src/view-service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { witness } from './witness.js';
import { witnessAndBuild } from './witness-and-build.js';
import { transparentAddress } from './transparent-address.js';
import { latestSwaps } from './latest-swaps.js';
import { lqtVotingNotes } from './lqt-voting-notes.js';
import { tournamentVotes } from './tournament-votes.js';

export type Impl = ServiceImpl<typeof ViewService>;

Expand Down Expand Up @@ -66,4 +68,6 @@ export const viewImpl: Impl = {
witnessAndBuild,
transparentAddress,
latestSwaps,
lqtVotingNotes,
tournamentVotes,
};
145 changes: 145 additions & 0 deletions packages/services/src/view-service/lqt-voting-notes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
LqtVotingNotesRequest,
LqtVotingNotesResponse,
NotesForVotingResponse,
} from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { createContextValues, createHandlerContext, HandlerContext } from '@connectrpc/connect';
import { ViewService } from '@penumbra-zone/protobuf';
import { servicesCtx } from '../ctx/prax.js';
import { IndexedDbMock, MockQuerier, MockServices } from '../test-utils.js';
import type { ServicesInterface } from '@penumbra-zone/types/services';
import { lqtVotingNotes } from './lqt-voting-notes.js';
import { Epoch } from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb';
import { LqtCheckNullifierResponse } from '@penumbra-zone/protobuf/penumbra/core/component/funding/v1/funding_pb';
import { TransactionId } from '@penumbra-zone/protobuf/penumbra/core/txhash/v1/txhash_pb';

describe('lqtVotingNotes request handler', () => {
let mockServices: MockServices;
let mockIndexedDb: IndexedDbMock;
let mockQuerier: MockQuerier;
let mockCtx: HandlerContext;

beforeEach(() => {
vi.resetAllMocks();

mockIndexedDb = {
getLQTHistoricalVote: vi.fn(),
getBlockHeightByEpoch: vi.fn(),
getNotesForVoting: vi.fn(),
};

mockServices = {
getWalletServices: vi.fn(() =>
Promise.resolve({ indexedDb: mockIndexedDb }),
) as MockServices['getWalletServices'],
};

mockCtx = createHandlerContext({
service: ViewService,
method: ViewService.methods.lqtVotingNotes,
protocolName: 'mock',
requestMethod: 'MOCK',
url: '/mock',
contextValues: createContextValues().set(servicesCtx, () =>
Promise.resolve(mockServices as unknown as ServicesInterface),
),
});
});

test('returns no voting notes if the nullifier has already been used for voting in the current epoch', async () => {
// voting notes mocked with static data, and the mock bypasses the logic in the real implementation,
// but that's fine.
mockIndexedDb.getNotesForVoting?.mockResolvedValueOnce(testData);
mockIndexedDb.getBlockHeightByEpoch?.mockResolvedValueOnce(epoch);

mockQuerier = {
funding: {
lqtCheckNullifier: vi.fn().mockResolvedValue(
new LqtCheckNullifierResponse({
transaction: new TransactionId({
inner: new Uint8Array([]),
}),
alreadyVoted: true,
epochIndex: 100n,
}),
),
},
};

mockServices = {
getWalletServices: vi.fn(() =>
Promise.resolve({ indexedDb: mockIndexedDb, querier: mockQuerier }),
) as MockServices['getWalletServices'],
};

const responses: LqtVotingNotesResponse[] = [];
const req = new LqtVotingNotesRequest({});
for await (const res of lqtVotingNotes(req, mockCtx)) {
responses.push(new LqtVotingNotesResponse(res));
}

expect(responses.length).toBe(0);
});

test('returns voting notes when the nullifier has not been used for voting in the current epoch', async () => {
mockIndexedDb.getNotesForVoting?.mockResolvedValueOnce(testData);
mockIndexedDb.getBlockHeightByEpoch?.mockResolvedValueOnce(epoch);

mockQuerier = {
funding: {
lqtCheckNullifier: vi.fn().mockResolvedValue(
new LqtCheckNullifierResponse({
transaction: new TransactionId({
inner: new Uint8Array([]),
}),
alreadyVoted: false,
epochIndex: 100n,
}),
),
},
};

mockServices = {
getWalletServices: vi.fn(() =>
Promise.resolve({ indexedDb: mockIndexedDb, querier: mockQuerier }),
) as MockServices['getWalletServices'],
};

const responses: LqtVotingNotesResponse[] = [];
const req = new LqtVotingNotesRequest({});
for await (const res of lqtVotingNotes(req, mockCtx)) {
responses.push(new LqtVotingNotesResponse(res));
}

expect(responses.length).toBe(2);
});
});

const testData: NotesForVotingResponse[] = [
NotesForVotingResponse.fromJson({
noteRecord: {
noteCommitment: {
inner: 'pXS1k2kvlph+vuk9uhqeoP1mZRc+f526a06/bg3EBwQ=',
},
},
identityKey: {
ik: 'VAv+z5ieJk7AcAIJoVIqB6boOj0AhZB2FKWsEidfvAE=',
},
}),
NotesForVotingResponse.fromJson({
noteRecord: {
noteCommitment: {
inner: '2XS1k2kvlph+vuk9uhqeoP1mZRc+f526a06/bg3EBwQ=',
},
},
identityKey: {
ik: 'pkxdxOn9EMqdjoCJdEGBKA8XY9P9RK9XmurIly/9yBA=',
},
}),
];

const epoch = new Epoch({
index: 100n,
startHeight: 5000n,
});
46 changes: 46 additions & 0 deletions packages/services/src/view-service/lqt-voting-notes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { Impl } from './index.js';
import { servicesCtx } from '../ctx/prax.js';
import { notesForVoting } from './notes-for-voting.js';
import {
LqtVotingNotesResponse,
NotesForVotingRequest,
SpendableNoteRecord,
} from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { Nullifier } from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb';

export const lqtVotingNotes: Impl['lqtVotingNotes'] = async function* (req, ctx) {
const services = await ctx.values.get(servicesCtx)();
const { indexedDb, querier } = await services.getWalletServices();

// Get the starting block height for the corresponding epoch index.
const epoch = await indexedDb.getBlockHeightByEpoch(req.epochIndex);

// Retrieve SNRs from storage ('ASSETS' in IndexedDB) for the specified subaccount that are eligible for voting
// at the start height of the current epoch. Alternatively, a wasm helper `get_voting_notes` can be used to
// perform the same function.
const notesForVotingRequest = new NotesForVotingRequest({
addressIndex: req.accountFilter,
votableAtHeight: epoch?.startHeight,
});
const votingNotes = notesForVoting(notesForVotingRequest, ctx);

// Iterate through each voting note and check if it has already been used for voting
// by performing a nullifier point query against the rpc provided by the funding service.
for await (const votingNote of votingNotes) {
if (!votingNote.noteRecord || !epoch?.index) {
continue;
}
const lqtCheckNullifierResponse = await querier.funding.lqtCheckNullifier(
epoch.index,
votingNote.noteRecord.nullifier as Nullifier,
);
if (lqtCheckNullifierResponse.alreadyVoted) {
continue;
}

const noteRecord = votingNote.noteRecord as SpendableNoteRecord;

// Yield the SNRs that haven't been used for voting yet.
yield new LqtVotingNotesResponse({ noteRecord });
}
};
Loading