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 14 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
3 changes: 3 additions & 0 deletions packages/perspective/src/plan/view-action-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,5 +456,8 @@ export const viewActionPlan =
return new ActionView({
actionView: actionPlan.action,
});

default:
throw new Error(`Unsupported action case: ${actionPlan.action.case}`);
}
};
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:08458e036959ca4a894d9a78c4469782b884e057",
"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;
saveLQTHistoricalVotes?: Mock;
getLQTHistoricalVotes?: 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 = {
getLQTHistoricalVotes: 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) 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);

const spendableNoteRecords: SpendableNoteRecord[] = [];

// 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) {
const lqtCheckNullifierResponse = await querier.funding.lqtCheckNullifier(
epoch.index,
votingNote.noteRecord.nullifier as Nullifier,
);
if (!lqtCheckNullifierResponse.alreadyVoted) {
spendableNoteRecords.push(votingNote.noteRecord as SpendableNoteRecord);
}
}
}

// Yield the SNRs that haven't been used for voting yet.
for (const record of spendableNoteRecords) {
yield new LqtVotingNotesResponse({ noteRecord: record });
}
};
82 changes: 82 additions & 0 deletions packages/services/src/view-service/tournament-votes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { TournamentVotesRequest } 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, MockServices } from '../test-utils.js';
import type { ServicesInterface } from '@penumbra-zone/types/services';
import { Epoch } from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb';
import { tournamentVotes } from './tournament-votes.js';
import { Amount } from '@penumbra-zone/protobuf/penumbra/core/num/v1/num_pb';
import { Value } from '@bufbuild/protobuf';

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

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

mockIndexedDb = {
getLQTHistoricalVotes: vi.fn(),
saveLQTHistoricalVotes: 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.tournamentVotes,
protocolName: 'mock',
requestMethod: 'MOCK',
url: '/mock',
contextValues: createContextValues().set(servicesCtx, () =>
Promise.resolve(mockServices as unknown as ServicesInterface),
),
});
});

test('returns historical liquidity tournament votes that have been previously been saved to storage', async () => {
mockIndexedDb.getBlockHeightByEpoch?.mockResolvedValueOnce(epoch);
mockIndexedDb.saveLQTHistoricalVotes?.mockResolvedValueOnce(mockVote);
mockIndexedDb.getLQTHistoricalVotes?.mockResolvedValueOnce(mockVote);

const req = new TournamentVotesRequest({});
const vote = await tournamentVotes(req, mockCtx);

expect(vote.votes?.length).toBe(1);
});
});

const epoch = new Epoch({
index: 100n,
startHeight: 5000n,
});

const mockVote = {
TransactionId: {
inner: new Uint8Array([1, 2, 3, 4]),
},
AssetMetadata: {
penumbraAssetId: {
inner: new Uint8Array(new Array(32).fill(1)),
},
},
VoteValue: Value.fromJson({
amount: {
lo: '1000',
hi: '0',
},
}),
RewardValue: Amount.fromJson({
lo: '500',
hi: '0',
}),
};
Loading