Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 <chain>` (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`.
Expand Down
17 changes: 12 additions & 5 deletions src/cli/sync-modules/sync-accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,25 @@ export class AccountSynchronizer extends Synchronizer<IAccount> {
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current check allows any field at index i to be accepted as valid if its name is either "from" or "to" and its type is "account_name", regardless of whether the index matches the expected field name (e.g., if from and to are swapped, or if a different field is named "from").

Since transferFields[i].name already contains the expected name ("from" at index 0, "to" at index 1), we can simplify and secure this check by verifying that fields[i].name matches transferFields[i].name directly.

Suggested change
if (fields[i] && (fields[i].name === "from" || fields[i].name === "to") && fields[i].type === 'account_name') {
valid = true;
continue;
}
if (fields[i] && fields[i].name === transferFields[i].name && fields[i].type === 'account_name') {
valid = true;
continue;
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — applied in 584db2b. Matching transferFields[i].name positionally (consistent with the standard-type branch). Verified no behavior change for real contracts (still 22/22 accepted against api.eosrio.io); it only rejects malformed/swapped ABIs.

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;
Expand Down
Loading