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): #2051: TransactionSummary #2083

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
9 changes: 9 additions & 0 deletions .changeset/twelve-stingrays-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@penumbra-zone/perspective': major
'@penumbra-zone/ui': minor
'@penumbra-zone/protobuf': patch
---

perspective: update Transaction classification, implement `findRelevantAssets` function
ui: add `TransactionSummary` components
protobuf: download new protobuf messages related to TransactionInfo
2 changes: 1 addition & 1 deletion apps/minifront/src/components/tx-details/tx-viewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const TxViewer = ({ txInfo }: { txInfo?: TransactionInfo }) => {
const [option, setOption] = useState(TxDetailsTab.PRIVATE);

// classify the transaction type
const transactionClassification = classifyTransaction(txInfo?.view);
const transactionClassification = classifyTransaction(txInfo?.view).type;

// filter for receiver view
const showReceiverTransactionView = transactionClassification === 'send';
Expand Down
7 changes: 6 additions & 1 deletion packages/perspective/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@
"exports": {
"./plan/*": "./src/plan/*.ts",
"./transaction/*": "./src/transaction/*.ts",
"./translators/*": "./src/translators/*.ts"
"./translators/*": "./src/translators/*.ts",
"./action-view/*": "./src/action-view/*.ts"
},
"publishConfig": {
"exports": {
Expand All @@ -38,6 +39,10 @@
"./translators/*": {
"types": "./dist/translators/*.d.ts",
"default": "./dist/translators/*.js"
},
"./action-view/*": {
"types": "./dist/action-view/*.d.ts",
"default": "./dist/action-view/*.js"
}
}
},
Expand Down
65 changes: 65 additions & 0 deletions packages/perspective/src/action-view/ibc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import {
FungibleTokenPacketData,
IbcRelay,
} from '@penumbra-zone/protobuf/penumbra/core/component/ibc/v1/ibc_pb';
import {
MsgAcknowledgement,
MsgRecvPacket,
MsgTimeout,
MsgTimeoutOnClose,
} from '@penumbra-zone/protobuf/ibc/core/channel/v1/tx_pb';
import { MsgUpdateClient } from '@penumbra-zone/protobuf/ibc/core/client/v1/tx_pb';
import { Packet } from '@penumbra-zone/protobuf/ibc/core/channel/v1/channel_pb';

export const parsePacketTokenData = (packet: Packet): FungibleTokenPacketData => {
const dataString = new TextDecoder().decode(packet.data);
return FungibleTokenPacketData.fromJsonString(dataString);
};

export interface IbcRelayData {
message: MsgRecvPacket | MsgUpdateClient | MsgTimeout | MsgTimeoutOnClose | MsgAcknowledgement;
packet?: Packet;
tokenData?: FungibleTokenPacketData;
}

/**
* IbcRelay action has a single field `rawAction` of type Any.
* This function unpacks the rawAction field into a more structured object.
* All Msg-* types except MsgUpdateClient have a Packet field that can also be unpacked
* to `tokenData` with more information about the IBC deposit.
*/
export const unpackIbcRelay = (value: IbcRelay): IbcRelayData | undefined => {
if (!value.rawAction) {
return undefined;
}

if (value.rawAction.is(MsgUpdateClient.typeName)) {
const message = new MsgUpdateClient();
value.rawAction.unpackTo(message);
return {
message,
};
}

let message: MsgRecvPacket | MsgTimeout | MsgTimeoutOnClose | MsgAcknowledgement;

if (value.rawAction.is(MsgRecvPacket.typeName)) {
message = new MsgRecvPacket();
} else if (value.rawAction.is(MsgTimeout.typeName)) {
message = new MsgTimeout();
} else if (value.rawAction.is(MsgTimeoutOnClose.typeName)) {
message = new MsgTimeoutOnClose();
} else if (value.rawAction.is(MsgAcknowledgement.typeName)) {
message = new MsgAcknowledgement();
} else {
return undefined;
}

value.rawAction.unpackTo(message);

return {
message,
packet: message.packet,
tokenData: message.packet && parsePacketTokenData(message.packet),
};
};
111 changes: 111 additions & 0 deletions packages/perspective/src/action-view/relevant-assets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { ActionView } from '@penumbra-zone/protobuf/penumbra/core/transaction/v1/transaction_pb';
import { AssetId, Metadata } from '@penumbra-zone/protobuf/penumbra/core/asset/v1/asset_pb';
import { getAsset1Metadata, getAsset2Metadata } from '@penumbra-zone/getters/swap-view';
import { getOutputData } from '@penumbra-zone/getters/swap-claim-view';
import { getNote as getSpendNote } from '@penumbra-zone/getters/spend-view';
import { getNote as getOutputNote } from '@penumbra-zone/getters/output-view';
import { getMetadata } from '@penumbra-zone/getters/value-view';
import { bech32mIdentityKey } from '@penumbra-zone/bech32m/penumbravalid';

export type RelevantAsset = AssetId | Metadata;

const returnAssets = (assets: (RelevantAsset | undefined)[]): RelevantAsset[] =>
assets.filter(Boolean) as RelevantAsset[];

/**
* Takes an action view and returns relevant assets for that action.
* Some actions store assets as Metadata and some as AssetId, so further processing is needed
* to convert AssetId to Metadata.
*
* Actions with asset information:
* - spend
* - output
* - swap
* - swapClaim
* - positionOpen
* - actionDutchAuctionSchedule
* - actionDutchAuctionWithdraw
* - actionLiquidityTournamentVote
*
* Some action views can compute asset information from its data:
* - delegate
* - undelegate
* - position close/withdraw (not implemented but can compute lpNft from positionId)
*/
export const findRelevantAssets = (action?: ActionView): RelevantAsset[] => {
if (!action) {
return [];
}

const view = action.actionView;

if (view.case === 'spend') {
const note = getSpendNote.optional(view.value);
return returnAssets([getMetadata(note?.value)]);
}

if (view.case === 'output') {
const note = getOutputNote.optional(view.value);
return returnAssets([getMetadata.optional(note?.value)]);
}

if (view.case === 'swap') {
return returnAssets([
getAsset1Metadata.optional(view.value),
getAsset2Metadata.optional(view.value),
]);
}

if (view.case === 'swapClaim') {
const bsod = getOutputData.optional(view.value);
return returnAssets([bsod?.tradingPair?.asset1, bsod?.tradingPair?.asset2]);
}

if (view.case === 'positionOpen') {
const pair = view.value.position?.phi?.pair;
return returnAssets([pair?.asset1, pair?.asset2]);
}

if (view.case === 'delegatorVote') {
const note =
view.value.delegatorVote.case === 'visible' ? view.value.delegatorVote.value.note : undefined;
return returnAssets([getMetadata.optional(note?.value)]);
}

// TODO: check this for validity
if (view.case === 'delegate') {
return returnAssets([
view.value.validatorIdentity &&
new Metadata({
display: `delegation_${bech32mIdentityKey(view.value.validatorIdentity)}`,
}),
]);
}

// TODO: check this for validity
if (view.case === 'undelegate') {
return returnAssets([
view.value.validatorIdentity &&
view.value.fromEpoch &&
new Metadata({
display: `unbonding_start_at_${view.value.fromEpoch.startHeight}_${bech32mIdentityKey(view.value.validatorIdentity)}`,
}),
]);
}

if (view.case === 'actionDutchAuctionSchedule') {
return returnAssets([view.value.inputMetadata, view.value.outputMetadata]);
}

if (view.case === 'actionDutchAuctionWithdraw') {
return returnAssets(view.value.reserves.map(valueView => getMetadata.optional(valueView)));
}

if (view.case === 'actionLiquidityTournamentVote') {
return view.value.liquidityTournamentVote.case === 'visible'
? returnAssets([getMetadata.optional(view.value.liquidityTournamentVote.value.note?.value)])
: [];
}

return [];
};
24 changes: 12 additions & 12 deletions packages/perspective/src/transaction/classify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('receive');
expect(classifyTransaction(transactionView).type).toBe('receive');
});

it('returns `send` for transactions with visible spends but at least one opaque output', () => {
Expand Down Expand Up @@ -113,7 +113,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('send');
expect(classifyTransaction(transactionView).type).toBe('send');
});

it('returns `internalTransfer` for transactions with fully visible spends, outputs, and addresses', () => {
Expand Down Expand Up @@ -175,7 +175,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('internalTransfer');
expect(classifyTransaction(transactionView).type).toBe('internalTransfer');
});

it('returns `swap` for transactions with a `swap` action', () => {
Expand Down Expand Up @@ -204,7 +204,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('swap');
expect(classifyTransaction(transactionView).type).toBe('swap');
});

it('returns `swapClaim` for transactions with a `swapClaim` action', () => {
Expand All @@ -227,7 +227,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('swapClaim');
expect(classifyTransaction(transactionView).type).toBe('swapClaim');
});

it('returns `delegate` for transactions with a `delegate` action', () => {
Expand Down Expand Up @@ -256,7 +256,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('delegate');
expect(classifyTransaction(transactionView).type).toBe('delegate');
});

it('returns `undelegate` for transactions with an `undelegate` action', () => {
Expand Down Expand Up @@ -285,7 +285,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('undelegate');
expect(classifyTransaction(transactionView).type).toBe('undelegate');
});

it('returns `undelegateClaim` for transactions with an `undelegateClaim` action', () => {
Expand Down Expand Up @@ -314,7 +314,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('undelegateClaim');
expect(classifyTransaction(transactionView).type).toBe('undelegateClaim');
});

it('returns `dutchAuctionSchedule` for transactions with an `actionDutchAuctionSchedule` action', () => {
Expand All @@ -324,7 +324,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionSchedule');
expect(classifyTransaction(transactionView).type).toBe('dutchAuctionSchedule');
});

it('returns `dutchAuctionEnd` for transactions with an `actionDutchAuctionEnd` action', () => {
Expand All @@ -334,7 +334,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionEnd');
expect(classifyTransaction(transactionView).type).toBe('dutchAuctionEnd');
});

it('returns `dutchAuctionWithdraw` for transactions with an `actionDutchAuctionWithdraw` action', () => {
Expand All @@ -344,7 +344,7 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('dutchAuctionWithdraw');
expect(classifyTransaction(transactionView).type).toBe('dutchAuctionWithdraw');
});

it("returns `unknown` for transactions that don't fit the above categories", () => {
Expand Down Expand Up @@ -384,6 +384,6 @@ describe('classifyTransaction()', () => {
},
});

expect(classifyTransaction(transactionView)).toBe('unknown');
expect(classifyTransaction(transactionView).type).toBe('unknown');
});
});
Loading