mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-16 01:06:59 +08:00
feat(claw402): preflight USDC balance before AI calls (#1479)
* chore: ignore nofx-server build artifact Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(claw402): preflight USDC balance before AI calls Short-circuit claw402 Call/CallWithRequestFull when the wallet balance can't cover the estimated cost of the call, surfacing ErrInsufficientFunds instead of letting x402 fail mid-flight after the sign step. - wallet: cached balance lookup (30s TTL, per-address mutex) to avoid hammering the Base RPC; separate error-returning and display-only APIs so callers can distinguish zero balance from an unreachable RPC. - claw402: 1.5× safety multiplier on the flat per-call estimate, 4.0× for reasoner models whose chain-of-thought cost can blow past the flat rate. Fail-open on RPC errors — x402 still gates actually-empty wallets, and we prefer availability over extra strictness. - shortAddr redacts the wallet in error strings to avoid leaking the full address into telemetry bundles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
67
wallet/balance_cache.go
Normal file
67
wallet/balance_cache.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package wallet
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// balanceCacheTTL is how long a balance reading is trusted before re-querying.
|
||||
const balanceCacheTTL = 30 * time.Second
|
||||
|
||||
type balanceEntry struct {
|
||||
value float64
|
||||
fetchedAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
balanceCache sync.Map
|
||||
balanceFetchMu sync.Map
|
||||
)
|
||||
|
||||
// QueryUSDCBalanceCached returns the USDC balance for an address, using a
|
||||
// short-lived cache to avoid hammering the Base RPC. Addresses are
|
||||
// case-insensitive.
|
||||
func QueryUSDCBalanceCached(address string) (float64, error) {
|
||||
key := strings.ToLower(strings.TrimSpace(address))
|
||||
if key == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
if v, ok := balanceCache.Load(key); ok {
|
||||
e := v.(balanceEntry)
|
||||
if time.Since(e.fetchedAt) < balanceCacheTTL {
|
||||
return e.value, nil
|
||||
}
|
||||
}
|
||||
|
||||
muAny, _ := balanceFetchMu.LoadOrStore(key, &sync.Mutex{})
|
||||
mu := muAny.(*sync.Mutex)
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
if v, ok := balanceCache.Load(key); ok {
|
||||
e := v.(balanceEntry)
|
||||
if time.Since(e.fetchedAt) < balanceCacheTTL {
|
||||
return e.value, nil
|
||||
}
|
||||
}
|
||||
|
||||
balance, err := QueryUSDCBalance(address)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
balanceCache.Store(key, balanceEntry{value: balance, fetchedAt: time.Now()})
|
||||
return balance, nil
|
||||
}
|
||||
|
||||
// InvalidateBalanceCache drops the cached balance for an address, forcing the
|
||||
// next query to hit the chain. Use after a known-spending action or when the
|
||||
// caller suspects the cache is stale.
|
||||
func InvalidateBalanceCache(address string) {
|
||||
key := strings.ToLower(strings.TrimSpace(address))
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
balanceCache.Delete(key)
|
||||
}
|
||||
Reference in New Issue
Block a user