Add cg contract command for token price lookup by contract address#22
Add cg contract command for token price lookup by contract address#22
cg contract command for token price lookup by contract address#22Conversation
…dress
Add a new command that looks up token price by contract address using two
data sources: CoinGecko's aggregated price (default) and GeckoTerminal's
DEX price (--onchain). Both modes produce identical output columns
(address, price, market cap, 24h volume, 24h change).
- Aggregated mode: queries /simple/token_price/{platform} with --platform
- Onchain mode: queries /onchain/simple/networks/{network}/token_price/{addresses}
with --network and --onchain flags (paid plans only)
- Non-USD currency support: aggregated uses API natively, onchain converts
via /exchange_rates endpoint
- Supports --dry-run, -o json, --vs, --export CSV
- Includes validation with discovery doc hints in error messages
- 16 command tests + 6 API-layer tests covering both modes
Address three review issues with the --onchain variant: 1. Catalog metadata: add per-mode `paid_modes` and `oas_specs` maps to the command annotation and catalog output so agents/LLMs know that --onchain requires a paid plan and uses coingecko-pro.json spec. 2. Dry-run output: --onchain --dry-run now always emits a pro-tier request (pro base URL, pro auth header, coingecko-pro.json spec) regardless of the user's actual tier. Demo users get a note: "Paid plan required for --onchain". 3. Flag help: --onchain description now says "(paid plan required)" so the restriction is visible in --help and the flag catalog.
The onchain-simple-price endpoint is available on both demo and paid tiers per CoinGecko docs (demo: api.coingecko.com, pro: pro-api). The previous implementation incorrectly treated it as paid-only. Changes: - Remove requirePaid() guard from OnchainSimpleTokenPrice - Remove forced pro-tier config override in onchain dry-run - Remove "(paid plan required)" from --onchain flag description - Remove PaidModes/OASSpecs per-mode metadata from contract entry (both modes work on demo tier, single OASSpec suffices) - Replace TestOnchainSimpleTokenPrice_RequiresPaid with _DemoTier test that verifies demo clients can call the endpoint - Replace TestContract_Onchain_RequiresPaid with _DemoTier test - Update dry-run and catalog tests to reflect demo-compatible behavior
- Remove redundant displayAddr variable and inline comments in contract.go - Improve printDryRunFull per-mode override logic: set command-level defaults first, then override only when the opKey exists in the per-mode maps (proper fallback for missing keys) - Add captureStdout test helper for capturing stdout in unit tests - Add TestDryRun_PartialOverrideFallback to verify fallback behavior when a command defines per-mode maps but the requested opKey is absent
There was a problem hiding this comment.
Pull request overview
Adds a new cg contract command to look up token prices by contract address, supporting CoinGecko aggregated pricing by platform and GeckoTerminal onchain DEX pricing by network, with dry-run/OAS catalog integration.
Changes:
- Introduces
cg contractcommand with flags for aggregated vs onchain mode, currency selection, and CSV export. - Extends the API client with token-price and exchange-rate endpoints plus response types.
- Refactors dry-run OAS spec/operation resolution to support per-mode overrides with fallback, and adds tests.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
cmd/contract.go |
Implements the new contract command (aggregated + onchain flow, output formatting, export). |
cmd/contract_test.go |
Adds command-level tests for validation, dry-run, outputs, conversion, export, and catalog metadata. |
internal/api/coins.go |
Adds API methods for simple token price, onchain token price, and exchange rates. |
internal/api/coins_test.go |
Adds unit tests for the new API client methods. |
internal/api/types.go |
Adds response types for token price, onchain token price, and exchange rates. |
cmd/dryrun.go |
Updates dry-run OAS spec/operation ID selection to support per-mode overrides with fallback. |
cmd/dryrun_test.go |
Adds regression test for partial override fallback behavior. |
cmd/commands.go |
Registers contract in the command catalog and extends annotations/catalog fields for OAS spec overrides and paid-modes metadata. |
cmd/test_helpers_test.go |
Adds captureStdout helper for dry-run tests. |
CLAUDE.md |
Documents the new command mapping and onchain currency conversion behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| priceStr, ok := resp.Data.Attributes.TokenPrices[address] | ||
| if !ok { | ||
| return fmt.Errorf("no data returned for address %s", address) | ||
| } |
There was a problem hiding this comment.
In onchain mode, the response maps are keyed by contract address strings. If the API normalizes keys (commonly lowercase), using the raw user input as the lookup key can incorrectly hit the "no data returned" path for checksum/mixed-case addresses. Consider normalizing the address for request + response lookup (e.g., strings.ToLower) while still displaying the original address if desired.
| marketCap, _ = strconv.ParseFloat(mcStr, 64) | ||
| } | ||
| if volStr, ok := resp.Data.Attributes.H24VolumeUSD[address]; ok { | ||
| volume, _ = strconv.ParseFloat(volStr, 64) | ||
| } | ||
| if chgStr, ok := resp.Data.Attributes.H24PriceChangePct[address]; ok { | ||
| change, _ = strconv.ParseFloat(chgStr, 64) |
There was a problem hiding this comment.
marketCap, volume, and change parsing errors are currently ignored (ParseFloat(..., 64) with _). If the API returns a non-numeric string (or an empty string) for these fields, the command will silently emit 0 values, which is misleading. Please handle these parse errors similarly to price (either return an error or at least surface a warning).
| marketCap, _ = strconv.ParseFloat(mcStr, 64) | |
| } | |
| if volStr, ok := resp.Data.Attributes.H24VolumeUSD[address]; ok { | |
| volume, _ = strconv.ParseFloat(volStr, 64) | |
| } | |
| if chgStr, ok := resp.Data.Attributes.H24PriceChangePct[address]; ok { | |
| change, _ = strconv.ParseFloat(chgStr, 64) | |
| marketCap, err = strconv.ParseFloat(mcStr, 64) | |
| if err != nil { | |
| return fmt.Errorf("parsing market cap: %w", err) | |
| } | |
| } | |
| if volStr, ok := resp.Data.Attributes.H24VolumeUSD[address]; ok { | |
| volume, err = strconv.ParseFloat(volStr, 64) | |
| if err != nil { | |
| return fmt.Errorf("parsing 24h volume: %w", err) | |
| } | |
| } | |
| if chgStr, ok := resp.Data.Attributes.H24PriceChangePct[address]; ok { | |
| change, err = strconv.ParseFloat(chgStr, 64) | |
| if err != nil { | |
| return fmt.Errorf("parsing 24h price change: %w", err) | |
| } |
| note = fmt.Sprintf("Additional request: GET /exchange_rates (currency conversion from USD to %s)", vs) | ||
| } | ||
| return printDryRunFull(cfg, "contract", "--onchain", | ||
| fmt.Sprintf("/onchain/simple/networks/%s/token_price/%s", url.PathEscape(network), address), |
There was a problem hiding this comment.
The onchain dry-run endpoint embeds the raw address in the URL path without escaping. Even if addresses are typically hex, this makes the URL construction inconsistent with other path parameters (platform/network) and can break dry-run output if multiple addresses/comma-separated values are ever supported. Use url.PathEscape(address) (or escape each address before joining) when building the path segment.
| fmt.Sprintf("/onchain/simple/networks/%s/token_price/%s", url.PathEscape(network), address), | |
| fmt.Sprintf("/onchain/simple/networks/%s/token_price/%s", url.PathEscape(network), url.PathEscape(address)), |
| } | ||
| var result OnchainTokenPriceResponse | ||
| err := c.get(ctx, fmt.Sprintf("/onchain/simple/networks/%s/token_price/%s?%s", | ||
| url.PathEscape(network), strings.Join(addresses, ","), params.Encode()), &result) |
There was a problem hiding this comment.
OnchainSimpleTokenPrice builds a URL path segment from strings.Join(addresses, ",") without url.PathEscape. This is inconsistent with other path params in this file and can produce invalid paths or unexpected routing if an address contains reserved characters. Consider escaping each address (or the joined string) before interpolating it into the path.
| url.PathEscape(network), strings.Join(addresses, ","), params.Encode()), &result) | |
| url.PathEscape(network), url.PathEscape(strings.Join(addresses, ",")), params.Encode()), &result) |
| func captureStdout(t *testing.T, fn func()) string { | ||
| t.Helper() | ||
| oldStdout := os.Stdout | ||
| r, w, _ := os.Pipe() | ||
| os.Stdout = w | ||
|
|
||
| fn() | ||
|
|
||
| _ = w.Close() | ||
| os.Stdout = oldStdout | ||
| var buf bytes.Buffer | ||
| _, _ = io.Copy(&buf, r) | ||
| return buf.String() | ||
| } |
There was a problem hiding this comment.
captureStdout mutates the global os.Stdout but doesn't restore it if fn() panics, and it doesn't close the read end of the pipe. Using defer to restore os.Stdout and to close both pipe ends will make the helper safer and prevent cross-test contamination/leaks on failures.
Summary
Adds
cg contract— a new command that looks up token price by contract address. Uses CoinGecko's aggregated price by default, or DEX price from GeckoTerminal with--onchain.Links
Changes
cmd/contract.go— newcg contractcommand with--address,--platform,--network,--onchain,--vs, and--exportflagsinternal/api/coins.go— three new API client methods:SimpleTokenPrice,OnchainSimpleTokenPrice,ExchangeRatesinternal/api/types.go— response types:TokenPriceResponse,OnchainTokenPriceResponse,ExchangeRatesResponsecmd/contract_test.go— comprehensive tests (validation, dry-run, aggregated/onchain JSON & table output, currency conversion, CSV export, catalog metadata)cmd/dryrun_test.go— tests for partial-override fallback in dry-run OAS spec/operation resolutioninternal/api/coins_test.go— unit tests for the three new API methodscmd/test_helpers_test.go—captureStdouthelper for dry-run testscmd/commands.go— registercontractincommandMetawith per-mode endpoints/OAS IDs; addPaidModes,OASSpecsfields to annotation and catalog structscmd/dryrun.go— refactor spec/operation ID resolution to support per-mode overrides with fallback to command-level defaultsCLAUDE.md— document new command mapping, onchain currency conversion, and platform/network discoverabilityPre-release Checklist
--platformand--onchainmutual exclusivity validated--vsnon-USD currency conversion via/exchange_ratesworks correctlyRollback Steps
main