Skip to content

fix(#821): agent matches balances by issuer, never by code - #939

Open
Anambraboi-1 wants to merge 1 commit into
Miracle656:mainfrom
Anambraboi-1:fix/issue-821-agent-balance-issuer
Open

Anambraboi-1 wants to merge 1 commit into
Miracle656:mainfrom
Anambraboi-1:fix/issue-821-agent-balance-issuer

Conversation

@Anambraboi-1

Copy link
Copy Markdown
Contributor

Summary

The agent's balance tool now checks every trustline against a registry by code and issuer. A counterfeit is never reported under a registered asset's name.

A note on the issue text: the code #821 describes isn't on main. txBuilder.ts:166-174 has no getRegisteredAsset(code) call and no bare-code alias, and the agent has no isRegisteredIssuer helper. What main actually does is key trustlines as CODE:ISSUER and hand them to the model with no verification. So a fake USDT0:G-FAKE… looked the same as the real one, and the model reported it as USDT0. This PR adds the missing registry and the issuer gate.

Changes (packages/agent)

  • src/assetRegistry.ts (new):
    • Issuers pinned per network, mirroring frontend/mobile/lib/assets.ts: mainnet USDC, USDY and USDT0; testnet USDC only (no testnet USDT0 or USDY).
    • getRegisteredAsset, isRegisteredIssuer (code and issuer must match) and classifyHolding.
  • getBalances() now returns { balances, holdings }:
    • balances stays keyed CODE:ISSUER and never has a bare-code alias.
    • holdings classifies each trustline. A verified holding names the asset, the issuer name and the issuer address. Anything else is verified: false, with a note starting UNVERIFIED that names its issuer; a fake of a registered code also names the real issuer.
    • Liquidity-pool shares are skipped instead of producing undefined:undefined keys.
  • agent.ts: the get_wallet_balance description and a new system-prompt rule (9) tell the model to relay each holding's note, always name the issuer, and never call an unverified holding by a registered name.

getBalances is internal (not exported from index.ts), and agent.ts is its only caller.

Overlap with #935: that PR adds a similar registry at packages/agent/src/assets.ts. This one uses a separate file (assetRegistry.ts) so the two don't collide as files, but whichever lands second should be folded into the other.

Related issue

Closes #821

Type of change

  • Bug fix
  • Tests

Component

  • Agent

Checklist

  • npm run typecheck passes (agent: tsc --noEmit clean)
  • npm run build passes (agent)
  • I added or updated tests where relevant

Screenshots / test output

  • txBuilder.test.ts covers a wallet holding both the real USDT0 (25) and a fake (5000):

    • no bare USDT0 key;
    • the real one is verified, with issuerName: 'Tether' and its issuer in the note;
    • the fake is UNVERIFIED, shows its own issuer, and has no registry name;
    • liquidity-pool shares are skipped.

    Against the old txBuilder.ts, 4 of these fail.

  • assetRegistry.test.ts checks that:

    • every pinned issuer passes StrKey.isValidEd25519PublicKey;
    • matching goes by issuer, not code;
    • there is no testnet USDT0;
    • unlisted and counterfeit holdings are unverified and show their issuer.

Agent suite: 7 suites / 64 tests passing.

getBalances() keyed trustlines as CODE:ISSUER but nothing told the model
which were genuine, so a counterfeit USDT0 read the same as the real one.
The agent had no asset registry, so the isRegisteredIssuer gate the issue
describes did not exist yet.

- src/assetRegistry.ts: issuers pinned per network (mainnet USDC, USDY,
  USDT0; testnet USDC), mirroring frontend/mobile/lib/assets.ts, with
  getRegisteredAsset, isRegisteredIssuer (code AND issuer) and
  classifyHolding.
- getBalances() returns { balances, holdings }. Balances stay keyed
  CODE:ISSUER, never by bare code; each trustline is classified by issuer.
  A verified holding names its issuer; anything else is UNVERIFIED with its
  issuer shown (and, for a fake of a registered code, the real issuer).
  Liquidity-pool shares are skipped instead of emitting undefined:undefined.
- The balance tool description and a system-prompt rule tell the model to
  relay those notes and never call an unverified holding by a registered
  name.

Closes Miracle656#821
@vercel

vercel Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Anambraboi-1 is attempting to deploy a commit to the miracle656's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Sep 30, 2026

Copy link
Copy Markdown

@Anambraboi-1 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Miracle656 Miracle656 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Good work — the rule this enforces is the right one, and getBalances classifying at source is a better place for it than the call site. But #935 landed an hour ago against the same issue family and it took the other half of this ground, so this needs a rebase and a scope trim before it can go in.

What's on main now (from #935, packages/agent/src/assets.ts): an agent-side registry keyed by network, classifyHolding / classifyBalances returning status: 'verified' | 'unverified' | 'unlisted' with a message, a get_asset_info tool, and agent.ts already returning { ...balances, holdings: classifyBalances(balances) }. The conflict is real: git merge stops on packages/agent/src/agent.ts in both the get_wallet_balance tool description and the numbered rule at the end of buildSystemPrompt.

Please do not land a second registry. packages/agent/src/assetRegistry.ts and packages/agent/src/assets.ts would be two hand-maintained copies of the same pinned issuers inside one package — which is exactly the drift #822 exists to catch, one directory deeper. Fold your work into assets.ts.

Three things here are worth keeping, and I'd like all three:

  1. The liquidity-pool guard in txBuilder.ts. This is a genuine bug you found and nobody else did. main still does:

    const key = `${(balance as any).asset_code}:${(balance as any).asset_issuer}`
    result[key] = balance.balance

    An LP-share balance has neither field, so the model is handed a key literally spelled undefined:undefined. #935's classifyBalances skips it when building holdings (its StrKey.isValidEd25519PublicKey check catches it), but the flat balances object still carries it. Your typeof code !== 'string' || typeof issuer !== 'string' continue fixes it at the source. Keep it, and keep the test for it.

  2. Classifying inside getBalances rather than in agent.ts. Moving it there means every future caller gets the issuer check for free instead of having to remember. If you do this, getBalances returns { balances, holdings } and agent.ts line 411–414 has to change with it — your current diff changes the return type but leaves JSON.stringify at the call site untouched, so the shape silently changes under the model. Please update that call site in the same commit, and grep for any other caller.

  3. USDY. #935's agent registry has only USDC and USDT0; yours has USDY too, which the wallet and mobile registries both carry. Adding it to assets.ts closes a real gap.

What to drop: the whole of assetRegistry.ts and __tests__/assetRegistry.test.ts, and the getRegisteredAsset / isRegisteredIssuer / classifyHolding duplicates inside them. Re-point txBuilder.ts at ./assets.js. Note that #935's classifyHolding returns { status, message }, not { verified, note }, so your txBuilder.test.ts assertions will need updating to match — keep the assertions themselves, they're testing the right thing.

On the system prompt: main's rule 8 already covers naming the issuer and reporting unverified holdings. Your wording about relaying each holding's note is close enough that a second rule would just repeat it — please fold anything it adds into the existing rule 8 rather than appending a rule 10.

I validated every hard-coded address in this diff with StrKey.isValidEd25519PublicKey; all four constants in assetRegistry.ts check out, including the counterfeit fixtures. No problems there.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Stop the agent matching balances by asset code

2 participants