Skip to content

Solana wallet + Treasures sol-venue trade signing#44

Merged
Zuhwa merged 32 commits into
mainfrom
feat/solana-wallet
Jun 22, 2026
Merged

Solana wallet + Treasures sol-venue trade signing#44
Zuhwa merged 32 commits into
mainfrom
feat/solana-wallet

Conversation

@psmiratisu

@psmiratisu psmiratisu commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

What

Adds Solana support to the CLI: standalone wallet commands plus the trade-loop signing needed to trade Treasures tokenized stocks on the Solana venue (and Solana-source LiFi legs) driven by the backend.

Wallet commands (acp wallet sol)

address, balance, sign-message, transfer (SOL + SPL), send-instructions — backed by the agent's Privy Solana wallet (same P256 signer as EVM).

Trade-loop signing (acp trade)

The backend's /plan/next state machine now emits Solana sign actions; the CLI handles them with the keystore/Privy signer (server never holds keys):

  • solana-message — ed25519 over the ownership-proof challenge, returned base64 (Treasures' contract; base58 is their refactor: remove job offering isPrivate field #1 documented rejection cause).
  • solana-tx — sign the versioned tx (Treasures order leg, or a Solana-source LiFi bridge) without broadcasting; the server/venue broadcasts.
  • solWallet is auto-attached to any request that could route through Solana (explicit --chain sol, a Solana source, or a tokenized-stock buy with no venue pinned), so the backend can quote/route the sol venue.

Validated live

End-to-end on mainnet against the deployed backend + Treasures staging:

  • acp trade --token AAPL --amount-usdc 1 --chain sol → ed25519 proof signed → versioned-tx leg signed → submitted → 0.00342 AAPL (xStocks) received for ~1 USDC, confirmed on-chain.

Notes

  • History includes an add+revert of a sponsored-sendInstructions gas path: acp-be no longer sponsors Solana gas, so the design moved to seed-at-funding + a self-paid top-up (the revert is intentional, net-zero).
  • Pairs with the backend PR (internal-trading-bot feat: equip ACP agents with phone numbers via AgentPhone #19) which implements the sol venue, auto-routing, and the gas model.

🤖 Generated with Claude Code


Note

Medium Risk
Touches fund movement and trade signing (Solana txs, Treasures venue, cross-chain swaps); mistakes in signing wire format or solWallet attachment could break or mis-route real trades, though keys stay client-side via Privy.

Overview
Documents and ships Solana alongside EVM for agent wallets and trading: acp wallet sol (address, balance, sign-message, transfer for SOL/SPL, send-instructions) using the same Privy/P256 signer, plus acp wallet balance with no flags now aggregates sponsored EVM chains and Solana (optional --cluster / Solana chain ids).

acp trade gains Solana-aware planning: solWallet is attached when routes may use Solana (explicit sol chain refs or unpinned tokenized-stock buys), and the /plan/next loop handles new sign actions — solana-message (ownership proof, signature returned base64) and solana-tx (sign versioned tx without local broadcast). Docs also cover acp trade stock-list, spot tokenized stocks (--amount-usdc / --amount-shares, distinct from perps via flags), and Solana→EVM swaps.

SKILL.md is updated so agents get JSON shapes and examples for the new wallet and trade flows.

Reviewed by Cursor Bugbot for commit a95ad11. Bugbot is set up for automated code reviews on this repo. Configure here.

Zuhwa and others added 4 commits June 12, 2026 14:10
…ersioned txs

Wires the trade loop to the backend's new Solana sign actions:
- sigType 'solana-message': Privy signs the raw challenge bytes (no
  envelope); the adapter's base58 output is re-encoded to BASE64 — the
  Treasures ownership-proof contract (base58 is their #1 documented
  rejection cause).
- sigType 'solana-tx': sign the serialized versioned tx WITHOUT
  broadcasting (server/venue broadcast the signed bytes), via the
  adapter's Privy signTransaction (base64 in/out).
- solWallet rides every /plan that could route through Solana: explicit
  sol venue/source, or a tokenized-stock BUY with no venue pinned — the
  backend then quotes both venues and executes the better one. Sells
  stay explicit (the backend can't see which venue holds shares).

Verified against the live Privy signer for Mochi3DSTest: the signature
checks out as raw ed25519 over the exact challenge bytes (local
ed25519.verify — the same check Treasures runs server-side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s top-up)

The backend emits a 'solana-instructions' action before a sol-venue
trade when the wallet's native SOL is below the gas floor — a Jupiter
USDC→SOL swap. Run it through the adapter's sponsored sendInstructions
(Alchemy fee payer) so a zero-SOL wallet can bootstrap, mapping the
backend's role strings to AccountRole bitflags and posting back the
broadcast signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread package-lock.json Outdated
Comment thread src/commands/trade.ts
Comment thread src/commands/trade.ts
- package-lock: re-resolve @virtuals-protocol/acp-node-v2 to the published
  0.1.4 registry tarball instead of the sibling ../acp-node-v2 link, so
  `npm ci` works on machines without that local path (was breaking CI/releases).
- trade: recognize the Privy Solana chain ids (500 devnet / 501 mainnet) in
  isSolanaChainRef, so a swap with --chain-in 501 attaches solWallet.
- trade: route opts.chain through isSolanaChainRef instead of an exact
  `=== "sol"` match, so a Treasures sell with --chain solana (or other casing)
  also attaches solWallet. isSolanaChainRef is now the single source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/commands/trade.ts Outdated
Bugbot follow-up on PR #44: couldRouteViaSolana covered --chain and
--chain-in but not --chain-out, so a swap/bridge whose destination is
Solana reached /trade/plan without the agent's Solana pubkey — the
recipient the backend needs to route/sign the destination leg. Add the
symmetric isSolanaChainRef(opts.chainOut) check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/commands/trade.ts Outdated
Bugbot follow-up on PR #44: the unpinned-buy heuristic fired on --token
alone, so `--token AAPL --amount-usdc 1 --chain eth` still attached
solWallet. With solWallet present the backend quotes both venues and may
pick sol, overriding the user's explicit --chain eth pin. Gate the buy
clause on opts.chain === undefined; an explicit --chain sol still routes
via the isSolanaChainRef(opts.chain) clause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/commands/trade.ts Outdated
…mount)

Bugbot follow-up on PR #44: the negative formulation classified any
--token without --side/--amount-shares/--chain as an unpinned buy, so a
malformed perp like `--token BTC --size 0.01` (missing --side) attached
solWallet. Define a buy positively: --token plus a spend amount
(--amount-usdc on eth, or --amount-in funded from another chain). This
covers both documented buy shapes and excludes perp/incomplete shapes
that carry no spend signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/commands/trade.ts
Bugbot follow-up on PR #44: the empty catch around getSolanaWalletAddress
flattened every failure (network, auth, agent lookup) into "no wallet",
silently dropping solWallet. Swallow only the NO_SOLANA_WALLET signal, and
only for a speculative unpinned buy; real failures now surface, and an
explicit Solana route (--chain/--chain-in/--chain-out sol) propagates any
error rather than planning a route it can't sign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/lib/agentFactory.ts
psmiratisu and others added 14 commits June 14, 2026 05:11
The Solana/Treasures work added spot tokenized-stock buy/sell, sol-venue
auto-routing, and swaps in/out of Solana — none of which were in the
docs. Adds to both README and SKILL.md:
- intent-routing rows for Solana-source swaps and tokenized-stock spot
- the stock-vs-perp rule (route by FLAG --amount-usdc/-shares vs --side,
  never by the ticker — AAPL is both a stock and an HL equity perp)
- examples: buy with held USDC, buy funded from another chain, sell,
  USDC@sol → USDC@Base
- command-table row + capability description for tokenized stocks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Read-only discovery wrapping the backend's GET /trade/instruments:
- no symbol → spot markets { stocks, hlSpot } (tokenized stocks + HL spot)
- with a symbol → every route for that asset, each naming the exact
  `token` ticker to pass (e.g. xyz:AAPL for the equity perp, AAPL spot)

Adds a GET helper (the trade client only had POST). Documents the command
and the funding model in README.md and SKILL.md — USDC is the settlement
currency, not a prerequisite; trades fund from any chain/token and the
backend auto-bridges, so the listing never implies pre-holding USDC on HL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…46)

* feat: add wallet policy management commands and update documentation

* refactor: update CLI commands to clarify dashboard approval requirements and improve user guidance for policy changes

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>
#47)

* feat: enhance wallet balance command to support querying all supported chain

* feat: add native currency resolution for token display in wallet commands

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>
Comment thread src/commands/trade.ts
throw new CliError(message, isKnownCode(code) ? code : "API_ERROR", recovery);
}
return (await res.json()) as T;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Duplicated HTTP helper logic between get and post

Low Severity

The new get() helper duplicates the HTTPS validation guard, error body parsing (JSON try/catch), error message construction, isKnownCode check, and CliError throw logic from post(). These ~20 lines of identical error-handling could be extracted into a shared helper, reducing the maintenance surface and the risk of future fixes applying to one but not the other.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 23fc7fb. Configure here.

Comment thread src/commands/trade.ts
Zuhwa and others added 5 commits June 16, 2026 17:57
…n balance support for all sponsored EVM chains and Solana
The backend now returns the agent's Treasures stock portfolio under
`data.stocks` on GET /agents/:id/assets. Surface it in the wallet
balance views.

- Add StockPosition type + data.stocks to AgentAssetsResponse (was
  silently dropped before).
- `wallet balance` and `wallet sol balance` now emit `stocks` in --json
  output and render a "Tokenized Stocks" table (TICKER, TOKEN, TOKENS,
  SHARES, USD) in TTY. The full portfolio is shown regardless of the
  queried chain (it isn't tied to the network).
- usd_value (precomputed upstream) preferred; falls back to
  tokens × usd_per_token; nulls render as "—".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract stockUsd(): prefer usd_value, fall back to tokens × usd_per_token,
"—" when unknown. Piped output previously used `usd_value ?? "0"`, showing
$0 for unknown values and disagreeing with the TTY table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The tokenized-stock table only displayed a computed USD value, hiding the
per-position pricing the backend already returns. Surface $/share,
$/token, average entry price, USD value, and unrealized PnL (signed) in
both the TTY table and the piped output. --json was already complete
(verbatim positions passthrough); this brings the human views in line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the tokenized-stock balance work onto the Solana-wallet branch.
Resolved src/commands/wallet.ts by folding stock rendering into the
unified renderBalances() helper (the Solana-wallet refactor) instead of
the old inline per-command rendering: stocks render once after the
per-network token tables (TTY), are included in --json, and emitted in
the piped output. Keeps the $/share, $/token, avg-entry, value, and PnL
columns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 3 total unresolved issues (including 2 from previous reviews).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ccc03a4. Configure here.

Comment thread src/commands/policy.ts
psmiratisu and others added 4 commits June 18, 2026 21:51
A dry run signs and submits nothing — the server returns a `preview`
action and runTradeLoop returns before any send/sign — yet the CLI eagerly
built the signer adapter (createProviderAdapter), so `--dry-run` failed
with "No signer configured" on agents without a key.

Pass `undefined` for the dry-run provider and assert it only in the
execution branches (send / EVM sign) via requireSigner(), which a dry run
never reaches. Live trades are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve conflicts to clear PR #44's merge conflicts with main:
- package.json/lock: take acp-node-v2 ^0.1.6 (main); lock regenerated
- chains.ts: keep Solana-aware getNativeCurrency (superset of main's)
- walletGate.ts: keep withSolanaWallet alongside main's approval-url mirroring
- trade.ts: keep --dry-run skip-signer behavior
- wallet.ts/README: keep branch versions (superset of main's #47 wallet-balance work)

Build + tsc pass (the pre-existing job.ts typecheck error exists on main too).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…de loop

A live HL-exit froze indefinitely mid-trade: a /trade/next call stalled with the
connection open, and since fetch had no timeout it never returned or threw — so
the trade loop's existing transient-retry never fired and it hung past the
backend's 10-min settle-timeout (the withdraw had settled on Arbitrum, but the
bridge leg was never reached).

- trade.ts post()/get(): add AbortSignal.timeout(120s) to the fetch. A stall now
  throws → `wait`-poll calls retry (existing path), others surface a clean
  REQUEST_TIMEOUT (code "TIMEOUT") instead of hanging.
- api/client.ts: same timeout on the shared ApiClient fetch (every CLI command),
  via a fetchWithTimeout helper.

120s is generous for slow legitimate calls (e.g. a LiFi quote inside /trade/next)
but bounded. Follow-up: auto-resume a timed-out /trade/next by tradeId once the
backend's per-step idempotency is confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A long trade (e.g. an HL-exit's multi-minute settle wait) can outlive the access
token: the loop captures it once at the start and never refreshes, and
resolveToken only refreshes when the token is ALREADY expired — so a token with
a few minutes left passes the start check, then expires mid-loop and the next
/trade/next returns 401, killing the trade (observed live: a ~7-min HL-exit died
401 after the withdraw settled but before the bridge).

- client.ts: export forceTokenRefresh() — mint a new access token via the stored
  refresh token unconditionally (the local expiry check can disagree with the
  server, so re-resolving isn't enough).
- trade.ts post(): on a 401, call the onAuthRefresh callback once and retry with
  the fresh token (not counted as a transient retry).
- runTradeLoop: hold a mutable currentToken and pass an onAuthRefresh that
  force-refreshes and updates it, so the rest of the loop uses the new token.

Pairs with the request-timeout fix: together, long-running trades no longer hang
on a stalled connection or die on an expired token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@Zuhwa Zuhwa merged commit 86d23a7 into main Jun 22, 2026
1 check passed
Zuhwa added a commit that referenced this pull request Jun 22, 2026
* feat(wallet): add Solana wallet support with commands for address, balance, message signing, and transfers

* feat(trade): sign Solana trade legs — ownership proofs (base64) and versioned txs

Wires the trade loop to the backend's new Solana sign actions:
- sigType 'solana-message': Privy signs the raw challenge bytes (no
  envelope); the adapter's base58 output is re-encoded to BASE64 — the
  Treasures ownership-proof contract (base58 is their #1 documented
  rejection cause).
- sigType 'solana-tx': sign the serialized versioned tx WITHOUT
  broadcasting (server/venue broadcast the signed bytes), via the
  adapter's Privy signTransaction (base64 in/out).
- solWallet rides every /plan that could route through Solana: explicit
  sol venue/source, or a tokenized-stock BUY with no venue pinned — the
  backend then quotes both venues and executes the better one. Sells
  stay explicit (the backend can't see which venue holds shares).

Verified against the live Privy signer for Mochi3DSTest: the signature
checks out as raw ed25519 over the exact challenge bytes (local
ed25519.verify — the same check Treasures runs server-side).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(trade): run sponsored solana-instructions actions (native-SOL gas top-up)

The backend emits a 'solana-instructions' action before a sol-venue
trade when the wallet's native SOL is below the gas floor — a Jupiter
USDC→SOL swap. Run it through the adapter's sponsored sendInstructions
(Alchemy fee payer) so a zero-SOL wallet can bootstrap, mapping the
backend's role strings to AccountRole bitflags and posting back the
broadcast signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* Revert "feat(trade): run sponsored solana-instructions actions (native-SOL gas top-up)"

This reverts commit d561aef.

* fix(trade): address Bugbot review on PR #44

- package-lock: re-resolve @virtuals-protocol/acp-node-v2 to the published
  0.1.4 registry tarball instead of the sibling ../acp-node-v2 link, so
  `npm ci` works on machines without that local path (was breaking CI/releases).
- trade: recognize the Privy Solana chain ids (500 devnet / 501 mainnet) in
  isSolanaChainRef, so a swap with --chain-in 501 attaches solWallet.
- trade: route opts.chain through isSolanaChainRef instead of an exact
  `=== "sol"` match, so a Treasures sell with --chain solana (or other casing)
  also attaches solWallet. isSolanaChainRef is now the single source of truth.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): attach solWallet for Solana --chain-out too

Bugbot follow-up on PR #44: couldRouteViaSolana covered --chain and
--chain-in but not --chain-out, so a swap/bridge whose destination is
Solana reached /trade/plan without the agent's Solana pubkey — the
recipient the backend needs to route/sign the destination leg. Add the
symmetric isSolanaChainRef(opts.chainOut) check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): don't attach solWallet when a non-sol venue is pinned

Bugbot follow-up on PR #44: the unpinned-buy heuristic fired on --token
alone, so `--token AAPL --amount-usdc 1 --chain eth` still attached
solWallet. With solWallet present the backend quotes both venues and may
pick sol, overriding the user's explicit --chain eth pin. Gate the buy
clause on opts.chain === undefined; an explicit --chain sol still routes
via the isSolanaChainRef(opts.chain) clause.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): define unpinned Treasures buy positively (require spend amount)

Bugbot follow-up on PR #44: the negative formulation classified any
--token without --side/--amount-shares/--chain as an unpinned buy, so a
malformed perp like `--token BTC --size 0.01` (missing --side) attached
solWallet. Define a buy positively: --token plus a spend amount
(--amount-usdc on eth, or --amount-in funded from another chain). This
covers both documented buy shapes and excludes perp/incomplete shapes
that carry no spend signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): only swallow genuine no-Solana-wallet errors

Bugbot follow-up on PR #44: the empty catch around getSolanaWalletAddress
flattened every failure (network, auth, agent lookup) into "no wallet",
silently dropping solWallet. Swallow only the NO_SOLANA_WALLET signal, and
only for a speculative unpinned buy; real failures now surface, and an
explicit Solana route (--chain/--chain-in/--chain-out sol) propagates any
error rather than planning a route it can't sign.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document tokenized-stock trading + Solana routing (README + SKILL)

The Solana/Treasures work added spot tokenized-stock buy/sell, sol-venue
auto-routing, and swaps in/out of Solana — none of which were in the
docs. Adds to both README and SKILL.md:
- intent-routing rows for Solana-source swaps and tokenized-stock spot
- the stock-vs-perp rule (route by FLAG --amount-usdc/-shares vs --side,
  never by the ticker — AAPL is both a stock and an HL equity perp)
- examples: buy with held USDC, buy funded from another chain, sell,
  USDC@sol → USDC@Base
- command-table row + capability description for tokenized stocks

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(trade): add `acp trade stock-list` discovery command

Read-only discovery wrapping the backend's GET /trade/instruments:
- no symbol → spot markets { stocks, hlSpot } (tokenized stocks + HL spot)
- with a symbol → every route for that asset, each naming the exact
  `token` ticker to pass (e.g. xyz:AAPL for the equity perp, AAPL spot)

Adds a GET helper (the trade client only had POST). Documents the command
and the funding model in README.md and SKILL.md — USDC is the settlement
currency, not a prerequisite; trades fund from any chain/token and the
backend auto-bridges, so the listing never implies pre-holding USDC on HL.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump @virtuals-protocol/acp-node-v2 version to 0.1.5 in package.json and package-lock.json

* fix: add approval gate for trade command

* fix: surface approval urls from wallet gate

* fix: restrict approval url detection

* fix: tighten approval text matching

* fix: surface trade approval errors

* fix: keep approval url change scoped

* fix: mirror approval urls from sdk output

* feat: simplify error message handling

* feat: add wallet policy management commands and update documentation (#46)

* feat: add wallet policy management commands and update documentation

* refactor: update CLI commands to clarify dashboard approval requirements and improve user guidance for policy changes

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>

* feat: enhance wallet balance command to support querying all supporte… (#47)

* feat: enhance wallet balance command to support querying all supported chain

* feat: add native currency resolution for token display in wallet commands

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>

* feat: update wallet balance command to include Solana support and improve documentation

* docs: update HL account status documentation to clarify on-chain token balance support for all sponsored EVM chains and Solana

* feat(wallet): show Treasures tokenized-stock positions in balance

The backend now returns the agent's Treasures stock portfolio under
`data.stocks` on GET /agents/:id/assets. Surface it in the wallet
balance views.

- Add StockPosition type + data.stocks to AgentAssetsResponse (was
  silently dropped before).
- `wallet balance` and `wallet sol balance` now emit `stocks` in --json
  output and render a "Tokenized Stocks" table (TICKER, TOKEN, TOKENS,
  SHARES, USD) in TTY. The full portfolio is shown regardless of the
  queried chain (it isn't tied to the network).
- usd_value (precomputed upstream) preferred; falls back to
  tokens × usd_per_token; nulls render as "—".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(wallet): unify stock USD computation across TTY and piped output

Extract stockUsd(): prefer usd_value, fall back to tokens × usd_per_token,
"—" when unknown. Piped output previously used `usd_value ?? "0"`, showing
$0 for unknown values and disagreeing with the TTY table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(wallet): show stock price, avg entry, and PnL in balance table

The tokenized-stock table only displayed a computed USD value, hiding the
per-position pricing the backend already returns. Surface $/share,
$/token, average entry price, USD value, and unrealized PnL (signed) in
both the TTY table and the piped output. --json was already complete
(verbatim positions passthrough); this brings the human views in line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): --dry-run no longer requires a registered signer

A dry run signs and submits nothing — the server returns a `preview`
action and runTradeLoop returns before any send/sign — yet the CLI eagerly
built the signer adapter (createProviderAdapter), so `--dry-run` failed
with "No signer configured" on agents without a key.

Pass `undefined` for the dry-run provider and assert it only in the
execution branches (send / EVM sign) via requireSigner(), which a dry run
never reaches. Live trades are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(trade): add request timeouts so a stalled call can't hang the trade loop

A live HL-exit froze indefinitely mid-trade: a /trade/next call stalled with the
connection open, and since fetch had no timeout it never returned or threw — so
the trade loop's existing transient-retry never fired and it hung past the
backend's 10-min settle-timeout (the withdraw had settled on Arbitrum, but the
bridge leg was never reached).

- trade.ts post()/get(): add AbortSignal.timeout(120s) to the fetch. A stall now
  throws → `wait`-poll calls retry (existing path), others surface a clean
  REQUEST_TIMEOUT (code "TIMEOUT") instead of hanging.
- api/client.ts: same timeout on the shared ApiClient fetch (every CLI command),
  via a fetchWithTimeout helper.

120s is generous for slow legitimate calls (e.g. a LiFi quote inside /trade/next)
but bounded. Follow-up: auto-resume a timed-out /trade/next by tradeId once the
backend's per-step idempotency is confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(trade): refresh the access token on a 401 mid-trade-loop

A long trade (e.g. an HL-exit's multi-minute settle wait) can outlive the access
token: the loop captures it once at the start and never refreshes, and
resolveToken only refreshes when the token is ALREADY expired — so a token with
a few minutes left passes the start check, then expires mid-loop and the next
/trade/next returns 401, killing the trade (observed live: a ~7-min HL-exit died
401 after the withdraw settled but before the bridge).

- client.ts: export forceTokenRefresh() — mint a new access token via the stored
  refresh token unconditionally (the local expiry check can disagree with the
  server, so re-resolving isn't enough).
- trade.ts post(): on a 401, call the onAuthRefresh callback once and retry with
  the fresh token (not counted as a transient retry).
- runTradeLoop: hold a mutable currentToken and pass an onAuthRefresh that
  force-refreshes and updates it, so the rest of the loop uses the new token.

Pairs with the request-timeout fix: together, long-running trades no longer hang
on a stalled connection or die on an expired token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document chain-out defaulting + Solana buy routing

The trade routing tables in README and SKILL only covered Solana→EVM and
treated --chain-out as always-required. Reflect the shipped behavior:

- --chain-out is optional, defaulting to --chain-in (omit to stay on the
  source chain); single-chain tokens like `sol` infer their own chain.
- Add the EVM→Solana (buy SOL/SPL) and Solana→Solana routing rows + a
  buy-SOL example; delivery recipient is auto-derived from the agent's
  Solana wallet, so --recipient is only for sending elsewhere.
- Fix the SKILL swap flag-reference row (was "both chains EVM", --chain-out
  required) to cover Solana and mark --chain-out/--recipient optional.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: tell agents acp trade is one command for multi-leg routes

Agents reading the trade docs could miss that a single `acp trade` is
decomposed into a full multi-leg route (e.g. HL sell → transfer → withdraw
→ bridge+swap) and try to chain `deposit`/`spot`/`bridge`/`swap` calls
themselves. Add an explicit "one command, never chain trades yourself"
callout to both README and SKILL, a real multi-leg example (PURR@HL →
ETH@Base), and a note that a multi-leg route can take minutes to settle
(not a hang) so agents don't re-issue.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Zuhwa <zuhwa@virtuals.io>
Co-authored-by: brianna <bpschang@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Andrew Khor <andrew@virtuals.io>
Co-authored-by: ai-virtual-b <bryan@virtuals.io>
Co-authored-by: miratisu_virtuals <89718498+psmiratisu@users.noreply.github.com>
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.

4 participants