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

generate tx view in slice #804

Merged
merged 2 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
10 changes: 1 addition & 9 deletions apps/extension/src/approve-transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { TransactionView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import { AuthorizeRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/custody/v1/custody_pb';
import { PartialMessage } from '@bufbuild/protobuf';
import type { Jsonified } from '@penumbra-zone/types/src/jsonified';
Expand All @@ -7,29 +6,22 @@ import { popup } from './popup';

export const approveTransaction = async (
partialAuthorizeRequest: PartialMessage<AuthorizeRequest>,
partialTransactionView: PartialMessage<TransactionView>,
) => {
const authorizeRequest = new AuthorizeRequest(partialAuthorizeRequest);
const transactionView = new TransactionView(partialTransactionView);

const popupResponse = await popup<TxApproval>({
type: PopupType.TxApproval,
request: {
authorizeRequest: new AuthorizeRequest(
authorizeRequest,
).toJson() as Jsonified<AuthorizeRequest>,
transactionView: new TransactionView(transactionView).toJson() as Jsonified<TransactionView>,
},
});

if (popupResponse) {
const resAuthorizeRequest = AuthorizeRequest.fromJson(popupResponse.authorizeRequest);
const resTransactionView = TransactionView.fromJson(popupResponse.transactionView);

if (
!authorizeRequest.equals(resAuthorizeRequest) ||
!transactionView.equals(resTransactionView)
)
if (!authorizeRequest.equals(resAuthorizeRequest))
throw new Error('Invalid response from popup');
}

Expand Down
3 changes: 0 additions & 3 deletions apps/extension/src/message/popup.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { TransactionView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import type { AuthorizeRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/custody/v1/custody_pb';
import type {
InternalMessage,
Expand Down Expand Up @@ -28,11 +27,9 @@ export type TxApproval = InternalMessage<
PopupType.TxApproval,
{
authorizeRequest: Jsonified<AuthorizeRequest>;
transactionView: Jsonified<TransactionView>;
},
null | {
authorizeRequest: Jsonified<AuthorizeRequest>;
transactionView: Jsonified<TransactionView>;
choice: UserChoice;
}
>;
Expand Down
35 changes: 23 additions & 12 deletions apps/extension/src/state/tx-approval.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { AuthorizeRequest } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/custody/v1/custody_pb';
import { AllSlices, SliceCreator } from '.';
import { PopupType, TxApproval } from '../message/popup';
import { TransactionView } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import {
TransactionPlan,
TransactionView,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import { viewClient } from '../clients';
import { ConnectError } from '@connectrpc/connect';
import { errorToJson } from '@connectrpc/connect/protocol-connect';
Expand All @@ -19,6 +22,12 @@ import {
asPublicTransactionView,
asReceiverTransactionView,
} from '@penumbra-zone/perspective/translators';
import { localExtStorage } from '@penumbra-zone/storage';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { viewTransactionPlan } from '@penumbra-zone/perspective/plan';

export interface TxApprovalSlice {
/**
Expand Down Expand Up @@ -49,16 +58,22 @@ export interface TxApprovalSlice {
}

export const createTxApprovalSlice = (): SliceCreator<TxApprovalSlice> => (set, get) => ({
acceptRequest: async (
{ request: { authorizeRequest: authReqJson, transactionView: txViewJson } },
responder,
) => {
acceptRequest: async ({ request: { authorizeRequest: authReqJson } }, responder) => {
const existing = get().txApproval;
if (existing.authorizeRequest ?? existing.transactionView ?? existing.responder)
throw new Error('Another request is still pending');
if (existing.responder) throw new Error('Another request is still pending');

const authorizeRequest = AuthorizeRequest.fromJson(authReqJson);
const transactionView = TransactionView.fromJson(txViewJson);

const getMetadata = async (assetId: AssetId) => {
const { denomMetadata } = await viewClient.assetMetadataById({ assetId });
return denomMetadata ?? new Metadata();
};

const transactionView = await viewTransactionPlan(
authorizeRequest.plan ?? new TransactionPlan(),
getMetadata,
(await localExtStorage.get('wallets'))[0]?.fullViewingKey ?? '',
);

// pregenerate views from various perspectives.
// TODO: should this be done in the component?
Expand Down Expand Up @@ -106,9 +121,6 @@ export const createTxApprovalSlice = (): SliceCreator<TxApprovalSlice> => (set,
throw new Error('Missing response data');

// zustand doesn't like jsonvalue so stringify
const transactionView = TransactionView.fromJsonString(
transactionViewString,
).toJson() as Jsonified<TransactionView>;
const authorizeRequest = AuthorizeRequest.fromJsonString(
authorizeRequestString,
).toJson() as Jsonified<AuthorizeRequest>;
Expand All @@ -117,7 +129,6 @@ export const createTxApprovalSlice = (): SliceCreator<TxApprovalSlice> => (set,
type: PopupType.TxApproval,
data: {
choice,
transactionView,
authorizeRequest,
},
});
Expand Down
83 changes: 44 additions & 39 deletions packages/perspective/plan/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import { Address } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/keys/v1/keys_pb';
import { describe, expect, test } from 'vitest';
import { describe, expect, test, vi } from 'vitest';
import { viewTransactionPlan } from '.';
import {
MemoView_Visible,
TransactionPlan,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import { bech32ToAddress } from '@penumbra-zone/bech32';
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';

describe('viewTransactionPlan()', () => {
const returnAddressAsBech32 =
'penumbra147mfall0zr6am5r45qkwht7xqqrdsp50czde7empv7yq2nk3z8yyfh9k9520ddgswkmzar22vhz9dwtuem7uxw0qytfpv7lk3q9dp8ccaw2fn5c838rfackazmgf3ahh09cxmz';
const returnAddress = new Address({ inner: bech32ToAddress(returnAddressAsBech32) });
const chainId = 'testnet';
const expiryHeight = 100n;
const metadataByAssetId = vi.fn(() => Promise.resolve(new Metadata()));
const mockFvk =
'penumbrafullviewingkey1vzfytwlvq067g2kz095vn7sgcft47hga40atrg5zu2crskm6tyyjysm28qg5nth2fqmdf5n0q530jreumjlsrcxjwtfv6zdmfpe5kqsa5lg09';

Expand All @@ -30,40 +32,43 @@ describe('viewTransactionPlan()', () => {
},
});

test('includes the return address if it exists', () => {
const txnView = viewTransactionPlan(validTxnPlan, {}, mockFvk);
test('includes the return address if it exists', async () => {
const txnView = await viewTransactionPlan(validTxnPlan, metadataByAssetId, mockFvk);
const memoViewValue = txnView.bodyView!.memoView!.memoView.value! as MemoView_Visible;

expect(
memoViewValue.plaintext!.returnAddress?.addressView.value?.address!.equals(returnAddress),
).toBe(true);
});

test('leaves out the return address when it does not exist', () => {
const txnPlan = new TransactionPlan({
transactionParameters: {
fee: {
amount: {
hi: 1n,
lo: 0n,
test('leaves out the return address when it does not exist', async () => {
const view = viewTransactionPlan(
new TransactionPlan({
transactionParameters: {
fee: {
amount: {
hi: 1n,
lo: 0n,
},
},
},
},
});
const txnView = viewTransactionPlan(txnPlan, {}, mockFvk);
const memoViewValue = txnView.bodyView!.memoView!.memoView.value! as MemoView_Visible;

expect(memoViewValue.plaintext?.returnAddress).toBeUndefined();
}),
metadataByAssetId,
mockFvk,
);
await expect(view).resolves.toHaveProperty('bodyView.memoView.memoView.value.plaintext.text');
await expect(view).resolves.not.toHaveProperty(
'bodyView.memoView.memoView.value.plaintext.returnAddress',
);
});

test('includes the fee', () => {
expect(
viewTransactionPlan(validTxnPlan, {}, mockFvk).bodyView!.transactionParameters!.fee,
).toBe(validTxnPlan.transactionParameters!.fee);
});
test('includes the fee', async () =>
expect(viewTransactionPlan(validTxnPlan, metadataByAssetId, mockFvk)).resolves.toMatchObject({
bodyView: { transactionParameters: { fee: validTxnPlan.transactionParameters!.fee } },
}));

test('throws when there is no fee', () => {
expect(() =>
test('throws when there is no fee', () =>
expect(
viewTransactionPlan(
new TransactionPlan({
memo: {
Expand All @@ -77,24 +82,24 @@ describe('viewTransactionPlan()', () => {
expiryHeight,
},
}),
{},
metadataByAssetId,
mockFvk,
),
).toThrow('No fee found in transaction plan');
});
).rejects.toThrow('No fee found in transaction plan'));

test('includes the memo', () => {
const txnView = viewTransactionPlan(validTxnPlan, {}, mockFvk);
const memoViewValue = txnView.bodyView!.memoView!.memoView.value! as MemoView_Visible;

expect(memoViewValue.plaintext!.text).toBe('Memo text here');
});
test('includes the memo', async () =>
expect(viewTransactionPlan(validTxnPlan, metadataByAssetId, mockFvk)).resolves.toMatchObject({
bodyView: { memoView: { memoView: { value: { plaintext: { text: 'Memo text here' } } } } },
}));

test('includes the transaction parameters', () => {
expect(viewTransactionPlan(validTxnPlan, {}, mockFvk).bodyView!.transactionParameters).toEqual({
fee: validTxnPlan.transactionParameters!.fee,
chainId,
expiryHeight,
});
});
test('includes the transaction parameters', () =>
expect(viewTransactionPlan(validTxnPlan, metadataByAssetId, mockFvk)).resolves.toMatchObject({
bodyView: {
transactionParameters: {
fee: validTxnPlan.transactionParameters!.fee,
chainId,
expiryHeight,
},
},
}));
});
16 changes: 10 additions & 6 deletions packages/perspective/plan/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Metadata } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import {
AssetId,
Metadata,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb';
import { getAddressView } from './get-address-view';
import {
TransactionPlan,
TransactionView,
} from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/transaction/v1/transaction_pb';
import { viewActionPlan } from './view-action-plan';
import type { Jsonified } from '@penumbra-zone/types/src/jsonified';

/**
* Given a `TransactionPlan`, returns a `TransactionView` that can be passed to
Expand All @@ -17,18 +19,20 @@ import type { Jsonified } from '@penumbra-zone/types/src/jsonified';
* properties. Its main purpose is to be able to render the transaction plan,
* not to be exhaustive.
*/
export const viewTransactionPlan = (
export const viewTransactionPlan = async (
txPlan: TransactionPlan,
metadataByAssetId: Record<string, Jsonified<Metadata>>,
metadataByAssetId: (id: AssetId) => Promise<Metadata>,
fullViewingKey: string,
): TransactionView => {
): Promise<TransactionView> => {
const returnAddress = txPlan.memo?.plaintext?.returnAddress;
const transactionParameters = txPlan.transactionParameters;
if (!transactionParameters?.fee) throw new Error('No fee found in transaction plan');

return new TransactionView({
bodyView: {
actionViews: txPlan.actions.map(viewActionPlan(metadataByAssetId, fullViewingKey)),
actionViews: await Promise.all(
txPlan.actions.map(viewActionPlan(metadataByAssetId, fullViewingKey)),
),
memoView: {
memoView: {
case: 'visible',
Expand Down
Loading
Loading