Skip to content

Commit a8a7e62

Browse files
committed
fix: notification dedup regression causing exchange triple-counting (#148 side-effect)
The v4.0 fix for #148 changed the action dedup grouping key to act_digest:action_ordinal, which prevented notification traces from merging with their parent action. This caused get_transaction to return 3 separate transfer entries instead of 1 action with 3 receipts. Indexer: - Use creator_action_ordinal as canonical ordinal for notifications - Notifications (creator>0) group with parent, roots (creator=0) stay separate - Fixes both #148 (distinct duplicates) and notification merging API (backward-compatible, no re-index required): - Add regroupActions() helper to re-merge fragmented data on read - Apply to both v1 and v2 get_transaction handlers - Add 'notified' field to v2 response (comma-separated receivers) Bump to v4.0.5
1 parent 3a4fa42 commit a8a7e62

File tree

7 files changed

+411
-33
lines changed

7 files changed

+411
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "hyperion-history",
3-
"version": "4.0.4",
3+
"version": "4.0.5",
44
"description": "Scalable Full History API Solution for Antelope based blockchains",
55
"main": "./build/indexer/launcher.js",
66
"scripts": {

src/api/helpers/regroup-actions.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Re-groups actions that were indexed as separate documents per notification
3+
* back into the correct format: one action per unique act_digest + root ordinal,
4+
* with all notification receipts merged into a single receipts array.
5+
*
6+
* This handles both correctly-indexed data (already grouped) and data indexed
7+
* with the broken dedup that stored one document per notification.
8+
*/
9+
export function regroupActions(actions: any[]): any[] {
10+
if (actions.length <= 1) return actions;
11+
12+
const groups = new Map<string, any>();
13+
14+
for (const action of actions) {
15+
// determine canonical ordinal: notifications group with their parent
16+
const creator = action.creator_action_ordinal ?? 0;
17+
const ordinal = action.action_ordinal ?? 1;
18+
const canonical = creator > 0 ? creator : ordinal;
19+
const digest = action.act_digest ?? '';
20+
const key = `${digest}:${canonical}`;
21+
22+
if (groups.has(key)) {
23+
// merge receipts into existing group
24+
const existing = groups.get(key);
25+
if (action.receipts && action.receipts.length > 0) {
26+
for (const receipt of action.receipts) {
27+
// avoid duplicates (already-grouped data)
28+
const isDup = existing.receipts.some(
29+
(r: any) => r.receiver === receipt.receiver
30+
);
31+
if (!isDup) {
32+
existing.receipts.push(receipt);
33+
}
34+
}
35+
}
36+
} else {
37+
// ensure receipts array exists
38+
if (!action.receipts) {
39+
action.receipts = [];
40+
}
41+
groups.set(key, action);
42+
}
43+
}
44+
45+
return [...groups.values()];
46+
}

src/api/routes/v1-history/get_transaction/get_transaction.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
22
import {mergeActionMeta, timedQuery} from "../../../helpers/functions.js";
33
import {API} from "@wharfkit/antelope";
4+
import {regroupActions} from "../../../helpers/regroup-actions.js";
45

56

67
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
@@ -120,7 +121,16 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
120121

121122

122123
if (hits.length > 0) {
123-
const actions = hits;
124+
const rawActions: any[] = [];
125+
for (let action of hits) {
126+
action = action._source;
127+
mergeActionMeta(action);
128+
rawActions.push(action);
129+
}
130+
131+
// re-group notifications that were indexed as separate documents
132+
const actions = regroupActions(rawActions);
133+
124134
response.trx.trx = {
125135
"expiration": "",
126136
"ref_block_num": 0,
@@ -134,9 +144,7 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
134144
"signatures": [],
135145
"context_free_data": []
136146
};
137-
for (let action of actions) {
138-
action = action._source;
139-
mergeActionMeta(action);
147+
for (const action of actions) {
140148
response.block_num = action.block_num;
141149
response.block_time = action['@timestamp'];
142150
for (const receipt of action.receipts) {

src/api/routes/v2-history/get_transaction/get_transaction.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {FastifyInstance, FastifyReply, FastifyRequest} from "fastify";
22
import {mergeActionMeta, timedQuery} from "../../../helpers/functions.js";
3+
import {regroupActions} from "../../../helpers/regroup-actions.js";
34
import {API} from "@wharfkit/antelope";
45

56
async function getTransaction(fastify: FastifyInstance, request: FastifyRequest) {
@@ -108,13 +109,28 @@ async function getTransaction(fastify: FastifyInstance, request: FastifyRequest)
108109
highestBlockNum = action._source.block_num;
109110
}
110111
}
111-
response.actions = [];
112+
const rawActions: any[] = [];
112113
for (let action of hits) {
113114
if (action._source.block_num === highestBlockNum) {
114115
mergeActionMeta(action._source);
115-
response.actions.push(action._source);
116+
rawActions.push(action._source);
116117
}
117118
}
119+
120+
// re-group notifications that were indexed as separate documents
121+
const grouped = regroupActions(rawActions);
122+
123+
// add notified field derived from receipts
124+
response.actions = grouped.map(action => {
125+
if (action.receipts && action.receipts.length > 0) {
126+
const receivers = new Set<string>(
127+
action.receipts.map((r: any) => r.receiver)
128+
);
129+
action.notified = [...receivers].join(',');
130+
}
131+
return action;
132+
});
133+
118134
response.executed = true;
119135
}
120136
return response;

src/indexer/helpers/action-dedup.ts

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,22 @@ import { ActionTrace } from "../../interfaces/action-trace.js";
55
*
66
* In Antelope, when an action notifies other accounts (e.g. eosio.token::transfer),
77
* the same action appears multiple times — once per receiver — with the same
8-
* `act_digest` and `action_ordinal` but different `receiver`.
8+
* `act_digest` but different `action_ordinal` and `receiver`. The
9+
* `creator_action_ordinal` links each notification back to the parent action.
910
*
1011
* These notification traces should be merged into a single document with
1112
* multiple receipts (the `receipts` array).
1213
*
13-
* However, genuinely distinct duplicate actions (same content, different
14-
* `action_ordinal`) must be kept as separate documents.
14+
* However, genuinely distinct duplicate actions (same content and `act_digest`,
15+
* but independent root actions with `creator_action_ordinal === 0`) must be
16+
* kept as separate documents.
1517
*
16-
* The grouping key is `act_digest + ":" + action_ordinal` to distinguish
17-
* between these two cases.
18+
* The grouping key is `act_digest + ":" + canonical_ordinal` where:
19+
* - Root actions (creator_action_ordinal === 0): canonical = action_ordinal
20+
* - Notifications (creator_action_ordinal > 0): canonical = creator_action_ordinal
21+
*
22+
* This ensures notifications merge with their parent while genuinely distinct
23+
* duplicate actions remain separate (fixes #148 without breaking notifications).
1824
*
1925
* @param processedTraces - Array of parsed action traces with receipt data
2026
* @returns Array of grouped action traces with merged receipts
@@ -25,9 +31,12 @@ export function groupActionTraces(processedTraces: ActionTrace[]): ActionTrace[]
2531
if (processedTraces.length > 1) {
2632
const traceGroups: Record<string, any[]> = {};
2733

28-
// collect receipts grouped by act_digest + action_ordinal
34+
// collect receipts grouped by act_digest + canonical ordinal
2935
for (const trace of processedTraces) {
30-
const groupKey = `${trace.receipt.act_digest}:${trace.action_ordinal}`;
36+
const canonical = trace.creator_action_ordinal > 0
37+
? trace.creator_action_ordinal
38+
: trace.action_ordinal;
39+
const groupKey = `${trace.receipt.act_digest}:${canonical}`;
3140
if (traceGroups[groupKey]) {
3241
traceGroups[groupKey].push(trace.receipt);
3342
} else {
@@ -37,7 +46,10 @@ export function groupActionTraces(processedTraces: ActionTrace[]): ActionTrace[]
3746

3847
// merge receipts into the first trace instance per group
3948
for (const trace of processedTraces) {
40-
const groupKey = `${trace.receipt.act_digest}:${trace.action_ordinal}`;
49+
const canonical = trace.creator_action_ordinal > 0
50+
? trace.creator_action_ordinal
51+
: trace.action_ordinal;
52+
const groupKey = `${trace.receipt.act_digest}:${canonical}`;
4153
if (traceGroups[groupKey]) {
4254
trace['receipts'] = [];
4355
for (const receipt of traceGroups[groupKey]) {

0 commit comments

Comments
 (0)