From 98fae8c19a8fca990e23b3c29e74aed701beb704 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Fri, 29 May 2026 03:47:08 -0300 Subject: [PATCH 1/2] fix(cli): resolve transfer struct via action type in sync-accounts scanABIs() looked up a contract's transfer parameter struct by the hard-coded name "transfer". Per the ABI spec the struct backing an action is named by the action's `type` field, which is frequently not the action name (e.g. paycash contracts declare a standard transfer under the struct "transfer_token"). The lookup returned undefined, so those valid token contracts were silently skipped and never backfilled into the MongoDB accounts collection. get_tokens then returned only the symbols the live indexer captured, giving "same contract, some symbols missing" on upgraded nodes. Resolve the struct through the transfer action's declared type, and guard against short field arrays. Verified against api.eosrio.io: the 10 previously-skipped contracts held by bu.klnk (token.pcash, cashescashes, swap.pcash, jqpua4jqkqwz, testnewswap1, testrenminbi, testrublesrb, testuralsurl, uralsuralsru, testusdbxusd) are now accepted, with no change to standard-transfer contracts. --- CHANGELOG.md | 6 ++++++ src/cli/sync-modules/sync-accounts.ts | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6aa82a1e..02fb04c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased (dev) + +### Fixes + +* **`/v2/state/get_tokens` missing balances — token-contract detection in `sync accounts`**: `./hyp-control sync accounts` (and `sync all`) resolved a contract's transfer parameter struct by the hard-coded struct name `"transfer"`. Per the ABI spec the struct backing an action is named by the action's `type` field, which is frequently *not* the action name — e.g. several contracts declare a fully standard transfer (`from:name, to:name, quantity:asset, memo:string`) under the struct name `transfer_token`. Those contracts were silently skipped, so their balances were never backfilled into the MongoDB `accounts` collection and `get_tokens` returned only the symbols the live indexer happened to capture — producing a "same contract, some symbols present and some missing" result on upgraded nodes. The struct is now resolved through the transfer action's declared `type`. **After upgrading, re-run `./hyp-control sync accounts ` (or `sync all`) to backfill the previously skipped contracts.** + ## 4.0.7 (2026-05-16) > **4.0.6 was skipped.** The 4.0.6 version sat on `main` untagged for a period and some operators deployed it directly from `main` before any release tag existed. To avoid ambiguity between those pre-tag production deployments and the official artifact, this work is released as **4.0.7**. There is no separate 4.0.6 entry. Going forward, `main` only advances to tagged releases and active development happens on `dev`. diff --git a/src/cli/sync-modules/sync-accounts.ts b/src/cli/sync-modules/sync-accounts.ts index fa4c8b65..898aa707 100644 --- a/src/cli/sync-modules/sync-accounts.ts +++ b/src/cli/sync-modules/sync-accounts.ts @@ -68,18 +68,25 @@ export class AccountSynchronizer extends Synchronizer { const abi = await this.client.v1.chain.get_abi(contract); const tables = new Set(abi.abi?.tables?.map(value => value.name)); if (tables.has("accounts") && tables.has("stat")) { - const actions = new Set(abi.abi?.actions?.map(value => value.name)); - if (actions.has("transfer")) { - const transferType = abi.abi?.structs?.find(s => s.name === 'transfer'); + // Resolve the transfer parameter struct through the transfer action's + // declared `type`, not the literal name "transfer". Per the ABI spec the + // struct backing an action is named by the action's `type` field, which is + // frequently NOT the action name — e.g. the paycash contracts (token.pcash, + // cashescashes, swap.pcash, ...) declare a standard transfer under the struct + // name "transfer_token". Looking the struct up by name === "transfer" returned + // undefined and silently dropped these valid token contracts from the backfill. + const transferAction = abi.abi?.actions?.find(a => a.name === "transfer"); + if (transferAction) { + const transferType = abi.abi?.structs?.find(s => s.name === transferAction.type); if (transferType && transferType.fields) { const fields = transferType.fields; let valid = true; for (let i = 0; i < transferFields.length; i++) { - if ((fields[i].name === "from" || fields[i].name === "to") && fields[i].type === 'account_name') { + if (fields[i] && (fields[i].name === "from" || fields[i].name === "to") && fields[i].type === 'account_name') { valid = true; continue; } - if (fields[i].name !== transferFields[i].name || fields[i].type !== transferFields[i].type) { + if (!fields[i] || fields[i].name !== transferFields[i].name || fields[i].type !== transferFields[i].type) { console.error(`Invalid token contract ${contract} ->> ${fields.map(f => (f.name + "(" + f.type + ")").padEnd(24, " ")).join(' ')}`); valid = false; break; From 584db2bc74003d78f4ea990dd0cbc8eebcd29453 Mon Sep 17 00:00:00 2001 From: Igor Lins e Silva <4753812+igorls@users.noreply.github.com> Date: Fri, 29 May 2026 03:50:20 -0300 Subject: [PATCH 2/2] fix(cli): match from/to positionally in legacy account_name check Address Gemini review on #170: the account_name legacy branch accepted any field named from/to regardless of position. Match transferFields[i].name positionally, consistent with the standard-type branch below it. No change for real contracts (verified 22/22 still accepted); only tightens malformed/ swapped ABIs. --- src/cli/sync-modules/sync-accounts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/sync-modules/sync-accounts.ts b/src/cli/sync-modules/sync-accounts.ts index 898aa707..fb63bed8 100644 --- a/src/cli/sync-modules/sync-accounts.ts +++ b/src/cli/sync-modules/sync-accounts.ts @@ -82,7 +82,7 @@ export class AccountSynchronizer extends Synchronizer { const fields = transferType.fields; let valid = true; for (let i = 0; i < transferFields.length; i++) { - if (fields[i] && (fields[i].name === "from" || fields[i].name === "to") && fields[i].type === 'account_name') { + if (fields[i] && fields[i].name === transferFields[i].name && fields[i].type === 'account_name') { valid = true; continue; }