From 6bfb30cd6c4d386631f17c305be1848125162dfd Mon Sep 17 00:00:00 2001 From: Carl Fluke Date: Wed, 9 Sep 2026 12:30:37 -0400 Subject: [PATCH 1/2] update prime-sdk, include errors, add new commands/endpoints --- CHANGELOG.md | 11 + COMMANDS.md | 42 +- README.md | 19 +- cmd/cmd.go | 5 +- cmd/financing/get_conversion_fees.go | 71 ++++ cmd/financing/get_cross_margin_liquidation.go | 87 ++++ cmd/financing/get_entity_rewards_rate.go | 83 ++++ cmd/financing/get_portfolio_rewards_rate.go | 83 ++++ .../list_cross_margin_liquidations.go | 103 +++++ .../list_trade_finance_obligations.go | 80 ++++ cmd/futures/get_derivative_positions.go | 73 ++++ .../get_derivatives_currency_summary.go | 71 ++++ cmd/futures/get_equity.go | 71 ++++ cmd/mcp/helpers.go | 21 +- cmd/mcp/server.go | 2 +- cmd/mcp/tools_financing.go | 388 ++++++++++++++++++ cmd/mcp/tools_futures.go | 100 +++++ cmd/mcp/tools_orders.go | 8 + cmd/mcp/tools_staking.go | 29 +- cmd/mcp/tools_transactions.go | 12 +- cmd/orders/edit.go | 4 + cmd/staking/portfolio_unstake.go | 13 +- cmd/staking/stake.go | 6 + cmd/staking/unstake.go | 6 + cmd/transactions/create_withdrawal.go | 4 + cmd/version.go | 2 +- go.mod | 14 +- go.sum | 22 +- utils/dictionary.go | 6 + utils/errors.go | 41 ++ utils/errors_test.go | 67 +++ 31 files changed, 1506 insertions(+), 38 deletions(-) create mode 100644 cmd/financing/get_conversion_fees.go create mode 100644 cmd/financing/get_cross_margin_liquidation.go create mode 100644 cmd/financing/get_entity_rewards_rate.go create mode 100644 cmd/financing/get_portfolio_rewards_rate.go create mode 100644 cmd/financing/list_cross_margin_liquidations.go create mode 100644 cmd/financing/list_trade_finance_obligations.go create mode 100644 cmd/futures/get_derivative_positions.go create mode 100644 cmd/futures/get_derivatives_currency_summary.go create mode 100644 cmd/futures/get_equity.go create mode 100644 utils/errors.go create mode 100644 utils/errors_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 33d38b9..0a5d005 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [0.6.0] - 2026-SEP-09 + +### Added + +- Bumped `prime-sdk-go` to v0.12.0 (local `replace` to `../prime-sdk-go` until the tag is published) +- New financing commands: `get-conversion-fees`, `get-cross-margin-liquidation`, `list-cross-margin-liquidations`, `list-trade-finance-obligations`, `get-entity-rewards-rate`, `get-portfolio-rewards-rate` +- New futures commands: `get-equity`, `get-derivatives-currency-summary`, `get-derivative-positions` +- MCP tools for the new endpoints, plus backfill for `get_market_data`, `get_cross_margin_prime_overview`, `get_cross_margin_risk_parameters`, `update_funding_settings` +- Optional flags: `orders edit --offset/--wig-level`, `staking portfolio-unstake --validator-provider`, `staking stake/unstake --metadata-external-id`, `transactions create-withdrawal --counterparty` +- API error output now includes `trace_id` and the spec subcode/code description by default + ## [0.5.1] - 2026-JUL-24 ### Added diff --git a/COMMANDS.md b/COMMANDS.md index 7606227..072dbb0 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -1,6 +1,6 @@ # Prime CLI Commands -A copy/paste-friendly reference for every `primectl` command in v0.5.0. Each command is shown as a runnable bash snippet that uses environment variables for the IDs you'll most often substitute. +A copy/paste-friendly reference for every `primectl` command in v0.6.0. Each command is shown as a runnable bash snippet that uses environment variables for the IDs you'll most often substitute. > Anything marked `<...>` is a placeholder you should replace before running. @@ -181,6 +181,21 @@ Most financing commands accept `--entity-id`. If omitted, the value falls back t --automatic-loan-enabled \ --automatic-excess-return-enabled \ --excess-funds-target-amount 1000 + +# Conversion fees (org-level; no entity/portfolio flag) +./primectl financing get-conversion-fees + +# Cross-margin liquidations +./primectl financing get-cross-margin-liquidation --entity-id "$ENTITY_ID" +./primectl financing get-cross-margin-liquidation --entity-id "$ENTITY_ID" --liquidation-id +./primectl financing list-cross-margin-liquidations --entity-id "$ENTITY_ID" --all +./primectl financing list-cross-margin-liquidations --entity-id "$ENTITY_ID" --status XM_LIQUIDATION_STATUS_LIQUIDATED --start-time 2026-01-01T00:00:00Z --end-time 2026-09-09T00:00:00Z + +./primectl financing list-trade-finance-obligations --entity-id "$ENTITY_ID" + +# Rewards rate (beta) +./primectl financing get-entity-rewards-rate --entity-id "$ENTITY_ID" +./primectl financing get-portfolio-rewards-rate --portfolio-id "$PORTFOLIO_ID" ``` ## futures @@ -199,6 +214,13 @@ All futures commands accept `--entity-id`. If omitted, the value falls back to t ./primectl futures schedule-sweep --entity-id "$ENTITY_ID" --amount 1000 --currency USD ./primectl futures cancel-sweep --entity-id "$ENTITY_ID" ./primectl futures set-settings --entity-id "$ENTITY_ID" --target-derivatives-excess 0.10 + +./primectl futures get-equity --entity-id "$ENTITY_ID" + +# International derivatives (portfolio-scoped) +./primectl futures get-derivatives-currency-summary --portfolio-id "$PORTFOLIO_ID" +./primectl futures get-derivative-positions --portfolio-id "$PORTFOLIO_ID" +./primectl futures get-derivative-positions --portfolio-id "$PORTFOLIO_ID" --product-id ``` ## invoices @@ -267,6 +289,13 @@ All futures commands accept `--entity-id`. If omitted, the value falls back to t --new-base-quantity 0.02 \ --new-limit-price 2050 +./primectl orders edit \ + --portfolio-id "$PORTFOLIO_ID" \ + --order-id \ + --new-base-quantity 0.02 \ + --offset 0.01 \ + --wig-level 0.5 + ./primectl orders create-quote \ --portfolio-id "$PORTFOLIO_ID" \ --product-id ETH-USD \ @@ -319,7 +348,9 @@ All futures commands accept `--entity-id`. If omitted, the value falls back to t ```bash ./primectl staking stake --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" +./primectl staking stake --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" --metadata-external-id my-stake-1 ./primectl staking unstake --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" --amount 1.0 +./primectl staking unstake --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" --amount 1.0 --metadata-external-id my-unstake-1 ./primectl staking claim-rewards --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" ./primectl staking get-status --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" ./primectl staking preview-unstake --portfolio-id "$PORTFOLIO_ID" --wallet-id "$WALLET_ID" --amount 1.0 @@ -327,6 +358,7 @@ All futures commands accept `--entity-id`. If omitted, the value falls back to t ./primectl staking portfolio-stake-initiate --portfolio-id "$PORTFOLIO_ID" --symbol ETH --amount 1.0 ./primectl staking portfolio-unstake --portfolio-id "$PORTFOLIO_ID" --symbol ETH --amount 1.0 +./primectl staking portfolio-unstake --portfolio-id "$PORTFOLIO_ID" --symbol ETH --validator-provider VALIDATOR_PROVIDER_COINBASE_CLOUD ./primectl staking query-validators \ --portfolio-id "$PORTFOLIO_ID" \ @@ -355,6 +387,14 @@ All futures commands accept `--entity-id`. If omitted, the value falls back to t --destination-type DESTINATION_BLOCKCHAIN \ --blockchain-address 0xabc123... +./primectl transactions create-withdrawal \ + --portfolio-id "$PORTFOLIO_ID" \ + --source-wallet-id "$WALLET_ID" \ + --symbol ETH \ + --amount 1.0 \ + --destination-type DESTINATION_COUNTERPARTY \ + --counterparty + ./primectl transactions create-conversion \ --portfolio-id "$PORTFOLIO_ID" \ --source-wallet-id \ diff --git a/README.md b/README.md index 1f001e1..4964694 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,9 @@ Finally, to run commands for each endpoint, use the following format to test eac ./primectl orders create-preview -b 0.001 -i ETH-USD -s BUY -t MARKET ``` -As of v0.5.0, the CLI covers the full surface area of [prime-sdk-go](https://github.com/coinbase/prime-sdk-go) v0.9.0, including the `advanced-transfers`, `futures`, and `positions` command groups. +As of v0.6.0, the CLI covers the full surface area of [prime-sdk-go](https://github.com/coinbase/prime-sdk-go) v0.12.0, including conversion fees, cross-margin liquidations, trade finance obligations, rewards rates, FCM equity, and international derivatives. + +API failures print the Prime error `code`, `subcode`, spec description, and `trace_id` on stderr so you can share the trace with support. ## MCP Server @@ -131,7 +133,7 @@ If `PRIME_CREDENTIALS` is already set in your shell environment, you can omit th ### Available tools -The MCP server exposes 98 tools across the Coinbase Prime API: +The MCP server exposes 113 tools across the Coinbase Prime API: | Tool | Description | |---|---| @@ -178,6 +180,16 @@ The MCP server exposes 98 tools across the Coinbase Prime API: | `list_margin_call_summaries` | List margin call summaries for an entity | | `list_margin_conversions` | List margin conversions for a portfolio (deprecated) | | `list_portfolio_interest_accruals` | List interest accruals for a portfolio | +| `get_market_data` | Get paginated market data for an entity | +| `get_cross_margin_prime_overview` | Get the Prime cross-margin overview for an entity | +| `get_cross_margin_risk_parameters` | Get cross-margin risk parameters for an entity | +| `update_funding_settings` | Update FCM funding settings for an entity | +| `get_conversion_fees` | Get organization stablecoin conversion fee tiers | +| `get_cross_margin_liquidation` | Get detailed cross-margin liquidation data for an entity | +| `list_cross_margin_liquidations` | List historical cross-margin liquidations for an entity | +| `list_trade_finance_obligations` | List trade finance obligations for an entity | +| `get_entity_rewards_rate` | Get current rewards rate and available tiers for an entity | +| `get_portfolio_rewards_rate` | Get current rewards rate and available tiers for a portfolio | | **Futures (FCM)** | | | `get_fcm_balance` | Get FCM balance summary for an entity | | `get_fcm_positions` | Get FCM futures positions for an entity | @@ -189,6 +201,9 @@ The MCP server exposes 98 tools across the Coinbase Prime API: | `list_fcm_sweeps` | List futures sweeps for an entity | | `schedule_fcm_sweep` | Schedule a futures sweep | | `cancel_fcm_sweep` | Cancel a scheduled futures sweep | +| `get_fcm_equity` | Get FCM equity data for an entity | +| `get_derivatives_currency_summary` | Get per-currency international derivatives balances for a portfolio | +| `get_derivative_positions` | Get active derivative positions for a portfolio | | **Invoices** | | | `list_invoices` | List invoices for an entity | | **Onchain Address Book** | | diff --git a/cmd/cmd.go b/cmd/cmd.go index 776c984..bffa1ca 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -16,11 +16,11 @@ package cmd import ( + "fmt" "os" "github.com/coinbase-samples/prime-cli/cmd/activities" "github.com/coinbase-samples/prime-cli/cmd/addressbook" - mcpcmd "github.com/coinbase-samples/prime-cli/cmd/mcp" "github.com/coinbase-samples/prime-cli/cmd/advancedtransfers" "github.com/coinbase-samples/prime-cli/cmd/allocations" "github.com/coinbase-samples/prime-cli/cmd/assets" @@ -29,6 +29,7 @@ import ( "github.com/coinbase-samples/prime-cli/cmd/financing" "github.com/coinbase-samples/prime-cli/cmd/futures" "github.com/coinbase-samples/prime-cli/cmd/invoices" + mcpcmd "github.com/coinbase-samples/prime-cli/cmd/mcp" "github.com/coinbase-samples/prime-cli/cmd/onchainaddressbook" "github.com/coinbase-samples/prime-cli/cmd/orders" "github.com/coinbase-samples/prime-cli/cmd/paymentmethods" @@ -50,8 +51,10 @@ var rootCmd = &cobra.Command{ } func Execute() { + rootCmd.SilenceErrors = true err := rootCmd.Execute() if err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", utils.FormatCLIError(err)) os.Exit(1) } } diff --git a/cmd/financing/get_conversion_fees.go b/cmd/financing/get_conversion_fees.go new file mode 100644 index 0000000..11efc01 --- /dev/null +++ b/cmd/financing/get_conversion_fees.go @@ -0,0 +1,71 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/spf13/cobra" +) + +var getConversionFeesCmd = &cobra.Command{ + Use: "get-conversion-fees", + Short: "Gets organization stablecoin conversion fee tiers", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + request := &prime.GetConversionFeesRequest{} + + response, err := getConversionFees(svc, request) + if err != nil { + return err + } + + if err := utils.PrintJsonDocs(cmd, response.Fees); err != nil { + return err + } + + return nil + }, +} + +func getConversionFees( + svc prime.FinancingService, + req *prime.GetConversionFeesRequest, +) (*prime.GetConversionFeesResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.GetConversionFees(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot get conversion fees: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(getConversionFeesCmd) +} diff --git a/cmd/financing/get_cross_margin_liquidation.go b/cmd/financing/get_cross_margin_liquidation.go new file mode 100644 index 0000000..f75ccc4 --- /dev/null +++ b/cmd/financing/get_cross_margin_liquidation.go @@ -0,0 +1,87 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/spf13/cobra" +) + +const liquidationIdFlag = "liquidation-id" + +var getCrossMarginLiquidationCmd = &cobra.Command{ + Use: "get-cross-margin-liquidation", + Short: "Gets detailed cross-margin liquidation data for an entity", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + entityId, err := utils.GetEntityId(cmd, client) + if err != nil { + return err + } + + request := &prime.GetCrossMarginLiquidationRequest{ + EntityId: entityId, + LiquidationId: utils.GetFlagStringValue(cmd, liquidationIdFlag), + } + + response, err := getCrossMarginLiquidation(svc, request) + if err != nil { + return err + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func getCrossMarginLiquidation( + svc prime.FinancingService, + req *prime.GetCrossMarginLiquidationRequest, +) (*prime.GetCrossMarginLiquidationResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.GetCrossMarginLiquidation(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot get cross margin liquidation: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(getCrossMarginLiquidationCmd) + + utils.AddEntityIdFlag(getCrossMarginLiquidationCmd) + getCrossMarginLiquidationCmd.Flags().String(liquidationIdFlag, "", "Optional liquidation ID") +} diff --git a/cmd/financing/get_entity_rewards_rate.go b/cmd/financing/get_entity_rewards_rate.go new file mode 100644 index 0000000..ee2b07a --- /dev/null +++ b/cmd/financing/get_entity_rewards_rate.go @@ -0,0 +1,83 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/spf13/cobra" +) + +var getEntityRewardsRateCmd = &cobra.Command{ + Use: "get-entity-rewards-rate", + Short: "Gets current rewards rate and available tiers for an entity", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + entityId, err := utils.GetEntityId(cmd, client) + if err != nil { + return err + } + + request := &prime.GetEntityRewardsRateRequest{ + EntityId: entityId, + } + + response, err := getEntityRewardsRate(svc, request) + if err != nil { + return err + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func getEntityRewardsRate( + svc prime.FinancingService, + req *prime.GetEntityRewardsRateRequest, +) (*prime.GetEntityRewardsRateResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.GetEntityRewardsRate(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot get entity rewards rate: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(getEntityRewardsRateCmd) + + utils.AddEntityIdFlag(getEntityRewardsRateCmd) +} diff --git a/cmd/financing/get_portfolio_rewards_rate.go b/cmd/financing/get_portfolio_rewards_rate.go new file mode 100644 index 0000000..fef2ae9 --- /dev/null +++ b/cmd/financing/get_portfolio_rewards_rate.go @@ -0,0 +1,83 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/spf13/cobra" +) + +var getPortfolioRewardsRateCmd = &cobra.Command{ + Use: "get-portfolio-rewards-rate", + Short: "Gets current rewards rate and available tiers for a portfolio", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + portfolioId, err := utils.GetPortfolioId(cmd, client) + if err != nil { + return err + } + + request := &prime.GetPortfolioRewardsRateRequest{ + PortfolioId: portfolioId, + } + + response, err := getPortfolioRewardsRate(svc, request) + if err != nil { + return err + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func getPortfolioRewardsRate( + svc prime.FinancingService, + req *prime.GetPortfolioRewardsRateRequest, +) (*prime.GetPortfolioRewardsRateResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.GetPortfolioRewardsRate(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot get portfolio rewards rate: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(getPortfolioRewardsRateCmd) + + utils.AddPortfolioIdFlag(getPortfolioRewardsRateCmd) +} diff --git a/cmd/financing/list_cross_margin_liquidations.go b/cmd/financing/list_cross_margin_liquidations.go new file mode 100644 index 0000000..63b2ac3 --- /dev/null +++ b/cmd/financing/list_cross_margin_liquidations.go @@ -0,0 +1,103 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/coinbase/prime-sdk-go/model" + "github.com/spf13/cobra" +) + +const ( + liquidationStatusFlag = "status" + startTimeFlag = "start-time" + endTimeFlag = "end-time" +) + +var listCrossMarginLiquidationsCmd = &cobra.Command{ + Use: "list-cross-margin-liquidations", + Short: "Lists historical cross-margin liquidations for an entity", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + entityId, err := utils.GetEntityId(cmd, client) + if err != nil { + return err + } + + status := utils.GetFlagStringValue(cmd, liquidationStatusFlag) + + return utils.HandleListCmd( + cmd, + func(paginationParams *model.PaginationParams) (*model.Pagination, error) { + request := &prime.ListCrossMarginLiquidationsRequest{ + EntityId: entityId, + Status: model.XMLiquidationStatus(status), + StartTime: utils.GetFlagStringValue(cmd, startTimeFlag), + EndTime: utils.GetFlagStringValue(cmd, endTimeFlag), + Pagination: paginationParams, + } + + response, err := listCrossMarginLiquidations(svc, request) + if err != nil { + return nil, err + } + + if err := utils.PrintJsonDocs(cmd, response.Liquidations); err != nil { + return nil, err + } + + return response.Pagination, nil + }, + ) + }, +} + +func listCrossMarginLiquidations( + svc prime.FinancingService, + req *prime.ListCrossMarginLiquidationsRequest, +) (*prime.ListCrossMarginLiquidationsResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.ListCrossMarginLiquidations(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot list cross margin liquidations: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(listCrossMarginLiquidationsCmd) + + utils.AddEntityIdFlag(listCrossMarginLiquidationsCmd) + utils.AddPaginationFlags(listCrossMarginLiquidationsCmd, true) + + listCrossMarginLiquidationsCmd.Flags().String(liquidationStatusFlag, "", "Filter by liquidation status") + listCrossMarginLiquidationsCmd.Flags().String(startTimeFlag, "", "Start time in RFC3339 format") + listCrossMarginLiquidationsCmd.Flags().String(endTimeFlag, "", "End time in RFC3339 format") +} diff --git a/cmd/financing/list_trade_finance_obligations.go b/cmd/financing/list_trade_finance_obligations.go new file mode 100644 index 0000000..f816f3a --- /dev/null +++ b/cmd/financing/list_trade_finance_obligations.go @@ -0,0 +1,80 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package financing + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/spf13/cobra" +) + +var listTradeFinanceObligationsCmd = &cobra.Command{ + Use: "list-trade-finance-obligations", + Short: "Lists trade finance obligations for an entity", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := prime.NewFinancingService(client) + + entityId, err := utils.GetEntityId(cmd, client) + if err != nil { + return err + } + + request := &prime.ListTradeFinanceObligationsRequest{ + EntityId: entityId, + } + + response, err := listTradeFinanceObligations(svc, request) + if err != nil { + return err + } + + if err := utils.PrintJsonDocs(cmd, response.Obligations); err != nil { + return err + } + + return nil + }, +} + +func listTradeFinanceObligations( + svc prime.FinancingService, + req *prime.ListTradeFinanceObligationsRequest, +) (*prime.ListTradeFinanceObligationsResponse, error) { + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + response, err := svc.ListTradeFinanceObligations(ctx, req) + if err != nil { + return nil, fmt.Errorf("cannot list trade finance obligations: %w", err) + } + + return response, nil +} + +func init() { + Cmd.AddCommand(listTradeFinanceObligationsCmd) + + utils.AddEntityIdFlag(listTradeFinanceObligationsCmd) +} diff --git a/cmd/futures/get_derivative_positions.go b/cmd/futures/get_derivative_positions.go new file mode 100644 index 0000000..4d8fad7 --- /dev/null +++ b/cmd/futures/get_derivative_positions.go @@ -0,0 +1,73 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package futures + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/futures" + + "github.com/spf13/cobra" +) + +var getDerivativePositionsCmd = &cobra.Command{ + Use: "get-derivative-positions", + Short: "Gets active derivative positions for a portfolio", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := futures.NewFuturesService(client) + + portfolioId, err := utils.GetPortfolioId(cmd, client) + if err != nil { + return fmt.Errorf("cannot get portfolio ID: %w", err) + } + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + request := &futures.GetDerivativePositionsRequest{ + PortfolioId: portfolioId, + ProductId: utils.GetFlagStringValue(cmd, utils.ProductIdFlag), + } + + response, err := svc.GetDerivativePositions(ctx, request) + if err != nil { + return fmt.Errorf("cannot get derivative positions: %w", err) + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func init() { + Cmd.AddCommand(getDerivativePositionsCmd) + + utils.AddPortfolioIdFlag(getDerivativePositionsCmd) + getDerivativePositionsCmd.Flags().String(utils.ProductIdFlag, "", "Optional product ID to filter positions") +} diff --git a/cmd/futures/get_derivatives_currency_summary.go b/cmd/futures/get_derivatives_currency_summary.go new file mode 100644 index 0000000..8ab4b1c --- /dev/null +++ b/cmd/futures/get_derivatives_currency_summary.go @@ -0,0 +1,71 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package futures + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/futures" + + "github.com/spf13/cobra" +) + +var getDerivativesCurrencySummaryCmd = &cobra.Command{ + Use: "get-derivatives-currency-summary", + Short: "Gets per-currency international derivatives balances for a portfolio", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := futures.NewFuturesService(client) + + portfolioId, err := utils.GetPortfolioId(cmd, client) + if err != nil { + return fmt.Errorf("cannot get portfolio ID: %w", err) + } + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + request := &futures.GetDerivativesCurrencySummaryRequest{ + PortfolioId: portfolioId, + } + + response, err := svc.GetDerivativesCurrencySummary(ctx, request) + if err != nil { + return fmt.Errorf("cannot get derivatives currency summary: %w", err) + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func init() { + Cmd.AddCommand(getDerivativesCurrencySummaryCmd) + + utils.AddPortfolioIdFlag(getDerivativesCurrencySummaryCmd) +} diff --git a/cmd/futures/get_equity.go b/cmd/futures/get_equity.go new file mode 100644 index 0000000..2b2adfb --- /dev/null +++ b/cmd/futures/get_equity.go @@ -0,0 +1,71 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package futures + +import ( + "fmt" + + "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/futures" + + "github.com/spf13/cobra" +) + +var getEquityCmd = &cobra.Command{ + Use: "get-equity", + Short: "Gets FCM equity data for an entity", + RunE: func(cmd *cobra.Command, args []string) error { + client, err := utils.GetClientFromEnv() + if err != nil { + return fmt.Errorf("failed to initialize client: %w", err) + } + + svc := futures.NewFuturesService(client) + + entityId, err := utils.GetEntityId(cmd, client) + if err != nil { + return fmt.Errorf("cannot get entity ID: %w", err) + } + + ctx, cancel := utils.GetContextWithTimeout() + defer cancel() + + request := &futures.GetFcmEquityRequest{ + EntityId: entityId, + } + + response, err := svc.GetFcmEquity(ctx, request) + if err != nil { + return fmt.Errorf("cannot get FCM equity: %w", err) + } + + jsonResponse, err := utils.FormatResponseAsJson(cmd, response) + if err != nil { + return err + } + + fmt.Println(jsonResponse) + + return nil + }, +} + +func init() { + Cmd.AddCommand(getEquityCmd) + + utils.AddEntityIdFlag(getEquityCmd) +} diff --git a/cmd/mcp/helpers.go b/cmd/mcp/helpers.go index 4e15e90..7722efd 100644 --- a/cmd/mcp/helpers.go +++ b/cmd/mcp/helpers.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/coinbase-samples/prime-cli/utils" "github.com/coinbase/prime-sdk-go/client" "github.com/coinbase/prime-sdk-go/model" mcplib "github.com/mark3labs/mcp-go/mcp" @@ -94,8 +95,24 @@ func marshalResult(v any) (*mcplib.CallToolResult, error) { return mcplib.NewToolResultText(string(data)), nil } -func toolErr(format string, a ...any) *mcplib.CallToolResult { - return mcplib.NewToolResultError(fmt.Sprintf(format, a...)) +func toolErr(format string, args ...any) *mcplib.CallToolResult { + message := fmt.Sprintf(format, withFormattedErrors(args)...) + return mcplib.NewToolResultError(message) +} + +// withFormattedErrors replaces error values with FormatCLIError strings so API +// failures include trace_id and the spec subcode description. +func withFormattedErrors(args []any) []any { + out := make([]any, len(args)) + for i, arg := range args { + err, isError := arg.(error) + if isError { + out[i] = utils.FormatCLIError(err) + continue + } + out[i] = arg + } + return out } // networkDetailsFor splits a compound network ID (e.g. "base-mainnet") into diff --git a/cmd/mcp/server.go b/cmd/mcp/server.go index c7d62fd..608ef9b 100644 --- a/cmd/mcp/server.go +++ b/cmd/mcp/server.go @@ -24,7 +24,7 @@ import ( func runMCPServer(_ *cobra.Command, _ []string) error { s := server.NewMCPServer( "coinbase-prime", - "0.4.2", + "0.6.0", server.WithToolCapabilities(false), ) diff --git a/cmd/mcp/tools_financing.go b/cmd/mcp/tools_financing.go index 6609759..91ea83a 100644 --- a/cmd/mcp/tools_financing.go +++ b/cmd/mcp/tools_financing.go @@ -21,6 +21,7 @@ import ( "github.com/coinbase-samples/prime-cli/utils" prime "github.com/coinbase/prime-sdk-go/financing" + "github.com/coinbase/prime-sdk-go/model" mcplib "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) @@ -190,6 +191,119 @@ func registerFinancingTools(s *server.MCPServer) { mcplib.Description("End date in RFC3339 format"), ), ), handleListPortfolioInterestAccruals) + + s.AddTool(mcplib.NewTool("get_market_data", + mcplib.WithDescription("Get paginated market data for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + mcplib.WithString("cursor", + mcplib.Description("Pagination cursor from a previous response"), + ), + mcplib.WithInteger("limit", + mcplib.Description("Maximum number of results to return"), + ), + mcplib.WithBoolean("fetch_all", + mcplib.Description("Fetch all pages automatically and return combined results. When true, cursor and limit are ignored."), + ), + ), handleGetMarketData) + + s.AddTool(mcplib.NewTool("get_cross_margin_prime_overview", + mcplib.WithDescription("Get the Prime cross-margin overview for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetCrossMarginPrimeOverview) + + s.AddTool(mcplib.NewTool("get_cross_margin_risk_parameters", + mcplib.WithDescription("Get cross-margin risk parameters for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetCrossMarginRiskParameters) + + s.AddTool(mcplib.NewTool("update_funding_settings", + mcplib.WithDescription("Update FCM funding settings for an entity (creates a PCS proposal)"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + mcplib.WithString("designated_funding_portfolio_id", + mcplib.Required(), + mcplib.Description("Derivatives funding portfolio ID"), + ), + mcplib.WithBoolean("automatic_conversion_enabled", + mcplib.Description("Convert USDC to USD automatically to meet FCM margin calls"), + ), + mcplib.WithBoolean("automatic_loan_enabled", + mcplib.Description("Allow Coinbase affiliates to initiate loans to meet FCM margin calls"), + ), + mcplib.WithBoolean("automatic_excess_return_enabled", + mcplib.Description("Sweep FCM balance above margin requirements back to the derivatives funding portfolio"), + ), + mcplib.WithString("excess_funds_target_amount", + mcplib.Description("Target amount to maintain in the futures account above margin requirements"), + ), + ), handleUpdateFundingSettings) + + s.AddTool(mcplib.NewTool("get_conversion_fees", + mcplib.WithDescription("Get organization stablecoin conversion fee tiers and month-to-date net conversion volume"), + ), handleGetConversionFees) + + s.AddTool(mcplib.NewTool("get_cross_margin_liquidation", + mcplib.WithDescription("Get detailed cross-margin liquidation data for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + mcplib.WithString("liquidation_id", + mcplib.Description("Optional liquidation ID"), + ), + ), handleGetCrossMarginLiquidation) + + s.AddTool(mcplib.NewTool("list_cross_margin_liquidations", + mcplib.WithDescription("List historical cross-margin liquidations for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + mcplib.WithString("status", + mcplib.Description("Filter by liquidation status"), + ), + mcplib.WithString("start_time", + mcplib.Description("Start time in RFC3339 format"), + ), + mcplib.WithString("end_time", + mcplib.Description("End time in RFC3339 format"), + ), + mcplib.WithString("cursor", + mcplib.Description("Pagination cursor from a previous response"), + ), + mcplib.WithInteger("limit", + mcplib.Description("Maximum number of results to return"), + ), + mcplib.WithBoolean("fetch_all", + mcplib.Description("Fetch all pages automatically and return combined results. When true, cursor and limit are ignored."), + ), + ), handleListCrossMarginLiquidations) + + s.AddTool(mcplib.NewTool("list_trade_finance_obligations", + mcplib.WithDescription("List trade finance obligations for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleListTradeFinanceObligations) + + s.AddTool(mcplib.NewTool("get_entity_rewards_rate", + mcplib.WithDescription("Get current rewards rate and available tiers for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetEntityRewardsRate) + + s.AddTool(mcplib.NewTool("get_portfolio_rewards_rate", + mcplib.WithDescription("Get current rewards rate and available tiers for a portfolio"), + mcplib.WithString("portfolio_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetPortfolioRewardsRate) } func handleGetBuyingPower(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { @@ -553,3 +667,277 @@ func handleListPortfolioInterestAccruals(ctx context.Context, req mcplib.CallToo return marshalResult(response) } + +func handleGetMarketData(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetMarketData(ctx2, &prime.GetMarketDataRequest{ + EntityId: entityId, + Pagination: paginationFor(req), + }) + if err != nil { + return toolErr("cannot get market data: %s", err), nil + } + + if req.GetBool("fetch_all", false) { + ctx3, cancel3 := fetchAllCtx(ctx) + defer cancel3() + items, err := response.Iterator().FetchAll(ctx3) + if err != nil { + return toolErr("failed to fetch all pages: %s", err), nil + } + return marshalResult(items) + } + + return marshalResult(response) +} + +func handleGetCrossMarginPrimeOverview(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetCrossMarginPrimeOverview(ctx2, &prime.GetCrossMarginPrimeOverviewRequest{ + EntityId: entityId, + }) + if err != nil { + return toolErr("cannot get cross margin prime overview: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetCrossMarginRiskParameters(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetCrossMarginRiskParameters(ctx2, &prime.GetCrossMarginRiskParametersRequest{ + EntityId: entityId, + }) + if err != nil { + return toolErr("cannot get cross margin risk parameters: %s", err), nil + } + + return marshalResult(response) +} + +func handleUpdateFundingSettings(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.UpdateFundingSettings(ctx2, &prime.UpdateFundingSettingsRequest{ + EntityId: entityId, + DesignatedFundingPortfolioId: req.GetString("designated_funding_portfolio_id", ""), + AutomaticConversionEnabled: req.GetBool("automatic_conversion_enabled", false), + AutomaticLoanEnabled: req.GetBool("automatic_loan_enabled", false), + AutomaticExcessReturnEnabled: req.GetBool("automatic_excess_return_enabled", false), + ExcessFundsTargetAmount: req.GetString("excess_funds_target_amount", ""), + }) + if err != nil { + return toolErr("cannot update funding settings: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetConversionFees(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetConversionFees(ctx2, &prime.GetConversionFeesRequest{}) + if err != nil { + return toolErr("cannot get conversion fees: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetCrossMarginLiquidation(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetCrossMarginLiquidation(ctx2, &prime.GetCrossMarginLiquidationRequest{ + EntityId: entityId, + LiquidationId: req.GetString("liquidation_id", ""), + }) + if err != nil { + return toolErr("cannot get cross margin liquidation: %s", err), nil + } + + return marshalResult(response) +} + +func handleListCrossMarginLiquidations(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.ListCrossMarginLiquidations(ctx2, &prime.ListCrossMarginLiquidationsRequest{ + EntityId: entityId, + Status: model.XMLiquidationStatus(req.GetString("status", "")), + StartTime: req.GetString("start_time", ""), + EndTime: req.GetString("end_time", ""), + Pagination: paginationFor(req), + }) + if err != nil { + return toolErr("cannot list cross margin liquidations: %s", err), nil + } + + if req.GetBool("fetch_all", false) { + ctx3, cancel3 := fetchAllCtx(ctx) + defer cancel3() + items, err := response.Iterator().FetchAll(ctx3) + if err != nil { + return toolErr("failed to fetch all pages: %s", err), nil + } + return marshalResult(items) + } + + return marshalResult(response) +} + +func handleListTradeFinanceObligations(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.ListTradeFinanceObligations(ctx2, &prime.ListTradeFinanceObligationsRequest{ + EntityId: entityId, + }) + if err != nil { + return toolErr("cannot list trade finance obligations: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetEntityRewardsRate(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetEntityRewardsRate(ctx2, &prime.GetEntityRewardsRateRequest{ + EntityId: entityId, + }) + if err != nil { + return toolErr("cannot get entity rewards rate: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetPortfolioRewardsRate(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + portfolioId, err := resolvePortfolioId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := prime.NewFinancingService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetPortfolioRewardsRate(ctx2, &prime.GetPortfolioRewardsRateRequest{ + PortfolioId: portfolioId, + }) + if err != nil { + return toolErr("cannot get portfolio rewards rate: %s", err), nil + } + + return marshalResult(response) +} diff --git a/cmd/mcp/tools_futures.go b/cmd/mcp/tools_futures.go index 4da05a7..6ebcc56 100644 --- a/cmd/mcp/tools_futures.go +++ b/cmd/mcp/tools_futures.go @@ -109,6 +109,30 @@ func registerFuturesTools(s *server.MCPServer) { mcplib.Description("Uses credentials default if omitted"), ), ), handleCancelFcmSweep) + + s.AddTool(mcplib.NewTool("get_fcm_equity", + mcplib.WithDescription("Get FCM equity data for an entity"), + mcplib.WithString("entity_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetFcmEquity) + + s.AddTool(mcplib.NewTool("get_derivatives_currency_summary", + mcplib.WithDescription("Get per-currency international derivatives balances for a portfolio"), + mcplib.WithString("portfolio_id", + mcplib.Description("Uses credentials default if omitted"), + ), + ), handleGetDerivativesCurrencySummary) + + s.AddTool(mcplib.NewTool("get_derivative_positions", + mcplib.WithDescription("Get active derivative positions for a portfolio"), + mcplib.WithString("portfolio_id", + mcplib.Description("Uses credentials default if omitted"), + ), + mcplib.WithString("product_id", + mcplib.Description("Optional product ID to filter positions"), + ), + ), handleGetDerivativePositions) } func handleGetFcmBalance(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { @@ -364,3 +388,79 @@ func handleCancelFcmSweep(ctx context.Context, req mcplib.CallToolRequest) (*mcp return marshalResult(response) } + +func handleGetFcmEquity(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + entityId, err := resolveEntityId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := futures.NewFuturesService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetFcmEquity(ctx2, &futures.GetFcmEquityRequest{ + EntityId: entityId, + }) + if err != nil { + return toolErr("cannot get FCM equity: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetDerivativesCurrencySummary(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + portfolioId, err := resolvePortfolioId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := futures.NewFuturesService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetDerivativesCurrencySummary(ctx2, &futures.GetDerivativesCurrencySummaryRequest{ + PortfolioId: portfolioId, + }) + if err != nil { + return toolErr("cannot get derivatives currency summary: %s", err), nil + } + + return marshalResult(response) +} + +func handleGetDerivativePositions(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToolResult, error) { + client, err := utils.GetClientFromEnv() + if err != nil { + return toolErr("failed to initialize client: %s", err), nil + } + + portfolioId, err := resolvePortfolioId(client, req) + if err != nil { + return toolErr("%s", err), nil + } + + svc := futures.NewFuturesService(client) + ctx2, cancel := mcpCtx(ctx) + defer cancel() + + response, err := svc.GetDerivativePositions(ctx2, &futures.GetDerivativePositionsRequest{ + PortfolioId: portfolioId, + ProductId: req.GetString("product_id", ""), + }) + if err != nil { + return toolErr("cannot get derivative positions: %s", err), nil + } + + return marshalResult(response) +} diff --git a/cmd/mcp/tools_orders.go b/cmd/mcp/tools_orders.go index e094088..d91de87 100644 --- a/cmd/mcp/tools_orders.go +++ b/cmd/mcp/tools_orders.go @@ -227,6 +227,12 @@ func registerOrderTools(s *server.MCPServer) { mcplib.WithString("client_order_id", mcplib.Description("Updated client order ID"), ), + mcplib.WithString("offset", + mcplib.Description("Peg offset for PEG orders"), + ), + mcplib.WithString("wig_level", + mcplib.Description("WIG level for PEG orders"), + ), ), handleEditOrder) s.AddTool(mcplib.NewTool("get_order_edit_history", @@ -637,6 +643,8 @@ func handleEditOrder(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.C BaseQuantity: req.GetString("new_base_quantity", ""), QuoteValue: req.GetString("new_quote_value", ""), LimitPrice: req.GetString("new_limit_price", ""), + Offset: req.GetString("offset", ""), + WigLevel: req.GetString("wig_level", ""), }) if err != nil { return toolErr("cannot edit order: %s", err), nil diff --git a/cmd/mcp/tools_staking.go b/cmd/mcp/tools_staking.go index 775f002..f18b10d 100644 --- a/cmd/mcp/tools_staking.go +++ b/cmd/mcp/tools_staking.go @@ -20,6 +20,7 @@ import ( "context" "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/model" primeStaking "github.com/coinbase/prime-sdk-go/staking" mcplib "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" @@ -40,6 +41,9 @@ func registerStakingTools(s *server.MCPServer) { mcplib.WithString("idempotency_key", mcplib.Description("Auto-generated if omitted"), ), + mcplib.WithString("metadata_external_id", + mcplib.Description("Optional client-generated external ID for the stake request"), + ), ), handleStake) s.AddTool(mcplib.NewTool("unstake", @@ -56,6 +60,9 @@ func registerStakingTools(s *server.MCPServer) { mcplib.WithString("idempotency_key", mcplib.Description("Auto-generated if omitted"), ), + mcplib.WithString("metadata_external_id", + mcplib.Description("Optional client-generated external ID for the unstake request"), + ), ), handleUnstake) s.AddTool(mcplib.NewTool("get_staking_status", @@ -135,11 +142,14 @@ func registerStakingTools(s *server.MCPServer) { mcplib.Description("Currency symbol to unstake (e.g. ETH)"), ), mcplib.WithString("amount", - mcplib.Description("Amount to unstake"), + mcplib.Description("Amount to unstake. Optional when using validator_provider"), ), mcplib.WithString("stake_protocol", mcplib.Description("Optional staking protocol identifier"), ), + mcplib.WithString("validator_provider", + mcplib.Description("ETH validator provider (e.g. VALIDATOR_PROVIDER_COINBASE_CLOUD)"), + ), mcplib.WithString("idempotency_key", mcplib.Description("Auto-generated if omitted"), ), @@ -192,6 +202,10 @@ func handleStake(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallT request.Inputs = primeStaking.CreateStakeInputs{Amount: amount} } + if externalId := req.GetString("metadata_external_id", ""); externalId != "" { + request.Metadata = &model.WalletStakingMetadata{ExternalId: externalId} + } + svc := primeStaking.NewStakingService(client) ctx2, cancel := mcpCtx(ctx) defer cancel() @@ -230,6 +244,10 @@ func handleUnstake(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.Cal request.Inputs = primeStaking.CreateUnstakeInputs{Amount: amount} } + if externalId := req.GetString("metadata_external_id", ""); externalId != "" { + request.Metadata = &model.WalletStakingMetadata{ExternalId: externalId} + } + svc := primeStaking.NewStakingService(client) ctx2, cancel := mcpCtx(ctx) defer cancel() @@ -413,10 +431,11 @@ func handlePortfolioUnstake(ctx context.Context, req mcplib.CallToolRequest) (*m defer cancel() response, err := svc.PortfolioUnstake(ctx2, &primeStaking.PortfolioUnstakeRequest{ - PortfolioId: portfolioId, - IdempotencyKey: idempotencyKey, - CurrencySymbol: req.GetString("symbol", ""), - Amount: req.GetString("amount", ""), + PortfolioId: portfolioId, + IdempotencyKey: idempotencyKey, + CurrencySymbol: req.GetString("symbol", ""), + Amount: req.GetString("amount", ""), + ValidatorProvider: model.ValidatorProvider(req.GetString("validator_provider", "")), }) if err != nil { return toolErr("cannot initiate portfolio unstake: %s", err), nil diff --git a/cmd/mcp/tools_transactions.go b/cmd/mcp/tools_transactions.go index cd4ea1e..e7aba32 100644 --- a/cmd/mcp/tools_transactions.go +++ b/cmd/mcp/tools_transactions.go @@ -160,6 +160,9 @@ func registerTransactionTools(s *server.MCPServer) { mcplib.WithString("payment_method_id", mcplib.Description("Payment method ID. Required when destination_type=DESTINATION_PAYMENT_METHOD. Use list_payment_methods to find IDs."), ), + mcplib.WithString("counterparty", + mcplib.Description("Counterparty ID. Required when destination_type=DESTINATION_COUNTERPARTY."), + ), mcplib.WithString("idempotency_key", mcplib.Description("Auto-generated if omitted"), ), @@ -480,7 +483,7 @@ func handleCreateWithdrawal(ctx context.Context, req mcplib.CallToolRequest) (*m ctx2, cancel := mcpCtx(ctx) defer cancel() - response, err := svc.CreateWalletWithdrawal(ctx2, &transactions.CreateWalletWithdrawalRequest{ + withdrawalReq := &transactions.CreateWalletWithdrawalRequest{ PortfolioId: portfolioId, SourceWalletId: sourceWalletId, Symbol: symbol, @@ -495,7 +498,12 @@ func handleCreateWithdrawal(ctx context.Context, req mcplib.CallToolRequest) (*m AccountIdentifier: req.GetString("account_identifier", ""), Network: networkDetailsFor(req.GetString("network_id", "")), }, - }) + } + if counterparty := req.GetString("counterparty", ""); counterparty != "" { + withdrawalReq.Counterparty = &model.CounterpartyDestination{CounterpartyId: counterparty} + } + + response, err := svc.CreateWalletWithdrawal(ctx2, withdrawalReq) if err != nil { return toolErr("cannot create withdrawal: %s", err), nil } diff --git a/cmd/orders/edit.go b/cmd/orders/edit.go index fb6e923..8e17b77 100644 --- a/cmd/orders/edit.go +++ b/cmd/orders/edit.go @@ -51,6 +51,8 @@ var editOrderCmd = &cobra.Command{ BaseQuantity: utils.GetFlagStringValue(cmd, utils.NewBaseQuantityFlag), QuoteValue: utils.GetFlagStringValue(cmd, utils.NewQuoteValueFlag), LimitPrice: utils.GetFlagStringValue(cmd, utils.NewLimitPriceFlag), + Offset: utils.GetFlagStringValue(cmd, utils.OffsetFlag), + WigLevel: utils.GetFlagStringValue(cmd, utils.WigLevelFlag), } response, err := ordersService.EditOrder(ctx, request) @@ -80,6 +82,8 @@ func init() { editOrderCmd.Flags().String(utils.NewBaseQuantityFlag, "", "Updated order size in base asset units") editOrderCmd.Flags().String(utils.NewQuoteValueFlag, "", "Updated order size in quote asset units") editOrderCmd.Flags().String(utils.NewLimitPriceFlag, "", "Updated limit price") + editOrderCmd.Flags().String(utils.OffsetFlag, "", "Peg offset for PEG orders") + editOrderCmd.Flags().String(utils.WigLevelFlag, "", "WIG level for PEG orders") utils.AddPortfolioIdFlag(editOrderCmd) } diff --git a/cmd/staking/portfolio_unstake.go b/cmd/staking/portfolio_unstake.go index 8fea4c6..6f21e41 100644 --- a/cmd/staking/portfolio_unstake.go +++ b/cmd/staking/portfolio_unstake.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/model" primeStaking "github.com/coinbase/prime-sdk-go/staking" "github.com/spf13/cobra" ) @@ -46,10 +47,11 @@ var portfolioUnstakeCmd = &cobra.Command{ } request := &primeStaking.PortfolioUnstakeRequest{ - PortfolioId: portfolioId, - IdempotencyKey: idempotencyKey, - CurrencySymbol: utils.GetFlagStringValue(cmd, utils.SymbolFlag), - Amount: utils.GetFlagStringValue(cmd, utils.AmountFlag), + PortfolioId: portfolioId, + IdempotencyKey: idempotencyKey, + CurrencySymbol: utils.GetFlagStringValue(cmd, utils.SymbolFlag), + Amount: utils.GetFlagStringValue(cmd, utils.AmountFlag), + ValidatorProvider: model.ValidatorProvider(utils.GetFlagStringValue(cmd, utils.ValidatorProviderFlag)), } ctx, cancel := utils.GetContextWithTimeout() @@ -77,6 +79,7 @@ func init() { utils.AddIdempotencyKeyFlag(portfolioUnstakeCmd) portfolioUnstakeCmd.Flags().String(utils.SymbolFlag, "", "Currency symbol to unstake (e.g. ETH)") - portfolioUnstakeCmd.Flags().String(utils.AmountFlag, "", "Amount to unstake") + portfolioUnstakeCmd.Flags().String(utils.AmountFlag, "", "Amount to unstake. Optional when using --validator-provider") portfolioUnstakeCmd.Flags().String(utils.StakeProtocolFlag, "", "Optional staking protocol identifier") + portfolioUnstakeCmd.Flags().String(utils.ValidatorProviderFlag, "", "ETH validator provider (e.g. VALIDATOR_PROVIDER_COINBASE_CLOUD)") } diff --git a/cmd/staking/stake.go b/cmd/staking/stake.go index 647fb70..65e7154 100644 --- a/cmd/staking/stake.go +++ b/cmd/staking/stake.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/model" primeStaking "github.com/coinbase/prime-sdk-go/staking" "github.com/spf13/cobra" ) @@ -56,6 +57,10 @@ var createStakeCmd = &cobra.Command{ request.Inputs = primeStaking.CreateStakeInputs{Amount: amount} } + if externalId := utils.GetFlagStringValue(cmd, utils.MetadataExternalIdFlag); externalId != "" { + request.Metadata = &model.WalletStakingMetadata{ExternalId: externalId} + } + ctx, cancel := utils.GetContextWithTimeout() defer cancel() @@ -82,4 +87,5 @@ func init() { utils.AddIdempotencyKeyFlag(createStakeCmd) createStakeCmd.Flags().String(utils.AmountFlag, "", "Optional amount to stake. If omitted, the wallet will stake or unstake the maximum amount available") + createStakeCmd.Flags().String(utils.MetadataExternalIdFlag, "", "Optional client-generated external ID for the stake request") } diff --git a/cmd/staking/unstake.go b/cmd/staking/unstake.go index 1b78b46..c1e3a56 100644 --- a/cmd/staking/unstake.go +++ b/cmd/staking/unstake.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/coinbase-samples/prime-cli/utils" + "github.com/coinbase/prime-sdk-go/model" primeStaking "github.com/coinbase/prime-sdk-go/staking" "github.com/spf13/cobra" ) @@ -56,6 +57,10 @@ var createUnstakeCmd = &cobra.Command{ request.Inputs = primeStaking.CreateUnstakeInputs{Amount: amount} } + if externalId := utils.GetFlagStringValue(cmd, utils.MetadataExternalIdFlag); externalId != "" { + request.Metadata = &model.WalletStakingMetadata{ExternalId: externalId} + } + ctx, cancel := utils.GetContextWithTimeout() defer cancel() @@ -82,4 +87,5 @@ func init() { utils.AddIdempotencyKeyFlag(createUnstakeCmd) createUnstakeCmd.Flags().String(utils.AmountFlag, "", "Optional amount to stake. If omitted, the wallet will stake or unstake the maximum amount available") + createUnstakeCmd.Flags().String(utils.MetadataExternalIdFlag, "", "Optional client-generated external ID for the unstake request") } diff --git a/cmd/transactions/create_withdrawal.go b/cmd/transactions/create_withdrawal.go index 3ea7b71..8e27ed5 100644 --- a/cmd/transactions/create_withdrawal.go +++ b/cmd/transactions/create_withdrawal.go @@ -73,6 +73,9 @@ var createWithdrawalCmd = &cobra.Command{ PaymentMethod: &transactions.CreateWalletWithdrawalPaymentMethod{Id: paymentMethodId}, BlockchainAddress: &model.BlockchainAddress{Address: address, AccountIdentifier: accountIdentifier}, } + if counterparty := utils.GetFlagStringValue(cmd, utils.CounterpartyFlag); counterparty != "" { + request.Counterparty = &model.CounterpartyDestination{CounterpartyId: counterparty} + } response, err := transactionsService.CreateWalletWithdrawal(ctx, request) if err != nil { return fmt.Errorf("cannot create withdrawal: %w", err) @@ -98,6 +101,7 @@ func init() { createWithdrawalCmd.Flags().String(utils.PaymentMethodIdFlag, "", "ID of the payment method") createWithdrawalCmd.Flags().String(utils.BlockchainAddressFlag, "", "Blockchain address") createWithdrawalCmd.Flags().String(utils.AccountIdentifierFlag, "", "Account identifier") + createWithdrawalCmd.Flags().String(utils.CounterpartyFlag, "", "Counterparty ID for DESTINATION_COUNTERPARTY withdrawals") utils.AddPortfolioIdFlag(createWithdrawalCmd) utils.AddIdempotencyKeyFlag(createWithdrawalCmd) diff --git a/cmd/version.go b/cmd/version.go index b533cbc..8838ba3 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -25,7 +25,7 @@ import ( "github.com/spf13/cobra" ) -var primectlVersion = `{"version":"0.5.1"}` +var primectlVersion = `{"version":"0.6.0"}` var versionCmd = &cobra.Command{ Use: "version", diff --git a/go.mod b/go.mod index 4db7d3d..05c68b1 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,17 @@ module github.com/coinbase-samples/prime-cli go 1.25.5 require ( - github.com/coinbase/prime-sdk-go v0.9.1 + github.com/coinbase/prime-sdk-go v0.12.0 github.com/google/uuid v1.6.0 github.com/mark3labs/mcp-go v0.55.0 github.com/spf13/cobra v1.9.1 - golang.org/x/term v0.41.0 + golang.org/x/term v0.43.0 ) +replace github.com/coinbase/prime-sdk-go => ../prime-sdk-go + require ( - github.com/coinbase/core-go v0.3.0 // indirect + github.com/coinbase/core-go v0.4.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -20,7 +22,7 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/yosida95/uritemplate/v3 v3.0.2 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect ) diff --git a/go.sum b/go.sum index e06fa0e..c84ece2 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ -github.com/coinbase/core-go v0.3.0 h1:LDJOzUqAheb32cYdQxuDKJiWc05y2fQwZATTUEegnlI= -github.com/coinbase/core-go v0.3.0/go.mod h1:jeIkuOCuwGxBXsgzF56FCPI89AWABRWr/xpXd6LYplQ= -github.com/coinbase/prime-sdk-go v0.9.1 h1:YdN/ES2PqvHH/X6WKYEnFF8a6CRpHLKKQqWKTqBwQuk= -github.com/coinbase/prime-sdk-go v0.9.1/go.mod h1:ZjJGp/vTejfl5aTV+vR7JeeQY9Vv8Vf6EXnspHXPJ8Y= +github.com/coinbase/core-go v0.4.0 h1:RsQGFyiIAaqcbnMcw8BhWEJ499vDKuOhNrbPJ+vVtqU= +github.com/coinbase/core-go v0.4.0/go.mod h1:vCTLyEjSfVToFara6yAYqzRvlTi2q1kdhhzNtIfPD8k= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -44,14 +42,14 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/utils/dictionary.go b/utils/dictionary.go index d81df5a..3e6e169 100644 --- a/utils/dictionary.go +++ b/utils/dictionary.go @@ -138,4 +138,10 @@ const ( ValidatorAddressFlag = "validator-address" StakeProtocolFlag = "protocol" + + OffsetFlag = "offset" + WigLevelFlag = "wig-level" + ValidatorProviderFlag = "validator-provider" + MetadataExternalIdFlag = "metadata-external-id" + CounterpartyFlag = "counterparty" ) diff --git a/utils/errors.go b/utils/errors.go new file mode 100644 index 0000000..fee2961 --- /dev/null +++ b/utils/errors.go @@ -0,0 +1,41 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "strings" + + primeerrors "github.com/coinbase/prime-sdk-go/model/errors" +) + +// FormatCLIError returns a user-facing error string. Prime API errors include +// trace_id and the spec subcode/code description via APIError.Format(). +func FormatCLIError(err error) string { + if err == nil { + return "" + } + apiErr, ok := primeerrors.From(err) + if !ok { + return err.Error() + } + formatted := apiErr.Format() + full := err.Error() + if old := apiErr.Error(); strings.Contains(full, old) { + return strings.Replace(full, old, formatted, 1) + } + return full + " (" + formatted + ")" +} diff --git a/utils/errors_test.go b/utils/errors_test.go new file mode 100644 index 0000000..ad70dc4 --- /dev/null +++ b/utils/errors_test.go @@ -0,0 +1,67 @@ +/** + * Copyright 2026-present Coinbase Global, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package utils + +import ( + "errors" + "fmt" + "strings" + "testing" + + primeerrors "github.com/coinbase/prime-sdk-go/model/errors" +) + +func TestFormatCLIError_NonAPIErrorUnchanged(t *testing.T) { + err := errors.New("cannot unmarshal credentials") + if got := FormatCLIError(err); got != err.Error() { + t.Fatalf("got %q, want %q", got, err.Error()) + } +} + +func TestFormatCLIError_Nil(t *testing.T) { + if got := FormatCLIError(nil); got != "" { + t.Fatalf("got %q, want empty", got) + } +} + +func TestFormatCLIError_WrappedAPIErrorIncludesTraceAndDescription(t *testing.T) { + apiErr := &primeerrors.APIError{ + Response: primeerrors.Response{ + Code: primeerrors.ErrorCodeValidationError, + Message: "invalid entity", + Subcode: primeerrors.SubcodeEntityIdInvalid, + TraceID: "trace-abc-123", + }, + StatusCode: 400, + URL: "/entities/x", + } + wrapped := fmt.Errorf("cannot get FCM balance: %w", apiErr) + + got := FormatCLIError(wrapped) + if !strings.Contains(got, "cannot get FCM balance:") { + t.Fatalf("missing wrap prefix: %q", got) + } + if !strings.Contains(got, "trace_id=trace-abc-123") { + t.Fatalf("missing trace_id: %q", got) + } + if !strings.Contains(got, "description=The entity_id is not a valid UUID.") { + t.Fatalf("missing subcode description: %q", got) + } + if !strings.Contains(got, "subcode=ENTITY_ID_INVALID") { + t.Fatalf("missing subcode: %q", got) + } +} From e5563c8093bd3024cfd16b413215a226172b1a05 Mon Sep 17 00:00:00 2001 From: Carl Fluke Date: Fri, 11 Sep 2026 14:05:23 -0400 Subject: [PATCH 2/2] use published sdk ver --- CHANGELOG.md | 2 +- go.mod | 2 -- go.sum | 2 ++ utils/errors.go | 2 +- utils/errors_test.go | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a5d005..458ff74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Added -- Bumped `prime-sdk-go` to v0.12.0 (local `replace` to `../prime-sdk-go` until the tag is published) +- Bumped `prime-sdk-go` to v0.12.0 - New financing commands: `get-conversion-fees`, `get-cross-margin-liquidation`, `list-cross-margin-liquidations`, `list-trade-finance-obligations`, `get-entity-rewards-rate`, `get-portfolio-rewards-rate` - New futures commands: `get-equity`, `get-derivatives-currency-summary`, `get-derivative-positions` - MCP tools for the new endpoints, plus backfill for `get_market_data`, `get_cross_margin_prime_overview`, `get_cross_margin_risk_parameters`, `update_funding_settings` diff --git a/go.mod b/go.mod index 05c68b1..fbc05e3 100644 --- a/go.mod +++ b/go.mod @@ -10,8 +10,6 @@ require ( golang.org/x/term v0.43.0 ) -replace github.com/coinbase/prime-sdk-go => ../prime-sdk-go - require ( github.com/coinbase/core-go v0.4.0 // indirect github.com/google/jsonschema-go v0.4.2 // indirect diff --git a/go.sum b/go.sum index c84ece2..20a544d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/coinbase/core-go v0.4.0 h1:RsQGFyiIAaqcbnMcw8BhWEJ499vDKuOhNrbPJ+vVtqU= github.com/coinbase/core-go v0.4.0/go.mod h1:vCTLyEjSfVToFara6yAYqzRvlTi2q1kdhhzNtIfPD8k= +github.com/coinbase/prime-sdk-go v0.12.0 h1:paumZHPp6lEijjKPNmu+Gpf0Rs0XGEEyClxbAQM6s4o= +github.com/coinbase/prime-sdk-go v0.12.0/go.mod h1:bDafaTIG4DSbwLBy1chYbU+NvdbaA85Vgmy/3zF/Lmk= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/utils/errors.go b/utils/errors.go index fee2961..7e1585e 100644 --- a/utils/errors.go +++ b/utils/errors.go @@ -23,7 +23,7 @@ import ( ) // FormatCLIError returns a user-facing error string. Prime API errors include -// trace_id and the spec subcode/code description via APIError.Format(). +// trace_id and the spec subcode/code description via ApiError.Format(). func FormatCLIError(err error) string { if err == nil { return "" diff --git a/utils/errors_test.go b/utils/errors_test.go index ad70dc4..5070800 100644 --- a/utils/errors_test.go +++ b/utils/errors_test.go @@ -25,7 +25,7 @@ import ( primeerrors "github.com/coinbase/prime-sdk-go/model/errors" ) -func TestFormatCLIError_NonAPIErrorUnchanged(t *testing.T) { +func TestFormatCLIError_NonApiErrorUnchanged(t *testing.T) { err := errors.New("cannot unmarshal credentials") if got := FormatCLIError(err); got != err.Error() { t.Fatalf("got %q, want %q", got, err.Error()) @@ -38,8 +38,8 @@ func TestFormatCLIError_Nil(t *testing.T) { } } -func TestFormatCLIError_WrappedAPIErrorIncludesTraceAndDescription(t *testing.T) { - apiErr := &primeerrors.APIError{ +func TestFormatCLIError_WrappedApiErrorIncludesTraceAndDescription(t *testing.T) { + apiErr := &primeerrors.ApiError{ Response: primeerrors.Response{ Code: primeerrors.ErrorCodeValidationError, Message: "invalid entity",