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

Commit

Permalink
Feature: beta account activity w/ parent & children receipts
Browse files Browse the repository at this point in the history
  • Loading branch information
luixo committed May 20, 2022
1 parent df7a8df commit 783f516
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 99 deletions.
104 changes: 66 additions & 38 deletions backend/src/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { sha256 } from "js-sha256";
import {
AccountActivityAction,
AccountActivityCursor,
AccountActivityElement,
AccountListInfo,
AccountTransactionsCount,
Action,
ActivityConnection,
Receipt,
TransactionBaseInfo,
} from "./types";
Expand Down Expand Up @@ -203,110 +205,136 @@ export const getIdsFromAccountChanges = (

const getActivityAction = (
actions: Action[],
{
transactionHash,
receiptId,
}: { transactionHash: string; receiptId?: string },
isRefund: boolean
isRefund?: boolean
): AccountActivityAction => {
if (actions.length === 0) {
throw new Error("Unexpected zero-length array of actions");
}
if (actions.length !== 1) {
return {
type: "batch",
transactionHash,
actions: actions.map((action) =>
getActivityAction([action], { transactionHash, receiptId }, isRefund)
),
actions: actions.map((action) => getActivityAction([action], isRefund)),
};
}
switch (actions[0].kind) {
case "AddKey":
return {
type: "access-key-created",
transactionHash,
receiptId,
};
case "CreateAccount":
return {
type: "account-created",
transactionHash,
receiptId,
};
case "DeleteAccount":
return {
type: "account-removed",
transactionHash,
receiptId,
};
case "DeleteKey":
return {
type: "access-key-removed",
transactionHash,
receiptId,
};
case "DeployContract":
return {
type: "contract-deployed",
transactionHash,
receiptId,
};
case "FunctionCall":
return {
type: "call-method",
transactionHash,
receiptId,
methodName: actions[0].args.method_name,
};
case "Stake":
return {
type: "restake",
transactionHash,
receiptId,
};
case "Transfer":
return {
type: isRefund ? "refund" : "transfer",
transactionHash,
receiptId,
};
}
};

const withActivityConnection = <T>(
input: T,
source?: Receipt | TransactionBaseInfo
): T & ActivityConnection => {
if (!source) {
return {
...input,
transactionHash: "",
};
}
if ("receiptId" in source) {
return {
...input,
transactionHash: source.originatedFromTransactionHash,
receiptId: source.receiptId,
};
}
return {
...input,
transactionHash: source.hash,
};
};

export const getAccountActivityAction = (
change: Awaited<ReturnType<typeof queryBalanceChanges>>[number],
receiptsMapping: Map<string, Receipt>,
transactionsMapping: Map<string, TransactionBaseInfo>,
blockHeightsMapping: Map<string, { hash: string }>
): AccountActivityAction => {
blockHeightsMapping: Map<string, { hash: string }>,
receiptRelations: Map<
string,
{ parentReceiptId: string | null; childrenReceiptIds: string[] }
>
): AccountActivityElement["action"] => {
switch (change.cause) {
case "CONTRACT_REWARD":
case "RECEIPT":
const connectedReceipt = receiptsMapping.get(change.receiptId!)!;
return getActivityAction(
connectedReceipt.actions,
const relation = receiptRelations.get(change.receiptId!)!;
const parentReceipt = relation.parentReceiptId
? receiptsMapping.get(relation.parentReceiptId)!
: undefined;
const childrenReceipts = relation.childrenReceiptIds.map(
(childrenReceiptId) => receiptsMapping.get(childrenReceiptId)!
);
return withActivityConnection(
{
receiptId: connectedReceipt.receiptId,
transactionHash: connectedReceipt.originatedFromTransactionHash,
...getActivityAction(
connectedReceipt.actions,
!change.involvedAccountId
),
parentAction: parentReceipt
? withActivityConnection(
getActivityAction(parentReceipt.actions),
parentReceipt
)
: undefined,
childrenActions: childrenReceipts.map((receipt) =>
withActivityConnection(getActivityAction(receipt.actions), receipt)
),
},
!change.involvedAccountId
connectedReceipt
);
case "TRANSACTION": {
const connectedTransaction = transactionsMapping.get(
change.transactionHash!
)!;
return getActivityAction(
connectedTransaction.actions,
{ transactionHash: connectedTransaction.hash },
!change.involvedAccountId
return withActivityConnection(
{
...getActivityAction(
connectedTransaction.actions,
!change.involvedAccountId
),
childrenActions: [],
},
connectedTransaction
);
}
case "VALIDATORS_REWARD":
const connectedBlock = blockHeightsMapping.get(change.blockTimestamp!)!;
return {
return withActivityConnection({
type: "validator-reward",
blockHash: connectedBlock.hash,
};
});
}
};
12 changes: 12 additions & 0 deletions backend/src/db-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1150,6 +1150,18 @@ export const queryReceiptsByIds = async (ids: string[]) => {
.execute();
};

export const queryRelatedReceiptsIds = async (ids: string[]) => {
return indexerDatabase
.selectFrom("execution_outcome_receipts")
.select([
"executed_receipt_id as executedReceiptId",
"produced_receipt_id as producedReceiptId",
])
.where("executed_receipt_id", "in", ids)
.orWhere("produced_receipt_id", "in", ids)
.execute();
};

export const queryContractInfo = async (accountId: string) => {
// find the latest update in analytics db
const latestUpdateResult = await analyticsDatabase
Expand Down
43 changes: 26 additions & 17 deletions backend/src/procedure-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -291,23 +291,32 @@ export const procedureHandlers: {
transactions.getTransactionsByHashes(idsToFetch.transactionHashes),
blocks.getBlockHeightsByTimestamps(idsToFetch.blocksTimestamps),
]);
return changes.map((change) => ({
timestamp: nanosecondsToMilliseconds(BigInt(change.blockTimestamp)),
involvedAccountId: change.involvedAccountId,
direction: change.direction === "INBOUND" ? "inbound" : "outbound",
deltaAmount: change.deltaNonStakedAmount,
action: accounts.getAccountActivityAction(
change,
receiptsMapping,
transactionsMapping,
blocksMapping
),
cursor: {
blockTimestamp: change.blockTimestamp,
shardId: change.shardId,
indexInChunk: change.indexInChunk,
},
}));
const {
receiptsMapping: receiptsMappingWithRelated,
relations: receiptRelations,
} = await receipts.getRelatedReceiptsByIds(receiptsMapping);
return changes
.filter(
(change) => change.direction !== "INBOUND" || change.cause !== "RECEIPT"
)
.map((change) => ({
timestamp: nanosecondsToMilliseconds(BigInt(change.blockTimestamp)),
involvedAccountId: change.involvedAccountId,
direction: change.direction === "INBOUND" ? "inbound" : "outbound",
deltaAmount: change.deltaNonStakedAmount,
action: accounts.getAccountActivityAction(
change,
receiptsMappingWithRelated,
transactionsMapping,
blocksMapping,
receiptRelations
),
cursor: {
blockTimestamp: change.blockTimestamp,
shardId: change.shardId,
indexInChunk: change.indexInChunk,
},
}));
},

// blocks
Expand Down
78 changes: 76 additions & 2 deletions backend/src/receipts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
queryIncludedReceiptsList,
queryExecutedReceiptsList,
queryReceiptsByIds,
queryRelatedReceiptsIds,
} from "./db-utils";

import {
Expand Down Expand Up @@ -95,14 +96,87 @@ export const getReceiptsByIds = async (
if (ids.length === 0) {
return new Map();
}
const receiptActions = await queryReceiptsByIds(ids);
const receipts = groupReceiptActionsIntoReceipts(receiptActions);
const receiptRows = await queryReceiptsByIds(ids);
const receipts = groupReceiptActionsIntoReceipts(receiptRows);
return receipts.reduce<Map<string, Receipt>>((acc, receipt) => {
acc.set(receipt.receiptId, receipt);
return acc;
}, new Map());
};

export const getRelatedReceiptsByIds = async (
prevReceiptsMapping: Map<string, Receipt>
): Promise<{
receiptsMapping: Map<string, Receipt>;
relations: Map<
string,
{ parentReceiptId: string | null; childrenReceiptIds: string[] }
>;
}> => {
const ids = [...prevReceiptsMapping.keys()];
if (ids.length === 0) {
return {
receiptsMapping: prevReceiptsMapping,
relations: new Map(),
};
}
const relatedResult = await queryRelatedReceiptsIds(ids);
const relations = ids.reduce<
Map<
string,
{ parentReceiptId: string | null; childrenReceiptIds: string[] }
>
>((acc, id) => {
let relatedIds: {
parentReceiptId: string | null;
childrenReceiptIds: string[];
} = {
parentReceiptId: null,
childrenReceiptIds: [],
};
const parentRow = relatedResult.find((row) => row.producedReceiptId === id);
if (parentRow) {
relatedIds.parentReceiptId = parentRow.executedReceiptId;
}
const childrenRows = relatedResult.filter(
(row) => row.executedReceiptId === id
);
if (childrenRows.length !== 0) {
relatedIds.childrenReceiptIds = childrenRows.map(
(row) => row.producedReceiptId
);
}
acc.set(id, relatedIds);
return acc;
}, new Map());
const prevReceiptIds = [...prevReceiptsMapping.keys()];
const lookupIds = [...relations.values()].reduce<Set<string>>(
(acc, relation) => {
if (
relation.parentReceiptId &&
!prevReceiptIds.includes(relation.parentReceiptId)
) {
acc.add(relation.parentReceiptId);
}
const filteredChildrenIds = relation.childrenReceiptIds.filter(
(id) => !prevReceiptIds.includes(id)
);
filteredChildrenIds.forEach((id) => acc.add(id));
return acc;
},
new Set()
);
const receiptRows = await queryReceiptsByIds([...lookupIds]);
const receipts = groupReceiptActionsIntoReceipts(receiptRows);
return {
receiptsMapping: receipts.reduce<Map<string, Receipt>>((acc, receipt) => {
acc.set(receipt.receiptId, receipt);
return acc;
}, new Map(prevReceiptsMapping)),
relations,
};
};

export const getReceiptsCountInBlock = async (
blockHash: string
): Promise<number | null> => {
Expand Down
Loading

0 comments on commit 783f516

Please sign in to comment.