From b6e32f8601ffee8ba1d6a5fd629e650aa7c62406 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez <grod220@gmail.com> Date: Tue, 24 Sep 2024 17:52:58 +0200 Subject: [PATCH] Detect relevant IbcRelay actions (#1805) * Save relevate ibc relay tx * changesets * Support ANY parsing w/ registry * review updates * update registry --- .changeset/afraid-coins-give.md | 5 + .changeset/clean-lions-carry.md | 5 + .changeset/old-emus-perform.md | 5 + .changeset/rotten-starfishes-watch.md | 5 + .changeset/smooth-trains-reflect.md | 5 + .changeset/wise-papayas-lay.md | 5 + apps/minifront/package.json | 2 +- .../src/components/tx-details/tx-viewer.tsx | 2 +- packages/bech32m/src/penumbracompat1.ts | 9 ++ packages/protobuf/src/registry.ts | 7 +- .../protobuf/src/services/cosmos-ibc-core.ts | 4 + packages/protobuf/src/web.ts | 10 +- packages/query/src/block-processor.ts | 1 + .../query/src/helpers/identify-txs.test.ts | 79 ++++++++-- packages/query/src/helpers/identify-txs.ts | 51 ++++++- packages/storage/package.json | 4 +- packages/storage/src/indexed-db/index.ts | 5 +- packages/storage/src/indexed-db/stream.ts | 3 +- packages/types/src/address.test.ts | 24 +++ packages/types/src/address.ts | 10 ++ packages/types/src/servers.ts | 6 + packages/ui/components/ui/tx/action-view.tsx | 3 +- .../ui/tx/actions-views/ibc-relay.tsx | 144 ++++++++++++++++++ .../ui/tx/actions-views/isc20-withdrawal.tsx | 3 +- packages/wasm/crate/src/keys.rs | 21 ++- packages/wasm/crate/src/view_server.rs | 12 +- packages/wasm/crate/tests/test_keys.rs | 28 +++- packages/wasm/src/address.ts | 9 +- packages/wasm/src/view-server.ts | 7 +- pnpm-lock.yaml | 16 +- 30 files changed, 430 insertions(+), 60 deletions(-) create mode 100644 .changeset/afraid-coins-give.md create mode 100644 .changeset/clean-lions-carry.md create mode 100644 .changeset/old-emus-perform.md create mode 100644 .changeset/rotten-starfishes-watch.md create mode 100644 .changeset/smooth-trains-reflect.md create mode 100644 .changeset/wise-papayas-lay.md create mode 100644 packages/types/src/address.test.ts create mode 100644 packages/types/src/address.ts create mode 100644 packages/ui/components/ui/tx/actions-views/ibc-relay.tsx diff --git a/.changeset/afraid-coins-give.md b/.changeset/afraid-coins-give.md new file mode 100644 index 0000000000..5419ea0c51 --- /dev/null +++ b/.changeset/afraid-coins-give.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/storage': patch +--- + +Bump registry version diff --git a/.changeset/clean-lions-carry.md b/.changeset/clean-lions-carry.md new file mode 100644 index 0000000000..b72ab37bca --- /dev/null +++ b/.changeset/clean-lions-carry.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/protobuf': minor +--- + +Add MsgUpdateClient to typeRegistry diff --git a/.changeset/old-emus-perform.md b/.changeset/old-emus-perform.md new file mode 100644 index 0000000000..2125f50d63 --- /dev/null +++ b/.changeset/old-emus-perform.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/bech32m': patch +--- + +Add isCompatAddress() func diff --git a/.changeset/rotten-starfishes-watch.md b/.changeset/rotten-starfishes-watch.md new file mode 100644 index 0000000000..86453bf6f6 --- /dev/null +++ b/.changeset/rotten-starfishes-watch.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/wasm': minor +--- + +Add is_controlled_address() support diff --git a/.changeset/smooth-trains-reflect.md b/.changeset/smooth-trains-reflect.md new file mode 100644 index 0000000000..b0560d0661 --- /dev/null +++ b/.changeset/smooth-trains-reflect.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/query': minor +--- + +Check for IbcRelays to add to txs diff --git a/.changeset/wise-papayas-lay.md b/.changeset/wise-papayas-lay.md new file mode 100644 index 0000000000..250d2a5207 --- /dev/null +++ b/.changeset/wise-papayas-lay.md @@ -0,0 +1,5 @@ +--- +'@penumbra-zone/types': minor +--- + +Update ViewServerInterface diff --git a/apps/minifront/package.json b/apps/minifront/package.json index 70664aaeaa..9b2b227eba 100644 --- a/apps/minifront/package.json +++ b/apps/minifront/package.json @@ -22,7 +22,7 @@ "@cosmos-kit/core": "^2.13.1", "@cosmos-kit/react": "^2.18.0", "@interchain-ui/react": "^1.23.29", - "@penumbra-labs/registry": "^11.2.0", + "@penumbra-labs/registry": "^11.3.1", "@penumbra-zone/bech32m": "workspace:*", "@penumbra-zone/client": "workspace:*", "@penumbra-zone/crypto-web": "workspace:*", diff --git a/apps/minifront/src/components/tx-details/tx-viewer.tsx b/apps/minifront/src/components/tx-details/tx-viewer.tsx index b2ce0ee03e..84e0e1570f 100644 --- a/apps/minifront/src/components/tx-details/tx-viewer.tsx +++ b/apps/minifront/src/components/tx-details/tx-viewer.tsx @@ -48,7 +48,7 @@ export const TxViewer = ({ txInfo }: { txInfo?: TransactionInfo }) => { // use React-Query to invoke custom hooks that call async translators. const { data: receiverView } = useQuery( - ['receiverView', txInfo, option], + ['receiverView', txInfo?.toJson({ typeRegistry }), option], () => fetchReceiverView( // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- TODO: justify diff --git a/packages/bech32m/src/penumbracompat1.ts b/packages/bech32m/src/penumbracompat1.ts index 1d8116cc76..8da7caec0b 100644 --- a/packages/bech32m/src/penumbracompat1.ts +++ b/packages/bech32m/src/penumbracompat1.ts @@ -12,4 +12,13 @@ export const compatAddressFromBech32 = (penumbracompat1: string): { [innerName]: [innerName]: fromBech32(penumbracompat1 as `${typeof prefix}1${string}`, prefix), }); +export const isCompatAddress = (check: string): check is `${typeof prefix}1${string}` => { + try { + compatAddressFromBech32(check); + return true; + } catch { + return false; + } +}; + export { PENUMBRA_BECH32M_ADDRESS_LENGTH, PENUMBRA_BECH32M_ADDRESS_PREFIX } from './index.js'; diff --git a/packages/protobuf/src/registry.ts b/packages/protobuf/src/registry.ts index 01c9f1ef03..0fa6d4e009 100644 --- a/packages/protobuf/src/registry.ts +++ b/packages/protobuf/src/registry.ts @@ -1,4 +1,4 @@ -import { IMessageTypeRegistry, createRegistry } from '@bufbuild/protobuf'; +import { createRegistry, IMessageTypeRegistry } from '@bufbuild/protobuf'; import * as ibcCore from './services/cosmos-ibc-core.js'; import * as penumbraCnidarium from './services/penumbra-cnidarium.js'; @@ -6,8 +6,6 @@ import * as penumbraCore from './services/penumbra-core.js'; import * as penumbraCustody from './services/penumbra-custody.js'; import * as penumbraUtil from './services/penumbra-util.js'; import * as penumbraView from './services/penumbra-view.js'; - -import { MsgRecvPacket } from '../gen/ibc/core/channel/v1/tx_pb.js'; import { ClientState, Header } from '../gen/ibc/lightclients/tendermint/v1/tendermint_pb.js'; import { DutchAuction } from '../gen/penumbra/core/component/auction/v1/auction_pb.js'; @@ -38,9 +36,6 @@ export const typeRegistry: IMessageTypeRegistry = createRegistry( ClientState, Header, - // gen/ibc/core/channel/v1/tx_pb - MsgRecvPacket, - // penumbra/core/component/auction/v1/auction_pb DutchAuction, ); diff --git a/packages/protobuf/src/services/cosmos-ibc-core.ts b/packages/protobuf/src/services/cosmos-ibc-core.ts index 7c6e55ba80..17d3049110 100644 --- a/packages/protobuf/src/services/cosmos-ibc-core.ts +++ b/packages/protobuf/src/services/cosmos-ibc-core.ts @@ -1,4 +1,8 @@ export { Query as IbcChannelService } from '../../gen/ibc/core/channel/v1/query_connect.js'; +export { Msg as IbcChannelMsgService } from '../../gen/ibc/core/channel/v1/tx_connect.js'; + export { Query as IbcClientService } from '../../gen/ibc/core/client/v1/query_connect.js'; export { Msg as IbcClientMsgService } from '../../gen/ibc/core/client/v1/tx_connect.js'; + export { Query as IbcConnectionService } from '../../gen/ibc/core/connection/v1/query_connect.js'; +export { Msg as IbcConnectionMsgService } from '../../gen/ibc/core/connection/v1/tx_connect.js'; diff --git a/packages/protobuf/src/web.ts b/packages/protobuf/src/web.ts index 27e5e837d8..10ec114021 100644 --- a/packages/protobuf/src/web.ts +++ b/packages/protobuf/src/web.ts @@ -1,6 +1,9 @@ -import type { +import { + IbcChannelMsgService, IbcChannelService, + IbcClientMsgService, IbcClientService, + IbcConnectionMsgService, IbcConnectionService, } from './services/cosmos-ibc-core.js'; import type { CustodyService } from './services/penumbra-custody.js'; @@ -11,11 +14,11 @@ import type { CommunityPoolService, CompactBlockService, DexService, - SimulationService, FeeService, GovernanceService, SctService, ShieldedPoolService, + SimulationService, StakeService, } from './services/penumbra-core.js'; import type { TendermintProxyService } from './services/penumbra-util.js'; @@ -30,8 +33,11 @@ export type PenumbraService = | typeof FeeService | typeof GovernanceService | typeof IbcChannelService + | typeof IbcChannelMsgService | typeof IbcClientService + | typeof IbcClientMsgService | typeof IbcConnectionService + | typeof IbcConnectionMsgService | typeof SctService | typeof ShieldedPoolService | typeof SimulationService diff --git a/packages/query/src/block-processor.ts b/packages/query/src/block-processor.ts index 74002b2eba..9cf35433d1 100644 --- a/packages/query/src/block-processor.ts +++ b/packages/query/src/block-processor.ts @@ -310,6 +310,7 @@ export class BlockProcessor implements BlockProcessorInterface { spentNullifiers, recordsByCommitment, blockTx, + addr => this.viewServer.isControlledAddress(addr), ); // this simply stores the new records with 'rehydrated' sources to idb diff --git a/packages/query/src/helpers/identify-txs.test.ts b/packages/query/src/helpers/identify-txs.test.ts index 943ede38b2..7f93a4f717 100644 --- a/packages/query/src/helpers/identify-txs.test.ts +++ b/packages/query/src/helpers/identify-txs.test.ts @@ -1,8 +1,5 @@ import { describe, expect, test } from 'vitest'; -import { - CommitmentSource, - Nullifier, -} from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb'; +import { Nullifier } from '@penumbra-zone/protobuf/penumbra/core/component/sct/v1/sct_pb'; import { StateCommitment } from '@penumbra-zone/protobuf/penumbra/crypto/tct/v1/tct_pb'; import { Action, @@ -10,6 +7,7 @@ import { TransactionBody, } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; import { + BLANK_TX_SOURCE, getCommitmentsFromActions, getNullifiersFromActions, identifyTransactions, @@ -27,10 +25,15 @@ import { SwapClaimBody, } from '@penumbra-zone/protobuf/penumbra/core/component/dex/v1/dex_pb'; import { SpendableNoteRecord, SwapRecord } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb'; - -const BLANK_TX_SOURCE = new CommitmentSource({ - source: { case: 'transaction', value: { id: new Uint8Array() } }, -}); +import { Any } from '@bufbuild/protobuf'; +import { + FungibleTokenPacketData, + IbcRelay, +} from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb'; +import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; +import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { Packet } from '@penumbra-zone/protobuf/ibc/core/channel/v1/channel_pb'; +import { MsgRecvPacket } from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; describe('getCommitmentsFromActions', () => { test('returns empty array when tx.body.actions is undefined', () => { @@ -213,7 +216,12 @@ describe('identifyTransactions', () => { const spentNullifiers = new Set<Nullifier>(); const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>(); - const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx); + const result = await identifyTransactions( + spentNullifiers, + commitmentRecords, + blockTx, + () => false, + ); expect(result.relevantTxs).toEqual([]); expect(result.recoveredSourceRecords).toEqual([]); @@ -311,12 +319,17 @@ describe('identifyTransactions', () => { const spentNullifiersBeforeSize = spentNullifiers.size; const commitmentRecordsBeforeSize = commitmentRecords.size; - const result = await identifyTransactions(spentNullifiers, commitmentRecords, [ - tx1, // relevant - tx2, // relevant - tx3, // not - tx4, // not - ]); + const result = await identifyTransactions( + spentNullifiers, + commitmentRecords, + [ + tx1, // relevant + tx2, // relevant + tx3, // not + tx4, // not + ], + () => false, + ); expect(result.relevantTxs.length).toBe(2); expect(result.recoveredSourceRecords.length).toBe(1); @@ -328,4 +341,40 @@ describe('identifyTransactions', () => { expect(spentNullifiersBeforeSize).toEqual(spentNullifiers.size); expect(commitmentRecordsBeforeSize).toEqual(commitmentRecords.size); }); + + test('identifies ibc relays', async () => { + const knownAddr = + 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4'; + const unknownAddr = + 'penumbracompat1147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahhwqq0da'; + const tx = new Transaction({ + body: { + actions: [createIbcRelay(knownAddr), createIbcRelay(unknownAddr)], + }, + }); + const blockTx = [tx]; + const spentNullifiers = new Set<Nullifier>(); + const commitmentRecords = new Map<StateCommitment, SpendableNoteRecord | SwapRecord>(); + + const result = await identifyTransactions(spentNullifiers, commitmentRecords, blockTx, addr => + addr.equals(new Address(addressFromBech32m(knownAddr))), + ); + + expect(result.relevantTxs.length).toBe(1); + expect(result.relevantTxs[0]?.data.equals(tx)).toBeTruthy(); + expect(result.recoveredSourceRecords.length).toBe(0); + }); }); + +const createIbcRelay = (receiver: string): Action => { + const tokenPacketData = new FungibleTokenPacketData({ receiver }); + const encoder = new TextEncoder(); + const relevantRelay = Any.pack( + new MsgRecvPacket({ + packet: new Packet({ data: encoder.encode(tokenPacketData.toJsonString()) }), + }), + ); + return new Action({ + action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) }, + }); +}; diff --git a/packages/query/src/helpers/identify-txs.ts b/packages/query/src/helpers/identify-txs.ts index 389c121f63..43068ddc20 100644 --- a/packages/query/src/helpers/identify-txs.ts +++ b/packages/query/src/helpers/identify-txs.ts @@ -7,11 +7,50 @@ import { SpendableNoteRecord, SwapRecord } from '@penumbra-zone/protobuf/penumbr import { Transaction } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; import { TransactionId } from '@penumbra-zone/protobuf/penumbra/core/txhash/v1/txhash_pb'; import { sha256Hash } from '@penumbra-zone/crypto-web/sha256'; +import { MsgRecvPacket } from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; +import { FungibleTokenPacketData } from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb'; +import { ViewServerInterface } from '@penumbra-zone/types/servers'; +import { parseIntoAddr } from '@penumbra-zone/types/address'; -const BLANK_TX_SOURCE = new CommitmentSource({ +export const BLANK_TX_SOURCE = new CommitmentSource({ source: { case: 'transaction', value: { id: new Uint8Array() } }, }); +// Identifies if a tx with a relay action of which the receiver is the user +const hasRelevantIbcRelay = ( + tx: Transaction, + isControlledAddr: ViewServerInterface['isControlledAddress'], +) => { + return tx.body?.actions.some(action => { + if (action.action.case !== 'ibcRelayAction') { + return false; + } + + if (!action.action.value.rawAction?.is(MsgRecvPacket.typeName)) { + return false; + } + + const recvPacket = new MsgRecvPacket(); + const success = action.action.value.rawAction.unpackTo(recvPacket); + if (!success) { + throw new Error('Error while trying to unpack Any to MsgRecvPacket'); + } + + if (!recvPacket.packet?.data) { + throw new Error('No FungibleTokenPacketData MsgRecvPacket'); + } + + try { + const dataString = new TextDecoder().decode(recvPacket.packet.data); + const { receiver } = FungibleTokenPacketData.fromJsonString(dataString); + const receivingAddr = parseIntoAddr(receiver); + return isControlledAddr(receivingAddr); + } catch (e) { + return false; + } + }); +}; + // Used as a type-check helper as .filter(Boolean) still results with undefined as a possible value const isDefined = <T>(value: T | null | undefined): value is NonNullable<T> => value !== null && value !== undefined; @@ -70,6 +109,7 @@ const searchRelevant = async ( tx: Transaction, spentNullifiers: Set<Nullifier>, commitmentRecords: Map<StateCommitment, SpendableNoteRecord | SwapRecord>, + isControlledAddr: ViewServerInterface['isControlledAddress'], ): Promise< { relevantTx: RelevantTx; recoveredSourceRecords: RecoveredSourceRecords } | undefined > => { @@ -99,6 +139,10 @@ const searchRelevant = async ( } } + if (hasRelevantIbcRelay(tx, isControlledAddr)) { + txId ??= await generateTxId(tx); + } + if (txId) { return { relevantTx: { id: txId, data: tx }, @@ -115,6 +159,7 @@ export const identifyTransactions = async ( spentNullifiers: Set<Nullifier>, commitmentRecords: Map<StateCommitment, SpendableNoteRecord | SwapRecord>, blockTx: Transaction[], + isControlledAddr: ViewServerInterface['isControlledAddress'], ): Promise<{ relevantTxs: RelevantTx[]; recoveredSourceRecords: RecoveredSourceRecords; @@ -122,7 +167,9 @@ export const identifyTransactions = async ( const relevantTxs: RelevantTx[] = []; const recoveredSourceRecords: RecoveredSourceRecords = []; - const searchPromises = blockTx.map(tx => searchRelevant(tx, spentNullifiers, commitmentRecords)); + const searchPromises = blockTx.map(tx => + searchRelevant(tx, spentNullifiers, commitmentRecords, isControlledAddr), + ); const results = await Promise.all(searchPromises); for (const result of results) { diff --git a/packages/storage/package.json b/packages/storage/package.json index 1f6df81f40..5fb091d8e3 100644 --- a/packages/storage/package.json +++ b/packages/storage/package.json @@ -39,13 +39,13 @@ "idb": "^8.0.0" }, "devDependencies": { - "@penumbra-labs/registry": "^11.2.0", + "@penumbra-labs/registry": "^11.3.1", "@penumbra-zone/protobuf": "workspace:*", "fetch-mock": "^10.0.7" }, "peerDependencies": { "@bufbuild/protobuf": "^1.10.0", - "@penumbra-labs/registry": "^11.2.0", + "@penumbra-labs/registry": "^11.3.1", "@penumbra-zone/bech32m": "workspace:*", "@penumbra-zone/getters": "workspace:*", "@penumbra-zone/protobuf": "workspace:*", diff --git a/packages/storage/src/indexed-db/index.ts b/packages/storage/src/indexed-db/index.ts index 00682acf85..67b9227eba 100644 --- a/packages/storage/src/indexed-db/index.ts +++ b/packages/storage/src/indexed-db/index.ts @@ -64,6 +64,7 @@ import { PartialMessage, PlainMessage } from '@bufbuild/protobuf'; import { getAmountFromRecord } from '@penumbra-zone/getters/spendable-note-record'; import { isZero } from '@penumbra-zone/types/amount'; import { IDB_VERSION } from './config.js'; +import { typeRegistry } from '@penumbra-zone/protobuf'; const assertBytes = (v?: Uint8Array, expect?: number, name = 'value'): v is Uint8Array => { if (expect !== undefined && v?.length !== expect) { @@ -363,7 +364,7 @@ export class IndexedDb implements IndexedDbInterface { const tx = new TransactionInfo({ id, height, transaction }); await this.u.update({ table: 'TRANSACTIONS', - value: tx.toJson() as Jsonified<TransactionInfo>, + value: tx.toJson({ typeRegistry }) as Jsonified<TransactionInfo>, }); } @@ -374,7 +375,7 @@ export class IndexedDb implements IndexedDbInterface { if (!jsonRecord) { return undefined; } - return TransactionInfo.fromJson(jsonRecord); + return TransactionInfo.fromJson(jsonRecord, { typeRegistry }); } async getFmdParams(): Promise<FmdParameters | undefined> { diff --git a/packages/storage/src/indexed-db/stream.ts b/packages/storage/src/indexed-db/stream.ts index eb760f708b..60f472958a 100644 --- a/packages/storage/src/indexed-db/stream.ts +++ b/packages/storage/src/indexed-db/stream.ts @@ -1,6 +1,7 @@ import { AnyMessage, JsonValue, Message, MessageType } from '@bufbuild/protobuf'; import { IDBPCursorWithValue } from 'idb'; import type { PenumbraDb, PenumbraStoreNames } from '@penumbra-zone/types/indexed-db'; +import { typeRegistry } from '@penumbra-zone/protobuf'; export class IdbCursorSource<N extends PenumbraStoreNames, T extends Message<T> = AnyMessage> implements UnderlyingDefaultSource<T> @@ -15,7 +16,7 @@ export class IdbCursorSource<N extends PenumbraStoreNames, T extends Message<T> void (async () => { let cursor = await this.cursor; while (cursor) { - cont.enqueue(this.messageType.fromJson(cursor.value as JsonValue)); + cont.enqueue(this.messageType.fromJson(cursor.value as JsonValue, { typeRegistry })); cursor = await cursor.continue(); } cont.close(); diff --git a/packages/types/src/address.test.ts b/packages/types/src/address.test.ts new file mode 100644 index 0000000000..9751b2d541 --- /dev/null +++ b/packages/types/src/address.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, test } from 'vitest'; +import { parseIntoAddr } from './address.js'; + +describe('parseIntoAddr', () => { + test('works with compat', () => { + expect(() => + parseIntoAddr( + 'penumbracompat1147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahhwqq0da', + ), + ).not.toThrow(); + }); + + test('works with normal addresses', () => { + expect(() => + parseIntoAddr( + 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4', + ), + ).not.toThrow(); + }); + + test('raises on invalid addresses', () => { + expect(() => parseIntoAddr('not_valid_format')).toThrow(); + }); +}); diff --git a/packages/types/src/address.ts b/packages/types/src/address.ts new file mode 100644 index 0000000000..97f1182f6c --- /dev/null +++ b/packages/types/src/address.ts @@ -0,0 +1,10 @@ +import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; +import { compatAddressFromBech32, isCompatAddress } from '@penumbra-zone/bech32m/penumbracompat1'; + +export const parseIntoAddr = (addrStr: string): Address => { + if (isCompatAddress(addrStr)) { + return new Address(compatAddressFromBech32(addrStr)); + } + return new Address(addressFromBech32m(addrStr)); +}; diff --git a/packages/types/src/servers.ts b/packages/types/src/servers.ts index b90d50a846..8d05ec4977 100644 --- a/packages/types/src/servers.ts +++ b/packages/types/src/servers.ts @@ -1,10 +1,16 @@ import { ScanBlockResult } from './state-commitment-tree.js'; import { CompactBlock } from '@penumbra-zone/protobuf/penumbra/core/component/compact_block/v1/compact_block_pb'; import { MerkleRoot } from '@penumbra-zone/protobuf/penumbra/crypto/tct/v1/tct_pb'; +import { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; export interface ViewServerInterface { scanBlock(compactBlock: CompactBlock, skipTrialDecrypt: boolean): Promise<boolean>; + flushUpdates(): ScanBlockResult; + resetTreeToStored(): Promise<void>; + getSctRoot(): MerkleRoot; + + isControlledAddress(address: Address): boolean; } diff --git a/packages/ui/components/ui/tx/action-view.tsx b/packages/ui/components/ui/tx/action-view.tsx index fdecac6e23..b32f040d9c 100644 --- a/packages/ui/components/ui/tx/action-view.tsx +++ b/packages/ui/components/ui/tx/action-view.tsx @@ -17,6 +17,7 @@ import { ValidatorVoteComponent } from './actions-views/validator-vote.tsx'; import { PositionOpenComponent } from './actions-views/position-open.tsx'; import { PositionCloseComponent } from './actions-views/position-close.tsx'; import { PositionWithdrawComponent } from './actions-views/position-withdraw.tsx'; +import { IbcRelayComponent } from './actions-views/ibc-relay.tsx'; type Case = Exclude<ActionView['actionView']['case'], undefined>; @@ -112,7 +113,7 @@ export const ActionViewComponent = ({ return <UnimplementedView label='Validator Definition' />; case 'ibcRelayAction': - return <UnimplementedView label='IBC Relay Action' />; + return <IbcRelayComponent value={actionView.value} />; case 'proposalSubmit': return <UnimplementedView label='Proposal Submit' />; diff --git a/packages/ui/components/ui/tx/actions-views/ibc-relay.tsx b/packages/ui/components/ui/tx/actions-views/ibc-relay.tsx new file mode 100644 index 0000000000..c911f6e490 --- /dev/null +++ b/packages/ui/components/ui/tx/actions-views/ibc-relay.tsx @@ -0,0 +1,144 @@ +import { ViewBox } from '../viewbox'; +import { ActionDetails } from './action-details'; +import { + FungibleTokenPacketData, + IbcRelay, +} from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb'; +import { MsgRecvPacket } from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb'; +import { MsgUpdateClient } from '@penumbra-zone/protobuf/ibc/core/client/v1/tx_pb'; +import { UnimplementedView } from './unimplemented-view.tsx'; +import { uint8ArrayToBase64 } from '@penumbra-zone/types/base64'; +import { getUtcTime } from './isc20-withdrawal.tsx'; +import { useMemo } from 'react'; + +// Packet data stored as json string encoded into bytes +const parsePacket = ({ packet }: MsgRecvPacket): FungibleTokenPacketData | undefined => { + if (!packet?.data) { + return undefined; + } + + try { + const dataString = new TextDecoder().decode(packet.data); + return FungibleTokenPacketData.fromJsonString(dataString); + } catch (e) { + return undefined; + } +}; + +const MsgResvComponent = ({ packet }: { packet: MsgRecvPacket }) => { + const packetData = useMemo(() => parsePacket(packet), [packet]); + + return ( + <ViewBox + label='IBC Relay: Msg Received' + visibleContent={ + <ActionDetails> + {packetData === undefined && ( + <ActionDetails.Row label='Data'>Unknown packet data</ActionDetails.Row> + )} + {!!packetData?.sender && ( + <ActionDetails.Row label='Sender'> + <ActionDetails.TruncatedText>{packetData.sender}</ActionDetails.TruncatedText> + </ActionDetails.Row> + )} + {!!packetData?.receiver && ( + <ActionDetails.Row label='Receiver'> + <ActionDetails.TruncatedText>{packetData.receiver}</ActionDetails.TruncatedText> + </ActionDetails.Row> + )} + {!!packetData?.denom && ( + <ActionDetails.Row label='Denom'>{packetData.denom}</ActionDetails.Row> + )} + {!!packetData?.amount && ( + <ActionDetails.Row label='Amount'>{packetData.amount}</ActionDetails.Row> + )} + {packetData && 'memo' in packetData && ( + <ActionDetails.Row label='Memo'>{packetData.memo}</ActionDetails.Row> + )} + {!!packet.packet?.sequence && ( + <ActionDetails.Row label='Sequence'>{Number(packet.packet.sequence)}</ActionDetails.Row> + )} + {!!packet.packet?.sourcePort && ( + <ActionDetails.Row label='Source Port'>{packet.packet.sourcePort}</ActionDetails.Row> + )} + {!!packet.packet?.sourceChannel && ( + <ActionDetails.Row label='Source Channel'> + {packet.packet.sourceChannel} + </ActionDetails.Row> + )} + {!!packet.packet?.destinationPort && ( + <ActionDetails.Row label='Destination Port'> + {packet.packet.destinationPort} + </ActionDetails.Row> + )} + {!!packet.packet?.destinationChannel && ( + <ActionDetails.Row label='Destination Channel'> + {packet.packet.destinationChannel} + </ActionDetails.Row> + )} + {!!packet.packet?.timeoutHeight?.revisionHeight && ( + <ActionDetails.Row label='Timeout revision height'> + {Number(packet.packet.timeoutHeight.revisionHeight)} + </ActionDetails.Row> + )} + {!!packet.packet?.timeoutHeight?.revisionNumber && ( + <ActionDetails.Row label='Timeout revision number'> + {Number(packet.packet.timeoutHeight.revisionNumber)} + </ActionDetails.Row> + )} + {!!packet.packet?.timeoutTimestamp && ( + <ActionDetails.Row label='Timeout timestamp'> + {getUtcTime(packet.packet.timeoutTimestamp)} + </ActionDetails.Row> + )} + <ActionDetails.Row label='Signer'>{packet.signer}</ActionDetails.Row> + {!!packet.proofHeight?.revisionHeight && ( + <ActionDetails.Row label='Proof revision height'> + {Number(packet.proofHeight.revisionHeight)} + </ActionDetails.Row> + )} + {!!packet.proofHeight?.revisionNumber && ( + <ActionDetails.Row label='Proof revision number'> + {Number(packet.proofHeight.revisionNumber)} + </ActionDetails.Row> + )} + <ActionDetails.Row label='Proof commitment'> + <ActionDetails.TruncatedText> + {uint8ArrayToBase64(packet.proofCommitment)} + </ActionDetails.TruncatedText> + </ActionDetails.Row> + </ActionDetails> + } + /> + ); +}; + +const UpdateClientComponent = ({ update }: { update: MsgUpdateClient }) => { + return ( + <ViewBox + label='IBC Relay: Update Client' + visibleContent={ + <ActionDetails> + <ActionDetails.Row label='Client id'>{update.clientId}</ActionDetails.Row> + <ActionDetails.Row label='Signer'>{update.signer}</ActionDetails.Row> + </ActionDetails> + } + /> + ); +}; + +export const IbcRelayComponent = ({ value }: { value: IbcRelay }) => { + if (value.rawAction?.is(MsgRecvPacket.typeName)) { + const packet = new MsgRecvPacket(); + value.rawAction.unpackTo(packet); + return <MsgResvComponent packet={packet} />; + } + + if (value.rawAction?.is(MsgUpdateClient.typeName)) { + const update = new MsgUpdateClient(); + value.rawAction.unpackTo(update); + return <UpdateClientComponent update={update} />; + } + + return <UnimplementedView label='IBC Relay' />; +}; diff --git a/packages/ui/components/ui/tx/actions-views/isc20-withdrawal.tsx b/packages/ui/components/ui/tx/actions-views/isc20-withdrawal.tsx index 1b3148e93a..8492fa70d5 100644 --- a/packages/ui/components/ui/tx/actions-views/isc20-withdrawal.tsx +++ b/packages/ui/components/ui/tx/actions-views/isc20-withdrawal.tsx @@ -4,7 +4,8 @@ import { ActionDetails } from './action-details'; import { joinLoHiAmount } from '@penumbra-zone/types/amount'; import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra'; -const getUtcTime = (time: bigint) => { +// Converts nanoseconds timestamp to UTC timestamp string +export const getUtcTime = (time: bigint) => { const formatter = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium', timeStyle: 'long', diff --git a/packages/wasm/crate/src/keys.rs b/packages/wasm/crate/src/keys.rs index 739919d27c..209d26a350 100644 --- a/packages/wasm/crate/src/keys.rs +++ b/packages/wasm/crate/src/keys.rs @@ -8,8 +8,7 @@ use penumbra_proof_params::{ SPEND_PROOF_PROVING_KEY, SWAPCLAIM_PROOF_PROVING_KEY, SWAP_PROOF_PROVING_KEY, }; use penumbra_proto::core::keys::v1 as pb; -use penumbra_proto::core::keys::v1::WalletId; -use penumbra_proto::{DomainType, Message}; +use penumbra_proto::DomainType; use rand_core::OsRng; use wasm_bindgen::prelude::*; @@ -75,9 +74,7 @@ pub fn get_wallet_id(full_viewing_key: &[u8]) -> WasmResult<Vec<u8>> { utils::set_panic_hook(); let fvk: FullViewingKey = FullViewingKey::decode(full_viewing_key)?; - // Can do `fvk.wallet_id().encode_to_vec()` when Domain impl added to WalletId in core - let wallet_id_proto = WalletId::from(fvk.wallet_id()); - Ok(wallet_id_proto.encode_to_vec()) + Ok(fvk.wallet_id().encode_to_vec()) } /// get address by index using FVK @@ -125,3 +122,17 @@ pub fn get_index_by_address(full_viewing_key: &[u8], address: &[u8]) -> WasmResu let result = serde_wasm_bindgen::to_value(&index)?; Ok(result) } + +/// Checks if address is controlled by full viewing key provided +#[wasm_bindgen] +pub fn is_controlled_address(full_viewing_key: &[u8], address: &[u8]) -> WasmResult<bool> { + utils::set_panic_hook(); + + let address: Address = Address::decode(address)?; + let fvk: FullViewingKey = FullViewingKey::decode(full_viewing_key)?; + Ok(is_controlled_inner(&fvk, &address)) +} + +pub fn is_controlled_inner(fvk: &FullViewingKey, address: &Address) -> bool { + fvk.address_index(address).is_some() +} diff --git a/packages/wasm/crate/src/view_server.rs b/packages/wasm/crate/src/view_server.rs index 0e23b2dea2..f2e67a480b 100644 --- a/packages/wasm/crate/src/view_server.rs +++ b/packages/wasm/crate/src/view_server.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use indexed_db_futures::IdbDatabase; use penumbra_asset::asset::{Id, Metadata}; use penumbra_compact_block::{CompactBlock, StatePayload}; -use penumbra_keys::FullViewingKey; +use penumbra_keys::{Address, FullViewingKey}; use penumbra_proto::DomainType; use penumbra_sct::Nullifier; use penumbra_shielded_pool::note; @@ -17,6 +17,7 @@ use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; use crate::error::WasmResult; +use crate::keys::is_controlled_inner; use crate::note_record::SpendableNoteRecord; use crate::storage::{init_idb_storage, Storage}; use crate::swap_record::SwapRecord; @@ -311,6 +312,15 @@ impl ViewServer { let root = self.sct.root(); Ok(root.encode_to_vec()) } + + /// Checks if address is controlled by view server full viewing key + #[wasm_bindgen] + pub fn is_controlled_address(&self, address: &[u8]) -> WasmResult<bool> { + utils::set_panic_hook(); + + let address: Address = Address::decode(address)?; + Ok(is_controlled_inner(&self.fvk, &address)) + } } pub fn load_tree(stored_tree: StoredTree) -> Tree { diff --git a/packages/wasm/crate/tests/test_keys.rs b/packages/wasm/crate/tests/test_keys.rs index 5df81218d8..8f295b5c36 100644 --- a/packages/wasm/crate/tests/test_keys.rs +++ b/packages/wasm/crate/tests/test_keys.rs @@ -1,10 +1,13 @@ extern crate core; +use penumbra_keys::keys::{AddressIndex, Bip44Path, SeedPhrase, SpendKey}; use penumbra_keys::FullViewingKey; use penumbra_proto::core::keys::v1::WalletId; use penumbra_proto::{DomainType, Message}; -use penumbra_wasm::keys::get_wallet_id; +use penumbra_wasm::keys::{get_wallet_id, is_controlled_inner}; +use rand_core::OsRng; use std::str::FromStr; +use wasm_bindgen_test::wasm_bindgen_test; #[test] fn successfully_get_wallet_id() { @@ -24,11 +27,28 @@ fn successfully_get_wallet_id() { assert_eq!(expected_bech32_str, walet_id_str); } -#[test] -#[ignore] -// revise this test since we cannot call wasm-bindgen imported functions on non-wasm targets +#[wasm_bindgen_test] fn raises_if_fvk_invalid() { let fvk = FullViewingKey::from_str("penumbrafullviewingkey1sjeaceqzgaeye2ksnz8q73mp6rpx2ykdtzs8wurrnhwdn8vqwuxhxtjdndrjc74udjh0uch0tatnrd93q50wp9pfk86h3lgpew8lsqsz2a6la").unwrap(); let err = get_wallet_id(fvk.encode_to_vec().as_slice()).unwrap_err(); assert_eq!("invalid length", err.to_string()); } + +#[test] +fn detects_controlled_addr() { + let fvk = FullViewingKey::from_str("penumbrafullviewingkey1sjeaceqzgaeye2ksnz8q73mp6rpx2ykdtzs8wurrnhwdn8vqwuxhxtjdndrjc74udjh0uch0tatnrd93q50wp9pfk86h3lgpew8lsqsz2a6la").unwrap(); + let (addr, _) = fvk.payment_address(AddressIndex::new(0)); + assert!(is_controlled_inner(&fvk, &addr)); +} + +#[test] +fn returns_false_on_unknown_addr() { + let fvk = FullViewingKey::from_str("penumbrafullviewingkey1sjeaceqzgaeye2ksnz8q73mp6rpx2ykdtzs8wurrnhwdn8vqwuxhxtjdndrjc74udjh0uch0tatnrd93q50wp9pfk86h3lgpew8lsqsz2a6la").unwrap(); + let other_address = + SpendKey::from_seed_phrase_bip44(SeedPhrase::generate(OsRng), &Bip44Path::new(0)) + .full_viewing_key() + .incoming() + .payment_address(AddressIndex::from(0u32)) + .0; + assert!(!is_controlled_inner(&fvk, &other_address)); +} diff --git a/packages/wasm/src/address.ts b/packages/wasm/src/address.ts index 12496436b1..99f8c128f2 100644 --- a/packages/wasm/src/address.ts +++ b/packages/wasm/src/address.ts @@ -1,4 +1,4 @@ -import { get_index_by_address } from '../wasm/index.js'; +import { get_index_by_address, is_controlled_address } from '../wasm/index.js'; import { Address, AddressIndex, @@ -19,10 +19,5 @@ export const isControlledAddress = (fullViewingKey: FullViewingKey, address?: Ad if (!address) { return false; } - - const viewableIndex = get_index_by_address( - fullViewingKey.toBinary(), - address.toBinary(), - ) as JsonValue; - return Boolean(viewableIndex); + return is_controlled_address(fullViewingKey.toBinary(), address.toBinary()); }; diff --git a/packages/wasm/src/view-server.ts b/packages/wasm/src/view-server.ts index bde894a12f..232ac86702 100644 --- a/packages/wasm/src/view-server.ts +++ b/packages/wasm/src/view-server.ts @@ -10,7 +10,8 @@ import { } from '@penumbra-zone/types/state-commitment-tree'; import type { IdbConstants } from '@penumbra-zone/types/indexed-db'; import type { ViewServerInterface } from '@penumbra-zone/types/servers'; -import { FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { Address, FullViewingKey } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; +import { isControlledAddress } from './address.js'; declare global { // eslint-disable-next-line no-var -- TODO: explain @@ -92,4 +93,8 @@ export class ViewServer implements ViewServerInterface { newSwaps: (new_swaps ?? []).map(s => SwapRecord.fromJson(s)), }; } + + isControlledAddress(address: Address): boolean { + return isControlledAddress(this.fullViewingKey, address); + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e3798aa9d..03d4d34189 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -180,8 +180,8 @@ importers: specifier: ^1.23.29 version: 1.23.29(@types/react@18.3.3)(babel-plugin-macros@3.1.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@penumbra-labs/registry': - specifier: ^11.2.0 - version: 11.2.0 + specifier: ^11.3.1 + version: 11.3.1 '@penumbra-zone/bech32m': specifier: workspace:* version: link:../../packages/bech32m @@ -539,8 +539,8 @@ importers: version: 8.0.0 devDependencies: '@penumbra-labs/registry': - specifier: ^11.2.0 - version: 11.2.0 + specifier: ^11.3.1 + version: 11.3.1 '@penumbra-zone/protobuf': specifier: workspace:* version: link:../protobuf @@ -739,7 +739,7 @@ importers: version: 3.0.1 postcss: specifier: ^8.4.38 - version: 7.0.39 + version: 8.4.39 react: specifier: ^18.3.1 version: 18.3.1 @@ -2905,8 +2905,8 @@ packages: resolution: {integrity: sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==} engines: {node: '>= 10.0.0'} - '@penumbra-labs/registry@11.2.0': - resolution: {integrity: sha512-kGxu6KWjFl581eEYMk9/ODehrxJT9YB9TxNxJOYSKXt+8ni9kovovcj5jdP7MkP7bGHrNEssSziWvjCg2nfuug==} + '@penumbra-labs/registry@11.3.1': + resolution: {integrity: sha512-0hBfPZW4Y3my6RzYSBGI3cwutW+C7KJXn5OLXOhhXPsH+VlexrxvKIWc8nJeUwRCTtBkRR0lUSwuIhnaw0tsyQ==} '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -14310,7 +14310,7 @@ snapshots: '@parcel/watcher-win32-ia32': 2.4.1 '@parcel/watcher-win32-x64': 2.4.1 - '@penumbra-labs/registry@11.2.0': {} + '@penumbra-labs/registry@11.3.1': {} '@pkgjs/parseargs@0.11.0': optional: true