-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* view service: LQT scaffolding (#2035) * view service: lqt voting notes scaffolding * view service: tournament votes rpc wip * wasm: extending planner with lqt voting action (#2037) * indexedDB: LQT historical votes table (#2038) * indexedDB: lqt historical votes table * workaround to normalize action cases * temporary measure for compilation purposes * linting * continue flushing out LQT integration * wasm: direct all available voting power to a single asset * wasm: informative expect messages * changeset * wasm: wasm bindgen tests * view server: expand vitest suite for new view service methods * wasm: clippy * protobuf: revert temporary workaround and consume fixed protos * action view: liquidity tournament visible and opaque views and translators * address feedback * linting * nit naming * refactor: account for LQT votes per delegation token * action views: fix opaque action view * more feedback * attempt to pass ci * satisfy linter * changeset * delete outdated changeset * stale ref; satisfy linter
- Loading branch information
Showing
36 changed files
with
1,399 additions
and
310 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
--- | ||
'@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 | ||
'@penumbra-zone/ui': minor | ||
--- | ||
|
||
LQT integration in web packages |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/perspective/src/translators/liquidity-tournament-vote-view.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
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 => { | ||
return new ActionLiquidityTournamentVoteView({ | ||
liquidityTournamentVote: { | ||
case: 'opaque', | ||
value: new ActionLiquidityTournamentVoteView_Opaque({ | ||
vote: liquidityTournamentVoteView?.liquidityTournamentVote.value?.vote, | ||
}), | ||
}, | ||
}); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
145 changes: 145 additions & 0 deletions
145
packages/services/src/view-service/lqt-voting-notes.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }); | ||
} | ||
}; |
Oops, something went wrong.