Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ui): #1713: WalletBalance UI component #1717

Merged
merged 6 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eleven-planes-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@repo/ui': minor
---

Add WalletBalance UI component
34 changes: 4 additions & 30 deletions packages/ui/src/AddressViewComponent/index.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';

import { AddressViewComponent } from '.';
import { AddressView } from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { addressFromBech32m } from '@penumbra-zone/bech32m/penumbra';
import styled from 'styled-components';

const EXAMPLE_VIEW_DECODED = new AddressView({
addressView: {
case: 'decoded',

value: {
address: { inner: new Uint8Array(80) },
index: {
account: 0,
randomizer: new Uint8Array([0, 0, 0]),
},
},
},
});

const EXAMPLE_VIEW_OPAQUE = new AddressView({
addressView: {
case: 'opaque',
value: {
address: addressFromBech32m(
'penumbra1e8k5cyds484dxvapeamwveh5khqv4jsvyvaf5wwxaaccgfghm229qw03pcar3ryy8smptevstycch0qk3uu0rgkvtjpxy3cu3rjd0agawqtlz6erev28a6sg69u7cxy0t02nd4',
),
},
},
});
import { ADDRESS_VIEW_DECODED, ADDRESS_VIEW_OPAQUE } from '../utils/bufs';

const MaxWidthWrapper = styled.div`
width: 100%;
Expand All @@ -42,8 +16,8 @@ const meta: Meta<typeof AddressViewComponent> = {
addressView: {
options: ['Sample decoded address view', 'Sample opaque address view'],
mapping: {
'Sample decoded address view': EXAMPLE_VIEW_DECODED,
'Sample opaque address view': EXAMPLE_VIEW_OPAQUE,
'Sample decoded address view': ADDRESS_VIEW_DECODED,
'Sample opaque address view': ADDRESS_VIEW_OPAQUE,
},
},
},
Expand All @@ -61,7 +35,7 @@ type Story = StoryObj<typeof AddressViewComponent>;

export const Basic: Story = {
args: {
addressView: EXAMPLE_VIEW_DECODED,
addressView: ADDRESS_VIEW_DECODED,
copyable: true,
},
};
45 changes: 5 additions & 40 deletions packages/ui/src/AddressViewComponent/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,14 @@
import {
Address,
AddressIndex,
AddressView,
AddressView_Decoded,
} from '@penumbra-zone/protobuf/penumbra/core/keys/v1/keys_pb';
import { AddressViewComponent } from '.';
import { describe, expect, it } from 'vitest';
import { render } from '@testing-library/react';
import { PenumbraUIProvider } from '../PenumbraUIProvider';

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]),
}),
}),
},
});
import { ADDRESS_VIEW_DECODED_ONE_TIME, ADDRESS_VIEW_DECODED } from '../utils/bufs';

describe('<AddressViewComponent />', () => {
describe('when `copyable` is `true`', () => {
it('does not show the copy icon when the address is a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithOneTimeAddress} copyable />,
<AddressViewComponent addressView={ADDRESS_VIEW_DECODED_ONE_TIME} copyable />,
{ wrapper: PenumbraUIProvider },
);

Expand All @@ -52,7 +17,7 @@ describe('<AddressViewComponent />', () => {

it('shows the copy icon when the address is not a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithNormalAddress} copyable />,
<AddressViewComponent addressView={ADDRESS_VIEW_DECODED} copyable />,
{ wrapper: PenumbraUIProvider },
);

Expand All @@ -63,7 +28,7 @@ describe('<AddressViewComponent />', () => {
describe('when `copyable` is `false`', () => {
it('does not show the copy icon when the address is a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithOneTimeAddress} copyable={false} />,
<AddressViewComponent addressView={ADDRESS_VIEW_DECODED_ONE_TIME} copyable={false} />,
{ wrapper: PenumbraUIProvider },
);

Expand All @@ -72,7 +37,7 @@ describe('<AddressViewComponent />', () => {

it('does not show the copy icon when the address is not a one-time address', () => {
const { queryByLabelText } = render(
<AddressViewComponent addressView={addressViewWithNormalAddress} copyable={false} />,
<AddressViewComponent addressView={ADDRESS_VIEW_DECODED} copyable={false} />,
{ wrapper: PenumbraUIProvider },
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,84 +1,61 @@
import { describe, expect, it } from 'vitest';
import { filterMetadataOrBalancesResponseByText } from './filterMetadataOrBalancesResponseByText';
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';

const um = new Metadata({
base: 'upenumbra',
name: 'Penumbra',

// Including some extra text in these values to make sure we don't get false
// positives in the tests, since e.g. a symbol of `UM` is entirely contained
// in the name `Penumbra`.
symbol: 'UMSymbol',
display: 'penumbraDisplay',
});

const umBalance = new BalancesResponse({
balanceView: {
valueView: {
case: 'knownAssetId',
value: {
metadata: um,
},
},
},
});
import { PENUMBRA_BALANCE, PENUMBRA_METADATA } from '../utils/bufs';

describe('filterMetadataOrBalancesResponseByText()', () => {
describe('when the search text is empty', () => {
it('returns `true`', () => {
expect(filterMetadataOrBalancesResponseByText('')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('')(PENUMBRA_METADATA)).toBe(true);
});
});

describe('when the search text is just whitespace', () => {
it('returns `true`', () => {
expect(filterMetadataOrBalancesResponseByText(' ')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText(' ')(PENUMBRA_METADATA)).toBe(true);
});
});

describe('when the value is a `Metadata`', () => {
it('returns `true` when the metadata name contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('Pen')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('Pen')(PENUMBRA_METADATA)).toBe(true);
});

it('returns `true` when the metadata symbol contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('UMSymbol')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('UM')(PENUMBRA_METADATA)).toBe(true);
});

it('returns `true` when the display contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('penumbraDisplay')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('penum')(PENUMBRA_METADATA)).toBe(true);
});

it('returns `true` when the base contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('upenumbra')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('upenumbra')(PENUMBRA_METADATA)).toBe(true);
});

it('is case-insensitive', () => {
expect(filterMetadataOrBalancesResponseByText('pen')(um)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('pen')(PENUMBRA_METADATA)).toBe(true);
});
});

describe('when the value is a `BalancesResponse`', () => {
it('returns `true` when the metadata name contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('Pen')(umBalance)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('Pen')(PENUMBRA_BALANCE)).toBe(true);
});

it('returns `true` when the metadata symbol contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('UMSymbol')(umBalance)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('UM')(PENUMBRA_BALANCE)).toBe(true);
});

it('returns `true` when the display contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('penumbraDisplay')(umBalance)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('penumbra')(PENUMBRA_BALANCE)).toBe(true);
});

it('returns `true` when the base contains the search text', () => {
expect(filterMetadataOrBalancesResponseByText('upenumbra')(umBalance)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('upenumbra')(PENUMBRA_BALANCE)).toBe(true);
});

it('is case-insensitive', () => {
expect(filterMetadataOrBalancesResponseByText('pen')(umBalance)).toBe(true);
expect(filterMetadataOrBalancesResponseByText('pen')(PENUMBRA_BALANCE)).toBe(true);
});
});
});
133 changes: 19 additions & 114 deletions packages/ui/src/AssetSelector/index.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,120 +2,25 @@ import type { Meta, StoryObj } from '@storybook/react';
import { useArgs } from '@storybook/preview-api';

import { AssetSelector } from '.';
import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { BalancesResponse } from '@penumbra-zone/protobuf/penumbra/view/v1/view_pb';
import { useState } from 'react';

const u8 = (length: number) => Uint8Array.from({ length }, () => Math.floor(Math.random() * 256));

const umAssetId = new AssetId({ inner: u8(32) });
const osmoAssetId = new AssetId({ inner: u8(32) });
const pizzaAssetId = new AssetId({ inner: u8(32) });

const um = new Metadata({
symbol: 'UM',
name: 'Penumbra',
penumbraAssetId: umAssetId,
base: 'upenumbra',
display: 'penumbra',
denomUnits: [{ denom: 'upenumbra' }, { denom: 'penumbra', exponent: 6 }],
});

const osmo = new Metadata({
symbol: 'OSMO',
name: 'Osmosis',
penumbraAssetId: osmoAssetId,
base: 'uosmo',
display: 'osmo',
denomUnits: [{ denom: 'uosmo' }, { denom: 'osmo', exponent: 6 }],
});

const pizza = new Metadata({
symbol: 'PIZZA',
name: 'Pizza',
penumbraAssetId: pizzaAssetId,
base: 'upizza',
display: 'pizza',
denomUnits: [{ denom: 'upizza' }, { denom: 'pizza', exponent: 6 }],
});

const umBalance0 = new BalancesResponse({
accountAddress: {
addressView: {
case: 'decoded',
value: {
index: {
account: 0,
},
},
},
},
balanceView: {
valueView: {
case: 'knownAssetId',
value: {
metadata: um,
amount: {
hi: 0n,
lo: 123_456_000n,
},
},
},
},
});

const osmoBalance0 = new BalancesResponse({
accountAddress: {
addressView: {
case: 'decoded',
value: {
index: {
account: 0,
},
},
},
},
balanceView: {
valueView: {
case: 'knownAssetId',
value: {
metadata: osmo,
amount: {
hi: 0n,
lo: 456_789_000n,
},
},
},
},
});

const umBalance1 = new BalancesResponse({
accountAddress: {
addressView: {
case: 'decoded',
value: {
index: {
account: 1,
},
},
},
},
balanceView: {
valueView: {
case: 'knownAssetId',
value: {
metadata: um,
amount: {
hi: 0n,
lo: 789_100_000n,
},
},
},
},
});

const mixedOptions: (BalancesResponse | Metadata)[] = [pizza, umBalance0, umBalance1, osmoBalance0];
const metadataOnlyOptions: Metadata[] = [pizza, um, osmo];
import {
OSMO_BALANCE,
OSMO_METADATA,
PENUMBRA2_BALANCE,
PENUMBRA_BALANCE,
PENUMBRA_METADATA,
PIZZA_METADATA,
} from '../utils/bufs';

const mixedOptions: (BalancesResponse | Metadata)[] = [
PIZZA_METADATA,
PENUMBRA_BALANCE,
PENUMBRA2_BALANCE,
OSMO_BALANCE,
];
const metadataOnlyOptions: Metadata[] = [PIZZA_METADATA, PENUMBRA_METADATA, OSMO_METADATA];

const meta: Meta<typeof AssetSelector> = {
component: AssetSelector,
Expand All @@ -132,7 +37,7 @@ type Story = StoryObj<typeof AssetSelector>;
export const MixedBalancesResponsesAndMetadata: Story = {
args: {
dialogTitle: 'Transfer Assets',
value: umBalance0,
value: PENUMBRA_BALANCE,
options: mixedOptions,
},

Expand All @@ -147,7 +52,7 @@ export const MixedBalancesResponsesAndMetadata: Story = {

export const MetadataOnly: Story = {
render: function Render() {
const [value, setValue] = useState<Metadata>(um);
const [value, setValue] = useState<Metadata>(PENUMBRA_METADATA);

return (
<AssetSelector
Expand Down
Loading
Loading