Skip to content

Commit

Permalink
generate tx view in slice (#804)
Browse files Browse the repository at this point in the history
  • Loading branch information
turbocrime authored Mar 20, 2024
1 parent a00639d commit 86ea7e3
Show file tree
Hide file tree
Showing 10 changed files with 211 additions and 264 deletions.
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

0 comments on commit 86ea7e3

Please sign in to comment.