From f06c2a866efc22bc86cf43117bcbb4f13bf928f9 Mon Sep 17 00:00:00 2001 From: Jesse Pinho Date: Tue, 12 Mar 2024 07:25:10 -0700 Subject: [PATCH] Build UI for claiming unbonding tokens (#659) * Account for undelegate claims in txn classifications * Add getters for getting an epoch index * Create an SCT client * Create a getter for a validator identity key from a metadata * Add asIdentityKey getter * Build out undelegateClaim actions * Account for undelegate claims in the planner * Install missing dep * Fix merge conflict issue * Put all claims into one transaction * Remove unnecessary loader call * Fix typo * Account for more errors from tendermint * Make the entire unbonding amount a tooltip * Update deps to take advantage of TransactionPlanner RPC change * Add validatorPenalty to the Staking querier * Add a validatorPenalty method handler to our impl of Staking * Create ActionDetails component * Add getters for undelegate claims * Display undelegate claims * Fix layout issue * Extract Separator component * Tweak comment * Fix bug with loading proving key * Extract a helper * Put staking slice in its own directory and extract a helper * Fix Rust tests * Run delegate script synchronously to avoid conflicts etc. * Polyfill Array.fromAsync in our test environment * Fix mock paths * Revert "Polyfill Array.fromAsync in our test environment" This reverts commit bb27e461e7fc27f765d59b80195c8bbfca63d57b. --- apps/minifront/src/clients.ts | 3 + .../staking/account/header/index.tsx | 60 ++++++++----- .../assemble-undelegate-claim-request.ts | 53 ++++++++++++ .../index.test.ts} | 9 +- .../state/{staking.ts => staking/index.ts} | 57 ++++++++++++- packages/getters/package.json | 1 + packages/getters/src/index.ts | 3 + packages/getters/src/metadata.test.ts | 60 ++++++++++++- packages/getters/src/metadata.ts | 49 +++++++++++ packages/getters/src/string.test.ts | 19 +++++ packages/getters/src/string.ts | 21 +++++ packages/getters/src/undelegate-claim-body.ts | 6 ++ packages/getters/src/undelegate-claim.ts | 18 ++++ packages/getters/src/value-view.ts | 21 ++++- packages/query/src/queriers/staking.ts | 10 ++- packages/query/src/queriers/tendermint.ts | 2 + .../view-transaction-plan/view-action-plan.ts | 10 +++ packages/router/src/grpc/staking/index.ts | 7 +- .../grpc/staking/validator-penalty.test.ts | 52 ++++++++++++ .../src/grpc/staking/validator-penalty.ts | 7 ++ packages/router/src/grpc/test-utils.ts | 7 +- packages/types/src/querier.ts | 4 + .../types/src/transaction/classification.ts | 4 +- .../types/src/transaction/classify.test.ts | 29 +++++++ packages/types/src/transaction/classify.ts | 3 + packages/types/src/translators/action-view.ts | 1 + .../components/ui/tx/view/action-details.tsx | 35 ++++++++ .../ui/components/ui/tx/view/action-view.tsx | 3 +- .../ui/tx/view/undelegate-claim.tsx | 31 +++++++ packages/ui/components/ui/tx/view/viewbox.tsx | 2 +- packages/wasm/crate/Cargo.lock | 85 ++++++++++--------- packages/wasm/crate/Cargo.toml | 30 +++---- packages/wasm/crate/src/planner.rs | 35 ++++++-- packages/wasm/crate/tests/build.rs | 1 + packages/wasm/src/build.ts | 2 +- pnpm-lock.yaml | 3 + scripts/delegate-to-validators.sh | 4 +- 37 files changed, 641 insertions(+), 106 deletions(-) create mode 100644 apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts rename apps/minifront/src/state/{staking.test.ts => staking/index.test.ts} (97%) rename apps/minifront/src/state/{staking.ts => staking/index.ts} (87%) create mode 100644 packages/getters/src/string.test.ts create mode 100644 packages/getters/src/string.ts create mode 100644 packages/getters/src/undelegate-claim-body.ts create mode 100644 packages/getters/src/undelegate-claim.ts create mode 100644 packages/router/src/grpc/staking/validator-penalty.test.ts create mode 100644 packages/router/src/grpc/staking/validator-penalty.ts create mode 100644 packages/ui/components/ui/tx/view/action-details.tsx create mode 100644 packages/ui/components/ui/tx/view/undelegate-claim.tsx diff --git a/apps/minifront/src/clients.ts b/apps/minifront/src/clients.ts index 137e629c19..504930f46a 100644 --- a/apps/minifront/src/clients.ts +++ b/apps/minifront/src/clients.ts @@ -4,6 +4,7 @@ import { SimulationService } from '@buf/penumbra-zone_penumbra.connectrpc_es/pen import { CustodyService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/custody/v1/custody_connect'; import { QueryService as StakeService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; import { Query as IbcClientService } from '@buf/cosmos_ibc.connectrpc_es/ibc/core/client/v1/query_connect'; +import { QueryService as SctService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/sct/v1/sct_connect'; export const viewClient = createPraxClient(ViewService); @@ -13,4 +14,6 @@ export const simulateClient = createPraxClient(SimulationService); export const ibcClient = createPraxClient(IbcClientService); +export const sctClient = createPraxClient(SctService); + export const stakeClient = createPraxClient(StakeService); diff --git a/apps/minifront/src/components/staking/account/header/index.tsx b/apps/minifront/src/components/staking/account/header/index.tsx index 52f30d265d..77e4f87e17 100644 --- a/apps/minifront/src/components/staking/account/header/index.tsx +++ b/apps/minifront/src/components/staking/account/header/index.tsx @@ -6,6 +6,7 @@ import { Tooltip, TooltipTrigger, TooltipContent, + Button, } from '@penumbra-zone/ui'; import { AccountSwitcher } from '@penumbra-zone/ui/components/ui/account-switcher'; import { ValueViewComponent } from '@penumbra-zone/ui/components/ui/tx/view/value'; @@ -34,8 +35,13 @@ const zeroBalanceUm = new ValueView({ * various token types related to staking. */ export const Header = () => { - const { account, setAccount, unstakedTokensByAccount, unbondingTokensByAccount } = - useStore(stakingSelector); + const { + account, + setAccount, + unstakedTokensByAccount, + unbondingTokensByAccount, + undelegateClaim, + } = useStore(stakingSelector); const unstakedTokens = unstakedTokensByAccount.get(account); const unbondingTokens = unbondingTokensByAccount.get(account); const accountSwitcherFilter = useStore(accountsSelector); @@ -46,33 +52,41 @@ export const Header = () => {
-
+
-
- - - - - An info icon - - -
-
- Total amount of UM you will receive when all your unbonding tokens are - claimed, assuming no slashing. -
- {unbondingTokens?.tokens.map(token => ( - - ))} + + + + + + +
+
+ Total amount of UM you will receive when all your unbonding tokens are + claimed, assuming no slashing.
- - - -
+ {unbondingTokens?.tokens.length && ( + <> + {unbondingTokens.tokens.map(token => ( + + ))} + + + + )} +
+
+
+
diff --git a/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts b/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts new file mode 100644 index 0000000000..2db1fe3f86 --- /dev/null +++ b/apps/minifront/src/state/staking/assemble-undelegate-claim-request.ts @@ -0,0 +1,53 @@ +import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; +import { + TransactionPlannerRequest_UndelegateClaim, + TransactionPlannerRequest, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; +import { + getStartEpochIndexFromValueView, + getValidatorIdentityKeyAsBech32StringFromValueView, + asIdentityKey, + getAmount, +} from '@penumbra-zone/getters'; +import { stakeClient, viewClient, sctClient } from '../../clients'; + +const getUndelegateClaimPlannerRequest = + (endEpochIndex: bigint) => async (unbondingToken: ValueView) => { + const startEpochIndex = getStartEpochIndexFromValueView(unbondingToken); + const validatorIdentityKeyAsBech32String = + getValidatorIdentityKeyAsBech32StringFromValueView(unbondingToken); + const identityKey = asIdentityKey(validatorIdentityKeyAsBech32String); + + const { penalty } = await stakeClient.validatorPenalty({ + startEpochIndex, + endEpochIndex, + identityKey, + }); + + return new TransactionPlannerRequest_UndelegateClaim({ + validatorIdentity: identityKey, + startEpochIndex, + penalty, + unbondingAmount: getAmount(unbondingToken), + }); + }; + +export const assembleUndelegateClaimRequest = async ({ + account, + unbondingTokens, +}: { + account: number; + unbondingTokens: ValueView[]; +}) => { + const { fullSyncHeight } = await viewClient.status({}); + const { epoch } = await sctClient.epochByHeight({ height: fullSyncHeight }); + const endEpochIndex = epoch?.index; + if (!endEpochIndex) return; + + return new TransactionPlannerRequest({ + undelegationClaims: await Promise.all( + unbondingTokens.map(getUndelegateClaimPlannerRequest(endEpochIndex)), + ), + source: { account }, + }); +}; diff --git a/apps/minifront/src/state/staking.test.ts b/apps/minifront/src/state/staking/index.test.ts similarity index 97% rename from apps/minifront/src/state/staking.test.ts rename to apps/minifront/src/state/staking/index.test.ts index 01e6c2b4de..368fbfba2a 100644 --- a/apps/minifront/src/state/staking.test.ts +++ b/apps/minifront/src/state/staking/index.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { StoreApi, UseBoundStore, create } from 'zustand'; -import { AllSlices, initializeStore } from '.'; +import { AllSlices, initializeStore } from '..'; import { ValidatorInfo, ValidatorInfoResponse, @@ -15,7 +15,7 @@ import { AddressView, IdentityKey, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; -import { accountsSelector } from './staking'; +import { accountsSelector } from '.'; const validator1IdentityKey = new IdentityKey({ ik: new Uint8Array([1, 2, 3]) }); const validator1Bech32IdentityKey = bech32IdentityKey(validator1IdentityKey); @@ -80,7 +80,7 @@ const mockStakeClient = vi.hoisted(() => ({ }), })); -vi.mock('../fetchers/balances', () => ({ +vi.mock('../../fetchers/balances', () => ({ getBalances: vi.fn(async () => Promise.resolve([ { @@ -168,7 +168,7 @@ const mockViewClient = vi.hoisted(() => ({ assetMetadataById: vi.fn(() => new Metadata()), })); -vi.mock('../clients', () => ({ +vi.mock('../../clients', () => ({ stakeClient: mockStakeClient, viewClient: mockViewClient, })); @@ -194,6 +194,7 @@ describe('Staking Slice', () => { loadUnstakedAndUnbondingTokensByAccount: expect.any(Function) as unknown, delegate: expect.any(Function) as unknown, undelegate: expect.any(Function) as unknown, + undelegateClaim: expect.any(Function) as unknown, onClickActionButton: expect.any(Function) as unknown, onClose: expect.any(Function) as unknown, setAmount: expect.any(Function) as unknown, diff --git a/apps/minifront/src/state/staking.ts b/apps/minifront/src/state/staking/index.ts similarity index 87% rename from apps/minifront/src/state/staking.ts rename to apps/minifront/src/state/staking/index.ts index 859e49068c..b1ff53263f 100644 --- a/apps/minifront/src/state/staking.ts +++ b/apps/minifront/src/state/staking/index.ts @@ -1,6 +1,6 @@ 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 { AllSlices, SliceCreator } from '..'; +import { getDelegationsForAccount } from '../../fetchers/staking'; import { getAmount, getAssetIdFromValueView, @@ -20,7 +20,7 @@ import { splitLoHi, } from '@penumbra-zone/types'; import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { BalancesByAccount, getBalancesByAccount } from '../fetchers/balances/by-account'; +import { BalancesByAccount, getBalancesByAccount } from '../../fetchers/balances/by-account'; import { localAssets, STAKING_TOKEN, @@ -29,9 +29,10 @@ import { } from '@penumbra-zone/constants'; import { AddressIndex } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; import { TransactionToast } from '@penumbra-zone/ui'; -import { authWitnessBuild, broadcast, getTxHash, plan, userDeniedTransaction } from './helpers'; +import { authWitnessBuild, broadcast, getTxHash, plan, userDeniedTransaction } from '../helpers'; import { TransactionPlannerRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb'; import { BigNumber } from 'bignumber.js'; +import { assembleUndelegateClaimRequest } from './assemble-undelegate-claim-request'; const STAKING_TOKEN_DISPLAY_DENOM_EXPONENT = (() => { const stakingAsset = localAssets.find(asset => asset.display === STAKING_TOKEN); @@ -76,6 +77,10 @@ export interface StakingSlice { * Build and submit the Undelegate transaction. */ undelegate: () => Promise; + /** + * Build and submit Undelegate Claim transaction(s). + */ + undelegateClaim: () => Promise; loadUnstakedAndUnbondingTokensByAccount: () => Promise; loading: boolean; error: unknown; @@ -279,6 +284,50 @@ export const createStakingSlice = (): SliceCreator => (set, get) = }); } }, + undelegateClaim: async () => { + const { account, unbondingTokensByAccount } = get().staking; + const unbondingTokens = unbondingTokensByAccount.get(account)?.tokens; + if (!unbondingTokens) return; + const toast = new TransactionToast('undelegateClaim'); + toast.onStart(); + + try { + const req = await assembleUndelegateClaimRequest({ account, unbondingTokens }); + if (!req) return; + const transactionPlan = await plan(req); + + // Reset form _after_ assembling the transaction planner request, since it + // depends on the state. + set(state => { + state.staking.action = undefined; + state.staking.validatorInfo = undefined; + }); + + const transaction = await authWitnessBuild({ transactionPlan }, status => + toast.onBuildStatus(status), + ); + const txHash = await getTxHash(transaction); + toast.txHash(txHash); + const { detectionHeight } = await broadcast({ transaction, awaitDetection: true }, status => + toast.onBroadcastStatus(status), + ); + toast.onSuccess(detectionHeight); + + // Reload unbonding tokens and unstaked tokens to reflect their updated + // balances. + void get().staking.loadUnstakedAndUnbondingTokensByAccount(); + } catch (e) { + if (userDeniedTransaction(e)) { + toast.onDenied(); + } else { + toast.onFailure(e); + } + } finally { + set(state => { + state.staking.amount = ''; + }); + } + }, loading: false, error: undefined, votingPowerByValidatorInfo: {}, diff --git a/packages/getters/package.json b/packages/getters/package.json index 68981105e9..3189fc26a1 100644 --- a/packages/getters/package.json +++ b/packages/getters/package.json @@ -8,6 +8,7 @@ "test": "vitest run" }, "dependencies": { + "@penumbra-zone/constants": "workspace:*", "bech32": "^2.0.0" } } diff --git a/packages/getters/src/index.ts b/packages/getters/src/index.ts index ec7415c7f8..def0c585e7 100644 --- a/packages/getters/src/index.ts +++ b/packages/getters/src/index.ts @@ -3,12 +3,15 @@ export * from './address-view'; export * from './funding-stream'; export * from './metadata'; export * from './rate-data'; +export * from './string'; export * from './swap'; export * from './swap-record'; export * from './spendable-note-record'; export * from './trading-pair'; export * from './transaction'; export * from './unclaimed-swaps-response'; +export * from './undelegate-claim'; +export * from './undelegate-claim-body'; export * from './validator'; export * from './validator-info'; export * from './validator-info-response'; diff --git a/packages/getters/src/metadata.test.ts b/packages/getters/src/metadata.test.ts index 99e092f910..ce05e18b0e 100644 --- a/packages/getters/src/metadata.test.ts +++ b/packages/getters/src/metadata.test.ts @@ -1,9 +1,13 @@ import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; -import { describe, expect, test } from 'vitest'; -import { getDisplayDenomExponent } from './metadata'; +import { describe, expect, it } from 'vitest'; +import { + getDisplayDenomExponent, + getStartEpochIndex, + getValidatorIdentityKeyAsBech32String, +} from './metadata'; describe('getDisplayDenomExponent()', () => { - test("gets the exponent from the denom unit whose `denom` is equal to the metadata's `display` property", () => { + it("gets the exponent from the denom unit whose `denom` is equal to the metadata's `display` property", () => { const penumbraMetadata = new Metadata({ display: 'penumbra', denomUnits: [ @@ -25,3 +29,53 @@ describe('getDisplayDenomExponent()', () => { expect(getDisplayDenomExponent(penumbraMetadata)).toBe(6); }); }); + +describe('getStartEpochIndex()', () => { + it("gets the epoch index, coerced to a `BigInt`, from an unbonding token's asset ID", () => { + const metadata = new Metadata({ display: 'uunbonding_epoch_123_penumbravalid1abc123' }); + + expect(getStartEpochIndex(metadata)).toBe(123n); + }); + + it("returns `undefined` for a non-unbonding token's metadata", () => { + const metadata = new Metadata({ display: 'penumbra' }); + + expect(getStartEpochIndex.optional()(metadata)).toBeUndefined(); + }); + + it('returns `undefined` for undefined metadata', () => { + expect(getStartEpochIndex.optional()(undefined)).toBeUndefined(); + }); +}); + +describe('getValidatorIdentityKeyAsBech32String()', () => { + describe('when passed metadata of a delegation token', () => { + const metadata = new Metadata({ display: 'delegation_penumbravalid1abc123' }); + + it("returns the bech32 representation of the validator's identity key", () => { + expect(getValidatorIdentityKeyAsBech32String(metadata)).toBe('penumbravalid1abc123'); + }); + }); + + describe('when passed metadata of an unbonding token', () => { + const metadata = new Metadata({ display: 'uunbonding_epoch_123_penumbravalid1abc123' }); + + it("returns the bech32 representation of the validator's identity key", () => { + expect(getValidatorIdentityKeyAsBech32String(metadata)).toBe('penumbravalid1abc123'); + }); + }); + + describe('when passed a token unrelated to validators', () => { + const metadata = new Metadata({ display: 'penumbra' }); + + it('returns `undefined`', () => { + expect(getValidatorIdentityKeyAsBech32String.optional()(metadata)).toBeUndefined(); + }); + }); + + describe('when passed undefined', () => { + it('returns `undefined`', () => { + expect(getValidatorIdentityKeyAsBech32String.optional()(undefined)).toBeUndefined(); + }); + }); +}); diff --git a/packages/getters/src/metadata.ts b/packages/getters/src/metadata.ts index 6c7c4c34c3..90235115b8 100644 --- a/packages/getters/src/metadata.ts +++ b/packages/getters/src/metadata.ts @@ -1,5 +1,10 @@ import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; import { createGetter } from './utils/create-getter'; +import { + DelegationCaptureGroups, + UnbondingCaptureGroups, + assetPatterns, +} from '@penumbra-zone/constants'; export const getAssetId = createGetter((metadata?: Metadata) => metadata?.penumbraAssetId); @@ -20,3 +25,47 @@ export const getDisplayDenomExponent = createGetter( (metadata?: Metadata) => metadata?.denomUnits.find(denomUnit => denomUnit.denom === metadata.display)?.exponent, ); + +/** + * Get the start epoch index from the metadata of an unbonding token -- that is, + * the epoch at which unbonding started. + * + * For metadata of a non-unbonding token, will return `undefined`. + */ +export const getStartEpochIndex = createGetter((metadata?: Metadata) => { + if (!metadata) return undefined; + + const unbondingMatch = assetPatterns.unbondingToken.exec(metadata.display); + + if (unbondingMatch) { + const { epoch } = unbondingMatch.groups as unknown as UnbondingCaptureGroups; + + if (epoch) return BigInt(epoch); + } + + return undefined; +}); + +/** + * Get the bech32 representation of a validator's identity key from the metadata + * of a delegation or unbonding token. + * + * For metadata of other token types, will return `undefined`. + */ +export const getValidatorIdentityKeyAsBech32String = createGetter((metadata?: Metadata) => { + if (!metadata) return undefined; + + const delegationMatch = assetPatterns.delegationToken.exec(metadata.display); + if (delegationMatch) { + const { bech32IdentityKey } = delegationMatch.groups as unknown as DelegationCaptureGroups; + return bech32IdentityKey; + } + + const unbondingMatch = assetPatterns.unbondingToken.exec(metadata.display); + if (unbondingMatch) { + const { bech32IdentityKey } = unbondingMatch.groups as unknown as UnbondingCaptureGroups; + return bech32IdentityKey; + } + + return undefined; +}); diff --git a/packages/getters/src/string.test.ts b/packages/getters/src/string.test.ts new file mode 100644 index 0000000000..9a1686d2f6 --- /dev/null +++ b/packages/getters/src/string.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest'; +import { asIdentityKey } from './string'; +import { IdentityKey } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; + +describe('asIdentityKey()', () => { + it('correctly decodes a bech32-encoded identity key into an `IdentityKey` object', () => { + const bech32IdentityKey = 'penumbravalid1qypqxpqtsysgc'; + + expect( + asIdentityKey(bech32IdentityKey).equals( + new IdentityKey({ ik: new Uint8Array([1, 2, 3, 4]) }), + ), + ).toBe(true); + }); + + it('returns `undefined` when passed an invalid string', () => { + expect(asIdentityKey.optional()('invalidstring')).toBeUndefined(); + }); +}); diff --git a/packages/getters/src/string.ts b/packages/getters/src/string.ts new file mode 100644 index 0000000000..85618ca28b --- /dev/null +++ b/packages/getters/src/string.ts @@ -0,0 +1,21 @@ +import { IdentityKey } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb'; +import { createGetter } from './utils/create-getter'; +import { bech32m } from 'bech32'; + +/** + * Given a bech32 representation of a validator's identity key, returns an + * `IdentityKey` object. + */ +export const asIdentityKey = createGetter((bech32IdentityKey?: string) => { + if (!bech32IdentityKey) return undefined; + + try { + const { words } = bech32m.decode(bech32IdentityKey); + + return new IdentityKey({ + ik: new Uint8Array(bech32m.fromWords(words)), + }); + } catch { + return undefined; + } +}); diff --git a/packages/getters/src/undelegate-claim-body.ts b/packages/getters/src/undelegate-claim-body.ts new file mode 100644 index 0000000000..5734de8a70 --- /dev/null +++ b/packages/getters/src/undelegate-claim-body.ts @@ -0,0 +1,6 @@ +import { UndelegateClaimBody } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { createGetter } from './utils/create-getter'; + +export const getValidatorIdentity = createGetter( + (undelegateClaimBody?: UndelegateClaimBody) => undelegateClaimBody?.validatorIdentity, +); diff --git a/packages/getters/src/undelegate-claim.ts b/packages/getters/src/undelegate-claim.ts new file mode 100644 index 0000000000..300a930c90 --- /dev/null +++ b/packages/getters/src/undelegate-claim.ts @@ -0,0 +1,18 @@ +import { + UndelegateClaim, + UndelegateClaimBody, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { createGetter } from './utils/create-getter'; +import { getValidatorIdentity } from './undelegate-claim-body'; + +export const getBody = createGetter((undelegateClaim?: UndelegateClaim) => undelegateClaim?.body); + +export const getValidatorIdentityFromUndelegateClaim = getBody.pipe(getValidatorIdentity); + +export const getStartEpochIndexFromUndelegateClaim = getBody.pipe( + // Defining this inline rather than exporting `getStartEpochIndex` from + // `undelegate-claim-body.ts`, since `getStartEpochIndex` is already defined + // elsewhere and thus would result in a naming conflict in the exports from + // this package. + createGetter((undelegateClaimBody?: UndelegateClaimBody) => undelegateClaimBody?.startEpochIndex), +); diff --git a/packages/getters/src/value-view.ts b/packages/getters/src/value-view.ts index d73e7e5201..b0ce934710 100644 --- a/packages/getters/src/value-view.ts +++ b/packages/getters/src/value-view.ts @@ -1,7 +1,11 @@ import { ValueView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb'; import { createGetter } from './utils/create-getter'; -import { getDisplayDenomExponent } from './metadata'; import { bech32AssetId } from './asset'; +import { + getDisplayDenomExponent, + getStartEpochIndex, + getValidatorIdentityKeyAsBech32String, +} from './metadata'; import { Any } from '@bufbuild/protobuf'; import { ValidatorInfo } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; import { getIdentityKeyFromValidatorInfo } from './validator-info'; @@ -34,6 +38,16 @@ export const getIdentityKeyFromValueView = getValidatorInfoFromValueView.pipe( getIdentityKeyFromValidatorInfo, ); +/** + * Get the bech32 representation of a validator's identity key from a + * `ValueView` containing a delegation or unbonding token. + * + * For `ValueView`s containing other token types, will return `undefined`. + */ +export const getValidatorIdentityKeyAsBech32StringFromValueView = getMetadata.pipe( + getValidatorIdentityKeyAsBech32String, +); + export const getDisplayDenomExponentFromValueView = getMetadata.pipe(getDisplayDenomExponent); export const getAssetIdFromValueView = createGetter((v?: ValueView) => { @@ -51,6 +65,11 @@ export const getAmount = createGetter( (valueView?: ValueView) => valueView?.valueView.value?.amount, ); +/** + * For a `ValueView` containing an unbonding token, gets the start epoch index. + */ +export const getStartEpochIndexFromValueView = getMetadata.pipe(getStartEpochIndex); + export const getDisplayDenomFromView = createGetter((view?: ValueView) => { if (view?.valueView.case === 'unknownAssetId') { if (!view.valueView.value.assetId) return undefined; diff --git a/packages/query/src/queriers/staking.ts b/packages/query/src/queriers/staking.ts index 7516d32341..e30f089cfa 100644 --- a/packages/query/src/queriers/staking.ts +++ b/packages/query/src/queriers/staking.ts @@ -2,7 +2,11 @@ import { PromiseClient } from '@connectrpc/connect'; import { createClient } from './utils'; import { StakingQuerierInterface } from '@penumbra-zone/types'; import { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; -import { ValidatorInfoResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { + ValidatorInfoResponse, + ValidatorPenaltyRequest, + ValidatorPenaltyResponse, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; export class StakingQuerier implements StakingQuerierInterface { private readonly client: PromiseClient; @@ -19,4 +23,8 @@ export class StakingQuerier implements StakingQuerierInterface { */ return this.client.validatorInfo({ showInactive: true }); } + + validatorPenalty(req: ValidatorPenaltyRequest): Promise { + return this.client.validatorPenalty(req); + } } diff --git a/packages/query/src/queriers/tendermint.ts b/packages/query/src/queriers/tendermint.ts index 5803e1f1d1..c2cc0620dd 100644 --- a/packages/query/src/queriers/tendermint.ts +++ b/packages/query/src/queriers/tendermint.ts @@ -10,6 +10,8 @@ const knownTendermintErrors = [ 'proof did not verify', 'is not a valid field element', 'is not a valid SCT root', + 'cannot claim unbonding tokens before the end epoch', + 'undelegation was prepared for next epoch', ]; export class TendermintQuerier implements TendermintQuerierInterface { diff --git a/packages/router/src/grpc/custody/view-transaction-plan/view-action-plan.ts b/packages/router/src/grpc/custody/view-transaction-plan/view-action-plan.ts index bd9cd91ec0..c4fb6c7611 100644 --- a/packages/router/src/grpc/custody/view-transaction-plan/view-action-plan.ts +++ b/packages/router/src/grpc/custody/view-transaction-plan/view-action-plan.ts @@ -220,6 +220,16 @@ export const viewActionPlan = case 'undelegate': return new ActionView({ actionView: actionPlan.action }); + case 'undelegateClaim': + return new ActionView({ + actionView: { + case: 'undelegateClaim', + value: { + body: actionPlan.action.value, + }, + }, + }); + case undefined: throw new Error('No action case in action plan'); default: diff --git a/packages/router/src/grpc/staking/index.ts b/packages/router/src/grpc/staking/index.ts index 5eb3554443..7be8c207ae 100644 --- a/packages/router/src/grpc/staking/index.ts +++ b/packages/router/src/grpc/staking/index.ts @@ -1,7 +1,12 @@ import { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; import { ServiceImpl } from '@connectrpc/connect'; + import { validatorInfo } from './validator-info'; +import { validatorPenalty } from './validator-penalty'; export type Impl = ServiceImpl; -export const stakingImpl: Pick = { validatorInfo }; +export const stakingImpl: Pick = { + validatorInfo, + validatorPenalty, +}; diff --git a/packages/router/src/grpc/staking/validator-penalty.test.ts b/packages/router/src/grpc/staking/validator-penalty.test.ts new file mode 100644 index 0000000000..9623674fb7 --- /dev/null +++ b/packages/router/src/grpc/staking/validator-penalty.test.ts @@ -0,0 +1,52 @@ +import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { validatorPenalty } from './validator-penalty'; +import { MockServices } from '../test-utils'; +import { HandlerContext, createContextValues, createHandlerContext } from '@connectrpc/connect'; +import { QueryService as StakingService } from '@buf/penumbra-zone_penumbra.connectrpc_es/penumbra/core/component/stake/v1/stake_connect'; +import { ServicesInterface } from '@penumbra-zone/types'; +import { servicesCtx } from '../../ctx'; +import { + ValidatorPenaltyRequest, + ValidatorPenaltyResponse, +} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; + +describe('ValidatorPenalty request handler', () => { + let mockServices: MockServices; + let mockStakingQuerierValidatorPenalty: Mock; + let mockCtx: HandlerContext; + const mockValidatorPenaltyResponse = new ValidatorPenaltyResponse({ + penalty: { inner: new Uint8Array([0, 1, 2, 3]) }, + }); + + beforeEach(() => { + vi.resetAllMocks(); + + mockStakingQuerierValidatorPenalty = vi.fn().mockResolvedValue(mockValidatorPenaltyResponse); + + mockServices = { + querier: { + staking: { validatorPenalty: mockStakingQuerierValidatorPenalty }, + }, + } satisfies MockServices; + + mockCtx = createHandlerContext({ + service: StakingService, + method: StakingService.methods.validatorInfo, + protocolName: 'mock', + requestMethod: 'MOCK', + url: '/mock', + contextValues: createContextValues().set( + servicesCtx, + mockServices as unknown as ServicesInterface, + ), + }); + }); + + it("returns the response from the staking querier's `validatorPenalty` method", async () => { + const req = new ValidatorPenaltyRequest(); + const result = await validatorPenalty(req, mockCtx); + + expect(mockStakingQuerierValidatorPenalty).toHaveBeenCalledWith(req); + expect(result as ValidatorPenaltyResponse).toEqual(mockValidatorPenaltyResponse); + }); +}); diff --git a/packages/router/src/grpc/staking/validator-penalty.ts b/packages/router/src/grpc/staking/validator-penalty.ts new file mode 100644 index 0000000000..32a2f04e61 --- /dev/null +++ b/packages/router/src/grpc/staking/validator-penalty.ts @@ -0,0 +1,7 @@ +import { Impl } from '.'; +import { servicesCtx } from '../../ctx'; + +export const validatorPenalty: Impl['validatorPenalty'] = async (req, ctx) => { + const services = ctx.values.get(servicesCtx); + return services.querier.staking.validatorPenalty(req); +}; diff --git a/packages/router/src/grpc/test-utils.ts b/packages/router/src/grpc/test-utils.ts index 5624086b86..cd6a7155da 100644 --- a/packages/router/src/grpc/test-utils.ts +++ b/packages/router/src/grpc/test-utils.ts @@ -36,11 +36,16 @@ export interface ShieldedPoolMock { export interface ViewServerMock { fullViewingKey?: Mock; } + +export interface StakingMock { + validatorPenalty?: Mock; +} export interface MockServices { - getWalletServices: Mock<[], Promise<{ indexedDb?: IndexedDbMock; viewServer?: ViewServerMock }>>; + getWalletServices?: Mock<[], Promise<{ indexedDb?: IndexedDbMock; viewServer?: ViewServerMock }>>; querier?: { tendermint?: TendermintMock; shieldedPool?: ShieldedPoolMock; + staking?: StakingMock; }; } diff --git a/packages/types/src/querier.ts b/packages/types/src/querier.ts index 3723cc7b43..a7cb8ccfd9 100644 --- a/packages/types/src/querier.ts +++ b/packages/types/src/querier.ts @@ -14,6 +14,8 @@ import { Transaction } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/co import { ValidatorInfoRequest, ValidatorInfoResponse, + ValidatorPenaltyRequest, + ValidatorPenaltyResponse, } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; export interface RootQuerierInterface { @@ -22,6 +24,7 @@ export interface RootQuerierInterface { tendermint: TendermintQuerierInterface; shieldedPool: ShieldedPoolQuerierInterface; ibcClient: IbcClientQuerierInterface; + staking: StakingQuerierInterface; cnidarium: CnidariumQuerierInterface; } @@ -56,6 +59,7 @@ export interface IbcClientQuerierInterface { export interface StakingQuerierInterface { allValidatorInfos(req: ValidatorInfoRequest): AsyncIterable; + validatorPenalty(req: ValidatorPenaltyRequest): Promise; } export interface CnidariumQuerierInterface { diff --git a/packages/types/src/transaction/classification.ts b/packages/types/src/transaction/classification.ts index 899f05f50e..6826faafd1 100644 --- a/packages/types/src/transaction/classification.ts +++ b/packages/types/src/transaction/classification.ts @@ -16,4 +16,6 @@ export type TransactionClassification = /** The transaction contains a `delegate` action. */ | 'delegate' /** The transaction contains an `undelegate` action. */ - | 'undelegate'; + | 'undelegate' + /** The transaction contains an `undelegateClaim` action. */ + | 'undelegateClaim'; diff --git a/packages/types/src/transaction/classify.test.ts b/packages/types/src/transaction/classify.test.ts index 92884fbeaf..c6c9577dce 100644 --- a/packages/types/src/transaction/classify.test.ts +++ b/packages/types/src/transaction/classify.test.ts @@ -288,6 +288,35 @@ describe('classifyTransaction()', () => { expect(classifyTransaction(transactionView)).toBe('undelegate'); }); + it('returns `undelegateClaim` for transactions with an `undelegateClaim` action', () => { + const transactionView = new TransactionView({ + bodyView: { + actionViews: [ + { + actionView: { + case: 'undelegateClaim', + value: {}, + }, + }, + { + actionView: { + case: 'spend', + value: {}, + }, + }, + { + actionView: { + case: 'output', + value: {}, + }, + }, + ], + }, + }); + + expect(classifyTransaction(transactionView)).toBe('undelegateClaim'); + }); + it("returns `unknown` for transactions that don't fit the above categories", () => { const transactionView = new TransactionView({ bodyView: { diff --git a/packages/types/src/transaction/classify.ts b/packages/types/src/transaction/classify.ts index f095cde41e..cda4f1c16a 100644 --- a/packages/types/src/transaction/classify.ts +++ b/packages/types/src/transaction/classify.ts @@ -11,6 +11,8 @@ export const classifyTransaction = (txv?: TransactionView): TransactionClassific if (txv.bodyView?.actionViews.some(a => a.actionView.case === 'swapClaim')) return 'swapClaim'; if (txv.bodyView?.actionViews.some(a => a.actionView.case === 'delegate')) return 'delegate'; if (txv.bodyView?.actionViews.some(a => a.actionView.case === 'undelegate')) return 'undelegate'; + if (txv.bodyView?.actionViews.some(a => a.actionView.case === 'undelegateClaim')) + return 'undelegateClaim'; const hasOpaqueSpend = txv.bodyView?.actionViews.some( a => a.actionView.case === 'spend' && a.actionView.value.spendView.case === 'opaque', @@ -82,6 +84,7 @@ export const TRANSACTION_LABEL_BY_CLASSIFICATION: Record diff --git a/packages/types/src/translators/action-view.ts b/packages/types/src/translators/action-view.ts index 0114d85be0..6e40d9d05a 100644 --- a/packages/types/src/translators/action-view.ts +++ b/packages/types/src/translators/action-view.ts @@ -24,6 +24,7 @@ export const asPublicActionView: Translator = actionView => { case 'delegate': case 'undelegate': + case 'undelegateClaim': return actionView; default: diff --git a/packages/ui/components/ui/tx/view/action-details.tsx b/packages/ui/components/ui/tx/view/action-details.tsx new file mode 100644 index 0000000000..1abf2a5aa9 --- /dev/null +++ b/packages/ui/components/ui/tx/view/action-details.tsx @@ -0,0 +1,35 @@ +import { ReactNode } from 'react'; + +/** + * Render key/value pairs inside a ``. + * + * @example + * ```tsx + * + * + * + * + * + * ``` + */ +export const ActionDetails = ({ children }: { children: ReactNode }) => { + return
{children}
; +}; + +const Separator = () => ( +
+); + +const ActionDetailsRow = ({ label, children }: { label: string; children: ReactNode }) => { + return ( +
+ {label} + + + + {children} +
+ ); +}; + +ActionDetails.Row = ActionDetailsRow; diff --git a/packages/ui/components/ui/tx/view/action-view.tsx b/packages/ui/components/ui/tx/view/action-view.tsx index 9bb102555f..6e2a7def15 100644 --- a/packages/ui/components/ui/tx/view/action-view.tsx +++ b/packages/ui/components/ui/tx/view/action-view.tsx @@ -6,6 +6,7 @@ import { SwapViewComponent } from './swap'; import { SwapClaimViewComponent } from './swap-claim'; import { DelegateComponent } from './delegate'; import { UndelegateComponent } from './undelegate'; +import { UndelegateClaimComponent } from './undelegate-claim'; const CASE_TO_LABEL: Record = { daoDeposit: 'DAO Deposit', @@ -63,7 +64,7 @@ export const ActionViewComponent = ({ av: { actionView } }: { av: ActionView }) return ; case 'undelegateClaim': - return ; + return ; case 'validatorDefinition': return ; diff --git a/packages/ui/components/ui/tx/view/undelegate-claim.tsx b/packages/ui/components/ui/tx/view/undelegate-claim.tsx new file mode 100644 index 0000000000..e51fe9a6fc --- /dev/null +++ b/packages/ui/components/ui/tx/view/undelegate-claim.tsx @@ -0,0 +1,31 @@ +import { UndelegateClaim } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/component/stake/v1/stake_pb'; +import { ViewBox } from './viewbox'; +import { IdentityKeyComponent } from '../../identity-key-component'; +import { + getStartEpochIndexFromUndelegateClaim, + getValidatorIdentityFromUndelegateClaim, +} from '@penumbra-zone/getters'; +import { ActionDetails } from './action-details'; + +/** Render an `UndelegateClaim` action. */ +export const UndelegateClaimComponent = ({ value }: { value: UndelegateClaim }) => { + const validatorIdentity = getValidatorIdentityFromUndelegateClaim(value); + const startEpochIndex = getStartEpochIndexFromUndelegateClaim(value); + + return ( + + + + + + + {startEpochIndex.toString()} + + + } + /> + ); +}; diff --git a/packages/ui/components/ui/tx/view/viewbox.tsx b/packages/ui/components/ui/tx/view/viewbox.tsx index c92aae33b8..27780085d4 100644 --- a/packages/ui/components/ui/tx/view/viewbox.tsx +++ b/packages/ui/components/ui/tx/view/viewbox.tsx @@ -13,7 +13,7 @@ export const ViewBox = ({ label, visibleContent }: ViewBoxProps) => { return (
diff --git a/packages/wasm/crate/Cargo.lock b/packages/wasm/crate/Cargo.lock index f1c096b15b..1c5d6518ac 100644 --- a/packages/wasm/crate/Cargo.lock +++ b/packages/wasm/crate/Cargo.lock @@ -776,8 +776,8 @@ dependencies = [ [[package]] name = "decaf377-fmd" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "ark-ff", "ark-serialize", @@ -790,8 +790,8 @@ dependencies = [ [[package]] name = "decaf377-ka" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "ark-ff", "decaf377 0.5.0", @@ -2087,8 +2087,8 @@ dependencies = [ [[package]] name = "penumbra-asset" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2109,6 +2109,7 @@ dependencies = [ "ibig", "num-bigint", "once_cell", + "pbjson-types", "penumbra-num", "penumbra-proto", "poseidon377", @@ -2124,8 +2125,8 @@ dependencies = [ [[package]] name = "penumbra-community-pool" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2154,8 +2155,8 @@ dependencies = [ [[package]] name = "penumbra-compact-block" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2186,8 +2187,8 @@ dependencies = [ [[package]] name = "penumbra-dex" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2238,8 +2239,8 @@ dependencies = [ [[package]] name = "penumbra-distributions" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "async-trait", @@ -2254,8 +2255,8 @@ dependencies = [ [[package]] name = "penumbra-fee" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2277,8 +2278,8 @@ dependencies = [ [[package]] name = "penumbra-funding" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "async-trait", @@ -2296,8 +2297,8 @@ dependencies = [ [[package]] name = "penumbra-governance" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2347,8 +2348,8 @@ dependencies = [ [[package]] name = "penumbra-ibc" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2380,8 +2381,8 @@ dependencies = [ [[package]] name = "penumbra-keys" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "aes", "anyhow", @@ -2424,8 +2425,8 @@ dependencies = [ [[package]] name = "penumbra-num" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2460,8 +2461,8 @@ dependencies = [ [[package]] name = "penumbra-proof-params" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ec", @@ -2486,8 +2487,8 @@ dependencies = [ [[package]] name = "penumbra-proto" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "async-trait", @@ -2513,8 +2514,8 @@ dependencies = [ [[package]] name = "penumbra-sct" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2544,8 +2545,8 @@ dependencies = [ [[package]] name = "penumbra-shielded-pool" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2591,8 +2592,8 @@ dependencies = [ [[package]] name = "penumbra-stake" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2632,8 +2633,8 @@ dependencies = [ [[package]] name = "penumbra-tct" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "ark-ed-on-bls12-377", "ark-ff", @@ -2660,8 +2661,8 @@ dependencies = [ [[package]] name = "penumbra-transaction" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "ark-ff", @@ -2710,8 +2711,8 @@ dependencies = [ [[package]] name = "penumbra-txhash" -version = "0.68.2" -source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.2#b37252bb584499f635fee863dcf948ab570361c2" +version = "0.68.3" +source = "git+https://github.com/penumbra-zone/penumbra.git?tag=v0.68.3#7633cee0d7567924bb6f0139b8853bb92cd0d6fe" dependencies = [ "anyhow", "blake2b_simd 1.0.2", diff --git a/packages/wasm/crate/Cargo.toml b/packages/wasm/crate/Cargo.toml index f13f63d69b..8d8755febb 100644 --- a/packages/wasm/crate/Cargo.toml +++ b/packages/wasm/crate/Cargo.toml @@ -15,21 +15,21 @@ default = ["console_error_panic_hook"] mock-database = [] [dependencies] -penumbra-asset = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-asset" } -penumbra-compact-block = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-compact-block", default-features = false } -penumbra-dex = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-dex", default-features = false } -penumbra-fee = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-fee", default-features = false } -penumbra-governance = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-governance", default-features = false } -penumbra-ibc = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-ibc", default-features = false } -penumbra-keys = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-keys" } -penumbra-num = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-num" } -penumbra-proof-params = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-proof-params", default-features = false } -penumbra-proto = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-proto", default-features = false } -penumbra-sct = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-sct", default-features = false } -penumbra-shielded-pool = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-shielded-pool", default-features = false } -penumbra-stake = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-stake", default-features = false } -penumbra-tct = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-tct" } -penumbra-transaction = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.2", package = "penumbra-transaction", default-features = false } +penumbra-asset = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-asset" } +penumbra-compact-block = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-compact-block", default-features = false } +penumbra-dex = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-dex", default-features = false } +penumbra-fee = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-fee", default-features = false } +penumbra-governance = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-governance", default-features = false } +penumbra-ibc = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-ibc", default-features = false } +penumbra-keys = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-keys" } +penumbra-num = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-num" } +penumbra-proof-params = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-proof-params", default-features = false } +penumbra-proto = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-proto", default-features = false } +penumbra-sct = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-sct", default-features = false } +penumbra-shielded-pool = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-shielded-pool", default-features = false } +penumbra-stake = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-stake", default-features = false } +penumbra-tct = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-tct" } +penumbra-transaction = { git = "https://github.com/penumbra-zone/penumbra.git", tag = "v0.68.3", package = "penumbra-transaction", default-features = false } anyhow = "1.0.80" ark-ff = { version = "0.4.2", features = ["std"] } diff --git a/packages/wasm/crate/src/planner.rs b/packages/wasm/crate/src/planner.rs index 33b8b4d5fa..c3fbf08aae 100644 --- a/packages/wasm/crate/src/planner.rs +++ b/packages/wasm/crate/src/planner.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use anyhow::anyhow; use ark_ff::UniformRand; -use decaf377::Fq; +use decaf377::{Fq, Fr}; use penumbra_asset::{asset, Balance, Value}; use penumbra_dex::swap_claim::SwapClaimPlan; use penumbra_dex::{ @@ -22,6 +22,7 @@ use penumbra_proto::view::v1::{ use penumbra_sct::params::SctParameters; use penumbra_shielded_pool::{fmd, OutputPlan, SpendPlan}; use penumbra_stake::rate::RateData; +use penumbra_stake::{IdentityKey, Penalty, UndelegateClaimPlan}; use penumbra_transaction::gas::GasCost; use penumbra_transaction::memo::MemoPlaintext; use penumbra_transaction::{plan::MemoPlan, ActionPlan, TransactionParameters, TransactionPlan}; @@ -314,11 +315,35 @@ pub async fn plan_transaction( actions.push(rate_data.build_undelegate(value.amount).into()); } - /* - for tpr::UndelegateClaim { .. } in request.undelegation_claims { - // need to wait for a new release of monorepo + for tpr::UndelegateClaim { + validator_identity, + start_epoch_index, + penalty, + unbonding_amount, + } in request.undelegation_claims + { + let validator_identity: IdentityKey = validator_identity + .ok_or_else(|| anyhow!("missing validator identity in undelegation claim"))? + .try_into()?; + let penalty: Penalty = penalty + .ok_or_else(|| anyhow!("missing penalty in undelegation claim"))? + .try_into()?; + let unbonding_amount: Amount = unbonding_amount + .ok_or_else(|| anyhow!("missing unbonding amount in undelegation claim"))? + .try_into()?; + + let undelegate_claim_plan = UndelegateClaimPlan { + validator_identity, + start_epoch_index, + penalty, + unbonding_amount, + balance_blinding: Fr::rand(&mut OsRng), + proof_blinding_r: Fq::rand(&mut OsRng), + proof_blinding_s: Fq::rand(&mut OsRng), + }; + + actions.push(ActionPlan::UndelegateClaim(undelegate_claim_plan)); } - */ #[allow(clippy::never_loop)] for ibc::v1::IbcRelay { .. } in request.ibc_relay_actions { diff --git a/packages/wasm/crate/tests/build.rs b/packages/wasm/crate/tests/build.rs index ef0aa7c038..b56e3280e1 100644 --- a/packages/wasm/crate/tests/build.rs +++ b/packages/wasm/crate/tests/build.rs @@ -399,6 +399,7 @@ mod tests { swap_claims: vec![], delegations: vec![], undelegations: vec![], + undelegation_claims: vec![], ibc_relay_actions: vec![], ics20_withdrawals: vec![], position_opens: vec![], diff --git a/packages/wasm/src/build.ts b/packages/wasm/src/build.ts index f64ceb038c..af06d54ad4 100644 --- a/packages/wasm/src/build.ts +++ b/packages/wasm/src/build.ts @@ -59,6 +59,6 @@ export const buildActionParallel = async ( const loadProvingKey = async (actionType: ActionType) => { const keyType = provingKeys[actionType]; if (!keyType) return; - const keyBin = (await fetch(`bin/${actionType}_pk.bin`)).arrayBuffer(); + const keyBin = (await fetch(`bin/${keyType}_pk.bin`)).arrayBuffer(); load_proving_key(await keyBin, keyType); }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b811bb8534..ff3a3f7cce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -438,6 +438,9 @@ importers: packages/getters: dependencies: + '@penumbra-zone/constants': + specifier: workspace:* + version: link:../constants bech32: specifier: ^2.0.0 version: 2.0.0 diff --git a/scripts/delegate-to-validators.sh b/scripts/delegate-to-validators.sh index 6459be425e..365826f3fe 100644 --- a/scripts/delegate-to-validators.sh +++ b/scripts/delegate-to-validators.sh @@ -57,6 +57,6 @@ for ((i=$START_INDEX; i<=$END_INDEX; i++)); do DIRECTORY="$BASE_DIRECTORY/validator-$i" VALIDATOR_IDENTITY_KEY=$(sed -n -E 's/(.*^identity_key = "([^"]+)"$.*)/\2/p' $DIRECTORY/validator.toml) - echo "Delegating $DELEGATION_AMOUNT""penumbra to $VALIDATOR_IDENTITY_KEY" - pcli tx delegate --to $VALIDATOR_IDENTITY_KEY $DELEGATION_AMOUNT"penumbra" & + echo "Running pcli tx delegate --to $VALIDATOR_IDENTITY_KEY $DELEGATION_AMOUNT""penumbra" + pcli tx delegate --to $VALIDATOR_IDENTITY_KEY $DELEGATION_AMOUNT"penumbra" done