From 29dd11a26a8aca2a87ba27f219d0583c742bbdbf Mon Sep 17 00:00:00 2001 From: Max Korsunov Date: Wed, 29 Jan 2025 11:15:18 +0400 Subject: [PATCH] feat: #1989: subaccount filter in `ownedPositionIds` (#2006) * feat(wasm): #1989: change `isControlledAddress` method to `getIndexByAddress` * feat(types): #1989: change `isControlledAddress` method to `getIndexByAddress` * feat(protobuf): #1989: sync latest changes in penumbra protobufs * feat(storage): #1989: add subaccount filter to `getOwnedPositionIds` method * feat(services): #1989: add subaccount filter to `ownedPositionIds` method in viewService * chore: changeset * fix(wasm): lint * revert(wasm, types): use the previous version of wasm methods * fix(storage): update idb tests --- .changeset/lucky-snails-clean.md | 11 +++ packages/protobuf/package.json | 2 +- .../src/view-service/owned-position-ids.ts | 3 +- packages/storage/src/indexed-db/index.ts | 20 ++++- .../src/indexed-db/indexed-db.test-data.ts | 4 + .../storage/src/indexed-db/indexed-db.test.ts | 75 +++++++++++++++---- packages/types/src/indexed-db.ts | 10 ++- 7 files changed, 105 insertions(+), 20 deletions(-) create mode 100644 .changeset/lucky-snails-clean.md diff --git a/.changeset/lucky-snails-clean.md b/.changeset/lucky-snails-clean.md new file mode 100644 index 0000000000..cd6a5e356a --- /dev/null +++ b/.changeset/lucky-snails-clean.md @@ -0,0 +1,11 @@ +--- +'@penumbra-zone/storage': major +'@penumbra-zone/protobuf': minor +'@penumbra-zone/services': minor +'@penumbra-zone/types': minor +--- + +- storage: add subaccount filter to `getOwnedPositionIds` method +- protobuf: sync latest changes in penumbra protobufs +- services: add subaccount filter to `ownedPositionIds` method in ViewService +- types: update indexedDB schema diff --git a/packages/protobuf/package.json b/packages/protobuf/package.json index 87b0dd9df6..d16ba0ea62 100644 --- a/packages/protobuf/package.json +++ b/packages/protobuf/package.json @@ -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:0a56a4f32c244e7eb277e02f6e85afbd", + "gen:penumbra": "buf generate buf.build/penumbra-zone/penumbra:649f1d61327144cb9a7e15c7ad210dcb", "lint": "eslint src", "lint:fix": "eslint src --fix", "lint:strict": "tsc --noEmit && eslint src --max-warnings 0", diff --git a/packages/services/src/view-service/owned-position-ids.ts b/packages/services/src/view-service/owned-position-ids.ts index 5a5797bd4f..e84f4ec34e 100644 --- a/packages/services/src/view-service/owned-position-ids.ts +++ b/packages/services/src/view-service/owned-position-ids.ts @@ -9,7 +9,8 @@ export const ownedPositionIds: Impl['ownedPositionIds'] = async function* (req, for await (const positionId of indexedDb.getOwnedPositionIds( req.positionState, req.tradingPair, + req.subaccount, )) { - yield { positionId: positionId }; + yield { positionId: positionId, subaccount: req.subaccount }; } }; diff --git a/packages/storage/src/indexed-db/index.ts b/packages/storage/src/indexed-db/index.ts index 77d83969da..d4af1427d9 100644 --- a/packages/storage/src/indexed-db/index.ts +++ b/packages/storage/src/indexed-db/index.ts @@ -570,6 +570,7 @@ export class IndexedDb implements IndexedDbInterface { async *getOwnedPositionIds( positionState: PositionState | undefined, tradingPair: TradingPair | undefined, + subaccount: AddressIndex | undefined, ) { yield* new ReadableStream({ start: async cont => { @@ -578,7 +579,10 @@ export class IndexedDb implements IndexedDbInterface { const position = Position.fromJson(cursor.value.position); if ( (!positionState || positionState.equals(position.state)) && - (!tradingPair || tradingPair.equals(position.phi?.pair)) + (!tradingPair || tradingPair.equals(position.phi?.pair)) && + (!subaccount || + (cursor.value.subaccount && + subaccount.equals(AddressIndex.fromJson(cursor.value.subaccount)))) ) { cont.enqueue(PositionId.fromJson(cursor.value.id)); } @@ -589,16 +593,25 @@ export class IndexedDb implements IndexedDbInterface { }); } - async addPosition(positionId: PositionId, position: Position): Promise { + async addPosition( + positionId: PositionId, + position: Position, + subaccount?: AddressIndex, + ): Promise { assertPositionId(positionId); const positionRecord = { id: positionId.toJson() as Jsonified, position: position.toJson() as Jsonified, + subaccount: subaccount && (subaccount.toJson() as Jsonified), }; await this.u.update({ table: 'POSITIONS', value: positionRecord }); } - async updatePosition(positionId: PositionId, newState: PositionState): Promise { + async updatePosition( + positionId: PositionId, + newState: PositionState, + subaccount?: AddressIndex, + ): Promise { assertPositionId(positionId); const key = uint8ArrayToBase64(positionId.inner); const positionRecord = await this.db.get('POSITIONS', key); @@ -615,6 +628,7 @@ export class IndexedDb implements IndexedDbInterface { value: { id: positionId.toJson() as Jsonified, position: position.toJson() as Jsonified, + subaccount: subaccount ? (subaccount.toJson() as Jsonified) : undefined, }, }); } diff --git a/packages/storage/src/indexed-db/indexed-db.test-data.ts b/packages/storage/src/indexed-db/indexed-db.test-data.ts index 614b024e5b..26a4b323df 100644 --- a/packages/storage/src/indexed-db/indexed-db.test-data.ts +++ b/packages/storage/src/indexed-db/indexed-db.test-data.ts @@ -11,6 +11,7 @@ import { Transaction } from '@penumbra-zone/protobuf/penumbra/core/transaction/v import type { ScanBlockResult } from '@penumbra-zone/types/state-commitment-tree'; import { base64ToUint8Array } from '@penumbra-zone/types/base64'; import { StateCommitment } from '@penumbra-zone/protobuf/penumbra/crypto/tct/v1/tct_pb'; +import { AddressIndex } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; const hash3312332298 = base64ToUint8Array('JbOzRkf0VKm4eIM0DS27N5igX8jxvPhAMpBWSr2bj/Q='); @@ -717,3 +718,6 @@ export const epoch3 = new Epoch({ index: 3n, startHeight: 300n, }); + +export const mainAccount = new AddressIndex({ account: 0 }); +export const firstSubaccount = new AddressIndex({ account: 1 }); diff --git a/packages/storage/src/indexed-db/indexed-db.test.ts b/packages/storage/src/indexed-db/indexed-db.test.ts index 9716d2846f..fdb984fb6e 100644 --- a/packages/storage/src/indexed-db/indexed-db.test.ts +++ b/packages/storage/src/indexed-db/indexed-db.test.ts @@ -28,6 +28,8 @@ import { tradingPairGmGn, transaction, transactionId, + mainAccount, + firstSubaccount, } from './indexed-db.test-data.js'; import { AddressIndex, WalletId } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; import { @@ -493,16 +495,26 @@ describe('IndexedDb', () => { }); describe('positions', () => { + it('returns empty array for zero positions', async () => { + const db = await IndexedDb.initialize({ ...generateInitialProps() }); + + const ownedPositions: PositionId[] = []; + for await (const positionId of db.getOwnedPositionIds(undefined, undefined, undefined)) { + ownedPositions.push(positionId as PositionId); + } + expect(ownedPositions.length).toBe(0); + }); + it('position should be added and their state should change', async () => { const db = await IndexedDb.initialize({ ...generateInitialProps() }); - await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); await db.updatePosition( positionIdGmPenumbraBuy, new PositionState({ state: PositionState_PositionStateEnum.CLOSED }), ); const ownedPositions: PositionId[] = []; - for await (const positionId of db.getOwnedPositionIds(undefined, undefined)) { + for await (const positionId of db.getOwnedPositionIds(undefined, undefined, undefined)) { ownedPositions.push(positionId as PositionId); } expect(ownedPositions.length).toBe(1); @@ -521,12 +533,12 @@ describe('IndexedDb', () => { it('should get all position ids', async () => { const db = await IndexedDb.initialize({ ...generateInitialProps() }); - await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy); - await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell); - await db.addPosition(positionIdGmGnSell, positionGmGnSell); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); + await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell, mainAccount); + await db.addPosition(positionIdGmGnSell, positionGmGnSell, firstSubaccount); const ownedPositions: PositionId[] = []; - for await (const positionId of db.getOwnedPositionIds(undefined, undefined)) { + for await (const positionId of db.getOwnedPositionIds(undefined, undefined, undefined)) { ownedPositions.push(positionId as PositionId); } expect(ownedPositions.length).toBe(3); @@ -534,14 +546,15 @@ describe('IndexedDb', () => { it('should get all position with given position state', async () => { const db = await IndexedDb.initialize({ ...generateInitialProps() }); - await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy); - await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell); - await db.addPosition(positionIdGmGnSell, positionGmGnSell); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); + await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell, mainAccount); + await db.addPosition(positionIdGmGnSell, positionGmGnSell, firstSubaccount); const ownedPositions: PositionId[] = []; for await (const positionId of db.getOwnedPositionIds( new PositionState({ state: PositionState_PositionStateEnum.CLOSED }), undefined, + undefined, )) { ownedPositions.push(positionId as PositionId); } @@ -550,16 +563,52 @@ describe('IndexedDb', () => { it('should get all position with given trading pair', async () => { const db = await IndexedDb.initialize({ ...generateInitialProps() }); - await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy); - await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell); - await db.addPosition(positionIdGmGnSell, positionGmGnSell); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); + await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell, mainAccount); + await db.addPosition(positionIdGmGnSell, positionGmGnSell, firstSubaccount); const ownedPositions: PositionId[] = []; - for await (const positionId of db.getOwnedPositionIds(undefined, tradingPairGmGn)) { + for await (const positionId of db.getOwnedPositionIds( + undefined, + tradingPairGmGn, + undefined, + )) { ownedPositions.push(positionId as PositionId); } expect(ownedPositions.length).toBe(1); }); + + it('should get all position with given subaccount index', async () => { + const db = await IndexedDb.initialize({ ...generateInitialProps() }); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); + await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell, mainAccount); + await db.addPosition(positionIdGmGnSell, positionGmGnSell, firstSubaccount); + + const ownedPositions: PositionId[] = []; + for await (const positionId of db.getOwnedPositionIds(undefined, undefined, mainAccount)) { + ownedPositions.push(positionId as PositionId); + } + expect(ownedPositions.length).toBe(2); + }); + + it('should filter positions correctly when all filters applied together', async () => { + const db = await IndexedDb.initialize({ ...generateInitialProps() }); + await db.addPosition(positionIdGmPenumbraBuy, positionGmPenumbraBuy, mainAccount); + await db.addPosition(positionIdGnPenumbraSell, positionGnPenumbraSell, mainAccount); + await db.addPosition(positionIdGmGnSell, positionGmGnSell, firstSubaccount); + + const ownedPositions: PositionId[] = []; + for await (const positionId of db.getOwnedPositionIds( + new PositionState({ state: PositionState_PositionStateEnum.CLOSED }), + tradingPairGmGn, + firstSubaccount, + )) { + ownedPositions.push(positionId as PositionId); + } + + expect(ownedPositions.length).toBe(1); + expect(ownedPositions[0]?.equals(positionIdGmGnSell)).toBeTruthy(); + }); }); describe('prices', () => { diff --git a/packages/types/src/indexed-db.ts b/packages/types/src/indexed-db.ts index bfc1137665..7259e378c7 100644 --- a/packages/types/src/indexed-db.ts +++ b/packages/types/src/indexed-db.ts @@ -97,9 +97,14 @@ export interface IndexedDbInterface { getOwnedPositionIds( positionState: PositionState | undefined, tradingPair: TradingPair | undefined, + subaccount: AddressIndex | undefined, ): AsyncGenerator; - addPosition(positionId: PositionId, position: Position): Promise; - updatePosition(positionId: PositionId, newState: PositionState): Promise; + addPosition(positionId: PositionId, position: Position, subaccount?: AddressIndex): Promise; + updatePosition( + positionId: PositionId, + newState: PositionState, + subaccount?: AddressIndex, + ): Promise; addEpoch(startHeight: bigint): Promise; getEpochByHeight(height: bigint): Promise; upsertValidatorInfo(validatorInfo: ValidatorInfo): Promise; @@ -297,6 +302,7 @@ export interface PenumbraDb extends DBSchema { export interface PositionRecord { id: Jsonified; // PositionId (must be JsonValue because ['id']['inner'] is a key ) position: Jsonified; // Position + subaccount?: Jsonified; // Position AddressIndex } export type Tables = Record>;