diff --git a/apps/extension/src/service-worker.ts b/apps/extension/src/service-worker.ts index 7562110367..9796f5d666 100644 --- a/apps/extension/src/service-worker.ts +++ b/apps/extension/src/service-worker.ts @@ -23,7 +23,13 @@ import { transportOptions } from '@penumbra-zone/types/registry'; // context import { CustodyService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/custody/v1/custody_connect'; -import { approverCtx, custodyCtx, servicesCtx } from '@penumbra-zone/router/src/ctx'; +import { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; +import { + approverCtx, + custodyCtx, + servicesCtx, + stakingClientCtx, +} from '@penumbra-zone/router/src/ctx'; import { createDirectClient } from '@penumbra-zone/transport-dom/direct'; import { approveTransaction } from './approve-transaction'; @@ -43,6 +49,7 @@ const services = new Services({ await services.initialize(); let custodyClient: PromiseClient | undefined; +let stakingClient: PromiseClient | undefined; const handler = connectChannelAdapter({ // jsonOptions contains typeRegistry providing ser/de jsonOptions: transportOptions.jsonOptions, @@ -55,10 +62,12 @@ const handler = connectChannelAdapter({ createRequestContext: req => { const contextValues = req.contextValues ?? createContextValues(); - // dynamically initialize custodyClient, or reuse if it's already available + // dynamically initialize clients, or reuse if already available custodyClient ??= createDirectClient(CustodyService, handler, transportOptions); + stakingClient ??= createDirectClient(StakingService, handler, transportOptions); contextValues.set(custodyCtx, custodyClient); + contextValues.set(stakingClientCtx, stakingClient); contextValues.set(servicesCtx, services); contextValues.set(approverCtx, approveTransaction); diff --git a/apps/minifront/src/fetchers/staking.ts b/apps/minifront/src/fetchers/staking.ts deleted file mode 100644 index e9264a1cb0..0000000000 --- a/apps/minifront/src/fetchers/staking.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { - AddressIndex, - IdentityKey, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { stakeClient, viewClient } from '../clients'; -import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { bech32IdentityKey, customizeSymbol } from '@penumbra-zone/types'; - -import { - getDisplayDenomFromView, - getIdentityKeyFromValidatorInfo, - getValidatorInfo, -} from '@penumbra-zone/getters'; -import { DelegationCaptureGroups, assetPatterns } from '@penumbra-zone/constants'; -import { Any } from '@bufbuild/protobuf'; -import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; -import { getBalances } from './balances'; -import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; - -const isDelegationBalance = (balance: BalancesResponse, identityKey: IdentityKey) => { - const match = assetPatterns.delegationToken.exec(getDisplayDenomFromView(balance.balanceView)); - if (!match) return false; - - const matchGroups = match.groups as unknown as DelegationCaptureGroups; - - return bech32IdentityKey(identityKey) === matchGroups.bech32IdentityKey; -}; - -const getDelegationTokenBaseDenom = (validatorInfo: ValidatorInfo) => - `udelegation_${bech32IdentityKey(getIdentityKeyFromValidatorInfo(validatorInfo))}`; - -/** - * Given an `AddressIndex`, yields `ValueView`s of the given address's balance - * of delegation tokens. Each `ValueView` has an `extendedMetadata` property - * containing the `ValidatorInfo` for the validator the delegation token is - * staked in. - * - * Note that one `ValueView` will be returned for each validator, even if the - * given address holds no tokens for that validator. (When there are no tokens - * for a given validators, the `ValueView` will have an amount of zero.) This - * ensures that you can display all validators by iterating over the response - * from this method, rather than having to call one method to get validators the - * user has stake in, and another for validators the user doesn't have stake in. - * - * @todo: Make this an RPC method, rather than doing it in the webapp. - * @todo: Make `showInactive` configurable via UI filters. - */ -export const getDelegationsForAccount = async function* (addressIndex: AddressIndex) { - const assetBalances = await getBalances({ accountFilter: addressIndex }); - const validatorInfoResponses = stakeClient.validatorInfo({ showInactive: false }); - - for await (const validatorInfoResponse of validatorInfoResponses) { - const validatorInfo = getValidatorInfo(validatorInfoResponse); - const extendedMetadata = new Any({ - typeUrl: ValidatorInfo.typeName, - value: validatorInfo.toBinary(), - }); - - const identityKey = getValidatorInfo.pipe(getIdentityKeyFromValidatorInfo)( - validatorInfoResponse, - ); - const delegation = assetBalances.find(balance => isDelegationBalance(balance, identityKey)); - - if (delegation) { - const withValidatorInfo = delegation.balanceView?.clone(); - - if (withValidatorInfo?.valueView.case !== 'knownAssetId') - throw new Error(`Unexpected ValueView case: ${withValidatorInfo?.valueView.case}`); - - withValidatorInfo.valueView.value.extendedMetadata = extendedMetadata; - - yield withValidatorInfo; - } else { - const { denomMetadata } = await viewClient.assetMetadataById({ - assetId: { altBaseDenom: getDelegationTokenBaseDenom(validatorInfo) }, - }); - - yield new ValueView({ - valueView: { - case: 'knownAssetId', - value: { - amount: { - hi: 0n, - lo: 0n, - }, - metadata: denomMetadata ? customizeSymbol(denomMetadata) : undefined, - extendedMetadata, - }, - }, - }); - } - } -}; diff --git a/apps/minifront/src/state/staking/index.test.ts b/apps/minifront/src/state/staking/index.test.ts index c10625dd57..685029764c 100644 --- a/apps/minifront/src/state/staking/index.test.ts +++ b/apps/minifront/src/state/staking/index.test.ts @@ -1,10 +1,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { StoreApi, UseBoundStore, create } from 'zustand'; import { AllSlices, initializeStore } from '..'; -import { - ValidatorInfo, - ValidatorInfoResponse, -} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; import { Metadata, ValueView, @@ -16,70 +13,54 @@ import { IdentityKey, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; import { THROTTLE_MS, accountsSelector } from '.'; +import { DelegationsByAddressIndexResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; const validator1IdentityKey = new IdentityKey({ ik: new Uint8Array([1, 2, 3]) }); const validator1Bech32IdentityKey = bech32IdentityKey(validator1IdentityKey); -const validatorInfoResponse1 = new ValidatorInfoResponse({ - validatorInfo: { - status: { - votingPower: { hi: 0n, lo: 2n }, - }, - validator: { - name: 'Validator 1', - identityKey: validator1IdentityKey, - }, +const validatorInfo1 = new ValidatorInfo({ + status: { + votingPower: { hi: 0n, lo: 2n }, + }, + validator: { + name: 'Validator 1', + identityKey: validator1IdentityKey, }, }); const validator2IdentityKey = new IdentityKey({ ik: new Uint8Array([4, 5, 6]) }); const validator2Bech32IdentityKey = bech32IdentityKey(validator2IdentityKey); -const validatorInfoResponse2 = new ValidatorInfoResponse({ - validatorInfo: { - status: { - votingPower: { hi: 0n, lo: 5n }, - }, - validator: { - name: 'Validator 2', - identityKey: validator2IdentityKey, - }, +const validatorInfo2 = new ValidatorInfo({ + status: { + votingPower: { hi: 0n, lo: 5n }, + }, + validator: { + name: 'Validator 2', + identityKey: validator2IdentityKey, }, }); const validator3IdentityKey = new IdentityKey({ ik: new Uint8Array([7, 8, 9]) }); -const validatorInfoResponse3 = new ValidatorInfoResponse({ - validatorInfo: { - status: { - votingPower: { hi: 0n, lo: 3n }, - }, - validator: { - name: 'Validator 3', - identityKey: validator3IdentityKey, - }, +const validatorInfo3 = new ValidatorInfo({ + status: { + votingPower: { hi: 0n, lo: 3n }, + }, + validator: { + name: 'Validator 3', + identityKey: validator3IdentityKey, }, }); const validator4IdentityKey = new IdentityKey({ ik: new Uint8Array([0]) }); -const validatorInfoResponse4 = new ValidatorInfoResponse({ - validatorInfo: { - status: { - votingPower: { hi: 0n, lo: 9n }, - }, - validator: { - name: 'Validator 4', - identityKey: validator4IdentityKey, - }, +const validatorInfo4 = new ValidatorInfo({ + status: { + votingPower: { hi: 0n, lo: 9n }, + }, + validator: { + name: 'Validator 4', + identityKey: validator4IdentityKey, }, }); -const mockStakeClient = vi.hoisted(() => ({ - validatorInfo: vi.fn(async function* () { - yield await Promise.resolve(validatorInfoResponse1); - yield await Promise.resolve(validatorInfoResponse2); - yield await Promise.resolve(validatorInfoResponse3); - yield await Promise.resolve(validatorInfoResponse4); - }), -})); - vi.mock('../../fetchers/balances', () => ({ getBalances: vi.fn(async () => Promise.resolve([ @@ -94,7 +75,7 @@ vi.mock('../../fetchers/balances', () => ({ }, extendedMetadata: { typeUrl: ValidatorInfo.typeName, - value: validatorInfoResponse1.validatorInfo?.toBinary(), + value: validatorInfo1.toBinary(), }, }, }, @@ -121,7 +102,7 @@ vi.mock('../../fetchers/balances', () => ({ }, extendedMetadata: { typeUrl: ValidatorInfo.typeName, - value: validatorInfoResponse2.validatorInfo?.toBinary(), + value: validatorInfo2.toBinary(), }, }, }, @@ -166,10 +147,75 @@ vi.mock('../../fetchers/balances', () => ({ const mockViewClient = vi.hoisted(() => ({ assetMetadataById: vi.fn(() => new Metadata()), + delegationsByAddressIndex: vi.fn(async function* () { + yield await Promise.resolve( + new DelegationsByAddressIndexResponse({ + valueView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 1n }, + extendedMetadata: { + typeUrl: ValidatorInfo.typeName, + value: validatorInfo1.toBinary(), + }, + }, + }, + }, + }), + ); + yield await Promise.resolve( + new DelegationsByAddressIndexResponse({ + valueView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 2n }, + extendedMetadata: { + typeUrl: ValidatorInfo.typeName, + value: validatorInfo2.toBinary(), + }, + }, + }, + }, + }), + ); + yield await Promise.resolve( + new DelegationsByAddressIndexResponse({ + valueView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 0n }, + extendedMetadata: { + typeUrl: ValidatorInfo.typeName, + value: validatorInfo3.toBinary(), + }, + }, + }, + }, + }), + ); + yield await Promise.resolve( + new DelegationsByAddressIndexResponse({ + valueView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 0n }, + extendedMetadata: { + typeUrl: ValidatorInfo.typeName, + value: validatorInfo4.toBinary(), + }, + }, + }, + }, + }), + ); + }), })); vi.mock('../../clients', () => ({ - stakeClient: mockStakeClient, viewClient: mockViewClient, })); @@ -219,18 +265,10 @@ describe('Staking Slice', () => { * before validator 3 at the end: we have a 0 balance of both, but validator * 4 has more voting power. */ - expect(getValidatorInfoFromValueView(delegations[0])).toEqual( - validatorInfoResponse2.validatorInfo, - ); - expect(getValidatorInfoFromValueView(delegations[1])).toEqual( - validatorInfoResponse1.validatorInfo, - ); - expect(getValidatorInfoFromValueView(delegations[2])).toEqual( - validatorInfoResponse4.validatorInfo, - ); - expect(getValidatorInfoFromValueView(delegations[3])).toEqual( - validatorInfoResponse3.validatorInfo, - ); + expect(getValidatorInfoFromValueView(delegations[0])).toEqual(validatorInfo2); + expect(getValidatorInfoFromValueView(delegations[1])).toEqual(validatorInfo1); + expect(getValidatorInfoFromValueView(delegations[2])).toEqual(validatorInfo4); + expect(getValidatorInfoFromValueView(delegations[3])).toEqual(validatorInfo3); }); it('calculates the percentage voting power once all delegations are loaded', async () => { diff --git a/apps/minifront/src/state/staking/index.ts b/apps/minifront/src/state/staking/index.ts index 0ee24cfd8d..1a8e56e6fe 100644 --- a/apps/minifront/src/state/staking/index.ts +++ b/apps/minifront/src/state/staking/index.ts @@ -1,6 +1,5 @@ import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; import { AllSlices, SliceCreator } from '..'; -import { getDelegationsForAccount } from '../../fetchers/staking'; import { getAmount, getAssetIdFromValueView, @@ -9,6 +8,7 @@ import { getDisplayDenomFromView, getRateData, getValidatorInfoFromValueView, + getValueView, getVotingPowerFromValidatorInfo, } from '@penumbra-zone/getters'; import { @@ -34,6 +34,7 @@ import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_ import { BigNumber } from 'bignumber.js'; import { assembleUndelegateClaimRequest } from './assemble-undelegate-claim-request'; import throttle from 'lodash/throttle'; +import { viewClient } from '../../clients'; const STAKING_TOKEN_DISPLAY_DENOM_EXPONENT = (() => { const stakingAsset = localAssets.find(asset => asset.display === STAKING_TOKEN); @@ -215,12 +216,13 @@ export const createStakingSlice = (): SliceCreator => (set, get) = }; const throttledFlushToState = throttle(flushToState, THROTTLE_MS, { trailing: true }); - for await (const delegation of getDelegationsForAccount(addressIndex)) { + for await (const response of viewClient.delegationsByAddressIndex({ addressIndex })) { if (newAbortController.signal.aborted) { throttledFlushToState.cancel(); return; } + const delegation = getValueView(response); delegationsToFlush.push(delegation); validatorInfos.push(getValidatorInfoFromValueView(delegation)); diff --git a/package.json b/package.json index ee18e599d3..a9bed24407 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "dependencies": { "@buf/cosmos_ibc.bufbuild_es": "1.7.2-20240215124455-b32ecf3ebbcb.1", "@buf/cosmos_ibc.connectrpc_es": "1.4.0-20240215124455-b32ecf3ebbcb.1", - "@buf/penumbra-zone_penumbra.bufbuild_es": "1.7.2-20240307042300-430cab1eb638.1", - "@buf/penumbra-zone_penumbra.connectrpc_es": "1.4.0-20240307042300-430cab1eb638.1", + "@buf/penumbra-zone_penumbra.bufbuild_es": "1.7.2-20240312215156-05b4a4f2471b.1", + "@buf/penumbra-zone_penumbra.connectrpc_es": "1.4.0-20240312215156-05b4a4f2471b.1", "@buf/tendermint_tendermint.bufbuild_es": "1.7.2-20231117195010-33ed361a9051.1", "@bufbuild/protobuf": "^1.7.2", "@connectrpc/connect": "^1.4.0", diff --git a/packages/getters/src/delegations-by-address-index-response.ts b/packages/getters/src/delegations-by-address-index-response.ts new file mode 100644 index 0000000000..891f1cb795 --- /dev/null +++ b/packages/getters/src/delegations-by-address-index-response.ts @@ -0,0 +1,7 @@ +import { DelegationsByAddressIndexResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; +import { createGetter } from './utils/create-getter'; + +export const getValueView = createGetter( + (delegationsByAddressIndexResponse?: DelegationsByAddressIndexResponse) => + delegationsByAddressIndexResponse?.valueView, +); diff --git a/packages/getters/src/index.ts b/packages/getters/src/index.ts index def0c585e7..e2a4e1ce0f 100644 --- a/packages/getters/src/index.ts +++ b/packages/getters/src/index.ts @@ -1,5 +1,6 @@ export * from './asset'; export * from './address-view'; +export * from './delegations-by-address-index-response'; export * from './funding-stream'; export * from './metadata'; export * from './rate-data'; diff --git a/packages/router/array-from-async.d.ts b/packages/router/array-from-async.d.ts new file mode 100644 index 0000000000..b26053d382 --- /dev/null +++ b/packages/router/array-from-async.d.ts @@ -0,0 +1,3 @@ +declare module 'array-from-async' { + export { default } from '@penumbra-zone/polyfills/Array.fromAsync'; +} diff --git a/packages/router/package.json b/packages/router/package.json index 0dfe0a7992..0e79fd0072 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -17,5 +17,8 @@ "@penumbra-zone/storage": "workspace:*", "@penumbra-zone/transport-dom": "workspace:*", "@penumbra-zone/wasm": "workspace:*" + }, + "devDependencies": { + "array-from-async": "^3.0.0" } } diff --git a/packages/router/src/ctx/index.ts b/packages/router/src/ctx/index.ts index a55bb95620..bf16899582 100644 --- a/packages/router/src/ctx/index.ts +++ b/packages/router/src/ctx/index.ts @@ -1,3 +1,4 @@ export * from './approver'; export * from './custody'; export * from './prax'; +export * from './staking-client'; diff --git a/packages/router/src/ctx/staking-client.ts b/packages/router/src/ctx/staking-client.ts new file mode 100644 index 0000000000..d27ef25172 --- /dev/null +++ b/packages/router/src/ctx/staking-client.ts @@ -0,0 +1,5 @@ +import { ContextKey, createContextKey, PromiseClient } from '@connectrpc/connect'; +import type { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; + +export const stakingClientCtx: ContextKey | undefined> = + createContextKey(undefined); diff --git a/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.test.ts b/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.test.ts new file mode 100644 index 0000000000..0cbfc42398 --- /dev/null +++ b/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.test.ts @@ -0,0 +1,347 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { delegationsByAddressIndex } from './delegations-by-address-index'; +import { ViewService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/view/v1/view_connect'; +import { + HandlerContext, + createHandlerContext, + createContextValues, + PromiseClient, +} from '@connectrpc/connect'; +import { stakingClientCtx } from '../../ctx'; +import { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; +import { + AssetMetadataByIdResponse, + BalancesResponse, + DelegationsByAddressIndexRequest, + DelegationsByAddressIndexRequest_Filter, + DelegationsByAddressIndexResponse, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; +import { STAKING_TOKEN_METADATA } from '@penumbra-zone/constants'; +import { + ValidatorInfoRequest, + ValidatorInfoResponse, + ValidatorState_ValidatorStateEnum, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { asIdentityKey, getAmount, getValidatorInfoFromValueView } from '@penumbra-zone/getters'; +import { PartialMessage } from '@bufbuild/protobuf'; +import { + Metadata, + ValueView, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; + +const mockBalances = vi.hoisted(() => vi.fn()); +vi.mock('./balances', () => ({ + balances: mockBalances, +})); + +vi.mock('./asset-metadata-by-id', () => ({ + assetMetadataById: () => + Promise.resolve(new AssetMetadataByIdResponse({ denomMetadata: new Metadata() })), +})); + +const activeValidatorBech32IdentityKey = + 'penumbravalid1zpwtnnmeu2fdqx9dslmd5sc44rja4jqlzqvzel8tajkk6ur7jyqq0cgcy9'; +const activeValidatorInfoResponse = new ValidatorInfoResponse({ + validatorInfo: { + validator: { + name: 'Active validator', + identityKey: asIdentityKey(activeValidatorBech32IdentityKey), + }, + status: { + state: { state: ValidatorState_ValidatorStateEnum.ACTIVE }, + }, + }, +}); + +const activeValidator2Bech32IdentityKey = + 'penumbravalid1tnsyu4tppg7rwgyl3wwcfxwfq6g6ahmlyywqvt77a2zlx6s34qpsxxh7qm'; +const activeValidator2InfoResponse = new ValidatorInfoResponse({ + validatorInfo: { + validator: { + name: 'Active validator 2', + identityKey: asIdentityKey(activeValidator2Bech32IdentityKey), + }, + status: { + state: { state: ValidatorState_ValidatorStateEnum.ACTIVE }, + }, + }, +}); + +const inactiveValidatorBech32IdentityKey = + 'penumbravalid1r6ja22cl476tluzea3w07r8kxl46ppqlckcvyzslg3ywsmqdnyys86t55e'; +const inactiveValidatorInfoResponse = new ValidatorInfoResponse({ + validatorInfo: { + validator: { + name: 'Inactive validator', + identityKey: asIdentityKey(inactiveValidatorBech32IdentityKey), + }, + status: { + state: { state: ValidatorState_ValidatorStateEnum.INACTIVE }, + }, + }, +}); + +const inactiveValidator2Bech32IdentityKey = + 'penumbravalid1acjrk7dhkd5tpal0m0rytsfytg5r9vc67y02v6fnv4qvrcr2kqxqgyn9wy'; +const inactiveValidator2InfoResponse = new ValidatorInfoResponse({ + validatorInfo: { + validator: { + name: 'Inactive validator 2', + identityKey: asIdentityKey(inactiveValidator2Bech32IdentityKey), + }, + status: { + state: { state: ValidatorState_ValidatorStateEnum.INACTIVE }, + }, + }, +}); + +const MOCK_ALL_VALIDATOR_INFOS = [ + activeValidatorInfoResponse, + activeValidator2InfoResponse, + inactiveValidatorInfoResponse, + inactiveValidator2InfoResponse, +]; + +const MOCK_ACTIVE_VALIDATOR_INFOS = [activeValidatorInfoResponse, activeValidator2InfoResponse]; + +const penumbraBalancesResponse = new BalancesResponse({ + accountAddress: { + addressView: { + case: 'decoded', + value: { + index: { account: 0 }, + }, + }, + }, + balanceView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 1n }, + metadata: STAKING_TOKEN_METADATA, + }, + }, + }, +}); + +const activeValidatorBalancesResponse = new BalancesResponse({ + accountAddress: { + addressView: { + case: 'decoded', + value: { + index: { account: 0 }, + }, + }, + }, + balanceView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 2n }, + metadata: { + base: `udelegation_${activeValidatorBech32IdentityKey}`, + display: `delegation_${activeValidatorBech32IdentityKey}`, + }, + }, + }, + }, +}); + +const inactiveValidatorBalancesResponse = new BalancesResponse({ + accountAddress: { + addressView: { + case: 'decoded', + value: { + index: { account: 0 }, + }, + }, + }, + balanceView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { hi: 0n, lo: 3n }, + metadata: { + base: `udelegation_${inactiveValidatorBech32IdentityKey}`, + display: `delegation_${inactiveValidatorBech32IdentityKey}`, + }, + }, + }, + }, +}); + +const MOCK_BALANCES = [ + penumbraBalancesResponse, + activeValidatorBalancesResponse, + inactiveValidatorBalancesResponse, +]; + +describe('DelegationsByAddressIndex request handler', () => { + const mockStakingClient = { + validatorInfo: vi.fn(), + }; + let mockCtx: HandlerContext; + + const mockBalancesResponse = { + next: vi.fn(), + [Symbol.asyncIterator]: () => mockBalancesResponse, + }; + + const mockAllValidatorInfosResponse = { + next: vi.fn(), + [Symbol.asyncIterator]: () => mockAllValidatorInfosResponse, + }; + + const mockActiveValidatorInfosResponse = { + next: vi.fn(), + [Symbol.asyncIterator]: () => mockActiveValidatorInfosResponse, + }; + + beforeEach(() => { + vi.resetAllMocks(); + mockBalances.mockReturnValue(mockBalancesResponse); + MOCK_BALANCES.forEach(value => mockBalancesResponse.next.mockResolvedValueOnce({ value })); + mockBalancesResponse.next.mockResolvedValueOnce({ done: true }); + + // Miniature mock staking client that actually switches what response it + // gives based on `req.showInactive`. + mockStakingClient.validatorInfo.mockImplementation((req: ValidatorInfoRequest) => + req.showInactive ? mockAllValidatorInfosResponse : mockActiveValidatorInfosResponse, + ); + MOCK_ALL_VALIDATOR_INFOS.forEach(value => + mockAllValidatorInfosResponse.next.mockResolvedValueOnce({ value }), + ); + mockAllValidatorInfosResponse.next.mockResolvedValueOnce({ done: true }); + MOCK_ACTIVE_VALIDATOR_INFOS.forEach(value => + mockActiveValidatorInfosResponse.next.mockResolvedValueOnce({ value }), + ); + mockActiveValidatorInfosResponse.next.mockResolvedValueOnce({ done: true }); + + mockCtx = createHandlerContext({ + service: ViewService, + method: ViewService.methods.fMDParameters, + protocolName: 'mock', + requestMethod: 'MOCK', + url: '/mock', + contextValues: createContextValues().set( + stakingClientCtx, + mockStakingClient as unknown as PromiseClient, + ), + }); + }); + + it("includes the address's balance in the `ValueView` for delegation tokens the address holds", async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ addressIndex: { account: 0 } }), + mockCtx, + )) { + results.push(result); + } + + const firstValueView = new ValueView(results[0]!.valueView); + + expect(getAmount(firstValueView)).toEqual({ hi: 0n, lo: 2n }); + }); + + it("includes `ValidatorInfo` in the `ValueView`'s `extendedMetadata` property", async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ addressIndex: { account: 0 } }), + mockCtx, + )) { + results.push(result); + } + + const firstValueView = new ValueView(results[0]!.valueView); + const validatorInfo = getValidatorInfoFromValueView(firstValueView); + + expect(validatorInfo.toJson()).toEqual(activeValidatorInfoResponse.validatorInfo!.toJson()); + }); + + describe('when no filter option is passed', () => { + it('returns one `ValueView` for each active validator', async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ addressIndex: { account: 0 } }), + mockCtx, + )) { + results.push(result); + } + + expect(results.length).toBe(2); + }); + + it('returns a zero-balance `ValueView` for validators the address has no tokens for', async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ addressIndex: { account: 0 } }), + mockCtx, + )) { + results.push(result); + } + + const secondValueView = new ValueView(results[1]!.valueView); + + expect(getAmount(secondValueView)).toEqual({ hi: 0n, lo: 0n }); + }); + }); + + describe('when the nonzero balances filter option is passed', () => { + it('returns one `ValueView` for each validator the address has tokens for', async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ + addressIndex: { account: 0 }, + filter: DelegationsByAddressIndexRequest_Filter.ALL_ACTIVE_WITH_NONZERO_BALANCES, + }), + mockCtx, + )) { + results.push(result); + } + + expect(results.length).toBe(1); + }); + }); + + describe('when the `ALL` filter option is passed', () => { + it('returns one `ValueView` for each validator, including inactive ones', async () => { + const results: ( + | DelegationsByAddressIndexResponse + | PartialMessage + )[] = []; + + for await (const result of delegationsByAddressIndex( + new DelegationsByAddressIndexRequest({ + addressIndex: { account: 0 }, + filter: DelegationsByAddressIndexRequest_Filter.ALL, + }), + mockCtx, + )) { + results.push(result); + } + + expect(results.length).toBe(4); + }); + }); +}); diff --git a/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.ts b/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.ts new file mode 100644 index 0000000000..9ecccfb2bd --- /dev/null +++ b/packages/router/src/grpc/view-protocol-server/delegations-by-address-index.ts @@ -0,0 +1,116 @@ +import { Impl } from '.'; + +import { IdentityKey } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; +import { bech32IdentityKey, customizeSymbol } from '@penumbra-zone/types'; +import Array from '@penumbra-zone/polyfills/Array.fromAsync'; +import { + getDisplayDenomFromView, + getIdentityKeyFromValidatorInfo, + getValidatorInfo, +} from '@penumbra-zone/getters'; +import { DelegationCaptureGroups, assetPatterns } from '@penumbra-zone/constants'; +import { Any, PartialMessage } from '@bufbuild/protobuf'; +import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { + AssetMetadataByIdRequest, + BalancesRequest, + BalancesResponse, + DelegationsByAddressIndexRequest_Filter, + DelegationsByAddressIndexResponse, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; +import { stakingClientCtx } from '../../ctx'; +import { balances } from './balances'; +import { + Metadata, + ValueView, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; +import { assetMetadataById } from './asset-metadata-by-id'; + +const isDelegationBalance = (balance: BalancesResponse, identityKey: IdentityKey) => { + const match = assetPatterns.delegationToken.exec(getDisplayDenomFromView(balance.balanceView)); + if (!match) return false; + + const matchGroups = match.groups as unknown as DelegationCaptureGroups; + + return bech32IdentityKey(identityKey) === matchGroups.bech32IdentityKey; +}; + +const getDelegationTokenBaseDenom = (validatorInfo: ValidatorInfo) => + `udelegation_${bech32IdentityKey(getIdentityKeyFromValidatorInfo(validatorInfo))}`; + +const addressHasDelegationTokens = ( + delegation?: PartialMessage, +): delegation is PartialMessage & { balanceView: ValueView } => + delegation?.balanceView instanceof ValueView; + +export const delegationsByAddressIndex: Impl['delegationsByAddressIndex'] = async function* ( + req, + ctx, +) { + const { addressIndex } = req; + if (!addressIndex) { + throw new Error('Missing `addressIndex` in `DelegationsByAddressIndex` request'); + } + + const stakingClient = ctx.values.get(stakingClientCtx); + if (!stakingClient) throw new Error('Staking context not found'); + + const assetBalances = await Array.fromAsync( + balances(new BalancesRequest({ accountFilter: addressIndex }), ctx), + ); + + for await (const validatorInfoResponse of stakingClient.validatorInfo({ + showInactive: req.filter === DelegationsByAddressIndexRequest_Filter.ALL, + })) { + const validatorInfo = getValidatorInfo(validatorInfoResponse); + const extendedMetadata = new Any({ + typeUrl: ValidatorInfo.typeName, + value: validatorInfo.toBinary(), + }); + + const identityKey = getValidatorInfo.pipe(getIdentityKeyFromValidatorInfo)( + validatorInfoResponse, + ); + const delegation = assetBalances.find(balance => + isDelegationBalance(new BalancesResponse(balance), identityKey), + ); + + if (addressHasDelegationTokens(delegation)) { + const withValidatorInfo = delegation.balanceView.clone(); + + if (withValidatorInfo.valueView.case !== 'knownAssetId') + throw new Error(`Unexpected ValueView case: ${withValidatorInfo.valueView.case}`); + + withValidatorInfo.valueView.value.extendedMetadata = extendedMetadata; + + yield new DelegationsByAddressIndexResponse({ valueView: withValidatorInfo }); + } else { + if (req.filter === DelegationsByAddressIndexRequest_Filter.ALL_ACTIVE_WITH_NONZERO_BALANCES) { + continue; + } + + const { denomMetadata } = await assetMetadataById( + new AssetMetadataByIdRequest({ + assetId: { altBaseDenom: getDelegationTokenBaseDenom(validatorInfo) }, + }), + ctx, + ); + + yield new DelegationsByAddressIndexResponse({ + valueView: { + valueView: { + case: 'knownAssetId', + value: { + amount: { + hi: 0n, + lo: 0n, + }, + metadata: denomMetadata ? customizeSymbol(new Metadata(denomMetadata)) : undefined, + extendedMetadata, + }, + }, + }, + }); + } + } +}; diff --git a/packages/router/src/grpc/view-protocol-server/index.ts b/packages/router/src/grpc/view-protocol-server/index.ts index 7d9ec9811a..ff46dfac6c 100644 --- a/packages/router/src/grpc/view-protocol-server/index.ts +++ b/packages/router/src/grpc/view-protocol-server/index.ts @@ -3,11 +3,12 @@ import type { ServiceImpl } from '@connectrpc/connect'; import { addressByIndex } from './address-by-index'; import { appParameters } from './app-parameters'; +import { assetMetadataById } from './asset-metadata-by-id'; import { assets } from './assets'; import { authorizeAndBuild } from './authorize-and-build'; import { balances } from './balances'; import { broadcastTransaction } from './broadcast-transaction'; -import { assetMetadataById } from './asset-metadata-by-id'; +import { delegationsByAddressIndex } from './delegations-by-address-index'; import { ephemeralAddress } from './ephemeral-address'; import { fMDParameters } from './fmd-parameters'; import { gasPrices } from './gas-prices'; @@ -33,11 +34,12 @@ export type Impl = ServiceImpl; export const viewImpl: Impl = { addressByIndex, appParameters, + assetMetadataById, assets, authorizeAndBuild, balances, broadcastTransaction, - assetMetadataById, + delegationsByAddressIndex, ephemeralAddress, fMDParameters, gasPrices, diff --git a/packages/router/tests-setup.js b/packages/router/tests-setup.js index c5ba9efad2..d55d2933c9 100644 --- a/packages/router/tests-setup.js +++ b/packages/router/tests-setup.js @@ -1,5 +1,8 @@ +import fromAsync from 'array-from-async'; import { vi } from 'vitest'; +Array.fromAsync = fromAsync; + // chrome.storage persistence middleware is run upon importing from `state/index.ts`. // For tests, this is problematic as it uses globals. This mocks those out. global.chrome = { diff --git a/packages/transport-dom/direct.ts b/packages/transport-dom/direct.ts index ad6a1a2498..4cad526af3 100644 --- a/packages/transport-dom/direct.ts +++ b/packages/transport-dom/direct.ts @@ -1,4 +1,9 @@ -import { TransportEvent, TransportMessage, isTransportMessage } from './messages'; +import { + TransportEvent, + TransportMessage, + isTransportMessage, + isTransportStream, +} from './messages'; import { ConnectError, createPromiseClient } from '@connectrpc/connect'; import { errorToJson } from '@connectrpc/connect/protocol-connect'; import { ChannelHandlerFn } from './adapter'; @@ -33,7 +38,10 @@ const directGetPort = error: errorToJson(ConnectError.from(error), jsonOptions), }), ); - servicePort.postMessage(transportResponse); + servicePort.postMessage( + transportResponse, + isTransportStream(transportResponse) ? [transportResponse.stream] : [], + ); }; // TODO: this only supports unary requests diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b349fa4f1d..90dd74f04e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,11 +15,11 @@ importers: specifier: 1.4.0-20240215124455-b32ecf3ebbcb.1 version: 1.4.0-20240215124455-b32ecf3ebbcb.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) '@buf/penumbra-zone_penumbra.bufbuild_es': - specifier: 1.7.2-20240307042300-430cab1eb638.1 - version: 1.7.2-20240307042300-430cab1eb638.1(@bufbuild/protobuf@1.7.2) + specifier: 1.7.2-20240312215156-05b4a4f2471b.1 + version: 1.7.2-20240312215156-05b4a4f2471b.1(@bufbuild/protobuf@1.7.2) '@buf/penumbra-zone_penumbra.connectrpc_es': - specifier: 1.4.0-20240307042300-430cab1eb638.1 - version: 1.4.0-20240307042300-430cab1eb638.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) + specifier: 1.4.0-20240312215156-05b4a4f2471b.1 + version: 1.4.0-20240312215156-05b4a4f2471b.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) '@buf/tendermint_tendermint.bufbuild_es': specifier: 1.7.2-20231117195010-33ed361a9051.1 version: 1.7.2-20231117195010-33ed361a9051.1(@bufbuild/protobuf@1.7.2) @@ -488,6 +488,10 @@ importers: '@penumbra-zone/wasm': specifier: workspace:* version: link:../wasm + devDependencies: + array-from-async: + specifier: ^3.0.0 + version: 3.0.0 packages/services: dependencies: @@ -2118,7 +2122,7 @@ packages: dev: true /@buf/cosmos_cosmos-proto.bufbuild_es@1.7.2-20211202220400-1935555c206d.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-proto.bufbuild_es/-/cosmos_cosmos-proto.bufbuild_es-1.7.2-20211202220400-1935555c206d.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-proto.bufbuild_es/-/cosmos_cosmos-proto.bufbuild_es-1.7.2-20211202220400-1935555c206d.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2126,7 +2130,7 @@ packages: dev: false /@buf/cosmos_cosmos-proto.connectrpc_es@1.4.0-20211202220400-1935555c206d.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-proto.connectrpc_es/-/cosmos_cosmos-proto.connectrpc_es-1.4.0-20211202220400-1935555c206d.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-proto.connectrpc_es/-/cosmos_cosmos-proto.connectrpc_es-1.4.0-20211202220400-1935555c206d.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2137,7 +2141,7 @@ packages: dev: false /@buf/cosmos_cosmos-sdk.bufbuild_es@1.7.2-20230522115704-e7a85cef453e.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-sdk.bufbuild_es/-/cosmos_cosmos-sdk.bufbuild_es-1.7.2-20230522115704-e7a85cef453e.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-sdk.bufbuild_es/-/cosmos_cosmos-sdk.bufbuild_es-1.7.2-20230522115704-e7a85cef453e.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2159,7 +2163,7 @@ packages: dev: false /@buf/cosmos_cosmos-sdk.connectrpc_es@1.4.0-20230522115704-e7a85cef453e.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-sdk.connectrpc_es/-/cosmos_cosmos-sdk.connectrpc_es-1.4.0-20230522115704-e7a85cef453e.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_cosmos-sdk.connectrpc_es/-/cosmos_cosmos-sdk.connectrpc_es-1.4.0-20230522115704-e7a85cef453e.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2187,7 +2191,7 @@ packages: dev: false /@buf/cosmos_gogo-proto.bufbuild_es@1.7.2-20221020125208-34d970b699f8.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.bufbuild_es/-/cosmos_gogo-proto.bufbuild_es-1.7.2-20221020125208-34d970b699f8.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.bufbuild_es/-/cosmos_gogo-proto.bufbuild_es-1.7.2-20221020125208-34d970b699f8.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2203,7 +2207,7 @@ packages: dev: false /@buf/cosmos_gogo-proto.connectrpc_es@1.4.0-20221020125208-34d970b699f8.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.connectrpc_es/-/cosmos_gogo-proto.connectrpc_es-1.4.0-20221020125208-34d970b699f8.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.connectrpc_es/-/cosmos_gogo-proto.connectrpc_es-1.4.0-20221020125208-34d970b699f8.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2214,7 +2218,7 @@ packages: dev: false /@buf/cosmos_gogo-proto.connectrpc_es@1.4.0-20230509103710-5e5b9fdd0180.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.connectrpc_es/-/cosmos_gogo-proto.connectrpc_es-1.4.0-20230509103710-5e5b9fdd0180.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_gogo-proto.connectrpc_es/-/cosmos_gogo-proto.connectrpc_es-1.4.0-20230509103710-5e5b9fdd0180.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2225,7 +2229,7 @@ packages: dev: false /@buf/cosmos_ibc.bufbuild_es@1.7.2-20230913112312-7ab44ae956a0.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ibc.bufbuild_es/-/cosmos_ibc.bufbuild_es-1.7.2-20230913112312-7ab44ae956a0.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ibc.bufbuild_es/-/cosmos_ibc.bufbuild_es-1.7.2-20230913112312-7ab44ae956a0.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2251,7 +2255,7 @@ packages: dev: false /@buf/cosmos_ibc.connectrpc_es@1.4.0-20230913112312-7ab44ae956a0.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ibc.connectrpc_es/-/cosmos_ibc.connectrpc_es-1.4.0-20230913112312-7ab44ae956a0.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ibc.connectrpc_es/-/cosmos_ibc.connectrpc_es-1.4.0-20230913112312-7ab44ae956a0.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2283,7 +2287,7 @@ packages: dev: false /@buf/cosmos_ics23.bufbuild_es@1.7.2-20221207100654-55085f7c710a.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ics23.bufbuild_es/-/cosmos_ics23.bufbuild_es-1.7.2-20221207100654-55085f7c710a.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ics23.bufbuild_es/-/cosmos_ics23.bufbuild_es-1.7.2-20221207100654-55085f7c710a.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2291,7 +2295,7 @@ packages: dev: false /@buf/cosmos_ics23.connectrpc_es@1.4.0-20221207100654-55085f7c710a.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ics23.connectrpc_es/-/cosmos_ics23.connectrpc_es-1.4.0-20221207100654-55085f7c710a.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/cosmos_ics23.connectrpc_es/-/cosmos_ics23.connectrpc_es-1.4.0-20221207100654-55085f7c710a.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2310,7 +2314,7 @@ packages: dev: false /@buf/googleapis_googleapis.bufbuild_es@1.7.2-20221214150216-75b4300737fb.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-1.7.2-20221214150216-75b4300737fb.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-1.7.2-20221214150216-75b4300737fb.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2318,7 +2322,7 @@ packages: dev: false /@buf/googleapis_googleapis.bufbuild_es@1.7.2-20230502210827-cc916c318597.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-1.7.2-20230502210827-cc916c318597.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.bufbuild_es/-/googleapis_googleapis.bufbuild_es-1.7.2-20230502210827-cc916c318597.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2337,7 +2341,7 @@ packages: dev: false /@buf/googleapis_googleapis.connectrpc_es@1.4.0-20221214150216-75b4300737fb.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.connectrpc_es/-/googleapis_googleapis.connectrpc_es-1.4.0-20221214150216-75b4300737fb.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.connectrpc_es/-/googleapis_googleapis.connectrpc_es-1.4.0-20221214150216-75b4300737fb.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2348,7 +2352,7 @@ packages: dev: false /@buf/googleapis_googleapis.connectrpc_es@1.4.0-20230502210827-cc916c318597.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.connectrpc_es/-/googleapis_googleapis.connectrpc_es-1.4.0-20230502210827-cc916c318597.1.tgz} + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/googleapis_googleapis.connectrpc_es/-/googleapis_googleapis.connectrpc_es-1.4.0-20230502210827-cc916c318597.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2358,8 +2362,8 @@ packages: - '@bufbuild/protobuf' dev: false - /@buf/penumbra-zone_penumbra.bufbuild_es@1.7.2-20240307042300-430cab1eb638.1(@bufbuild/protobuf@1.7.2): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/penumbra-zone_penumbra.bufbuild_es/-/penumbra-zone_penumbra.bufbuild_es-1.7.2-20240307042300-430cab1eb638.1.tgz} + /@buf/penumbra-zone_penumbra.bufbuild_es@1.7.2-20240312215156-05b4a4f2471b.1(@bufbuild/protobuf@1.7.2): + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/penumbra-zone_penumbra.bufbuild_es/-/penumbra-zone_penumbra.bufbuild_es-1.7.2-20240312215156-05b4a4f2471b.1.tgz} peerDependencies: '@bufbuild/protobuf': ^1.7.2 dependencies: @@ -2372,8 +2376,8 @@ packages: '@bufbuild/protobuf': 1.7.2 dev: false - /@buf/penumbra-zone_penumbra.connectrpc_es@1.4.0-20240307042300-430cab1eb638.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): - resolution: {registry: https://buf.build/gen/npm/v1/, tarball: https://buf.build/gen/npm/v1/@buf/penumbra-zone_penumbra.connectrpc_es/-/penumbra-zone_penumbra.connectrpc_es-1.4.0-20240307042300-430cab1eb638.1.tgz} + /@buf/penumbra-zone_penumbra.connectrpc_es@1.4.0-20240312215156-05b4a4f2471b.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0): + resolution: {tarball: https://buf.build/gen/npm/v1/@buf/penumbra-zone_penumbra.connectrpc_es/-/penumbra-zone_penumbra.connectrpc_es-1.4.0-20240312215156-05b4a4f2471b.1.tgz} peerDependencies: '@connectrpc/connect': ^1.4.0 dependencies: @@ -2383,7 +2387,7 @@ packages: '@buf/cosmos_ibc.connectrpc_es': 1.4.0-20230913112312-7ab44ae956a0.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) '@buf/cosmos_ics23.connectrpc_es': 1.4.0-20221207100654-55085f7c710a.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) '@buf/googleapis_googleapis.connectrpc_es': 1.4.0-20221214150216-75b4300737fb.1(@bufbuild/protobuf@1.7.2)(@connectrpc/connect@1.4.0) - '@buf/penumbra-zone_penumbra.bufbuild_es': 1.7.2-20240307042300-430cab1eb638.1(@bufbuild/protobuf@1.7.2) + '@buf/penumbra-zone_penumbra.bufbuild_es': 1.7.2-20240312215156-05b4a4f2471b.1(@bufbuild/protobuf@1.7.2) '@connectrpc/connect': 1.4.0(@bufbuild/protobuf@1.7.2) transitivePeerDependencies: - '@bufbuild/protobuf' @@ -6782,6 +6786,10 @@ packages: resolution: {integrity: sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==} dev: true + /array-from-async@3.0.0: + resolution: {integrity: sha512-gV8/L4y2QB5JTXL9DMdtspGyed2M3V6nMnSN+nNg8ejyUlAAbKAjRS6pfWWINjU/MuFJFMGWPazHPor7hThXQw==} + dev: true + /array-includes@3.1.7: resolution: {integrity: sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==} engines: {node: '>= 0.4'}