Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

Commit

Permalink
Feature: beta transaction details view (#1048)
Browse files Browse the repository at this point in the history
* Feature: beta transaction details

* Fix date
  • Loading branch information
shelegdmitriy authored Jul 22, 2022
1 parent c44e970 commit 3103446
Show file tree
Hide file tree
Showing 23 changed files with 1,486 additions and 1,307 deletions.
140 changes: 130 additions & 10 deletions backend/src/router/transaction/get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { Context } from "../../context";
import { RPC } from "../../types";
import * as nearApi from "../../utils/near";
import { validators } from "../validators";
import { indexerDatabase } from "../../database/databases";
import {
indexerActivityDatabase,
indexerDatabase,
} from "../../database/databases";
import { Action, mapRpcActionToAction } from "../../utils/actions";
import { mapRpcTransactionStatus } from "../../utils/transaction-status";
import {
Expand All @@ -15,13 +18,13 @@ import {
import { nanosecondsToMilliseconds } from "../../utils/bigint";
import { div } from "../../database/utils";

type ParsedReceipt = Omit<NestedReceiptWithOutcome, "outcome"> & {
outcome: Omit<NestedReceiptWithOutcome["outcome"], "nestedReceipts"> & {
type ParsedReceiptOld = Omit<NestedReceiptWithOutcomeOld, "outcome"> & {
outcome: Omit<NestedReceiptWithOutcomeOld["outcome"], "nestedReceipts"> & {
receiptIds: string[];
};
};

type NestedReceiptWithOutcome = {
type NestedReceiptWithOutcomeOld = {
id: string;
predecessorId: string;
receiverId: string;
Expand All @@ -32,10 +35,53 @@ type NestedReceiptWithOutcome = {
gasBurnt: number;
status: ReceiptExecutionStatus;
logs: string[];
nestedReceipts: NestedReceiptWithOutcomeOld[];
};
};

const collectNestedReceiptWithOutcomeOld = (
idOrHash: string,
parsedMap: Map<string, ParsedReceiptOld>
): NestedReceiptWithOutcomeOld => {
const parsedElement = parsedMap.get(idOrHash)!;
const { receiptIds, ...restOutcome } = parsedElement.outcome;
return {
...parsedElement,
outcome: {
...restOutcome,
nestedReceipts: receiptIds.map((id) =>
collectNestedReceiptWithOutcomeOld(id, parsedMap)
),
},
};
};

type NestedReceiptWithOutcome = Omit<NestedReceiptWithOutcomeOld, "outcome"> & {
outcome: Omit<
NestedReceiptWithOutcomeOld["outcome"],
"nestedReceipts" | "blockHash"
> & {
block: {
hash: string;
height: number;
timestamp: number;
};
nestedReceipts: NestedReceiptWithOutcome[];
};
};

type ParsedReceipt = Omit<NestedReceiptWithOutcome, "outcome"> & {
outcome: Omit<NestedReceiptWithOutcome["outcome"], "nestedReceipts"> & {
receiptIds: string[];
};
};

type ParsedBlock = {
hash: string;
height: number;
timestamp: number;
};

const collectNestedReceiptWithOutcome = (
idOrHash: string,
parsedMap: Map<string, ParsedReceipt>
Expand Down Expand Up @@ -95,9 +141,9 @@ const parseReceipt = (
};
};

const parseOutcome = (
const parseOutcomeOld = (
outcome: RPC.ExecutionOutcomeWithIdView
): ParsedReceipt["outcome"] => {
): ParsedReceiptOld["outcome"] => {
return {
blockHash: outcome.block_hash,
tokensBurnt: outcome.outcome.tokens_burnt,
Expand All @@ -108,6 +154,17 @@ const parseOutcome = (
};
};

const parseOutcome = (
outcome: RPC.ExecutionOutcomeWithIdView,
blocksMap: Map<string, ParsedBlock>
): ParsedReceipt["outcome"] => {
const { blockHash, ...oldParsedOutcome } = parseOutcomeOld(outcome);
return {
...oldParsedOutcome,
block: blocksMap.get(blockHash)!,
};
};

export const router = trpc
.router<Context>()
.query("byHashOld", {
Expand Down Expand Up @@ -143,10 +200,10 @@ export const router = trpc
);
return mapping.set(receiptOutcome.id, {
...receipt,
outcome: parseOutcome(receiptOutcome),
outcome: parseOutcomeOld(receiptOutcome),
});
},
new Map<string, ParsedReceipt>()
new Map<string, ParsedReceiptOld>()
);

return {
Expand All @@ -161,7 +218,7 @@ export const router = trpc
mapRpcActionToAction(action)
),
status: mapRpcTransactionStatus(rpcTransaction.status),
receipt: collectNestedReceiptWithOutcome(
receipt: collectNestedReceiptWithOutcomeOld(
rpcTransaction.transaction_outcome.outcome.receipt_ids[0],
receiptsMap
),
Expand All @@ -181,7 +238,7 @@ export const router = trpc
.selectFrom("transactions")
.select([
"signer_account_id as signerId",
(eb) => div(eb, "blocks.block_timestamp", 1000 * 1000, "timestamp"),
(eb) => div(eb, "block_timestamp", 1000 * 1000, "timestamp"),
])
.where("transaction_hash", "=", hash)
.executeTakeFirst();
Expand All @@ -192,6 +249,28 @@ export const router = trpc
"EXPERIMENTAL_tx_status",
[hash, databaseTransaction.signerId]
);
const blocks = await indexerDatabase
.selectFrom("blocks")
.select([
"block_height as height",
"block_hash as hash",
(eb) => div(eb, "block_timestamp", 1000 * 1000, "timestamp"),
])
.where(
"block_hash",
"in",
rpcTransaction.receipts_outcome.map((outcome) => outcome.block_hash)
)
.execute();
const blocksMap = blocks.reduce(
(map, row) =>
map.set(row.hash, {
hash: row.hash,
height: parseInt(row.height),
timestamp: parseInt(row.timestamp),
}),
new Map<string, ParsedBlock>()
);

const transactionFee = getTransactionFee(
rpcTransaction.transaction_outcome,
Expand All @@ -202,6 +281,23 @@ export const router = trpc
rpcTransaction.transaction.actions.map(mapRpcActionToAction);
const transactionAmount = getDeposit(txActions);

const receiptsMap = rpcTransaction.receipts_outcome.reduce(
(mapping, receiptOutcome) => {
const receipt = parseReceipt(
rpcTransaction.receipts.find(
(receipt) => receipt.receipt_id === receiptOutcome.id
),
receiptOutcome,
rpcTransaction.transaction
);
return mapping.set(receiptOutcome.id, {
...receipt,
outcome: parseOutcome(receiptOutcome, blocksMap),
});
},
new Map<string, ParsedReceipt>()
);

return {
hash,
timestamp: parseInt(databaseTransaction.timestamp),
Expand All @@ -210,6 +306,10 @@ export const router = trpc
fee: transactionFee.toString(),
amount: transactionAmount.toString(),
status: mapRpcTransactionStatus(rpcTransaction.status),
receipt: collectNestedReceiptWithOutcome(
rpcTransaction.transaction_outcome.outcome.receipt_ids[0],
receiptsMap
),
};
},
})
Expand All @@ -227,4 +327,24 @@ export const router = trpc
.executeTakeFirst();
return transactionInfo;
},
})
.query("accountBalanceChange", {
input: z.strictObject({
accountId: validators.accountId,
receiptId: validators.receiptId,
}),
resolve: async ({ input: { accountId, receiptId } }) => {
const balanceChanges = await indexerActivityDatabase
.selectFrom("balance_changes")
.select(["absolute_nonstaked_amount as absoluteNonStakedAmount"])
.where("affected_account_id", "=", accountId)
.where("receipt_id", "=", receiptId)
.orderBy("index_in_chunk", "desc")
.limit(1)
.executeTakeFirst();
if (!balanceChanges) {
return null;
}
return balanceChanges.absoluteNonStakedAmount;
},
});
1 change: 1 addition & 0 deletions common/src/types/procedures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export type TransactionPreview = TransactionListResponse["items"][number];

export type Transaction = NonNullable<TRPCQueryOutput<"transaction.byHash">>;
export type TransactionStatus = Transaction["status"];
export type TransactionReceipt = Transaction["receipt"];

export type Action = TransactionPreview["actions"][number];
export type TransactionOld = NonNullable<
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"react-hot-toast": "^2.2.0",
"react-i18next": "^11.15.1",
"react-infinite-scroll-component": "^5.1.0",
"react-json-view": "^1.21.3",
"react-paginate": "^7.1.5",
"react-query": "^3.39.0",
"styled-components": "^4.4.0",
Expand Down
1 change: 1 addition & 0 deletions frontend/public/static/images/icon-arrow-down-black.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 22 additions & 0 deletions frontend/src/components/beta/common/AccountLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as React from "react";
import Link from "../../utils/Link";
import { shortenString } from "../../../libraries/formatting";
import { styled } from "../../../libraries/styles";

const AccountLinkWrapper = styled("a", {
whiteSpace: "nowrap",
});

export interface Props {
accountId: string;
}

const AccountLink: React.FC<Props> = React.memo(({ accountId }) => {
return (
<Link href={`/beta/accounts/${accountId}`} passHref>
<AccountLinkWrapper>{shortenString(accountId)}</AccountLinkWrapper>
</Link>
);
});

export default AccountLink;
21 changes: 21 additions & 0 deletions frontend/src/components/beta/common/BlockLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as React from "react";
import Link from "../../utils/Link";
import { styled } from "../../../libraries/styles";

export interface Props {
blockHash: string;
blockHeight: number;
}

const LinkWrapper = styled("a", {
whiteSpace: "nowrap",
cursor: "pointer",
});

const BlockLink: React.FC<Props> = React.memo(({ blockHash, blockHeight }) => (
<Link href="/blocks/[hash]" as={`/blocks/${blockHash}`}>
<LinkWrapper>{`#${blockHeight}`}</LinkWrapper>
</Link>
));

export default BlockLink;
53 changes: 53 additions & 0 deletions frontend/src/components/beta/common/CodeArgs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as React from "react";
import { hexy } from "hexy";

import { styled } from "../../../libraries/styles";

import CodePreview from "../../utils/CodePreview";
import JsonView from "./JsonView";

const HexArgs = styled("div", {
padding: "10px 0",

"& > div": {
background: "#f8f8f8",
borderRadius: 4,
color: "#3f4246",
padding: 20,
fontSize: 14,
fontWeight: 500,

"& textarea, pre": {
background: "inherit",
color: "inherit",
fontFamily: "inherit",
fontSize: "inherit",
border: "none",
padding: 0,
},
},
});

const CodeArgs: React.FC<{ args: string }> = React.memo(({ args }) => {
const decodedArgs = Buffer.from(args, "base64");

let prettyArgs: object | string;
try {
const parsedJSONArgs = JSON.parse(decodedArgs.toString());
prettyArgs =
typeof parsedJSONArgs === "boolean"
? JSON.stringify(parsedJSONArgs)
: parsedJSONArgs;
} catch {
prettyArgs = hexy(decodedArgs, { format: "twos" });
}
return typeof prettyArgs === "object" ? (
<JsonView args={prettyArgs} />
) : (
<HexArgs>
<CodePreview collapseHeight={200} maxHeight={600} value={prettyArgs} />
</HexArgs>
);
});

export default CodeArgs;
24 changes: 24 additions & 0 deletions frontend/src/components/beta/common/JsonView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import dynamic from "next/dynamic";
import * as React from "react";
// https://github.com/mac-s-g/react-json-view/issues/296#issuecomment-803497117
const DynamicReactJson = dynamic(import("react-json-view"), { ssr: false });

type Props = {
args: object;
};

const JsonView: React.FC<Props> = React.memo(({ args }) => (
<DynamicReactJson
src={args}
name={null}
iconStyle="triangle"
displayObjectSize={false}
displayDataTypes={false}
style={{
fontSize: "14px",
padding: "10px 0",
}}
/>
));

export default JsonView;
1 change: 0 additions & 1 deletion frontend/src/components/beta/common/UtcLabel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from "react";
import { styled } from "../../../libraries/styles";

const UTC = styled("div", {
fontFamily: "SF Mono",
fontWeight: 600,
fontSize: 12,
lineHeight: "20px",
Expand Down
Loading

0 comments on commit 3103446

Please sign in to comment.