diff --git a/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx
index 5d714e1f87..dda8ac37ef 100644
--- a/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx
+++ b/apps/minifront/src/components/v2/dashboard-layout/assets-page/index.tsx
@@ -1 +1,20 @@
-export const AssetsPage = () =>
Assets page
;
+import { Table } from '@repo/ui/Table';
+
+export const AssetsPage = () => (
+
+
Account #1}>
+
+
+ Asset
+ Estimate
+
+
+
+
+ test
+ test
+
+
+
+
+);
diff --git a/packages/ui/src/AddressViewComponent/address-view.stories.tsx b/packages/ui/src/AddressViewComponent/address-view.stories.tsx
new file mode 100644
index 0000000000..7a1e98b391
--- /dev/null
+++ b/packages/ui/src/AddressViewComponent/address-view.stories.tsx
@@ -0,0 +1,63 @@
+import type { Meta, StoryObj } from '@storybook/react';
+
+import { AddressViewComponent } from '.';
+import {
+ Address,
+ AddressIndex,
+ AddressView,
+ AddressView_Decoded,
+} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
+import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra';
+
+const meta: Meta = {
+ component: AddressViewComponent,
+ title: 'AddressViewComponent',
+ tags: ['autodocs'],
+};
+export default meta;
+
+type Story = StoryObj;
+
+const EXAMPLE_VIEW = new AddressView({
+ addressView: {
+ case: 'decoded',
+
+ value: new AddressView_Decoded({
+ address: new Address({ inner: new Uint8Array(80) }),
+ index: new AddressIndex({
+ account: 0,
+ randomizer: new Uint8Array([0, 0, 0]),
+ }),
+ }),
+ },
+});
+
+const EXAMPLE_VIEW_OPAQUE = new AddressView({
+ addressView: {
+ case: 'opaque',
+ value: {
+ address: addressFromBech32m(
+ 'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4',
+ ),
+ },
+ },
+});
+
+export const Decoded: Story = {
+ args: {
+ view: EXAMPLE_VIEW,
+ },
+};
+
+export const Copiable: Story = {
+ args: {
+ view: EXAMPLE_VIEW,
+ copyable: true,
+ },
+};
+
+export const Opaque: Story = {
+ args: {
+ view: EXAMPLE_VIEW_OPAQUE,
+ },
+};
diff --git a/packages/ui/src/AddressViewComponent/address-view.test.tsx b/packages/ui/src/AddressViewComponent/address-view.test.tsx
new file mode 100644
index 0000000000..0083484d5f
--- /dev/null
+++ b/packages/ui/src/AddressViewComponent/address-view.test.tsx
@@ -0,0 +1,77 @@
+import {
+ Address,
+ AddressIndex,
+ AddressView,
+ AddressView_Decoded,
+} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
+import { AddressViewComponent } from '.';
+import { describe, expect, test } from 'vitest';
+import { render } from '@testing-library/react';
+
+const addressViewWithOneTimeAddress = new AddressView({
+ addressView: {
+ case: 'decoded',
+
+ value: new AddressView_Decoded({
+ address: new Address({ inner: new Uint8Array(80) }),
+ index: new AddressIndex({
+ account: 0,
+ // A one-time address is defined by a randomizer with at least one
+ // non-zero byte.
+ randomizer: new Uint8Array([1, 2, 3]),
+ }),
+ }),
+ },
+});
+
+const addressViewWithNormalAddress = new AddressView({
+ addressView: {
+ case: 'decoded',
+
+ value: new AddressView_Decoded({
+ address: new Address({ inner: new Uint8Array(80) }),
+ index: new AddressIndex({
+ account: 0,
+ randomizer: new Uint8Array([0, 0, 0]),
+ }),
+ }),
+ },
+});
+
+describe('', () => {
+ describe('when `copyable` is `true`', () => {
+ test('does not show the copy icon when the address is a one-time address', () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId('CopyToClipboardIconButton__icon')).toBeNull();
+ });
+
+ test('shows the copy icon when the address is not a one-time address', () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId('CopyToClipboardIconButton__icon')).not.toBeNull();
+ });
+ });
+
+ describe('when `copyable` is `false`', () => {
+ test('does not show the copy icon when the address is a one-time address', () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId('CopyToClipboardIconButton__icon')).toBeNull();
+ });
+
+ test('does not show the copy icon when the address is not a one-time address', () => {
+ const { queryByTestId } = render(
+ ,
+ );
+
+ expect(queryByTestId('CopyToClipboardIconButton__icon')).toBeNull();
+ });
+ });
+});
diff --git a/packages/ui/src/AddressViewComponent/index.tsx b/packages/ui/src/AddressViewComponent/index.tsx
new file mode 100644
index 0000000000..03cd8ae901
--- /dev/null
+++ b/packages/ui/src/AddressViewComponent/index.tsx
@@ -0,0 +1,51 @@
+import { AddressIcon } from '../address/address-icon';
+import { AddressView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb.js';
+import { CopyToClipboardIconButton } from '../copy-to-clipboard/copy-to-clipboard-icon-button';
+import { AddressComponent } from '../address/address-component';
+import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
+
+interface AddressViewProps {
+ view: AddressView | undefined;
+ copyable?: boolean;
+}
+
+// Renders an address or an address view.
+// If the view is given and is "visible", the account information will be displayed instead.
+export const AddressViewComponent = ({ view, copyable = true }: AddressViewProps) => {
+ if (!view?.addressView.value?.address) {
+ return <>>;
+ }
+
+ const encodedAddress = bech32mAddress(view.addressView.value.address);
+
+ const accountIndex =
+ view.addressView.case === 'decoded' ? view.addressView.value.index?.account : undefined;
+ const isOneTimeAddress =
+ view.addressView.case === 'decoded'
+ ? !view.addressView.value.index?.randomizer.every(v => v === 0) // Randomized (and thus, a one-time address) if the randomizer is not all zeros.
+ : undefined;
+
+ const addressIndexLabel = isOneTimeAddress ? 'IBC Deposit Address for Account #' : 'Account #';
+
+ copyable = isOneTimeAddress ? false : copyable;
+
+ return (
+
+ {accountIndex !== undefined ? (
+ <>
+
+
+ {addressIndexLabel}
+ {accountIndex}
+
+ >
+ ) : (
+
+ )}
+
+ {copyable &&
}
+
+ );
+};