Skip to content

Commit

Permalink
Display jailed validators (#1640)
Browse files Browse the repository at this point in the history
* Display jailed validators

* query metadata too

* refactor to record

* Add tests

* add changeset
  • Loading branch information
grod220 authored Aug 5, 2024
1 parent bd43d49 commit f650f48
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 131 deletions.
5 changes: 5 additions & 0 deletions .changeset/three-sheep-arrive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@penumbra-zone/services': minor
---

Return jailed validators in delegation balances request
299 changes: 203 additions & 96 deletions packages/services/src/view-service/delegations-by-address-index.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { delegationsByAddressIndex } from './delegations-by-address-index.js';
import { ViewService, StakeService } from '@penumbra-zone/protobuf';
import { StakeService, ViewService } from '@penumbra-zone/protobuf';
import {
createContextValues,
createHandlerContext,
Expand All @@ -22,10 +22,11 @@ import {
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb.js';
import { getAmount, getValidatorInfoFromValueView } from '@penumbra-zone/getters/value-view';
import { identityKeyFromBech32m } from '@penumbra-zone/bech32m/penumbravalid';
import { PartialMessage } from '@bufbuild/protobuf';
import { Any, PartialMessage } from '@bufbuild/protobuf';
import {
Metadata,
ValueView,
ValueView_KnownAssetId,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';

vi.mock('@penumbra-zone/wasm/metadata', () => ({
Expand Down Expand Up @@ -98,6 +99,21 @@ const inactiveValidator2InfoResponse = new ValidatorInfoResponse({
},
});

// udelegation_penumbravalid163d86qvg7c6fv33a2rqfxdr4xjjst9xg49exup0x9g9t9plnm5qqcvjyyp
const jailedValidatorBech32IdentityKey =
'penumbravalid1mr4j6nh3za3wjptjr2uj2ssr3fg0gxxqgqg9vgjl7luqa3qur5zs3fj5w6';
const jailedValidatorInfoResponse = new ValidatorInfoResponse({
validatorInfo: {
validator: {
name: 'Jailed validator',
identityKey: identityKeyFromBech32m(jailedValidatorBech32IdentityKey),
},
status: {
state: { state: ValidatorState_ValidatorStateEnum.JAILED },
},
},
});

const MOCK_ALL_VALIDATOR_INFOS = [
activeValidatorInfoResponse,
activeValidator2InfoResponse,
Expand Down Expand Up @@ -173,6 +189,29 @@ const inactiveValidatorBalancesResponse = new BalancesResponse({
},
});

const jailedValidatorBalancesResponse = new BalancesResponse({
accountAddress: {
addressView: {
case: 'decoded',
value: {
index: { account: 0 },
},
},
},
balanceView: {
valueView: {
case: 'knownAssetId',
value: {
amount: { hi: 0n, lo: 4738495n },
metadata: {
base: `udelegation_${jailedValidatorBech32IdentityKey}`,
display: `delegation_${jailedValidatorBech32IdentityKey}`,
},
},
},
},
});

const MOCK_BALANCES = [
penumbraBalancesResponse,
activeValidatorBalancesResponse,
Expand All @@ -182,6 +221,7 @@ const MOCK_BALANCES = [
describe('DelegationsByAddressIndex request handler', () => {
const mockStakeClient = {
validatorInfo: vi.fn(),
getValidatorInfo: vi.fn(),
};
let mockCtx: HandlerContext;

Expand All @@ -200,78 +240,41 @@ describe('DelegationsByAddressIndex request handler', () => {
[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`.
mockStakeClient.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(
stakeClientCtx,
mockStakeClient as unknown as PromiseClient<typeof StakeService>,
),
describe('only active/inactive responses', () => {
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`.
mockStakeClient.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(
stakeClientCtx,
mockStakeClient as unknown as PromiseClient<typeof StakeService>,
),
});
});
});

it("includes the address's balance in the `ValueView` for delegation tokens the address holds", async () => {
const results: (
| DelegationsByAddressIndexResponse
| PartialMessage<DelegationsByAddressIndexResponse>
)[] = [];

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<DelegationsByAddressIndexResponse>
)[] = [];

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 () => {
it("includes the address's balance in the `ValueView` for delegation tokens the address holds", async () => {
const results: (
| DelegationsByAddressIndexResponse
| PartialMessage<DelegationsByAddressIndexResponse>
Expand All @@ -284,10 +287,12 @@ describe('DelegationsByAddressIndex request handler', () => {
results.push(result);
}

expect(results.length).toBe(2);
const firstValueView = new ValueView(results[0]!.valueView);

expect(getAmount(firstValueView)).toEqual({ hi: 0n, lo: 2n });
});

it('returns a zero-balance `ValueView` for validators the address has no tokens for', async () => {
it("includes `ValidatorInfo` in the `ValueView`'s `extendedMetadata` property", async () => {
const results: (
| DelegationsByAddressIndexResponse
| PartialMessage<DelegationsByAddressIndexResponse>
Expand All @@ -300,35 +305,121 @@ describe('DelegationsByAddressIndex request handler', () => {
results.push(result);
}

const secondValueView = new ValueView(results[1]!.valueView);
const firstValueView = new ValueView(results[0]!.valueView);
const validatorInfo = getValidatorInfoFromValueView(firstValueView);

expect(getAmount(secondValueView)).toEqual({ hi: 0n, lo: 0n });
expect(validatorInfo.toJson()).toEqual(activeValidatorInfoResponse.validatorInfo!.toJson());
});
});

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<DelegationsByAddressIndexResponse>
)[] = [];
describe('when no filter option is passed', () => {
it('returns one `ValueView` for each active validator', async () => {
const results: (
| DelegationsByAddressIndexResponse
| PartialMessage<DelegationsByAddressIndexResponse>
)[] = [];

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<DelegationsByAddressIndexResponse>
)[] = [];

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 });
});
});

for await (const result of delegationsByAddressIndex(
new DelegationsByAddressIndexRequest({
addressIndex: { account: 0 },
filter: DelegationsByAddressIndexRequest_Filter.ALL_ACTIVE_WITH_NONZERO_BALANCES,
}),
mockCtx,
)) {
results.push(result);
}
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<DelegationsByAddressIndexResponse>
)[] = [];

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);
});
});

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<DelegationsByAddressIndexResponse>
)[] = [];

for await (const result of delegationsByAddressIndex(
new DelegationsByAddressIndexRequest({
addressIndex: { account: 0 },
filter: DelegationsByAddressIndexRequest_Filter.ALL,
}),
mockCtx,
)) {
results.push(result);
}

expect(results.length).toBe(4);
});
});
});

describe('when the `ALL` filter option is passed', () => {
it('returns one `ValueView` for each validator, including inactive ones', async () => {
describe('when user has a jailed validator it its balances', () => {
beforeEach(() => {
vi.resetAllMocks();
mockBalances.mockReturnValue(mockBalancesResponse);
[...MOCK_BALANCES, jailedValidatorBalancesResponse].forEach(value =>
mockBalancesResponse.next.mockResolvedValueOnce({ value }),
);
mockBalancesResponse.next.mockResolvedValueOnce({ done: true });

mockStakeClient.validatorInfo.mockReturnValue(mockAllValidatorInfosResponse);

[...MOCK_ALL_VALIDATOR_INFOS, jailedValidatorInfoResponse].forEach(value =>
mockAllValidatorInfosResponse.next.mockResolvedValueOnce({ value }),
);
mockAllValidatorInfosResponse.next.mockResolvedValueOnce({ done: true });

mockCtx = createHandlerContext({
service: ViewService,
method: ViewService.methods.fMDParameters,
protocolName: 'mock',
requestMethod: 'MOCK',
url: '/mock',
contextValues: createContextValues().set(
stakeClientCtx,
mockStakeClient as unknown as PromiseClient<typeof StakeService>,
),
});
});

it('returns the jailed balance back', async () => {
const results: (
| DelegationsByAddressIndexResponse
| PartialMessage<DelegationsByAddressIndexResponse>
Expand All @@ -344,7 +435,23 @@ describe('DelegationsByAddressIndex request handler', () => {
results.push(result);
}

expect(results.length).toBe(4);
// balance augmented with extended metadata of validator info
const expectedResponse = new ValueView({
valueView: {
case: 'knownAssetId',
value: {
amount: jailedValidatorBalancesResponse.balanceView!.valueView.value!.amount,
metadata: (
jailedValidatorBalancesResponse.balanceView!.valueView.value as ValueView_KnownAssetId
).metadata,
extendedMetadata: Any.pack(jailedValidatorInfoResponse.validatorInfo!),
},
},
});

expect((results[4]!.valueView! as ValueView).equals(expectedResponse)).toBeTruthy();

expect(results.length).toBe(5);
});
});
});
Loading

0 comments on commit f650f48

Please sign in to comment.