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/protobuf/src/registry.ts b/packages/protobuf/src/registry.ts index 8b00b78240..0fa6d4e009 100644 --- a/packages/protobuf/src/registry.ts +++ b/packages/protobuf/src/registry.ts @@ -6,9 +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 { MsgUpdateClient } from '../gen/ibc/core/client/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'; @@ -39,12 +36,6 @@ export const typeRegistry: IMessageTypeRegistry = createRegistry( ClientState, Header, - // gen/ibc/core/channel/v1/tx_pb - MsgRecvPacket, - - // gen/ibc/core/client/v1/tx_pb - MsgUpdateClient, - // 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 ce727e1eeb..17d3049110 100644 --- a/packages/protobuf/src/services/cosmos-ibc-core.ts +++ b/packages/protobuf/src/services/cosmos-ibc-core.ts @@ -1,5 +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 { Msg as IbcChannelMsgService } from '../../gen/ibc/core/channel/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 7495e64b98..10ec114021 100644 --- a/packages/protobuf/src/web.ts +++ b/packages/protobuf/src/web.ts @@ -3,6 +3,7 @@ import { IbcChannelService, IbcClientMsgService, IbcClientService, + IbcConnectionMsgService, IbcConnectionService, } from './services/cosmos-ibc-core.js'; import type { CustodyService } from './services/penumbra-custody.js'; @@ -36,6 +37,7 @@ export type PenumbraService = | typeof IbcClientService | typeof IbcClientMsgService | typeof IbcConnectionService + | typeof IbcConnectionMsgService | typeof SctService | typeof ShieldedPoolService | typeof SimulationService diff --git a/packages/query/src/helpers/identify-txs.test.ts b/packages/query/src/helpers/identify-txs.test.ts index 3699d17979..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,10 +7,10 @@ import { TransactionBody, } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb'; import { + BLANK_TX_SOURCE, getCommitmentsFromActions, getNullifiersFromActions, identifyTransactions, - parseIntoAddr, } from './identify-txs.js'; import { Output, @@ -38,10 +35,6 @@ 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'; -const BLANK_TX_SOURCE = new CommitmentSource({ - source: { case: 'transaction', value: { id: new Uint8Array() } }, -}); - describe('getCommitmentsFromActions', () => { test('returns empty array when tx.body.actions is undefined', () => { const tx = new Transaction(); @@ -385,25 +378,3 @@ const createIbcRelay = (receiver: string): Action => { action: { case: 'ibcRelayAction', value: new IbcRelay({ rawAction: relevantRelay }) }, }); }; - -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/query/src/helpers/identify-txs.ts b/packages/query/src/helpers/identify-txs.ts index c6935f1aac..43068ddc20 100644 --- a/packages/query/src/helpers/identify-txs.ts +++ b/packages/query/src/helpers/identify-txs.ts @@ -8,25 +8,16 @@ import { Transaction } from '@penumbra-zone/protobuf/penumbra/core/transaction/v 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 { Address } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb'; import { FungibleTokenPacketData } from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb'; -import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra'; -import { compatAddressFromBech32, isCompatAddress } from '@penumbra-zone/bech32m/penumbracompat1'; 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() } }, }); -export const parseIntoAddr = (addrStr: string): Address => { - if (isCompatAddress(addrStr)) { - return new Address(compatAddressFromBech32(addrStr)); - } - return new Address(addressFromBech32m(addrStr)); -}; - // Identifies if a tx with a relay action of which the receiver is the user -export const hasRelevantIbcRelay = ( +const hasRelevantIbcRelay = ( tx: Transaction, isControlledAddr: ViewServerInterface['isControlledAddress'], ) => { 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, + value: tx.toJson({ typeRegistry }) as Jsonified, }); } @@ -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 { 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 = AnyMessage> implements UnderlyingDefaultSource @@ -15,7 +16,7 @@ export class IdbCursorSource 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/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; @@ -112,7 +113,7 @@ export const ActionViewComponent = ({ return ; case 'ibcRelayAction': - return ; + return ; case 'proposalSubmit': return ; 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..4025a1fd00 --- /dev/null +++ b/packages/ui/components/ui/tx/actions-views/ibc-relay.tsx @@ -0,0 +1,141 @@ +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 ( + + {!!packetData?.sender && ( + + {packetData.sender} + + )} + {!!packetData?.receiver && ( + + {packetData.receiver} + + )} + {!!packetData?.denom && ( + {packetData.denom} + )} + {!!packetData?.amount && ( + {packetData.amount} + )} + {packetData && 'memo' in packetData && ( + {packetData.memo} + )} + {!!packet.packet?.sequence && ( + {Number(packet.packet.sequence)} + )} + {!!packet.packet?.sourcePort && ( + {packet.packet.sourcePort} + )} + {!!packet.packet?.sourceChannel && ( + + {packet.packet.sourceChannel} + + )} + {!!packet.packet?.destinationPort && ( + + {packet.packet.destinationPort} + + )} + {!!packet.packet?.destinationChannel && ( + + {packet.packet.destinationChannel} + + )} + {!!packet.packet?.timeoutHeight?.revisionHeight && ( + + {Number(packet.packet.timeoutHeight.revisionHeight)} + + )} + {!!packet.packet?.timeoutHeight?.revisionNumber && ( + + {Number(packet.packet.timeoutHeight.revisionNumber)} + + )} + {!!packet.packet?.timeoutTimestamp && ( + + {getUtcTime(packet.packet.timeoutTimestamp)} + + )} + {packet.signer} + {!!packet.proofHeight?.revisionHeight && ( + + {Number(packet.proofHeight.revisionHeight)} + + )} + {!!packet.proofHeight?.revisionNumber && ( + + {Number(packet.proofHeight.revisionNumber)} + + )} + + + {uint8ArrayToBase64(packet.proofCommitment)} + + + + } + /> + ); +}; + +const UpdateClientComponent = ({ update }: { update: MsgUpdateClient }) => { + return ( + + {update.clientId} + {update.signer} + + } + /> + ); +}; + +export const IbcRelayComponent = ({ value }: { value: IbcRelay }) => { + if (value.rawAction?.is(MsgRecvPacket.typeName)) { + const packet = new MsgRecvPacket(); + value.rawAction.unpackTo(packet); + return ; + } + + if (value.rawAction?.is(MsgUpdateClient.typeName)) { + const update = new MsgUpdateClient(); + value.rawAction.unpackTo(update); + return ; + } + + return ; +}; 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',