Skip to content

Commit

Permalink
Generate names for common Flow CLI transactions (#182)
Browse files Browse the repository at this point in the history
* fix interaction source sanitization before compare

* hardcode templates for common flow-cli transactions

* fix interaction name when forked from history

* display tx name in transaction tables
  • Loading branch information
bartolomej authored Aug 26, 2023
1 parent 193e973 commit a261c0b
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 194 deletions.
74 changes: 2 additions & 72 deletions frontend/src/pages/blocks/details/Details.tsx
Original file line number Diff line number Diff line change
@@ -1,87 +1,23 @@
import React, { FunctionComponent, useEffect } from "react";
import { NavLink, useParams } from "react-router-dom";
import { Breadcrumb, useNavigation } from "../../../hooks/use-navigation";
import Label from "../../../components/label/Label";
import Value from "../../../components/value/Value";
import classes from "./Details.module.scss";
import FullScreenLoading from "../../../components/fullscreen-loading/FullScreenLoading";
import { useGetBlock, useGetTransactionsByBlock } from "../../../hooks/use-api";
import { FlowUtils } from "../../../utils/flow-utils";
import { createColumnHelper } from "@tanstack/table-core";
import { Transaction } from "@flowser/shared";
import Table from "../../../components/table/Table";
import MiddleEllipsis from "../../../components/ellipsis/MiddleEllipsis";
import { ExecutionStatus } from "components/status/ExecutionStatus";
import {
DetailsCard,
DetailsCardColumn,
} from "components/details-card/DetailsCard";
import { TextUtils } from "../../../utils/text-utils";
import { GrcpStatus } from "../../../components/status/GrcpStatus";
import { DecoratedPollingEntity } from "contexts/timeout-polling.context";
import { enableDetailsIntroAnimation } from "../../../config/common";
import { SizedBox } from "../../../components/sized-box/SizedBox";
import { AccountLink } from "../../../components/account/link/AccountLink";
import { StyledTabs } from "../../../components/tabs/StyledTabs";
import { TransactionsTable } from "../../transactions/main/TransactionsTable";

type RouteParams = {
blockId: string;
};

const txTableColHelper =
createColumnHelper<DecoratedPollingEntity<Transaction>>();

const txTableColumns = [
txTableColHelper.accessor("id", {
header: () => <Label variant="medium">TRANSACTION ID</Label>,
cell: (info) => (
<Value>
<NavLink to={`/transactions/details/${info.getValue()}`}>
<MiddleEllipsis className={classes.hash}>
{info.getValue()}
</MiddleEllipsis>
</NavLink>
</Value>
),
}),
txTableColHelper.accessor("payer", {
header: () => <Label variant="medium">PAYER</Label>,
cell: (info) => (
<Value>
<AccountLink address={info.getValue()} />
</Value>
),
}),
txTableColHelper.accessor("proposalKey", {
header: () => <Label variant="medium">PROPOSER</Label>,
cell: (info) => (
<Value>
{info.row.original.proposalKey ? (
<AccountLink address={info.row.original.proposalKey.address} />
) : (
"-"
)}
</Value>
),
}),
txTableColHelper.accessor("status.grcpStatus", {
header: () => <Label variant="medium">EXECUTION STATUS</Label>,
cell: (info) => (
<div>
<ExecutionStatus status={info.row.original.status} />
</div>
),
}),
txTableColHelper.accessor("status.grcpStatus", {
header: () => <Label variant="medium">API STATUS</Label>,
cell: (info) => (
<div>
<GrcpStatus status={info.row.original.status} />
</div>
),
}),
];

const Details: FunctionComponent = () => {
const { blockId } = useParams<RouteParams>();
const { setBreadcrumbs } = useNavigation();
Expand Down Expand Up @@ -144,13 +80,7 @@ const Details: FunctionComponent = () => {
{
id: "transactions",
label: "Transactions",
content: (
<Table<DecoratedPollingEntity<Transaction>>
data={transactions}
columns={txTableColumns}
enableIntroAnimations={enableDetailsIntroAnimation}
/>
),
content: <TransactionsTable transactions={transactions} />,
},
]}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { FlowserIcon } from "../../../../components/icons/Icons";
import { SizedBox } from "../../../../components/sized-box/SizedBox";
import { Spinner } from "../../../../components/spinner/Spinner";
import { useInteractionRegistry } from "../../contexts/interaction-registry.context";
import { useInteractionName } from "../../hooks/use-interaction-name";
import { useTransactionName } from "../../hooks/use-transaction-name";

export function InteractionHistory(): ReactElement {
const { data: blocks, firstFetch } = useGetPollingBlocks();
Expand Down Expand Up @@ -55,8 +55,8 @@ function BlockItem(props: BlockItemProps) {
});
const firstTransaction = data[0];

const transactionName = useInteractionName({
sourceCode: firstTransaction?.script,
const transactionName = useTransactionName({
transaction: firstTransaction,
});

function onForkAsTemplate() {
Expand All @@ -65,7 +65,7 @@ function BlockItem(props: BlockItemProps) {
}
create({
id: block.id,
name: `Tx from block #${block.height}`,
name: transactionName ?? `Tx from block #${block.height}`,
code: firstTransaction.script,
fclValuesByIdentifier: new Map(
firstTransaction.arguments.map((arg) => [
Expand Down
33 changes: 0 additions & 33 deletions frontend/src/pages/interactions/hooks/use-interaction-name.ts

This file was deleted.

106 changes: 106 additions & 0 deletions frontend/src/pages/interactions/hooks/use-transaction-name.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { useInteractionRegistry } from "../contexts/interaction-registry.context";
import { useMemo } from "react";
import { Transaction } from "@flowser/shared";

type UseInteractionNameProps = {
transaction: Transaction | undefined;
};

enum TransactionKind {
DEPLOY_CONTRACT,
INITIALIZE_ACCOUNT,
}

const hardcodedTemplates: [string, TransactionKind][] = [
[
`transaction(name: String, code: String ) {
prepare(signer: AuthAccount) {
signer.contracts.add(name: name, code: code.decodeHex() )
}
}`,
TransactionKind.DEPLOY_CONTRACT,
],
[
`import Crypto
transaction(publicKeys: [Crypto.KeyListEntry], contracts: {String: String}) {
prepare(signer: AuthAccount) {
let account = AuthAccount(payer: signer)
// add all the keys to the account
for key in publicKeys {
account.keys.add(publicKey: key.publicKey, hashAlgorithm: key.hashAlgorithm, weight: key.weight)
}
// add contracts if provided
for contract in contracts.keys {
account.contracts.add(name: contract, code: contracts[contract]!.decodeHex())
}
}
}`,
TransactionKind.INITIALIZE_ACCOUNT,
],
];

const transactionKindBySource = new Map<string, TransactionKind>(
hardcodedTemplates.map((entry) => [sanitizeCadenceSource(entry[0]), entry[1]])
);

export function useTransactionName(
props: UseInteractionNameProps
): string | undefined {
const { transaction } = props;
const { templates } = useInteractionRegistry();

return useMemo(() => {
if (!transaction?.script) {
return undefined;
}

const sanitizedTargetCode = sanitizeCadenceSource(transaction.script);
const matchingTemplateName = templates.find(
(template) =>
template.code &&
sanitizeCadenceSource(template.code) === sanitizedTargetCode
)?.name;

return matchingTemplateName ?? getDynamicName(transaction);
}, [transaction, templates]);
}

function getDynamicName(transaction: Transaction) {
const kind = transactionKindBySource.get(
sanitizeCadenceSource(transaction.script)
);

switch (kind) {
case TransactionKind.DEPLOY_CONTRACT:
return `Deploy ${
getArgumentValueById(transaction, "name") ?? "contract"
}`;
case TransactionKind.INITIALIZE_ACCOUNT:
return "Init signer account";
default:
return undefined;
}
}

function getArgumentValueById(transaction: Transaction, id: string) {
return JSON.parse(
transaction.arguments.find((argument) => argument.identifier === id)
?.valueAsJson ?? ""
);
}

function sanitizeCadenceSource(code: string) {
// Ignore imports for comparison,
// since those can differ due to address replacement.
// See: https://developers.flow.com/tooling/fcl-js/api#address-replacement
const strippedImports = code
.split("\n")
.filter((line) => !line.startsWith("import"))
.join("\n");

// Replace all whitespace and newlines
return strippedImports.replaceAll(/[\n\t ]/g, "");
}
87 changes: 3 additions & 84 deletions frontend/src/pages/transactions/main/Main.tsx
Original file line number Diff line number Diff line change
@@ -1,98 +1,17 @@
import React, { FunctionComponent, useEffect } from "react";
import classes from "./Main.module.scss";
import { useNavigation } from "../../../hooks/use-navigation";
import { useGetPollingTransactions } from "../../../hooks/use-api";
import { createColumnHelper } from "@tanstack/table-core";
import { Transaction } from "@flowser/shared";
import Label from "../../../components/label/Label";
import Value from "../../../components/value/Value";
import { NavLink } from "react-router-dom";
import MiddleEllipsis from "../../../components/ellipsis/MiddleEllipsis";
import Table from "../../../components/table/Table";
import { ExecutionStatus } from "components/status/ExecutionStatus";
import { GrcpStatus } from "../../../components/status/GrcpStatus";
import ReactTimeago from "react-timeago";
import { DecoratedPollingEntity } from "contexts/timeout-polling.context";
import { AccountLink } from "../../../components/account/link/AccountLink";

// TRANSACTIONS TABLE
const columnHelper = createColumnHelper<DecoratedPollingEntity<Transaction>>();

export const transactionTableColumns = [
columnHelper.accessor("id", {
header: () => <Label variant="medium">ID</Label>,
cell: (info) => (
<Value>
<NavLink to={`/transactions/details/${info.getValue()}`}>
<MiddleEllipsis className={classes.hash}>
{info.getValue()}
</MiddleEllipsis>
</NavLink>
</Value>
),
}),
columnHelper.accessor("blockId", {
header: () => <Label variant="medium">BLOCK ID</Label>,
cell: (info) => (
<Value>
<NavLink to={`/blocks/details/${info.getValue()}`}>
<MiddleEllipsis className={classes.hash}>
{info.getValue()}
</MiddleEllipsis>
</NavLink>
</Value>
),
}),
columnHelper.accessor("payer", {
header: () => <Label variant="medium">PAYER</Label>,
cell: (info) => (
<Value>
<AccountLink address={info.getValue()} />
</Value>
),
}),
columnHelper.accessor("status.executionStatus", {
header: () => <Label variant="medium">EXECUTION</Label>,
cell: (info) => (
<div>
<ExecutionStatus status={info.row.original.status} />
</div>
),
}),
columnHelper.accessor("status.grcpStatus", {
header: () => <Label variant="medium">API</Label>,
cell: (info) => (
<div>
<GrcpStatus status={info.row.original.status} />
</div>
),
}),
columnHelper.accessor("createdAt", {
header: () => <Label variant="medium">CREATED</Label>,
cell: (info) => (
<Value>
<ReactTimeago date={info.getValue()} />
</Value>
),
}),
];
import { TransactionsTable } from "./TransactionsTable";

const Main: FunctionComponent = () => {
const { showNavigationDrawer } = useNavigation();
const { data, firstFetch, error } = useGetPollingTransactions();
const { data } = useGetPollingTransactions();

useEffect(() => {
showNavigationDrawer(false);
}, []);

return (
<Table<DecoratedPollingEntity<Transaction>>
isInitialLoading={firstFetch}
error={error}
data={data}
columns={transactionTableColumns}
/>
);
return <TransactionsTable transactions={data} />;
};

export default Main;
Loading

0 comments on commit a261c0b

Please sign in to comment.