diff --git a/cobo_cli/cli.py b/cobo_cli/cli.py index 2ed937b..a0f9ba4 100644 --- a/cobo_cli/cli.py +++ b/cobo_cli/cli.py @@ -21,6 +21,7 @@ open, post_api, put_api, + skill, webhook, ) from cobo_cli.data.auth_methods import AuthMethodType @@ -132,6 +133,7 @@ def version(): cli.add_command(env) cli.add_command(logs) cli.add_command(auth) +cli.add_command(skill) cli.add_command(webhook) # Add API commands diff --git a/cobo_cli/commands/__init__.py b/cobo_cli/commands/__init__.py index 971f398..50eeea3 100644 --- a/cobo_cli/commands/__init__.py +++ b/cobo_cli/commands/__init__.py @@ -13,6 +13,7 @@ from .open import open from .post import post_api from .put import put_api +from .skill import skill from .webhook import webhook __all__ = [ @@ -31,5 +32,6 @@ "auth", "logs", "graphql", + "skill", "webhook", ] diff --git a/cobo_cli/commands/config.py b/cobo_cli/commands/config.py index 6a8b076..5e672d8 100644 --- a/cobo_cli/commands/config.py +++ b/cobo_cli/commands/config.py @@ -1,4 +1,5 @@ import os +import shlex import click @@ -85,5 +86,60 @@ def show_config_path(ctx: click.Context): click.echo(f"Configuration file path: {absolute_path}") +# (env_var_name, settings_attr) — single source of truth for export list +ENV_EXPORT = [ + ("COBO_ENV", "environment"), + ("COBO_API_SECRET", "api_secret"), + ("COBO_API_KEY", "api_key"), + ("COBO_API_HOST", "api_host"), +] + + +def _format_shell(name: str, value: str) -> str: + return f"export {name}={shlex.quote(value)}" + + +def _format_powershell(name: str, value: str) -> str: + safe = str(value).replace("'", "''") + return f"$env:{name} = '{safe}'" + + +def _format_cmd(name: str, value: str) -> str: + escaped = str(value).replace("^", "^^").replace('"', '^"') + return f'set "{name}={escaped}"' + + +_ENV_FORMATTERS = { + "shell": _format_shell, + "powershell": _format_powershell, + "cmd": _format_cmd, +} + + +@config.command("env") +@click.option( + "--format", + "fmt", + type=click.Choice(["shell", "powershell", "cmd"]), + default="shell", + help="Output format: shell (bash/zsh), powershell, or cmd (Windows CMD).", +) +@click.pass_context +def config_env(ctx: click.Context, fmt: str): + """Print env vars from current config for use in your shell. + + macOS/Linux (bash/zsh): eval $(cobo config env) + Windows PowerShell: cobo config env --format powershell | Invoke-Expression + Windows CMD: cobo config env --format cmd > env.bat && env.bat + """ + command_context: CommandContext = ctx.obj + config_manager = command_context.config_manager + formatter = _ENV_FORMATTERS[fmt] + for name, attr in ENV_EXPORT: + value = getattr(config_manager.settings, attr, None) + if value is not None: + click.echo(formatter(name, value)) + + if __name__ == "__main__": config() diff --git a/cobo_cli/commands/keys.py b/cobo_cli/commands/keys.py index afbf7fb..3ee82a5 100644 --- a/cobo_cli/commands/keys.py +++ b/cobo_cli/commands/keys.py @@ -7,8 +7,11 @@ from dotenv import get_key, load_dotenv, set_key # Import dotenv functions from nacl.signing import SigningKey +from cobo_cli.data.auth_methods import AuthMethodType from cobo_cli.data.context import CommandContext +from cobo_cli.data.environments import EnvironmentType from cobo_cli.data.manifest import Manifest +from cobo_cli.utils.api import make_request logger = logging.getLogger(__name__) @@ -195,5 +198,57 @@ def handle_app_key_generation(pubkey: str, secret: str, force: bool, file: str = set_key(dotenv_path, "APP_SECRET", secret, quote_mode="never") +@keys.command( + "register", help="Register a dev API key to Cobo platform (dev environment only)." +) +@click.option( + "--pubkey", + type=str, + help="Public key to register. Defaults to api_key in config.", +) +@click.pass_context +def register_key(ctx: click.Context, pubkey: str): + """Register an API public key to Cobo platform.""" + command_context: CommandContext = ctx.obj + + if ( + command_context.env != EnvironmentType.DEVELOPMENT + and command_context.env != EnvironmentType.SANDBOX + ): + raise ClickException( + "This command can only be used in the dev environment. " + "Use 'cobo env set dev' to switch." + ) + + if not pubkey: + pubkey = command_context.config_manager.get_config("api_key") + + if not pubkey: + raise ClickException("No public key provided or found in config.") + + response = make_request( + ctx, + "POST", + "/developers/cli_dev_api_key", + auth=AuthMethodType.USER, + json={"api_key": pubkey}, + ) + + if response.status_code != 200 and response.status_code != 201: + try: + error_data = response.json() + error_msg = ( + error_data.get("error_message") + or error_data.get("message") + or response.text + ) + except Exception: + error_msg = response.text + raise ClickException(f"Failed to register API key: {error_msg}") + + click.echo("API key registered successfully.") + click.echo(f"Public key: {pubkey}") + + if __name__ == "__main__": keys() diff --git a/cobo_cli/commands/skill.py b/cobo_cli/commands/skill.py new file mode 100644 index 0000000..c3972e0 --- /dev/null +++ b/cobo_cli/commands/skill.py @@ -0,0 +1,211 @@ +import shutil +from pathlib import Path +from typing import Dict + +import click + +# Supported AI coding agents and their skill directory names +AGENT_SKILL_DIRS: Dict[str, str] = { + "claude": ".claude", + "cursor": ".cursor", +} + +AGENT_DISPLAY_NAMES: Dict[str, str] = { + "claude": "Claude Code", + "cursor": "Cursor", +} + +SKILL_NAME = "cobo-waas" + + +def _get_skill_path(agent: str, scope: str) -> Path: + """Get the skill installation path for an agent and scope.""" + agent_dir = AGENT_SKILL_DIRS[agent] + if scope == "global": + return Path.home() / agent_dir / "skills" / SKILL_NAME + else: # local + return Path.cwd() / agent_dir / "skills" / SKILL_NAME + + +def _get_all_skill_paths(agent: str) -> Dict[str, Path]: + """Get both global and local paths for an agent.""" + return { + "global": _get_skill_path(agent, "global"), + "local": _get_skill_path(agent, "local"), + } + + +@click.group( + "skill", + invoke_without_command=True, + context_settings=dict(help_option_names=["-h", "--help"]), + help="Install and manage AI coding agent skills.", +) +@click.pass_context +def skill(ctx: click.Context): + """Install and manage AI coding agent skills.""" + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + + +@skill.command("install") +@click.argument("agent", type=click.Choice(["claude", "cursor", "all"])) +@click.option( + "-s", + "--scope", + type=click.Choice(["global", "local"]), + default="global", + help="Installation scope: 'global' (~/.claude/skills) or 'local' (./.claude/skills in current project)", +) +@click.option("-f", "--force", is_flag=True, help="Overwrite existing skill") +def install(agent: str, scope: str, force: bool): + """Install cobo-waas skill for an AI coding agent. + + AGENT can be: claude, cursor, or all + + \b + Examples: + cobo skill install claude # Install globally for Claude Code + cobo skill install claude --scope local # Install in current project + cobo skill install all # Install globally for all agents + """ + skill_src = _get_package_skill_path() + + if agent == "all": + agents = list(AGENT_SKILL_DIRS.keys()) + else: + agents = [agent] + + for agent_name in agents: + target = _get_skill_path(agent_name, scope) + display_name = AGENT_DISPLAY_NAMES.get(agent_name, agent_name) + scope_label = "global" if scope == "global" else "project" + + if target.exists() and not force: + click.echo(f"Skill already exists at {target}. Use --force to overwrite.") + continue + + # Create parent directory if needed + target.parent.mkdir(parents=True, exist_ok=True) + + # Remove existing skill if force is set + if target.exists(): + shutil.rmtree(target) + + # Copy skill files + shutil.copytree(skill_src, target) + click.echo(f"Installed {SKILL_NAME} ({scope_label}) for {display_name}") + click.echo(f" Path: {target}") + + +@skill.command("list") +def list_skills(): + """List available skills.""" + click.echo("Available skills:") + click.echo(f" {SKILL_NAME} - Cobo WaaS 2.0 API operations for AI coding agents") + click.echo("") + click.echo("Supported agents: claude, cursor") + click.echo("") + click.echo("Usage:") + click.echo(" cobo skill install claude # Install globally") + click.echo(" cobo skill install claude --scope local # Install in project") + click.echo(" cobo skill install all # Install for all agents") + + +@skill.command("remove") +@click.argument("agent", type=click.Choice(["claude", "cursor", "all"])) +@click.option( + "-s", + "--scope", + type=click.Choice(["global", "local", "all"]), + default="all", + help="Which installation to remove: 'global', 'local', or 'all' (default)", +) +def remove(agent: str, scope: str): + """Remove installed skill from an AI coding agent. + + AGENT can be: claude, cursor, or all + + \b + Examples: + cobo skill remove claude # Remove all installations for Claude + cobo skill remove claude --scope global # Remove only global installation + cobo skill remove all # Remove from all agents + """ + if agent == "all": + agents = list(AGENT_SKILL_DIRS.keys()) + else: + agents = [agent] + + scopes_to_check = ["global", "local"] if scope == "all" else [scope] + + for agent_name in agents: + display_name = AGENT_DISPLAY_NAMES.get(agent_name, agent_name) + removed_any = False + + for s in scopes_to_check: + target = _get_skill_path(agent_name, s) + if target.exists(): + shutil.rmtree(target) + click.echo(f"Removed {SKILL_NAME} ({s}) from {display_name}") + removed_any = True + + if not removed_any: + click.echo(f"No skill installed for {display_name}") + + +@skill.command("status") +def status(): + """Show skill installation status.""" + click.echo("Skill installation status:") + click.echo("") + + for agent_name in AGENT_SKILL_DIRS.keys(): + display_name = AGENT_DISPLAY_NAMES.get(agent_name, agent_name) + paths = _get_all_skill_paths(agent_name) + + global_installed = paths["global"].exists() + local_installed = paths["local"].exists() + + click.echo(f" {display_name}:") + + if global_installed: + click.echo(" Global: Installed") + click.echo(f" Path: {paths['global']}") + else: + click.echo(" Global: Not installed") + + if local_installed: + click.echo(" Local: Installed") + click.echo(f" Path: {paths['local']}") + else: + click.echo(f" Local: Not installed (would be at {paths['local']})") + + click.echo("") + + +def _get_package_skill_path() -> Path: + """Get the path to skills bundled with the package.""" + # Method 1: Development mode (skills/ in repo root) + dev_path = Path(__file__).parent.parent.parent / "skills" / SKILL_NAME + if dev_path.exists(): + return dev_path + + # Method 2: Installed package (skills/ alongside cobo_cli/) + try: + import cobo_cli + + pkg_path = Path(cobo_cli.__file__).parent.parent / "skills" / SKILL_NAME + if pkg_path.exists(): + return pkg_path + except ImportError: + pass + + raise click.ClickException( + f"Could not find {SKILL_NAME} skill in package. " + "Please reinstall cobo-cli: pip install --force-reinstall cobo-cli" + ) + + +if __name__ == "__main__": + skill() diff --git a/cobo_cli/commands/tests/testenv/README.md b/cobo_cli/commands/tests/testenv/README.md deleted file mode 100644 index 6903c58..0000000 --- a/cobo_cli/commands/tests/testenv/README.md +++ /dev/null @@ -1,15 +0,0 @@ -Run unit test -==== - -1. 在本地运行redis服务,保证`127.0.0.1:6379`可以连接redis服务 - -2. 运行单元测试 - -``` -# 项目根目录下 -$> tests/testenv/run_unittest.sh - -# 重建tox环境 -$> tests/testenv/run_unittest.sh -r -``` - diff --git a/cobo_cli/commands/tests/testenv/__init__.py b/cobo_cli/commands/tests/testenv/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/cobo_cli/commands/tests/testenv/clean b/cobo_cli/commands/tests/testenv/clean deleted file mode 100755 index b10f101..0000000 --- a/cobo_cli/commands/tests/testenv/clean +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash - -SCRIPT=$(readlink -f "$0") -BASEDIR=$(dirname "$SCRIPT") - -cd $(dirname $BASEDIR) -rm -rf allure-results coverage.xml - diff --git a/cobo_cli/commands/tests/testenv/run_unittest.sh b/cobo_cli/commands/tests/testenv/run_unittest.sh deleted file mode 100755 index c971f43..0000000 --- a/cobo_cli/commands/tests/testenv/run_unittest.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# 'testenv' moved from ${PROJ_ROOT} to ${PROJ_ROOT}/tests - -SCRIPT=$(readlink -f "$0") -# tests/testenv -TESTENV=$(dirname "$SCRIPT") -# tests -BASEDIR=$(dirname "$TESTENV") -PROJECT_DIR=$(dirname "$BASEDIR") - -CONF=$TESTENV/tox_ut.ini -WORKDIR=$PROJECT_DIR/.tox - -ARG=$1 -OPTS="-c $CONF --workdir $WORKDIR" - -if [ "$ARG" == "-r" ]; then - OPTS="$OPTS -r" -fi - -tox $OPTS diff --git a/cobo_cli/commands/tests/testenv/settings.py b/cobo_cli/commands/tests/testenv/settings.py deleted file mode 100644 index 0168806..0000000 --- a/cobo_cli/commands/tests/testenv/settings.py +++ /dev/null @@ -1,14 +0,0 @@ -import os - -TEST_TYPES = ("inttest", "unittest") - -test_type = os.getenv("TEST_TYPE", "unittest") -if test_type not in TEST_TYPES: - test_type = "unittest" - -settings_name = test_type + "_settings" -if settings_name == "unittest_settings": - try: - from tests.testenv.unittest_settings import * # noqa - except ImportError: - pass diff --git a/cobo_cli/commands/tests/testenv/tox_ut.ini b/cobo_cli/commands/tests/testenv/tox_ut.ini deleted file mode 100644 index 3502b6d..0000000 --- a/cobo_cli/commands/tests/testenv/tox_ut.ini +++ /dev/null @@ -1,38 +0,0 @@ -[tox] -skipsdist=True -envlist = py310-ut - -[testenv] -setenv = - BUILD_TYPE=test - TEST_TYPE=unittest - RUN_ENV={env:RUN_ENV:dev} - -# Change project root dir -# testenv moved from ${proj_root} to ${proj_root}/tests -changedir = {toxinidir}/../.. - -# Suppress tox warning -# WARNING:test command found but not installed in testenv -whitelist_externals= - /usr/bin/find - /bin/rm - -allowlist_externals= - {toxinidir}/clean - -# Use aliyun pypi mirror to speedup install process -install_command = pip install -q -i https://codeartifact.1cobo.com/pypi/default/simple/ {opts} {packages} - -commands = - # tox will not install new packages when requirements changing unless "tox -r" recreate env - # run pre-commit to make things fail quick - pip3 install poetry - poetry install - pre-commit run -a - # ossaudit -f requirements.txt - {toxinidir}/clean - # Append more arguments inside "pytest" sections with "addopts" - # poetry run pytest --exitfirst - - diff --git a/cobo_cli/commands/tests/testenv/unittest_settings.py b/cobo_cli/commands/tests/testenv/unittest_settings.py deleted file mode 100644 index aaeb462..0000000 --- a/cobo_cli/commands/tests/testenv/unittest_settings.py +++ /dev/null @@ -1,47 +0,0 @@ -import os - -from django.conf import settings - -# Log - -LOGGING = settings.LOGGING - -LOGGING["loggers"] = {} -LOGGING["handlers"] = {"null": {"level": "DEBUG", "class": "logging.NullHandler"}} -LOGGING["root"] = {"level": "ERROR", "handlers": ["console"]} -LOGGING["loggers"]["django"] = { - "handlers": ["console"], - "level": "DEBUG", - "propagate": False, - "formatter": "verbose", -} - -LOGGING["handlers"]["console"] = { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "verbose", -} - -LOGGING["loggers"]["django.celery"] = {"level": "DEBUG", "handlers": ["console"]} -LOGGING["loggers"]["celery"] = {"level": "DEBUG", "handlers": ["console"]} - -COMMON_LOG_FILE = "/dev/null" -CELERY_LOG_FILE = "/dev/null" -SYNC_SERVICE_LOG_FILE = "/dev/null" -SIGNER_LOG_FILE = "/dev/null" -TXDB_LOG_FILE = "/dev/null" - -ENABLE_SQL_LOGGING = os.getenv("SQL_LOGGING", False) -if ENABLE_SQL_LOGGING in ("1", "True", "true", "Y", "y"): - ENABLE_SQL_LOGGING = True -elif ENABLE_SQL_LOGGING in ("0", "False", "false", "N", "n"): - ENABLE_SQL_LOGGING = False -else: - ENABLE_SQL_LOGGING = False - -if not ENABLE_SQL_LOGGING: - LOGGING["loggers"]["django.db.backends"] = { - "handlers": ["null"], - "propagate": False, - "level": "DEBUG", - } diff --git a/pyproject.toml b/pyproject.toml index 15590b5..c08366f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,14 @@ [tool.poetry] name = "cobo-cli" -version = "0.1.0" +version = "0.1.3" description = "Cobo Command-line Tool" authors = ["Cobo "] license = "MIT" readme = "README.md" packages = [{ include = "cobo_cli" }] +include = [ + { path = "skills", format = ["sdist", "wheel"] } +] [tool.poetry.dependencies] python = "^3.9" diff --git a/skills/cobo-waas/SKILL.md b/skills/cobo-waas/SKILL.md new file mode 100644 index 0000000..6f0f59d --- /dev/null +++ b/skills/cobo-waas/SKILL.md @@ -0,0 +1,315 @@ +--- +name: cobo-waas +description: Use when interacting with Cobo WaaS 2.0 or Cobo Portal via cobo-cli. Optimized for agentic coding tools (Claude Code, Codex, Cursor, Gemini CLI) with CLI-first workflows for config, keys, wallets, transfers, logs, and GraphQL. +--- + +# Cobo WaaS Skill + +This skill uses `cobo-cli` to perform WaaS 2.0 and Portal tasks (wallets, transfers, logs). It is self-contained for agentic coding tools (Claude Code, Codex, Cursor, Gemini CLI). + +## Quick Start + +Fastest path to your first API call: + +```bash +# 1. Verify installation +cobo version # If fails: pip install cobo-cli + +# 2. Set environment +cobo env dev # Use dev for testing + +# 3. Generate API keys +cobo keys generate --key-type API # Save the output! + +# 4. Register key in Portal +cobo open developer # Add the PUBLIC key as API key + +# 5. Set auth method +cobo auth apikey + +# 6. Test it works +cobo get /wallets --limit 1 # Should return JSON +``` + +**Run SDK samples (Python/Node/Go/Java):** load env from config once, then run your script in the same shell: + +- **macOS / Linux (bash, zsh):** `eval $(cobo config env)` +- **Windows PowerShell:** `cobo config env --format powershell | Invoke-Expression` +- **Windows CMD:** `cobo config env --format cmd > env.bat && env.bat` + +Then e.g. `python your_script.py` or `node app.js`. + +## When to Use + +- The user wants to interact with Cobo WaaS 2.0 or Portal using the CLI +- The task needs a CLI-only path instead of MCP +- The task includes configuring env/auth, generating keys, wallet operations, transfers, logs, or GraphQL queries +- The user wants to generate SDK code for Python, Node.js, Go, or Java + +## State Detection + +Before starting, determine the current setup state: + +```bash +# Is CLI installed? +cobo version + +# Is it configured? +cobo config list + +# Does auth work? +cobo get /wallets --limit 1 +``` + +| Result | State | Next Step | +|--------|-------|-----------| +| `command not found` | CLI not installed | `pip install cobo-cli` | +| Config empty | Not configured | Run Quick Start | +| 401 error | Keys not registered | `cobo keys register` (dev/sandbox) or register in Portal | +| error_code 2024 | Unauthorized | Perform authorization operations | +| JSON response | Ready | Proceed with task | + +For detailed diagnostics, see `references/diagnostics.md`. + +## Before Generating API Requests (reduce errors) + +**Always look up the endpoint’s parameters before generating CLI or SDK code.** + +1. **Identify the API path** from the user’s intent (e.g. “list wallets” → `GET /wallets`, “create wallet” → `POST /wallets`). +2. **Look up parameters:** run `cobo doc ` (e.g. `cobo doc /wallets`) and read the printed operation description (parameters, types, required/optional). +3. **Then generate** the exact `cobo get/post ...` or SDK call using those parameter names and types. + +Example: before writing `cobo get /wallets ...` or SDK code for listing wallets, run `cobo doc /wallets` and use the output to build the correct query (e.g. `limit`, `cursor`). This reduces wrong parameter names, wrong types, and missing required fields. + +## Reference Navigation + +Choose the right reference for your task: + +| Task | Reference File | +|------|---------------| +| First-time setup | `references/onboarding.md` | +| Check current state | `references/diagnostics.md` | +| Handle credentials safely | `references/security.md` | +| Common API endpoints | `references/api-quickref.md` | +| Understand terminology | `references/concepts.md` | +| End-to-end workflows | `references/recipes.md` | +| Fix errors | `references/errors.md`, `references/troubleshooting.md` | +| CLI command syntax | `references/cli.md` | +| Auth setup details | `references/auth.md` | +| GraphQL queries | `references/graphql.md` | +| Generate SDK code | `references/sdk-patterns.md`, `references/samples/*` | + +## SDK / generated code rules (avoid over-engineering) + +Applies to **all languages** (Python, Node.js, Go, Java). When generating SDK or script code, **follow the template for that language exactly**; do not add extra structure the user did not ask for. + +1. **Use the template as the single source of style.** For each language the canonical style is in `assets/templates/` (`python.md`, `ts-node.md`, `go.md`, `java.md`). Copy the **minimal** setup and one short block per operation (e.g. list wallets = a few lines: call the API, then a short loop or print). Same requirement for Python, Node/TS, Go, and Java. +2. **Do not add** unless the user explicitly asks (in any language): + - Wrapper/helper functions that hide the API (e.g. `getClient()`, `listWallets(api, ...)`) + - Every optional parameter in signatures — only pass what the task needs (e.g. `limit=10`) + - Converting responses to custom shapes (e.g. plain dict/JSON, custom DTOs) + - Long module/function docstrings, usage instructions in comments, or heavy error handling +3. **Match the template snippet length.** In every language, template examples are a few lines per operation. Generated code should be the same size unless the user asks for more. +4. **When in doubt, generate less.** Prefer the minimal version from the template for that language; the user can ask for more structure or error handling if needed. + +## Use-case Oriented Workflow + +Pick the smallest slice that solves the task: + +1. **Configure environment and auth** - `references/onboarding.md` +2. **Generate and validate keypair** - `references/auth.md` +3. **Discover and describe API endpoints** - Run `cobo doc ` (e.g. `cobo doc /wallets`) for parameter details, then `references/cli.md` +4. **Create wallets and addresses** - `references/recipes.md` Recipe 2-3 +5. **Create transfers and check status** - `references/recipes.md` Recipe 4-5 +6. **Debug failures** - `references/errors.md`, `references/troubleshooting.md` +7. **Set up webhooks** - `references/recipes.md` Recipe 7 +8. **Run GraphQL queries** - `references/graphql.md` +9. **Generate SDK code** - `references/sdk-patterns.md`, `references/samples/*` + +## Output Format for Agents + +After each action, return a short JSON object: + +### Success Response + +```json +{ + "action": "create_wallet", + "status": "ok", + "ids": { + "wallet_id": "f47ac10b-58cc-..." + }, + "links": { + "portal_wallet_url": "https://portal.cobo.com/wallets/f47ac10b-..." + }, + "next_step": "Create address with: cobo post /wallets/{wallet_id}/addresses --chain_id ETH", + "notes": "Custodial Asset wallet created successfully" +} +``` + +### Error Response + +```json +{ + "action": "create_wallet", + "status": "error", + "error": { + "code": 401, + "message": "Unauthorized" + }, + "diagnosis": "API key not registered in Portal", + "recovery": "Register public key: cobo keys register (dev/sandbox) or cobo open developer (prod)", + "reference": "references/errors.md#401-unauthorized" +} +``` + +### Error Code Response Format + +API responses may include both HTTP status codes and business error codes (`error_code`). They serve different purposes: + +- **HTTP Status Code**: Indicates the HTTP-level result (200=success, 4xx=client error, 5xx=server error) +- **Business Error Code (`error_code`)**: Provides specific business logic error details in the response body + +Example error response with both: +```json +{ + "error_code": 2024, + "error_message": "Unauthorized", + "error_id": "48d1f0c3382f4c50a11ff529a2c3a65a" +} +``` +HTTP status: 401 Unauthorized + +## Recovery Patterns + +When operations fail, follow this pattern: + +1. **Identify the error** - Check HTTP status code, `error_code` in response body, and message +2. **Diagnose** - Run relevant diagnostic commands +3. **Consult reference** - Check `references/errors.md` or `references/troubleshooting.md` +4. **Apply fix** - Follow the recovery steps based on error type +5. **Verify** - Re-run the original operation + +### HTTP Status Code Recovery + +| HTTP Status | Description | Quick Fix | +|-------------|-------------|-----------| +| 200 | Success | No action needed | +| 400 | Bad Request | Check request parameters: `cobo post /path --describe` | +| 401 | Unauthorized | Check API Key, API signature, or timestamp. Generate keys: `cobo keys generate --key-type API` + register: `cobo keys register` (dev/sandbox) or Portal | +| 403 | Forbidden | Check key permissions in Portal: `cobo open developer` | +| 404 | Not Found | Verify endpoint: `cobo get --list` | +| 405 | Method Not Allowed | Use supported HTTP method (GET, POST, etc.) | +| 406 | Not Acceptable | Ensure request content format is JSON | +| 429 | Too Many Requests | Reduce request frequency and retry later | +| 500 | Internal Server Error | Check server configuration, including Org Access Tokens expiration, then retry | +| 502 | Bad Gateway | Check connection and retry later | +| 503 | Service Unavailable | Retry later | + +### Business Error Code Recovery + +When response includes `error_code` field, use specific recovery steps: + +| Error Code | Description | Solution | +|------------|-------------|----------| +| 1000 | Internal server error (may include Org Access Tokens expired) | Check server configuration, verify Org Access Tokens, retry later | +| 1003, 2003 | Missing required parameters | Provide all required parameters | +| 1006, 2006 | Invalid parameter format or unsupported values | Provide valid parameters in expected format | +| 2000 | Internal error during processing | Retry later | +| 2002 | Unsupported HTTP method | Use supported HTTP method | +| 2010 | Rate limit exceeded | Retry later | +| 2021 | Request handler missing or not implemented | Provide valid handler for request | +| 2022 | Missing required request headers | Include all required request headers | +| 2023 | API signature missing or invalid | Verify API signature is correct. Reference signature calculation guide | +| 2024 | API Key authentication failed | 1. Use API Key matching current environment (dev/prod)
2. Ensure API Key is registered and active
3. If permanent API Key, ensure request from whitelisted IP
Register: `cobo keys register` (dev/sandbox) or `cobo open developer` (prod) | +| 2025, 4001 | Forbidden access to requested resource | Check permissions associated with API Key. Reference permissions guide | +| 2026 | Too many requests | Retry later | +| 2028 | Requested resource not found | Check request URL | +| 2029 | Invalid status attribute provided | Provide valid value for status attribute | +| 2040 | Resource with same key already exists | Use unique key | +| 2050, 2052 | No available plan or usage limit exceeded | Purchase plan or upgrade existing plan | +| 2051 | Current plan expired | Renew plan to continue service | +| 30001, 12009 | Duplicate request ID | Use unique request ID: `tx-$(date +%s)-$(openssl rand -hex 4)` | +| 30007 | Invalid amount (not valid number or wrong format/range) | Provide valid amount in expected format and range | +| 30008 | Invalid absolute amount (too small/large or zero when non-zero required) | Ensure absolute amount meets required conditions | +| 30010 | Amount below dust threshold | Increase amount to exceed dust threshold | +| 30011 | Amount below minimum deposit threshold | Increase deposit amount to meet minimum threshold | +| 30012, 12007 | Insufficient balance for requested operation | Ensure source address has sufficient balance for transfer amount | +| 30013 | Insufficient balance to pay transaction fees | Ensure source address has sufficient balance for transaction fees | +| 30014 | Invalid destination address | Provide valid destination address | +| 30023 | Invalid trading account type (Exchange wallets only) | Provide valid trading account type | +| 30032 | Invalid private key share holder group (MPC wallets only) | Check if valid master or signing group is configured | +| 4001 | Forbidden access to requested resource | Check permissions associated with API Key | +| 60010 | Specified token not enabled for this team | Enable token for your team | +| 12002 | Cobo does not support specified token | Select supported token. Call List supported tokens endpoint | +| 12025 | Invalid UTXO specified in included_utxos or excluded_utxos | Verify UTXOs specified in included_utxos or excluded_utxos | + +### Common Error Code Combinations + +Some errors may return multiple error codes indicating the same issue: +- **1003, 2003**: Missing required parameters +- **1006, 2006**: Invalid parameter format +- **12007, 30012**: Insufficient balance +- **12009, 30001**: Duplicate request ID +- **2025, 4001**: Forbidden access +- **2050, 2052**: Plan limit issues + +## SDK Bridge + +When to transition from CLI to SDK: + +| Scenario | Recommendation | +|----------|----------------| +| One-off operations | CLI | +| Quick exploration | CLI | +| Production automation | SDK | +| Complex workflows | SDK | +| Error handling needed | SDK | + +To generate SDK code: +1. Consult `references/sdk-patterns.md` for patterns +2. Use `references/samples/{python,ts-node,go,java}.md` for complete examples + +## Guardrails + +**Security:** +- Never echo secrets or private keys in output +- Redact `api_secret` values: show as `[REDACTED]` +- Check `references/security.md` for credential handling rules + +**Environment:** +- Always confirm the target environment (sandbox, dev, prod) +- Production affects real assets - verify before operations +- Prefer dev/sandbox for testing + +**Idempotency:** +- Use unique `request_id` for transactions: `tx-$(date +%s)-$(openssl rand -hex 4)` +- Never reuse request_ids to avoid duplicate transactions + +**Error Handling:** +- Always check both HTTP status code and `error_code` in response body +- When encountering `error_code: 2024` or HTTP 401, perform authorization operations +- Provide specific recovery steps based on error code (see Recovery Patterns section) +- For rate limiting (429, 2010, 2026), implement exponential backoff retry logic +- For duplicate request ID errors (30001, 12009), generate new unique request_id and retry + +## References + +| Reference | Description | +|-----------|-------------| +| `references/onboarding.md` | First-time setup guide with decision tree | +| `references/diagnostics.md` | State inspection and quick checks | +| `references/security.md` | Credential handling guardrails | +| `references/api-quickref.md` | Common endpoint quick reference | +| `references/concepts.md` | Glossary and terminology | +| `references/recipes.md` | End-to-end workflow recipes | +| `references/troubleshooting.md` | Decision tree diagnostics | +| `references/errors.md` | Comprehensive error catalog | +| `references/cli.md` | CLI command patterns | +| `references/auth.md` | Auth and key setup | +| `references/graphql.md` | GraphQL usage | +| `references/sdk-patterns.md` | SDK vibe coding guide | +| `references/samples/python.md` | Python SDK examples | +| `references/samples/ts-node.md` | Node.js/TypeScript SDK examples | +| `references/samples/go.md` | Go SDK examples | +| `references/samples/java.md` | Java SDK examples | diff --git a/skills/cobo-waas/assets/templates/go.md b/skills/cobo-waas/assets/templates/go.md new file mode 100644 index 0000000..ae822cb --- /dev/null +++ b/skills/cobo-waas/assets/templates/go.md @@ -0,0 +1,593 @@ +# Go SDK Sample (WaaS 2.0) + +Complete Go SDK examples for Cobo WaaS 2.0 operations. + +## Installation + +```bash +go get github.com/CoboGlobal/cobo-waas2-go-sdk +``` + +## go.mod + +```go +module example.com/cobo-waas-example + +go 1.22 + +require github.com/CoboGlobal/cobo-waas2-go-sdk v1.0.0 +``` + +## Environment (all languages) + +- **macOS / Linux:** `eval $(cobo config env)` +- **Windows PowerShell:** `cobo config env --format powershell | Invoke-Expression` +- **Windows CMD:** `cobo config env --format cmd > env.bat && env.bat` + +## Configuration from Environment + +```go +package main + +import ( + "context" + "fmt" + "os" + + cobo_waas2 "github.com/CoboGlobal/cobo-waas2-go-sdk/cobo_waas2" + "github.com/CoboGlobal/cobo-waas2-go-sdk/cobo_waas2/crypto" +) + +func getContext() context.Context { + // Read from environment variables (recommended) + apiSecret := os.Getenv("COBO_API_SECRET") + if apiSecret == "" { + panic("COBO_API_SECRET environment variable not set") + } + + env := os.Getenv("COBO_ENV") + if env == "" { + env = "dev" + } + + // Set environment + var coboEnv cobo_waas2.Env + switch env { + case "prod": + coboEnv = cobo_waas2.ProdEnv + case "sandbox": + coboEnv = cobo_waas2.SandboxEnv + default: + coboEnv = cobo_waas2.DevEnv + } + + ctx := context.Background() + ctx = context.WithValue(ctx, cobo_waas2.ContextEnv, coboEnv) + ctx = context.WithValue(ctx, cobo_waas2.ContextPortalSigner, crypto.Ed25519Signer(apiSecret)) + + return ctx +} + +func getClient() *cobo_waas2.APIClient { + cfg := cobo_waas2.NewConfiguration() + return cobo_waas2.NewAPIClient(cfg) +} +``` + +## List Wallets + +```go +func listWallets(ctx context.Context, client *cobo_waas2.APIClient) { + // List all wallets + resp, httpResp, err := client.WalletsAPI.ListWallets(ctx). + Limit(10). + Execute() + + if err != nil { + fmt.Printf("Error: %v\n", err) + fmt.Printf("HTTP Response: %v\n", httpResp) + return + } + + fmt.Printf("Found %d wallets\n", len(resp.Data)) + for _, wallet := range resp.Data { + fmt.Printf(" %s: %s (%s)\n", + wallet.GetWalletId(), + wallet.GetName(), + wallet.GetWalletType()) + } +} + +// Filter by wallet type +func listCustodialWallets(ctx context.Context, client *cobo_waas2.APIClient) { + walletType := cobo_waas2.WALLETTYPE_CUSTODIAL + + resp, _, err := client.WalletsAPI.ListWallets(ctx). + WalletType(walletType). + Limit(50). + Execute() + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + for _, wallet := range resp.Data { + fmt.Printf("%s: %s\n", wallet.GetWalletId(), wallet.GetName()) + } +} + +// Pagination +func listAllWallets(ctx context.Context, client *cobo_waas2.APIClient) []cobo_waas2.WalletInfo { + var allWallets []cobo_waas2.WalletInfo + var after string + + for { + req := client.WalletsAPI.ListWallets(ctx).Limit(100) + if after != "" { + req = req.After(after) + } + + resp, _, err := req.Execute() + if err != nil { + break + } + + allWallets = append(allWallets, resp.Data...) + + if resp.Pagination == nil || resp.Pagination.After == nil { + break + } + after = *resp.Pagination.After + } + + return allWallets +} +``` + +## Create Custodial Wallet + +```go +func createCustodialWallet(ctx context.Context, client *cobo_waas2.APIClient, name string) (*cobo_waas2.CreatedWalletInfo, error) { + walletType := cobo_waas2.WALLETTYPE_CUSTODIAL + walletSubtype := cobo_waas2.WALLETSUBTYPE_ASSET + + params := cobo_waas2.CreateWalletParams{ + CreateCustodialWalletParams: &cobo_waas2.CreateCustodialWalletParams{ + Name: name, + WalletType: walletType, + WalletSubtype: walletSubtype, + }, + } + + wallet, httpResp, err := client.WalletsAPI.CreateWallet(ctx). + CreateWalletParams(params). + Execute() + + if err != nil { + fmt.Printf("Error: %v\n", err) + fmt.Printf("HTTP Response: %v\n", httpResp) + return nil, err + } + + fmt.Printf("Created wallet: %s\n", wallet.GetWalletId()) + fmt.Printf(" Name: %s\n", wallet.GetName()) + fmt.Printf(" Type: %s/%s\n", wallet.GetWalletType(), wallet.GetWalletSubtype()) + + return wallet, nil +} +``` + +## Create MPC Org-Controlled Wallet + +```go +func createMpcWallet(ctx context.Context, client *cobo_waas2.APIClient, name string, vaultId string) (*cobo_waas2.CreatedWalletInfo, error) { + walletType := cobo_waas2.WALLETTYPE_MPC + walletSubtype := cobo_waas2.WALLETSUBTYPE_ORG_CONTROLLED + + params := cobo_waas2.CreateWalletParams{ + CreateMpcWalletParams: &cobo_waas2.CreateMpcWalletParams{ + Name: name, + WalletType: walletType, + WalletSubtype: walletSubtype, + VaultId: vaultId, + }, + } + + wallet, _, err := client.WalletsAPI.CreateWallet(ctx). + CreateWalletParams(params). + Execute() + + if err != nil { + return nil, err + } + + fmt.Printf("Created MPC wallet: %s\n", wallet.GetWalletId()) + return wallet, nil +} + +// Get vault_id first +func getVaultId(ctx context.Context, client *cobo_waas2.APIClient) (string, error) { + resp, _, err := client.WalletsAPI.ListMpcVaults(ctx).Execute() + if err != nil { + return "", err + } + + if len(resp.Data) == 0 { + return "", fmt.Errorf("no vaults found") + } + + return resp.Data[0].GetVaultId(), nil +} +``` + +## Create Address + +```go +func createAddress(ctx context.Context, client *cobo_waas2.APIClient, walletId string, chainId string) (*cobo_waas2.AddressInfo, error) { + // Get supported chains first + chains, _, err := client.WalletsAPI.ListSupportedChains(ctx, walletId).Execute() + if err != nil { + return nil, err + } + + fmt.Printf("Supported chains: ") + for _, chain := range chains.Data { + fmt.Printf("%s ", chain.GetChainId()) + } + fmt.Println() + + // Create address + params := cobo_waas2.CreateAddressRequest{ + ChainId: chainId, + } + + address, _, err := client.WalletsAPI.CreateAddress(ctx, walletId). + CreateAddressRequest(params). + Execute() + + if err != nil { + return nil, err + } + + fmt.Printf("Created address: %s\n", address.GetAddress()) + fmt.Printf(" Chain: %s\n", address.GetChainId()) + fmt.Printf(" Address ID: %s\n", address.GetAddressId()) + + return address, nil +} +``` + +## Create Transfer + +```go +import ( + "fmt" + "time" + "crypto/rand" + "encoding/hex" +) + +func generateRequestId() string { + b := make([]byte, 4) + rand.Read(b) + return fmt.Sprintf("tx-%d-%s", time.Now().Unix(), hex.EncodeToString(b)) +} + +func createTransfer( + ctx context.Context, + client *cobo_waas2.APIClient, + sourceWalletId string, + destinationAddress string, + tokenId string, + amount string, +) (*cobo_waas2.TransferResult, error) { + // Validate destination first + validation, _, err := client.WalletsAPI.CheckAddressValidity(ctx). + ChainId(tokenId). + Address(destinationAddress). + Execute() + + if err != nil { + return nil, err + } + + if !validation.GetIsValid() { + return nil, fmt.Errorf("invalid address: %s", validation.GetMessage()) + } + + // Create unique request_id + requestId := generateRequestId() + + // Build transfer params + source := cobo_waas2.TransferSource{ + WalletTransferSource: &cobo_waas2.WalletTransferSource{ + SourceType: cobo_waas2.TRANSFERSOURCETYPE_WALLET, + WalletId: sourceWalletId, + }, + } + + destination := cobo_waas2.TransferDestination{ + AddressTransferDestination: &cobo_waas2.AddressTransferDestination{ + DestinationType: cobo_waas2.TRANSFERDESTINATIONTYPE_ADDRESS, + Address: destinationAddress, + Amount: amount, + }, + } + + params := cobo_waas2.TransferParams{ + RequestId: requestId, + Source: source, + TokenId: tokenId, + Destination: destination, + } + + transaction, _, err := client.TransactionsAPI.CreateTransfer(ctx). + TransferParams(params). + Execute() + + if err != nil { + return nil, err + } + + fmt.Printf("Created transaction: %s\n", transaction.GetTransactionId()) + fmt.Printf(" Status: %s\n", transaction.GetStatus()) + fmt.Printf(" Request ID: %s\n", transaction.GetRequestId()) + + return transaction, nil +} +``` + +## Check Transaction Status + +```go +func getTransactionStatus(ctx context.Context, client *cobo_waas2.APIClient, transactionId string) (*cobo_waas2.TransactionDetail, error) { + tx, _, err := client.TransactionsAPI.GetTransaction(ctx, transactionId).Execute() + if err != nil { + return nil, err + } + + fmt.Printf("Transaction %s\n", tx.GetTransactionId()) + fmt.Printf(" Status: %s\n", tx.GetStatus()) + fmt.Printf(" Token: %s\n", tx.GetTokenId()) + fmt.Printf(" Created: %d\n", tx.GetCreatedTimestamp()) + + return tx, nil +} + +// Poll until complete +func waitForCompletion( + ctx context.Context, + client *cobo_waas2.APIClient, + transactionId string, + timeout time.Duration, + interval time.Duration, +) (*cobo_waas2.TransactionDetail, error) { + terminalStates := map[cobo_waas2.TransactionStatus]bool{ + cobo_waas2.TRANSACTIONSTATUS_COMPLETED: true, + cobo_waas2.TRANSACTIONSTATUS_FAILED: true, + cobo_waas2.TRANSACTIONSTATUS_REJECTED: true, + cobo_waas2.TRANSACTIONSTATUS_CANCELLED: true, + } + + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + tx, _, err := client.TransactionsAPI.GetTransaction(ctx, transactionId).Execute() + if err != nil { + return nil, err + } + + fmt.Printf(" Status: %s\n", tx.GetStatus()) + + if terminalStates[tx.GetStatus()] { + return tx, nil + } + + time.Sleep(interval) + } + + return nil, fmt.Errorf("transaction %s did not complete in %v", transactionId, timeout) +} +``` + +## Error Handling + +```go +import ( + "net/http" + "time" +) + +func safeApiCall[T any]( + operation func() (T, *http.Response, error), + maxRetries int, +) (T, error) { + var zero T + retryDelay := time.Second + + for attempt := 0; attempt < maxRetries; attempt++ { + result, httpResp, err := operation() + + if err == nil { + return result, nil + } + + // Extract status code + statusCode := 0 + if httpResp != nil { + statusCode = httpResp.StatusCode + } + + fmt.Printf("API Error %d: %v\n", statusCode, err) + + switch statusCode { + case 401: + return zero, fmt.Errorf("authentication failed - check COBO_API_SECRET") + case 403: + return zero, fmt.Errorf("permission denied - check API key permissions") + case 404: + return zero, fmt.Errorf("resource not found") + case 429: + // Rate limited - wait and retry + waitTime := retryDelay * time.Duration(1<= 500 && attempt < maxRetries-1 { + time.Sleep(retryDelay) + continue + } + } + + return zero, err + } + + return zero, fmt.Errorf("failed after %d attempts", maxRetries) +} + +// Usage +func example(ctx context.Context, client *cobo_waas2.APIClient) { + wallet, err := safeApiCall(func() (*cobo_waas2.WalletInfo, *http.Response, error) { + return client.WalletsAPI.GetWalletById(ctx, "wallet_id").Execute() + }, 3) + + if err != nil { + fmt.Printf("Error: %v\n", err) + return + } + + fmt.Printf("Wallet: %s\n", wallet.GetName()) +} +``` + +## Complete Example + +```go +package main + +import ( + "context" + "fmt" + "os" + "time" + + cobo_waas2 "github.com/CoboGlobal/cobo-waas2-go-sdk/cobo_waas2" + "github.com/CoboGlobal/cobo-waas2-go-sdk/cobo_waas2/crypto" +) + +func main() { + // Setup + apiSecret := os.Getenv("COBO_API_SECRET") + if apiSecret == "" { + fmt.Println("Set COBO_API_SECRET environment variable") + os.Exit(1) + } + + env := os.Getenv("COBO_ENV") + if env == "" { + env = "dev" + } + + var coboEnv cobo_waas2.Env + switch env { + case "prod": + coboEnv = cobo_waas2.ProdEnv + default: + coboEnv = cobo_waas2.DevEnv + } + + ctx := context.Background() + ctx = context.WithValue(ctx, cobo_waas2.ContextEnv, coboEnv) + ctx = context.WithValue(ctx, cobo_waas2.ContextPortalSigner, crypto.Ed25519Signer(apiSecret)) + + cfg := cobo_waas2.NewConfiguration() + client := cobo_waas2.NewAPIClient(cfg) + + // List existing wallets + fmt.Println("Listing wallets...") + walletType := cobo_waas2.WALLETTYPE_CUSTODIAL + + resp, _, err := client.WalletsAPI.ListWallets(ctx). + WalletType(walletType). + Limit(10). + Execute() + + if err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } + + for _, w := range resp.Data { + fmt.Printf(" %s: %s\n", w.GetName(), w.GetWalletId()) + } + + // Create wallet if none exist + var wallet *cobo_waas2.WalletInfo + if len(resp.Data) == 0 { + fmt.Println("\nCreating wallet...") + + params := cobo_waas2.CreateWalletParams{ + CreateCustodialWalletParams: &cobo_waas2.CreateCustodialWalletParams{ + Name: fmt.Sprintf("Test Wallet %d", time.Now().Unix()), + WalletType: cobo_waas2.WALLETTYPE_CUSTODIAL, + WalletSubtype: cobo_waas2.WALLETSUBTYPE_ASSET, + }, + } + + created, _, err := client.WalletsAPI.CreateWallet(ctx). + CreateWalletParams(params). + Execute() + + if err != nil { + fmt.Printf("Error creating wallet: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Created: %s\n", created.GetWalletId()) + // Convert CreatedWalletInfo to WalletInfo for subsequent operations + wallet = &cobo_waas2.WalletInfo{ + WalletId: created.WalletId, + Name: created.Name, + } + } else { + wallet = &resp.Data[0] + } + + // Create address + fmt.Printf("\nCreating address for wallet %s...\n", wallet.GetWalletId()) + + addressParams := cobo_waas2.CreateAddressRequest{ + ChainId: "ETH", + } + + address, _, err := client.WalletsAPI.CreateAddress(ctx, wallet.GetWalletId()). + CreateAddressRequest(addressParams). + Execute() + + if err != nil { + fmt.Println("Address may already exist, listing...") + + addresses, _, _ := client.WalletsAPI.ListAddresses(ctx, wallet.GetWalletId()).Execute() + for _, addr := range addresses.Data { + fmt.Printf(" %s: %s\n", addr.GetChainId(), addr.GetAddress()) + } + } else { + fmt.Printf("Address: %s\n", address.GetAddress()) + } + + fmt.Println("\nDone!") +} +``` + +## Environment Setup + +```bash +# Set environment variables +export COBO_API_SECRET="" +export COBO_ENV="dev" + +# Run +go run main.go +``` diff --git a/skills/cobo-waas/assets/templates/java.md b/skills/cobo-waas/assets/templates/java.md new file mode 100644 index 0000000..9064e0b --- /dev/null +++ b/skills/cobo-waas/assets/templates/java.md @@ -0,0 +1,635 @@ +# Java SDK Sample (WaaS 2.0) + +Complete Java SDK examples for Cobo WaaS 2.0 operations. + +## Installation + +### Maven + +```xml + + com.cobo.waas2 + cobo-waas2 + 1.0.0 + +``` + +### Gradle + +```groovy +implementation 'com.cobo.waas2:cobo-waas2:1.0.0' +``` + +## Environment (all languages) + +- **macOS / Linux:** `eval $(cobo config env)` +- **Windows PowerShell:** `cobo config env --format powershell | Invoke-Expression` +- **Windows CMD:** `cobo config env --format cmd > env.bat && env.bat` + +## Configuration from Environment + +```java +import com.cobo.waas2.ApiClient; +import com.cobo.waas2.Configuration; +import com.cobo.waas2.Env; + +public class CoboConfig { + public static ApiClient getApiClient() { + // Read from environment variables (recommended) + String apiSecret = System.getenv("COBO_API_SECRET"); + if (apiSecret == null || apiSecret.isEmpty()) { + throw new RuntimeException("COBO_API_SECRET environment variable not set"); + } + + String envStr = System.getenv("COBO_ENV"); + if (envStr == null || envStr.isEmpty()) { + envStr = "dev"; + } + + // Get default client and configure + ApiClient client = Configuration.getDefaultApiClient(); + client.setPrivKey(apiSecret); + + // Set environment + Env env; + switch (envStr.toLowerCase()) { + case "prod": + env = Env.PROD; + break; + case "sandbox": + env = Env.SANDBOX; + break; + default: + env = Env.DEV; + } + client.setEnv(env); + + return client; + } +} +``` + +## List Wallets + +```java +import com.cobo.waas2.api.WalletsApi; +import com.cobo.waas2.model.ListWallets200Response; +import com.cobo.waas2.model.WalletInfo; +import com.cobo.waas2.model.WalletType; + +public class ListWalletsExample { + public static void listWallets(ApiClient client) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + // List all wallets + ListWallets200Response response = walletsApi.listWallets( + null, // walletType + null, // walletSubtype + null, // projectId + null, // vaultId + 10, // limit + null, // before + null // after + ); + + System.out.println("Found " + response.getData().size() + " wallets"); + for (WalletInfo wallet : response.getData()) { + System.out.printf(" %s: %s (%s)%n", + wallet.getWalletId(), + wallet.getName(), + wallet.getWalletType()); + } + } catch (Exception e) { + System.err.println("Error listing wallets: " + e.getMessage()); + } + } + + // Filter by wallet type + public static void listCustodialWallets(ApiClient client) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + ListWallets200Response response = walletsApi.listWallets( + WalletType.CUSTODIAL, + null, // walletSubtype + null, // projectId + null, // vaultId + 50, // limit + null, // before + null // after + ); + + for (WalletInfo wallet : response.getData()) { + System.out.printf("%s: %s%n", wallet.getWalletId(), wallet.getName()); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + // Pagination + public static List listAllWallets(ApiClient client) { + WalletsApi walletsApi = new WalletsApi(client); + List allWallets = new ArrayList<>(); + String after = null; + + while (true) { + try { + ListWallets200Response response = walletsApi.listWallets( + null, null, null, null, 100, null, after + ); + + allWallets.addAll(response.getData()); + + if (response.getPagination() == null || + response.getPagination().getAfter() == null) { + break; + } + after = response.getPagination().getAfter(); + } catch (Exception e) { + break; + } + } + + return allWallets; + } +} +``` + +## Create Custodial Wallet + +```java +import com.cobo.waas2.api.WalletsApi; +import com.cobo.waas2.model.CreateWalletParams; +import com.cobo.waas2.model.CreateCustodialWalletParams; +import com.cobo.waas2.model.CreatedWalletInfo; +import com.cobo.waas2.model.WalletType; +import com.cobo.waas2.model.WalletSubtype; + +public class CreateWalletExample { + public static CreatedWalletInfo createCustodialWallet(ApiClient client, String name) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + CreateCustodialWalletParams custodialParams = new CreateCustodialWalletParams(); + custodialParams.setName(name); + custodialParams.setWalletType(WalletType.CUSTODIAL); + custodialParams.setWalletSubtype(WalletSubtype.ASSET); + + CreateWalletParams params = new CreateWalletParams(custodialParams); + + CreatedWalletInfo wallet = walletsApi.createWallet(params); + + System.out.println("Created wallet: " + wallet.getWalletId()); + System.out.println(" Name: " + wallet.getName()); + System.out.println(" Type: " + wallet.getWalletType() + "/" + wallet.getWalletSubtype()); + + return wallet; + } catch (Exception e) { + System.err.println("Error creating wallet: " + e.getMessage()); + throw new RuntimeException(e); + } + } +} +``` + +## Create MPC Org-Controlled Wallet + +```java +import com.cobo.waas2.model.CreateMpcWalletParams; + +public class CreateMpcWalletExample { + public static CreatedWalletInfo createMpcWallet(ApiClient client, String name, String vaultId) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + CreateMpcWalletParams mpcParams = new CreateMpcWalletParams(); + mpcParams.setName(name); + mpcParams.setWalletType(WalletType.MPC); + mpcParams.setWalletSubtype(WalletSubtype.ORG_CONTROLLED); + mpcParams.setVaultId(vaultId); + + CreateWalletParams params = new CreateWalletParams(mpcParams); + + CreatedWalletInfo wallet = walletsApi.createWallet(params); + + System.out.println("Created MPC wallet: " + wallet.getWalletId()); + return wallet; + } catch (Exception e) { + System.err.println("Error creating MPC wallet: " + e.getMessage()); + throw new RuntimeException(e); + } + } + + // Get vault_id first + public static String getVaultId(ApiClient client) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + var response = walletsApi.listMpcVaults(null, null, null); + if (response.getData().isEmpty()) { + throw new RuntimeException("No vaults found"); + } + return response.getData().get(0).getVaultId(); + } catch (Exception e) { + throw new RuntimeException("Error getting vault: " + e.getMessage()); + } + } +} +``` + +## Create Address + +```java +import com.cobo.waas2.model.AddressInfo; +import com.cobo.waas2.model.CreateAddressRequest; + +public class CreateAddressExample { + public static AddressInfo createAddress(ApiClient client, String walletId, String chainId) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + // Get supported chains + var chains = walletsApi.listSupportedChains(walletId, null, null, null); + System.out.print("Supported chains: "); + for (var chain : chains.getData()) { + System.out.print(chain.getChainId() + " "); + } + System.out.println(); + + // Create address + CreateAddressRequest request = new CreateAddressRequest(); + request.setChainId(chainId); + + AddressInfo address = walletsApi.createAddress(walletId, request); + + System.out.println("Created address: " + address.getAddress()); + System.out.println(" Chain: " + address.getChainId()); + System.out.println(" Address ID: " + address.getAddressId()); + + return address; + } catch (Exception e) { + System.err.println("Error creating address: " + e.getMessage()); + throw new RuntimeException(e); + } + } +} +``` + +## Create Transfer + +```java +import com.cobo.waas2.api.TransactionsApi; +import com.cobo.waas2.model.TransferParams; +import com.cobo.waas2.model.TransferSource; +import com.cobo.waas2.model.TransferDestination; +import com.cobo.waas2.model.WalletTransferSource; +import com.cobo.waas2.model.AddressTransferDestination; +import com.cobo.waas2.model.TransferResult; +import java.util.UUID; + +public class CreateTransferExample { + public static TransferResult createTransfer( + ApiClient client, + String sourceWalletId, + String destinationAddress, + String tokenId, + String amount) { + + WalletsApi walletsApi = new WalletsApi(client); + TransactionsApi transactionsApi = new TransactionsApi(client); + + try { + // Validate destination first + var validation = walletsApi.checkAddressValidity(tokenId, destinationAddress); + if (!validation.getIsValid()) { + throw new RuntimeException("Invalid address: " + validation.getMessage()); + } + + // Create unique request_id + String requestId = "tx-" + System.currentTimeMillis() + "-" + + UUID.randomUUID().toString().substring(0, 8); + + // Build source + WalletTransferSource walletSource = new WalletTransferSource(); + walletSource.setSourceType(TransferSourceType.WALLET); + walletSource.setWalletId(sourceWalletId); + TransferSource source = new TransferSource(walletSource); + + // Build destination + AddressTransferDestination addressDest = new AddressTransferDestination(); + addressDest.setDestinationType(TransferDestinationType.ADDRESS); + addressDest.setAddress(destinationAddress); + addressDest.setAmount(amount); + TransferDestination destination = new TransferDestination(addressDest); + + // Create transfer params + TransferParams params = new TransferParams(); + params.setRequestId(requestId); + params.setSource(source); + params.setTokenId(tokenId); + params.setDestination(destination); + + TransferResult transaction = transactionsApi.createTransfer(params); + + System.out.println("Created transaction: " + transaction.getTransactionId()); + System.out.println(" Status: " + transaction.getStatus()); + System.out.println(" Request ID: " + transaction.getRequestId()); + + return transaction; + } catch (Exception e) { + System.err.println("Error creating transfer: " + e.getMessage()); + throw new RuntimeException(e); + } + } +} +``` + +## Check Transaction Status + +```java +import com.cobo.waas2.model.TransactionDetail; +import com.cobo.waas2.model.TransactionStatus; +import java.util.Set; + +public class TransactionStatusExample { + public static TransactionDetail getTransactionStatus(ApiClient client, String transactionId) { + TransactionsApi transactionsApi = new TransactionsApi(client); + + try { + TransactionDetail tx = transactionsApi.getTransaction(transactionId); + + System.out.println("Transaction " + tx.getTransactionId()); + System.out.println(" Status: " + tx.getStatus()); + System.out.println(" Token: " + tx.getTokenId()); + System.out.println(" Created: " + tx.getCreatedTimestamp()); + + return tx; + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + // Poll until complete + public static TransactionDetail waitForCompletion( + ApiClient client, + String transactionId, + long timeoutMs, + long intervalMs) { + + TransactionsApi transactionsApi = new TransactionsApi(client); + + Set terminalStates = Set.of( + TransactionStatus.COMPLETED, + TransactionStatus.FAILED, + TransactionStatus.REJECTED, + TransactionStatus.CANCELLED + ); + + long deadline = System.currentTimeMillis() + timeoutMs; + + while (System.currentTimeMillis() < deadline) { + try { + TransactionDetail tx = transactionsApi.getTransaction(transactionId); + System.out.println(" Status: " + tx.getStatus()); + + if (terminalStates.contains(tx.getStatus())) { + return tx; + } + + Thread.sleep(intervalMs); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + throw new RuntimeException("Transaction " + transactionId + + " did not complete in " + timeoutMs + "ms"); + } +} +``` + +## Error Handling + +```java +import com.cobo.waas2.ApiException; +import java.util.function.Supplier; + +public class ErrorHandlingExample { + public static T safeApiCall(Supplier operation, int maxRetries) { + int retryDelay = 1000; + + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + return operation.get(); + + } catch (ApiException e) { + int status = e.getCode(); + System.out.println("API Error " + status + ": " + e.getMessage()); + + switch (status) { + case 401: + throw new RuntimeException("Authentication failed - check COBO_API_SECRET"); + case 403: + throw new RuntimeException("Permission denied - check API key permissions"); + case 404: + throw new RuntimeException("Resource not found: " + e.getResponseBody()); + case 429: + // Rate limited - wait and retry + int waitTime = retryDelay * (1 << attempt); + System.out.println("Rate limited, waiting " + waitTime + "ms..."); + try { + Thread.sleep(waitTime); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + continue; + default: + if (status >= 500 && attempt < maxRetries - 1) { + try { + Thread.sleep(retryDelay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + continue; + } + } + + throw new RuntimeException(e); + } catch (Exception e) { + System.out.println("Unexpected error: " + e.getMessage()); + if (attempt < maxRetries - 1) { + try { + Thread.sleep(retryDelay); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + continue; + } + throw new RuntimeException(e); + } + } + + throw new RuntimeException("Failed after " + maxRetries + " attempts"); + } + + // Usage + public static void example(ApiClient client) { + WalletsApi walletsApi = new WalletsApi(client); + + try { + WalletInfo wallet = safeApiCall( + () -> walletsApi.getWalletById("wallet_id"), + 3 + ); + System.out.println("Wallet: " + wallet.getName()); + } catch (RuntimeException e) { + System.err.println("Error: " + e.getMessage()); + } + } +} +``` + +## Complete Example + +```java +package com.example.cobowaas; + +import com.cobo.waas2.ApiClient; +import com.cobo.waas2.Configuration; +import com.cobo.waas2.Env; +import com.cobo.waas2.api.WalletsApi; +import com.cobo.waas2.api.TransactionsApi; +import com.cobo.waas2.model.*; + +public class CoboWaasExample { + public static void main(String[] args) { + // Setup + String apiSecret = System.getenv("COBO_API_SECRET"); + if (apiSecret == null || apiSecret.isEmpty()) { + System.err.println("Set COBO_API_SECRET environment variable"); + System.exit(1); + } + + String envStr = System.getenv("COBO_ENV"); + if (envStr == null) envStr = "dev"; + + ApiClient client = Configuration.getDefaultApiClient(); + client.setPrivKey(apiSecret); + + Env env = envStr.equalsIgnoreCase("prod") ? Env.PROD : Env.DEV; + client.setEnv(env); + + WalletsApi walletsApi = new WalletsApi(client); + TransactionsApi transactionsApi = new TransactionsApi(client); + + try { + // List existing wallets + System.out.println("Listing wallets..."); + ListWallets200Response walletResponse = walletsApi.listWallets( + WalletType.CUSTODIAL, null, null, null, 10, null, null + ); + + for (WalletInfo w : walletResponse.getData()) { + System.out.printf(" %s: %s%n", w.getName(), w.getWalletId()); + } + + // Create wallet if none exist + WalletInfo wallet; + if (walletResponse.getData().isEmpty()) { + System.out.println("\nCreating wallet..."); + + CreateCustodialWalletParams custodialParams = new CreateCustodialWalletParams(); + custodialParams.setName("Test Wallet " + System.currentTimeMillis()); + custodialParams.setWalletType(WalletType.CUSTODIAL); + custodialParams.setWalletSubtype(WalletSubtype.ASSET); + + CreateWalletParams params = new CreateWalletParams(custodialParams); + CreatedWalletInfo created = walletsApi.createWallet(params); + + System.out.println("Created: " + created.getWalletId()); + + // Convert for subsequent operations + wallet = new WalletInfo(); + wallet.setWalletId(created.getWalletId()); + wallet.setName(created.getName()); + } else { + wallet = walletResponse.getData().get(0); + } + + // Create address + System.out.printf("%nCreating address for wallet %s...%n", wallet.getWalletId()); + + try { + CreateAddressRequest addressRequest = new CreateAddressRequest(); + addressRequest.setChainId("ETH"); + + AddressInfo address = walletsApi.createAddress( + wallet.getWalletId(), addressRequest + ); + + System.out.println("Address: " + address.getAddress()); + } catch (Exception e) { + System.out.println("Address may already exist, listing..."); + + var addresses = walletsApi.listAddresses( + wallet.getWalletId(), null, null, null, null + ); + + for (var addr : addresses.getData()) { + System.out.printf(" %s: %s%n", + addr.getChainId(), addr.getAddress()); + } + } + + System.out.println("\nDone!"); + + } catch (Exception e) { + System.err.println("Error: " + e.getMessage()); + e.printStackTrace(); + System.exit(1); + } + } +} +``` + +## Environment Setup + +```bash +# Set environment variables +export COBO_API_SECRET="" +export COBO_ENV="dev" + +# Run with Maven +mvn exec:java -Dexec.mainClass="com.example.cobowaas.CoboWaasExample" + +# Run with Gradle +gradle run +``` + +## build.gradle Example + +```groovy +plugins { + id 'java' + id 'application' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'com.cobo.waas2:cobo-waas2:1.0.0' +} + +application { + mainClass = 'com.example.cobowaas.CoboWaasExample' +} + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} +``` diff --git a/skills/cobo-waas/assets/templates/python.md b/skills/cobo-waas/assets/templates/python.md new file mode 100644 index 0000000..30c5579 --- /dev/null +++ b/skills/cobo-waas/assets/templates/python.md @@ -0,0 +1,165 @@ +# Python SDK Sample (WaaS 2.0) + +## Installation + +```bash +pip install cobo-waas2 +``` + +## Environment (all languages) + +Load env from cobo config, then run your script: + +- **macOS / Linux:** `eval $(cobo config env)` +- **Windows PowerShell:** `cobo config env --format powershell | Invoke-Expression` +- **Windows CMD:** `cobo config env --format cmd > env.bat && env.bat` + +## Setup + +```python +import os +from cobo_waas2 import Configuration, ApiClient +from cobo_waas2.api import WalletsApi + +api_secret = os.environ["COBO_API_SECRET"] +env = os.environ.get("COBO_ENV", "dev") + +config = Configuration( + api_private_key=api_secret, + host=f"https://api.{env}.cobo.com/v2" if env != "prod" else "https://api.cobo.com/v2", +) +client = ApiClient(config) +wallets_api = WalletsApi(client) +``` + +## List Wallets + +```python +response = wallets_api.list_wallets(limit=10) +for w in response.data: + wallet = w.actual_instance + print(f"{wallet.wallet_id}: {wallet.name}") +``` + +## Create Custodial Wallet + +```python +from cobo_waas2.models import CreateWalletParams, CreateCustodialWalletParams, WalletType, WalletSubtype + +# IMPORTANT: Must wrap params with CreateWalletParams(actual_instance=...) +params = CreateWalletParams(actual_instance=CreateCustodialWalletParams( + name="My Wallet", + wallet_type=WalletType.CUSTODIAL, + wallet_subtype=WalletSubtype.ASSET +)) + +response = wallets_api.create_wallet(create_wallet_params=params) +wallet = response.actual_instance # IMPORTANT: Use .actual_instance +print(f"Created: {wallet.wallet_id}") +``` + +## Create MPC Wallet + +```python +from cobo_waas2.api import WalletsMpcWalletsApi +from cobo_waas2.models import CreateWalletParams, CreateMpcWalletParams, WalletType, WalletSubtype + +# Get vault_id first (endpoint: /wallets/mpc/vaults) +mpc_api = WalletsMpcWalletsApi(client) +vaults = mpc_api.list_mpc_vaults("Org-Controlled") +vault_id = vaults.data[0].vault_id + +params = CreateWalletParams(actual_instance=CreateMpcWalletParams( + name="MPC Wallet", + wallet_type=WalletType.MPC, + wallet_subtype=WalletSubtype.ORG_CONTROLLED, + vault_id=vault_id +)) + +response = wallets_api.create_wallet(create_wallet_params=params) +wallet = response.actual_instance +print(f"Created: {wallet.wallet_id}") +``` + +## Create Address + +```python +# IMPORTANT: Requires 'count' param, returns a LIST +response = wallets_api.create_address( + wallet_id=wallet.wallet_id, + create_address_request=CreateAddressRequest(chain_id="ETH", count=1) +) +address = response.data[0] # Response is a list +print(f"Address: {address.address}") +``` + +## Create Transfer + +```python +import time +from cobo_waas2.api import TransactionsApi +from cobo_waas2.models import TransferParams, TransferSource, TransferDestination, WalletSubtype, CustodialTransferSource, AddressTransferDestination, AddressTransferDestinationAccountOutput + +tx_api = TransactionsApi(client) + +params = TransferParams( + request_id=f"tx-{int(time.time())}-{os.urandom(4).hex()}", + source=TransferSource( + actual_instance=CustodialTransferSource( + source_type=WalletSubtype.ASSET, + wallet_id=wallet.wallet_id + ) + ), + token_id="ETH", + destination=TransferDestination( + actual_instance=AddressTransferDestination( + destination_type="Address", + account_output=AddressTransferDestinationAccountOutput( + address="0x...", + amount="0.01" + ) + ) + ) +) + +response = tx_api.create_transfer_transaction(transfer_params=params) +tx = response.actual_instance +print(f"TX: {tx.transaction_id}, Status: {tx.status}") +``` + +## List Webhook Events + +```python +from cobo_waas2.api import DevelopersWebhooksApi + +webhooks_api = DevelopersWebhooksApi(client) +# NOTE: Returns list directly, not .data +events = webhooks_api.list_webhook_event_definitions() +for e in events: + print(f"{e.event_type}") +``` + +## Error Handling + +```python +from cobo_waas2.exceptions import ApiException + +try: + response = wallets_api.list_wallets() +except ApiException as e: + print(f"Error {e.status}: {e.body}") +``` + +--- + +## Required Permissions + +| Operation | Permission | +|-----------|------------| +| List/Get wallets | Wallet - Read | +| Create wallet | Wallet - Write | +| Create address | Address - Write | +| Create transfer | Transaction - Write | +| Webhooks | Webhook - Read/Write | + +Configure in Portal: `cobo open developer` diff --git a/skills/cobo-waas/assets/templates/ts-node.md b/skills/cobo-waas/assets/templates/ts-node.md new file mode 100644 index 0000000..43898c6 --- /dev/null +++ b/skills/cobo-waas/assets/templates/ts-node.md @@ -0,0 +1,501 @@ +# JavaScript / Node.js SDK Sample (WaaS 2.0) + +Complete Node.js/TypeScript SDK examples for Cobo WaaS 2.0 operations. + +## Installation + +```bash +npm install @cobo/cobo-waas2 --save +``` + +## Environment (all languages) + +- **macOS / Linux:** `eval $(cobo config env)` +- **Windows PowerShell:** `cobo config env --format powershell | Invoke-Expression` +- **Windows CMD:** `cobo config env --format cmd > env.bat && env.bat` + +## Configuration from Environment + +```javascript +const CoboWaas2 = require("@cobo/cobo-waas2"); + +// Read from environment variables (recommended) +const apiSecret = process.env.COBO_API_SECRET; +if (!apiSecret) { + throw new Error("COBO_API_SECRET environment variable not set"); +} + +const env = process.env.COBO_ENV || "dev"; + +// Initialize the API client +const apiClient = CoboWaas2.ApiClient.instance; +apiClient.setPrivateKey(apiSecret); + +// Set environment +const envMap = { + dev: CoboWaas2.Env.DEV, + prod: CoboWaas2.Env.PROD, + sandbox: CoboWaas2.Env.SANDBOX, +}; +apiClient.setEnv(envMap[env] || CoboWaas2.Env.DEV); +``` + +### TypeScript Configuration + +```typescript +import * as CoboWaas2 from "@cobo/cobo-waas2"; + +const apiSecret = process.env.COBO_API_SECRET; +if (!apiSecret) { + throw new Error("COBO_API_SECRET environment variable not set"); +} + +const apiClient = CoboWaas2.ApiClient.instance; +apiClient.setPrivateKey(apiSecret); +apiClient.setEnv(CoboWaas2.Env.DEV); +``` + +## List Wallets + +```javascript +const walletsApi = new CoboWaas2.WalletsApi(); + +// List all wallets +async function listWallets() { + try { + const response = await walletsApi.listWallets({ limit: 10 }); + console.log(`Found ${response.data.length} wallets`); + + response.data.forEach(wallet => { + console.log(` ${wallet.wallet_id}: ${wallet.name} (${wallet.wallet_type})`); + }); + + return response.data; + } catch (error) { + console.error("Error listing wallets:", error.message); + throw error; + } +} + +// Filter by wallet type +async function listCustodialWallets() { + const response = await walletsApi.listWallets({ + walletType: "Custodial", + limit: 50 + }); + return response.data; +} + +// Pagination +async function listAllWallets() { + const allWallets = []; + let after = null; + + while (true) { + const response = await walletsApi.listWallets({ limit: 100, after }); + allWallets.push(...response.data); + + if (!response.pagination || !response.pagination.after) { + break; + } + after = response.pagination.after; + } + + return allWallets; +} +``` + +## Create Custodial Wallet + +```javascript +async function createCustodialWallet(name) { + const params = { + name: name, + wallet_type: "Custodial", + wallet_subtype: "Asset" + }; + + try { + const wallet = await walletsApi.createWallet(params); + console.log(`Created wallet: ${wallet.wallet_id}`); + console.log(` Name: ${wallet.name}`); + console.log(` Type: ${wallet.wallet_type}/${wallet.wallet_subtype}`); + return wallet; + } catch (error) { + console.error("Error creating wallet:", error.message); + throw error; + } +} +``` + +## Create MPC Org-Controlled Wallet + +```javascript +async function createMpcWallet(name, vaultId) { + const params = { + name: name, + wallet_type: "MPC", + wallet_subtype: "Org-Controlled", + vault_id: vaultId + }; + + try { + const wallet = await walletsApi.createWallet(params); + console.log(`Created MPC wallet: ${wallet.wallet_id}`); + return wallet; + } catch (error) { + console.error("Error creating MPC wallet:", error.message); + throw error; + } +} + +// Get vault_id first +async function getVaultId() { + // Using raw API call for vaults + const response = await apiClient.callApi( + "/vaults", + "GET", + {}, + {}, + {}, + {}, + null, + [], + ["application/json"], + ["application/json"] + ); + return response.data[0].vault_id; +} +``` + +## Create Address + +```javascript +async function createAddress(walletId, chainId = "ETH") { + try { + // Get supported chains + const chains = await walletsApi.listSupportedChains(walletId); + console.log(`Supported chains: ${chains.data.map(c => c.chain_id).join(", ")}`); + + // Create address + const address = await walletsApi.createAddress(walletId, { chain_id: chainId }); + console.log(`Created address: ${address.address}`); + console.log(` Chain: ${address.chain_id}`); + console.log(` Address ID: ${address.address_id}`); + + return address; + } catch (error) { + console.error("Error creating address:", error.message); + throw error; + } +} +``` + +## Create Transfer + +```javascript +const transactionsApi = new CoboWaas2.TransactionsApi(); + +async function createTransfer(sourceWalletId, destinationAddress, tokenId, amount) { + // Validate destination first + const validation = await walletsApi.checkAddressValidity({ + chainId: tokenId.split("_")[0] || tokenId, + address: destinationAddress + }); + + if (!validation.is_valid) { + throw new Error(`Invalid address: ${validation.message}`); + } + + // Create unique request_id + const requestId = `tx-${Date.now()}-${Math.random().toString(36).substr(2, 8)}`; + + const params = { + request_id: requestId, + source: { + source_type: "Wallet", + wallet_id: sourceWalletId + }, + token_id: tokenId, + destination: { + destination_type: "Address", + address: destinationAddress, + amount: amount + } + }; + + try { + const transaction = await transactionsApi.createTransfer(params); + console.log(`Created transaction: ${transaction.transaction_id}`); + console.log(` Status: ${transaction.status}`); + console.log(` Request ID: ${transaction.request_id}`); + return transaction; + } catch (error) { + console.error("Error creating transfer:", error.message); + throw error; + } +} +``` + +## Check Transaction Status + +```javascript +async function getTransactionStatus(transactionId) { + const tx = await transactionsApi.getTransaction(transactionId); + console.log(`Transaction ${tx.transaction_id}`); + console.log(` Status: ${tx.status}`); + console.log(` Token: ${tx.token_id}`); + console.log(` Created: ${tx.created_timestamp}`); + return tx; +} + +// Poll until complete +async function waitForCompletion(transactionId, timeout = 300000, interval = 10000) { + const terminalStates = new Set(["Completed", "Failed", "Rejected", "Cancelled"]); + const start = Date.now(); + + while (Date.now() - start < timeout) { + const tx = await transactionsApi.getTransaction(transactionId); + console.log(` Status: ${tx.status}`); + + if (terminalStates.has(tx.status)) { + return tx; + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + + throw new Error(`Transaction ${transactionId} did not complete in ${timeout}ms`); +} + +// Usage +async function executeAndWait() { + const tx = await createTransfer( + "wallet_id", + "0x742d35Cc...", + "ETH", + "0.01" + ); + + const finalTx = await waitForCompletion(tx.transaction_id); + + if (finalTx.status === "Completed") { + console.log(`Success! TX hash: ${finalTx.transaction_hash}`); + } else { + console.log(`Failed: ${finalTx.fail_reason}`); + } +} +``` + +## Error Handling + +```javascript +async function safeApiCall(apiFunc, ...args) { + const maxRetries = 3; + let retryDelay = 1000; + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + return await apiFunc(...args); + + } catch (error) { + const status = error.response?.status; + console.log(`API Error ${status}: ${error.message}`); + + if (status === 401) { + throw new Error("Authentication failed - check COBO_API_SECRET"); + } + + if (status === 403) { + throw new Error("Permission denied - check API key permissions"); + } + + if (status === 404) { + throw new Error(`Resource not found: ${error.response?.data}`); + } + + if (status === 429) { + // Rate limited - wait and retry + const waitTime = retryDelay * Math.pow(2, attempt); + console.log(`Rate limited, waiting ${waitTime}ms...`); + await new Promise(resolve => setTimeout(resolve, waitTime)); + continue; + } + + if (status >= 500) { + // Server error - retry + if (attempt < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, retryDelay)); + continue; + } + } + + throw error; + } + } + + throw new Error(`Failed after ${maxRetries} attempts`); +} + +// Usage +try { + const wallet = await safeApiCall( + () => walletsApi.getWalletById("wallet_id") + ); +} catch (error) { + console.error(`Error: ${error.message}`); +} +``` + +## Complete Example Script + +```javascript +#!/usr/bin/env node +/** + * Complete Cobo WaaS 2.0 workflow example for Node.js + */ + +const CoboWaas2 = require("@cobo/cobo-waas2"); + +async function main() { + // Setup + const apiSecret = process.env.COBO_API_SECRET; + if (!apiSecret) { + throw new Error("Set COBO_API_SECRET environment variable"); + } + + const env = process.env.COBO_ENV || "dev"; + + const apiClient = CoboWaas2.ApiClient.instance; + apiClient.setPrivateKey(apiSecret); + + const envMap = { + dev: CoboWaas2.Env.DEV, + prod: CoboWaas2.Env.PROD, + }; + apiClient.setEnv(envMap[env] || CoboWaas2.Env.DEV); + + const walletsApi = new CoboWaas2.WalletsApi(); + const transactionsApi = new CoboWaas2.TransactionsApi(); + + // List existing wallets + console.log("Listing wallets..."); + const wallets = await walletsApi.listWallets({ + walletType: "Custodial", + limit: 10 + }); + + wallets.data.forEach(w => { + console.log(` ${w.name}: ${w.wallet_id}`); + }); + + // Create wallet if none exist + let wallet; + if (wallets.data.length === 0) { + console.log("\nCreating wallet..."); + wallet = await walletsApi.createWallet({ + name: `Test Wallet ${Date.now()}`, + wallet_type: "Custodial", + wallet_subtype: "Asset" + }); + console.log(`Created: ${wallet.wallet_id}`); + } else { + wallet = wallets.data[0]; + } + + // Create address + console.log(`\nCreating address for wallet ${wallet.wallet_id}...`); + try { + const address = await walletsApi.createAddress( + wallet.wallet_id, + { chain_id: "ETH" } + ); + console.log(`Address: ${address.address}`); + } catch (error) { + if (error.response?.status === 400) { + console.log("Address may already exist, listing..."); + const addresses = await walletsApi.listAddresses(wallet.wallet_id); + addresses.data.forEach(addr => { + console.log(` ${addr.chain_id}: ${addr.address}`); + }); + } else { + throw error; + } + } + + console.log("\nDone!"); +} + +main().catch(error => { + console.error("Fatal error:", error.message); + process.exit(1); +}); +``` + +## TypeScript Complete Example + +```typescript +#!/usr/bin/env ts-node +/** + * Complete Cobo WaaS 2.0 workflow example for TypeScript + */ + +import * as CoboWaas2 from "@cobo/cobo-waas2"; + +async function main(): Promise { + const apiSecret = process.env.COBO_API_SECRET; + if (!apiSecret) { + throw new Error("Set COBO_API_SECRET environment variable"); + } + + const apiClient = CoboWaas2.ApiClient.instance; + apiClient.setPrivateKey(apiSecret); + apiClient.setEnv(CoboWaas2.Env.DEV); + + const walletsApi = new CoboWaas2.WalletsApi(); + + // List wallets with type safety + const response = await walletsApi.listWallets({ limit: 10 }); + + response.data.forEach((wallet: CoboWaas2.Wallet) => { + console.log(`${wallet.wallet_id}: ${wallet.name}`); + }); +} + +main().catch(console.error); +``` + +## Environment Setup + +```bash +# Set environment variables +export COBO_API_SECRET="" +export COBO_ENV="dev" + +# Run JavaScript +node example.js + +# Run TypeScript +npx ts-node example.ts +``` + +## Package.json Example + +```json +{ + "name": "cobo-waas-example", + "version": "1.0.0", + "scripts": { + "start": "node example.js", + "start:ts": "ts-node example.ts" + }, + "dependencies": { + "@cobo/cobo-waas2": "^1.0.0" + }, + "devDependencies": { + "@types/node": "^18.0.0", + "ts-node": "^10.9.0", + "typescript": "^5.0.0" + } +} +``` diff --git a/skills/cobo-waas/references/api-quickref.md b/skills/cobo-waas/references/api-quickref.md new file mode 100644 index 0000000..b22f3c8 --- /dev/null +++ b/skills/cobo-waas/references/api-quickref.md @@ -0,0 +1,24 @@ +# API Quick Reference + +Get full Cobo WaaS 2.0 endpoint and description from [LLM](https://www.cobo.com/developers/llms.txt) + +Use `cobo --list` for full endpoint list. + +## Endpoint Discovery + +```bash +# List all GET endpoints +cobo get --list + +# List all POST endpoints +cobo post --list + +# Describe endpoint parameters +cobo post /wallets --describe + +# Describe specific parameter +cobo post /wallets --describe --wallet_type + +# Open API docs +cobo doc /wallets +``` diff --git a/skills/cobo-waas/references/auth.md b/skills/cobo-waas/references/auth.md new file mode 100644 index 0000000..6b16dfa --- /dev/null +++ b/skills/cobo-waas/references/auth.md @@ -0,0 +1,26 @@ +# Auth and key setup + +## API key workflow (apikey auth) +1) Generate keypair: + `cobo keys generate --key-type API` + +2) Register the public key: + - **CLI (dev/sandbox):** `cobo keys register` (requires `cobo login --user` first) + - **Portal (all envs):** `cobo open developer` → Add the public key as an API key. + +3) Set auth method: + `cobo auth apikey` + +4) Confirm config: + `cobo config list` + +## User auth (portal access) +- Login as user: `cobo login --user` +- Set auth method: `cobo auth user` + +## Environment selection +- `cobo env sandbox|dev|prod` + +## Notes +- Never print secrets in logs or agent outputs. +- If a command fails due to missing keys, re-check config and Portal registration. diff --git a/skills/cobo-waas/references/cli.md b/skills/cobo-waas/references/cli.md new file mode 100644 index 0000000..53ccf16 --- /dev/null +++ b/skills/cobo-waas/references/cli.md @@ -0,0 +1,47 @@ +# CLI command patterns + +## Preflight +- Check version: `cobo version` +- Check config: `cobo config list` +- Set environment: `cobo env sandbox|dev|prod` +- Set auth method: `cobo auth apikey|user|org|none` +- Get help: `cobo --help` or `cobo --help` + +## Discover endpoints +- **Look up parameters before generating code (recommended):** `cobo doc /path` — e.g. `cobo doc /wallets` prints parameter names, types, and requirements for that path; use this output to build correct `cobo get/post` or SDK calls. +- List operations: `cobo get --list`, `cobo post --list`, `cobo put --list`, `cobo delete --list` +- Describe an endpoint: `cobo get /path --describe` or `cobo post /path --describe` +- Describe specific parameters: `cobo get /path --describe --param_name` +- Open docs: `cobo doc` (browser) or `cobo doc /path` (print API details for path) + +## Call APIs (WaaS 2.0) +- GET: `cobo get /path --param value` +- POST: `cobo post /path --param value` +- PUT: `cobo put /path --param value` +- DELETE: `cobo delete /path --param value` + +## Keys +- Generate API keypair: `cobo keys generate --key-type API` +- Register API key (dev/sandbox only): `cobo keys register` (uses api_key from config) or `cobo keys register --pubkey ` +- Validate keypair: `cobo keys validate --alg ed25519 --secret --pubkey ` + +## GraphQL (advanced) +- Inline query: `cobo graphql -q '{ wallets { wallet_id name } }'` +- With variables: `cobo graphql -q 'query($id:String!){ wallet(wallet_id:$id){ name } }' -v '{\"id\":\"\"}'` +- From file: `cobo graphql -f ./query.graphql -v '{\"id\":\"\"}'` +- Raw output: `cobo graphql -q '{ ... }' --raw` + +## Logs and webhooks +- Tail logs: `cobo logs tail` +- Filter logs: `cobo logs tail --http-method POST --request-path /v2/transactions` +- Webhook events: `cobo webhook events` +- Listen: `cobo webhook listen --events "wallets.transaction.succeeded"` +- Trigger test: `cobo webhook trigger wallets.transaction.succeeded` + +## Portal links +- Open portal: `cobo open portal` +- Open developer console: `cobo open developer` +- Open wallets page: `cobo open wallets` + +## Debugging +- Enable debug: `cobo --enable-debug get /wallets` diff --git a/skills/cobo-waas/references/concepts.md b/skills/cobo-waas/references/concepts.md new file mode 100644 index 0000000..f5635a7 --- /dev/null +++ b/skills/cobo-waas/references/concepts.md @@ -0,0 +1,213 @@ +# Concepts and Glossary + +Quick reference for Cobo-specific terminology and concepts. + +## Wallet Types + +### Custodial Wallets + +Cobo manages the private keys. Simplest to set up and use. + +| Subtype | Description | Use Case | +|---------|-------------|----------| +| `Asset` | Standard asset custody | Treasury, holding | +| `Web3` | Web3 interaction enabled | DeFi, NFTs, dApps | + +**Create Custodial wallet:** +```bash +cobo post /wallets \ + --wallet_type Custodial \ + --wallet_subtype Asset \ + --name "Treasury Wallet" +``` + +### MPC Wallets + +Multi-Party Computation. Private key is split across multiple parties. + +| Subtype | Description | Key Shares | +|---------|-------------|------------| +| `Org-Controlled` | Organization controls all shares | Cobo + Organization | +| `User-Controlled` | End users hold their shares | Cobo + User + (Optional) Organization | + +**Create MPC Org-Controlled wallet:** +```bash +cobo post /wallets \ + --wallet_type MPC \ + --wallet_subtype Org-Controlled \ + --name "MPC Treasury" \ + --vault_id "" +``` + +## Auth Methods + +| Method | CLI Command | Description | Use Case | +|--------|-------------|-------------|----------| +| `apikey` | `cobo auth apikey` | API key authentication | Automation, scripts, CI/CD | +| `user` | `cobo auth user` | User token from Portal login | Interactive Portal access | +| `org` | `cobo auth org` | Organization token | Portal apps (Cobo Apps) | +| `none` | `cobo auth none` | No authentication | Public endpoints only | + +### API Key Authentication + +1. Generate keypair: `cobo keys generate --key-type API` +2. Register public key in Portal Developer Console +3. CLI uses private key to sign requests + +### User Authentication + +1. Login: `cobo login --user` +2. Opens browser for Portal authentication +3. Token stored locally + +## Environments + +| Environment | Host | CLI Command | Use Case | +|-------------|------|-------------|----------| +| Sandbox | `api.sandbox.cobo.com` | `cobo env sandbox` | Isolated testing | +| Development | `api.dev.cobo.com` | `cobo env dev` | Development with test assets | +| Production | `api.cobo.com` | `cobo env prod` | Real assets | + +**Important**: API keys are environment-specific. A key registered in `dev` won't work in `prod`. + +## Common IDs + +### wallet_id + +Unique identifier for a wallet. + +- Format: UUID (e.g., `f47ac10b-58cc-4372-a567-0e02b2c3d479`) +- Scope: Environment-specific +- Get via: `cobo get /wallets` + +### transaction_id + +Unique identifier for a transaction. + +- Format: UUID +- Scope: Environment-specific +- Get via: `cobo get /transactions` or returned on creation + +### request_id + +Client-provided idempotency key for transactions. + +- Format: String (you define it) +- Purpose: Prevents duplicate transactions +- Best practice: Include timestamp: `tx-$(date +%s)-$(openssl rand -hex 4)` + +### chain_id + +Blockchain network identifier. + +> **Important**: Chain availability differs by wallet type and environment. Always check supported/enabled chains first: +> ```bash +> cobo get /wallets/chains --wallet_type Custodial +> ``` + +Using following command to get enabled chains +> ```bash +> cobo get /wallets/enabled_chains --wallet_type Custodial +> ` +> + +### token_id + +Token identifier combining chain and token. + +> **Important**: Token availability differs by wallet type and environment. Always check supported/enabled tokens first: + +Using following command to get supported chains +> ```bash +> cobo get /wallets/tokens --wallet_type Custodial +> ``` + +Using following command to get enabled chains +> ```bash +> cobo get /wallets/enabled_tokens --wallet_type Custodial +> ` + + +### vault_id + +MPC vault identifier (for MPC wallets only). + +- Required when creating MPC wallets +- Get via: `cobo get /wallets/mpc/vaults --vault_type Org-Controlled` (not `/vaults`) + +### address_id + +Address identifier within a wallet. + +- Get via: `cobo get /wallets/{wallet_id}/addresses` + +## Transaction Lifecycle + +- Use [Transaction Statuses](https://www.cobo.com/developers/v2/guides/transactions/status.md) - primary reference for transaction statuses +- Use [Custodial Wallets Transaction Flow](https://www.cobo.com/developers/v2/guides/transactions/transaction-process-custodial.md) - primary reference for transaction status flow for Custodial Wallets (Asset Wallets) +- Use [MPC and Web3 Wallets Transaction Flow](https://www.cobo.com/developers/v2/guides/transactions/transaction-process-mpc.md) - primary reference for transaction status flow for MPC Wallets and Web3 Wallets + +### Transaction Types, Sources and Destinations +Use [Transaction Types, Sources and Destinations](https://www.cobo.com/developers/v2/guides/transactions/sources-and-destinations.md) as the primary reference. + +## Fees + +### Fee Levels + +| Level | Description | +|-------|-------------| +| `Slow` | Lower fee, longer confirmation | +| `Average` | Standard fee and time | +| `Fast` | Higher fee, faster confirmation | + +### Fee Estimation + +Use [Fee Estimation](https://www.cobo.com/developers/v2/guides/transactions/estimate-fees.md) as the primary reference. + +```bash +cobo post /transactions/estimate_fee \ + --request_id "fee-estimate-1" \ + --source '{"source_type":"Wallet","wallet_id":"..."}' \ + --token_id "ETH" \ + --destination '{"destination_type":"Address","address":"...","amount":"0.01"}' +``` + +## Webhooks + +Use [Webhook Event Type](https://www.cobo.com/developers/v2/guides/webhooks-callbacks/webhook-event-type.md) as the primary reference. + +### Webhook Flow + +1. Configure endpoint: `cobo post /webhooks/endpoints --url "..." --events "[...]"` +2. Cobo sends POST with event data +3. Your endpoint returns 200 OK +4. Cobo retries on failure + +## API Versioning + +Current version: v2 (WaaS 2.0) + +Base URLs: +- Dev: `https://api.dev.cobo.com/v2` +- Prod: `https://api.cobo.com/v2` +- Sandbox: `https://api.sandbox.cobo.com/v2` + +## Pagination + +| Parameter | Description | +|-----------|-------------| +| `limit` | Items per page (1-100, default 10) | +| `before` | Cursor for previous page | +| `after` | Cursor for next page | + +Response includes pagination info: +```json +{ + "data": [...], + "pagination": { + "before": "cursor_prev", + "after": "cursor_next", + "total_count": 100 + } +} +``` diff --git a/skills/cobo-waas/references/diagnostics.md b/skills/cobo-waas/references/diagnostics.md new file mode 100644 index 0000000..7e53329 --- /dev/null +++ b/skills/cobo-waas/references/diagnostics.md @@ -0,0 +1,314 @@ +# State Inspection and Diagnostics + +This guide helps agents verify current Cobo CLI setup state and diagnose issues. + +## Quick Diagnostics Checklist + +Run these commands in order to assess the current state: + +```bash +# 1. CLI Installation +cobo version +# Expected: cobo-cli, version X.Y.Z + +# 2. Configuration State +cobo config list +# Expected: Shows env, auth_method, api_key, api_secret + +# 3. API Connectivity +cobo get /wallets --limit 1 +# Expected: JSON response (may be empty array) + +# 4. Auth Validation +cobo get /wallets/{any_valid_wallet_id} +# Expected: Wallet details or 404 (not 401/403) +``` + +## Diagnostic Commands + +### Check CLI Installation + +```bash +cobo version +``` + +| Output | State | Action | +|--------|-------|--------| +| `cobo-cli, version X.Y.Z` | Installed | Continue | +| `command not found` | Not installed | `pip install cobo-cli` | +| `ModuleNotFoundError` | Broken install | `pip uninstall cobo-cli && pip install cobo-cli` | + +### Check Configuration + +```bash +cobo config list +``` + +| Output | State | Action | +|--------|-------|--------| +| Shows env, auth_method, keys | Configured | Continue | +| Empty or minimal | Not configured | Run setup | +| Missing api_key/api_secret | Keys not generated | `cobo keys generate --key-type API` | +| Missing auth_method | Auth not set | `cobo auth apikey` | + +### Check Environment + +```bash +cobo config list | grep "^environment:" +``` + +| Output | Environment | API Host | +|------------------------|-------------|----------| +| `environment: sandbox` | Sandbox | api.sandbox.cobo.com | +| `environment: dev` | Development | api.dev.cobo.com | +| `environment: prod` | Production | api.cobo.com | +| No output | Not set | `cobo env dev` | + +### Check Auth Method + +```bash +cobo config list | grep "^auth_method:" +``` + +| Output | Method | Use Case | +|-----------------------|--------|----------| +| `auth_method: apikey` | API key | Automation | +| `auth_method: user` | User token | Portal access | +| `auth_method: org` | Org token | Portal apps | +| `auth_method: none` | No auth | Public endpoints | +| No output | Not set | `cobo auth apikey` | + +### Check API Keys + +```bash +# Check if keys exist (without exposing values) +cobo config list | grep -E "^api_(key|secret):" | cut -d= -f1 +``` + +| Output | State | Action | +|--------|-------|--------| +| Both `api_key` and `api_secret` | Keys present | Verify registration | +| Only `api_key` | Secret missing | Regenerate keys | +| Neither | Keys not generated | `cobo keys generate --key-type API` | + +## API Connectivity Tests + +### Basic Connectivity + +```bash +cobo get /wallets --limit 1 +``` + +| Result | Diagnosis | Action | +|--------|-----------|--------| +| JSON response | API working | Continue | +| Connection error | Network issue | Check VPN, proxy, firewall | +| Timeout | Slow network or API | Retry, check status page | + +### Auth Validation + +```bash +cobo get /wallets --limit 1 +``` + +| HTTP Status | Diagnosis | Action | +|-------------|-----------|--------| +| 200 | Auth working | Setup complete | +| 401 | Auth failed | Check keys, register in Portal | +| 403 | Permissions | Update key permissions in Portal | +| 404 | Endpoint issue | Check API version | + +### Environment Validation + +```bash +# Verify environment by checking accessible resources +cobo get /wallets --limit 1 +``` + +If you get unexpected results: +- Wallets missing that should exist → Wrong environment +- 401 error → Key not registered in current environment + +## Full Diagnostic Script + +```bash +#!/bin/bash +echo "=== Cobo CLI Diagnostics ===" + +echo -e "\n1. Version:" +cobo version 2>&1 + +echo -e "\n2. Environment:" +cobo config list | grep "^environment:" || echo "NOT SET" + +echo -e "\n3. Auth Method:" +cobo config list | grep "^auth_method:" || echo "NOT SET" + +echo -e "\n4. API Keys:" +cobo config list | grep -E "^api_(key|secret):" | cut -d= -f1 | sort + +echo -e "\n5. Connectivity Test:" +cobo get /wallets --limit 1 2>&1 | head -5 + +echo -e "\n=== End Diagnostics ===" +``` + +## State Matrix + +| Env Set | Auth Set | Keys Present | Keys Registered | State | +|---------|----------|--------------|-----------------|-------| +| No | No | No | - | Fresh install | +| Yes | No | No | - | Partial setup | +| Yes | Yes | No | - | Missing keys | +| Yes | Yes | Yes | No | Keys not registered | +| Yes | Yes | Yes | Yes | Ready | + +## Common Diagnostic Scenarios + +### Scenario 1: Fresh Install + +**Symptoms**: `cobo config list` shows empty or minimal output + +**Diagnosis**: +```bash +cobo config list +# Output: (empty or only default values) +``` + +**Resolution**: +```bash +cobo env dev +cobo keys generate --key-type API +cobo open developer # Register key +cobo auth apikey +``` + +### Scenario 2: Keys Not Registered + +**Symptoms**: 401 errors, but keys exist in config + +**Diagnosis**: +```bash +# Keys exist +cobo config list | grep api_key +# Output: api_key=abc123... + +# But auth fails +cobo get /wallets --limit 1 +# Output: 401 Unauthorized +``` + +**Resolution**: +```bash +cobo open developer +# Register the public key shown in config +``` + +### Scenario 3: Wrong Environment + +**Symptoms**: Resources not found that should exist + +**Diagnosis**: +```bash +# Check environment +cobo config list | grep env +# Output: env=dev + +# But resources are in prod +# Switch environment or verify where resources exist +``` + +**Resolution**: +```bash +cobo env prod # If resources are in prod +# Or verify resources in dev +cobo get /wallets --limit 50 +``` + +### Scenario 4: Permissions Issue + +**Symptoms**: 403 Forbidden on specific operations + +**Diagnosis**: +```bash +# List works +cobo get /wallets --limit 1 +# Output: 200 OK + +# But create fails +cobo post /wallets --name "Test" --wallet_type Custodial --wallet_subtype Asset +# Output: 403 Forbidden +``` + +**Resolution**: +```bash +cobo open developer +# Edit API key permissions to include required scopes +``` + +## Environment-Specific Checks + +### Development Environment + +```bash +cobo env dev +cobo config list | grep env +# Expected: env=dev + +cobo get /wallets --limit 1 +# Should work with dev-registered key +``` + +### Production Environment + +```bash +cobo env prod +cobo config list | grep env +# Expected: env=prod + +# CAUTION: Production affects real assets +cobo get /wallets --limit 1 +# Should work with prod-registered key +``` + +## Diagnostic Output Format + +When reporting diagnostic results, use this format: + +```json +{ + "diagnostics": { + "cli_installed": true, + "cli_version": "1.2.3", + "env": "dev", + "auth_method": "apikey", + "keys_present": true, + "keys_registered": true, + "api_connectivity": "ok", + "issues": [] + }, + "next_steps": [] +} +``` + +Example with issues: + +```json +{ + "diagnostics": { + "cli_installed": true, + "cli_version": "1.2.3", + "env": "dev", + "auth_method": "apikey", + "keys_present": true, + "keys_registered": false, + "api_connectivity": "401", + "issues": ["API key not registered in Portal"] + }, + "next_steps": [ + "Run: cobo open developer", + "Add the public key as an API key", + "Verify with: cobo get /wallets --limit 1" + ] +} +``` diff --git a/skills/cobo-waas/references/errors.md b/skills/cobo-waas/references/errors.md new file mode 100644 index 0000000..470602f --- /dev/null +++ b/skills/cobo-waas/references/errors.md @@ -0,0 +1,33 @@ +# Errors and Troubleshooting + +Use [Error codes](https://www.cobo.com/developers/v2/guides/overview/error-codes.md) as the primary reference for error catalog with diagnosis and recovery solutions. + +## Debug Commands + +```bash +# Enable debug output (shows request/response details) +cobo --enable-debug get /wallets + +# Check config state +cobo config list + +# Validate keypair +cobo keys validate --alg ed25519 \ + --secret --pubkey + +# Test connectivity +cobo get /wallets --limit 1 + +# Check endpoint documentation +cobo doc /wallets +``` + +## Getting Help + +If errors persist: + +1. **Enable debug**: `cobo --enable-debug ` +2. **Check docs**: `cobo doc /endpoint` +3. **Review logs**: `cobo logs tail` +4. **Open Portal**: `cobo open portal` +5. **Contact support**: Include error message and debug output diff --git a/skills/cobo-waas/references/flows.md b/skills/cobo-waas/references/flows.md new file mode 100644 index 0000000..30c0d02 --- /dev/null +++ b/skills/cobo-waas/references/flows.md @@ -0,0 +1,67 @@ +# Use-case flows + +## Help-first tip +- Use `cobo --help` or `cobo --help` whenever a command or flag is unclear. + +## Configure environment and auth +1) Set environment: + `cobo env sandbox|dev|prod` +2) Set auth method: + `cobo auth apikey|user|org|none` +3) Verify config: + `cobo config list` + +## Keypair lifecycle +1) Generate API keys: + `cobo keys generate --key-type API` +2) Register public key in Portal (Developer Console). + `cobo open developer` +3) Validate keypair (optional): + `cobo keys validate --alg ed25519 --secret --pubkey ` + +## Discover and describe endpoints +1) **Look up API parameters (do this before generating requests):** + `cobo doc ` e.g. `cobo doc /wallets` — shows parameters, types, required/optional for that path. +2) List operations: + `cobo get --list` / `cobo post --list` +3) Describe an endpoint: + `cobo get /path --describe` or `cobo post /path --describe` +4) Describe a specific parameter: + `cobo post /wallets --describe --wallet_type` + +## Wallet operations +- List wallets: + `cobo get /wallets` +- Create wallet (confirm via --describe): + `cobo post /wallets --name "Test Wallet" --wallet_type MPC --wallet_subtype Org-Controlled` +- Get wallet details: + `cobo get /wallets/` + +## Address validation +- Validate destination address: + `cobo get /wallets/check_address_validity --address "" --chain_id ""` + +## Create transfer +- Create transfer (confirm via --describe): + `cobo post /transactions/transfer --request_id "tx-" --token_id "" --source '{"source_type":"Wallet","wallet_id":""}' --destination '{"destination_type":"Address","address":"","amount":""}'` + +## Check transfer status +- Get transaction status: + `cobo get /transactions/` + +## Webhook debugging +- List event types: + `cobo webhook events` +- Listen to events: + `cobo webhook listen --events "wallets.transaction.succeeded"` +- Trigger a test event: + `cobo webhook trigger wallets.transaction.succeeded` + +## Inspect logs +- Tail logs: + `cobo logs tail` +- Filter by endpoint: + `cobo logs tail --request-path /v2/transactions` + +## GraphQL queries (advanced) +- See `references/graphql.md` for templates. diff --git a/skills/cobo-waas/references/graphql.md b/skills/cobo-waas/references/graphql.md new file mode 100644 index 0000000..9775971 --- /dev/null +++ b/skills/cobo-waas/references/graphql.md @@ -0,0 +1,21 @@ +# GraphQL usage (advanced) + +## Basic usage +- Inline query (JSON with query key): + `cobo graphql -q '{"query":"{ wallet(walletId:) { walletId name } }"}'` + +## With variables +- Query: + `cobo graphql -q 'query($id:String!){ wallet(walletId:$id){ walletId name } }' -v '{"id":""}'` + +## From file +- Save a query in `query.graphql` and run: + `cobo graphql -f ./query.graphql -v '{"id":""}'` + +## Output formatting +- Raw JSON output: + `cobo graphql -q '{"query":"{ wallet(walletId:) { walletId name } }"}' --raw` + +## Tips +- Ensure auth and env are set (`cobo auth ...`, `cobo env ...`). +- If a query fails, add `--enable-debug` to see request details. diff --git a/skills/cobo-waas/references/onboarding.md b/skills/cobo-waas/references/onboarding.md new file mode 100644 index 0000000..408e6b5 --- /dev/null +++ b/skills/cobo-waas/references/onboarding.md @@ -0,0 +1,200 @@ +# First-Time Setup Guide + +This guide helps agents onboard new users to Cobo CLI with a decision-tree approach. + +## Quick State Detection + +Run these commands to determine current setup state: + +```bash +# 1. Is CLI installed? +cobo version +# Expected: cobo-cli, version X.Y.Z +# If fails: pip install cobo-cli + +# 2. Is it configured? +cobo config list +# Expected: Shows env, auth_method, api_key/api_secret +# If empty: Needs configuration + +# 3. Does auth work? +cobo get /wallets --limit 1 +# Expected: JSON response with wallets array +# If 401/403: Auth not configured or key not registered +``` + +## Decision Tree + +``` +Is CLI installed? (cobo version) +├── No → pip install cobo-cli → Continue +└── Yes + ↓ +Is environment set? (cobo config list → check 'env') +├── No → Choose environment (see below) → Continue +└── Yes + ↓ +Is auth method set? (cobo config list → check 'auth_method') +├── No → Choose auth method (see below) → Continue +└── Yes + ↓ +Does auth work? (cobo get /wallets --limit 1) +├── 401/403 → Key not registered in Portal → Register key +├── Connection error → Check network/VPN +└── Success → Ready to use! +``` + +## Environment Selection + +| Environment | Host | Use Case | +|-------------|------|----------| +| `sandbox` | api.sandbox.cobo.com | Isolated testing, no real assets | +| `dev` | api.dev.cobo.com | Development with test assets | +| `prod` | api.cobo.com | Production with real assets | + +**Recommendation**: Start with `dev` for most development work. + +```bash +# Set environment +cobo env dev + +# Verify +cobo config list +``` + +## Auth Method Selection + +| Method | Use Case | Requires | +|--------|----------|----------| +| `apikey` | Automation, scripts, CI/CD | API keypair registered in Portal | +| `user` | Interactive Portal access | Browser login | +| `org` | Portal apps (Cobo Apps) | App context + org login | +| `none` | Public endpoints only | Nothing | + +**Recommendation**: Use `apikey` for automation workflows. + +## API Key Setup (Recommended for Agents) + +### Step 1: Generate Keypair + +```bash +cobo keys generate --key-type API +``` + +Output: +``` +API Key (Public Key): <64-char hex> +API Secret (Private Key): <64-char hex> +``` + +**IMPORTANT**: The secret is only shown once. Store it securely. + +### Step 2: Register Public Key + +**Option A: CLI (dev/sandbox environments, recommended)** + +```bash +# Login first (if not already logged in) +cobo login --user + +# Register the key directly from CLI +cobo keys register +# Or specify a pubkey: cobo keys register --pubkey +``` + +**Option B: Portal (all environments)** + +```bash +# Open Developer Console +cobo open developer +``` + +In the Portal: +1. Go to Developer Console → API Keys +2. Click "Add API Key" +3. Paste the **public key** (NOT the secret) +4. Select permissions (e.g., "All" for development) +5. Save + +### Step 3: Configure CLI Auth + +```bash +# Set auth method +cobo auth apikey + +# Verify config shows api_key and api_secret +cobo config list +``` + +### Step 4: Verify Setup + +```bash +# Test API call +cobo get /wallets --limit 1 +``` + +Expected: JSON response (may be empty array if no wallets exist) + +## Verification Checklist + +Run all checks to confirm setup: + +```bash +# Check 1: CLI version +cobo version + +# Check 2: Environment +cobo config list | grep env + +# Check 3: Auth method +cobo config list | grep auth_method + +# Check 4: API connectivity +cobo get /wallets --limit 1 + +# Check 5: Can create resources (optional) +cobo post /wallets --describe # Should show required params +``` + +## Common First-Time Issues + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `command not found: cobo` | CLI not installed | `pip install cobo-cli` | +| `No api_key in config` | Keys not generated | `cobo keys generate --key-type API` | +| `401 Unauthorized` | Key not registered in Portal | Register public key in Developer Console | +| `403 Forbidden` | Key lacks permissions | Update key permissions in Portal | +| Empty config output | Fresh install | Run environment and auth setup | + +## Quick Start Commands + +For a brand new setup, run these in order: + +```bash +# 1. Install +pip install cobo-cli + +# 2. Set environment +cobo env dev + +# 3. Generate keys +cobo keys generate --key-type API +# Save the output securely! + +# 4. Register key (dev/sandbox: CLI; prod: Portal) +cobo login --user && cobo keys register +# Or for prod: cobo open developer → Add the PUBLIC key as an API key + +# 5. Set auth method +cobo auth apikey + +# 6. Verify +cobo get /wallets --limit 1 +``` + +## Next Steps + +After successful setup: +- Create your first wallet: See `recipes.md` +- Explore API endpoints: `cobo get --list` +- Set up webhooks: `cobo webhook events` diff --git a/skills/cobo-waas/references/recipes.md b/skills/cobo-waas/references/recipes.md new file mode 100644 index 0000000..ca2edd3 --- /dev/null +++ b/skills/cobo-waas/references/recipes.md @@ -0,0 +1,463 @@ +# Recipes: End-to-End Workflows + +Complete workflows for common Cobo WaaS tasks. Each recipe includes prerequisites, steps, permissions, and verification. + +## Permissions Quick Reference + +| Recipe | Required Permissions | +|--------|--------------------------------------------------------| +| 1. First API Call | Wallet - Read | +| 2. Custodial Wallet | Wallet - Read/Write, Address - Read/Write | +| 3. MPC Wallet | Wallet - Read/Write, Address - Write, MPC Vault - Read | +| 4. Transfer | Transaction - Write, Wallet - Read | +| 5. Monitor TX | Transaction - Read | +| 6. Debug TX | Transaction - Read, Wallet - Read | +| 7. Webhooks | Webhook - Read/Write | + +Configure permissions: `cobo open developer` → Edit API Key → Permissions + +--- + +## Recipe 1: First API Call + +Minimal path from fresh install to successful API call. + +### Prerequisites +- Python 3.8+ +- Internet connection + +### Steps + +```bash +# 1. Install CLI +pip install cobo-cli + +# 2. Verify installation +cobo version + +# 3. Set environment (dev for testing) +cobo env dev + +# 4. Generate API keys +cobo keys generate --key-type API +# SAVE THE OUTPUT - secret shown only once! + +# 5. Register key in Portal +cobo open developer +# In Portal: Developer Console → API Keys → Add Key +# Paste the PUBLIC key, select permissions, save + +# 6. Set auth method +cobo auth apikey + +# 7. Verify setup +cobo config list + +# 8. Make first API call +cobo get /wallets --limit 1 +``` + +### Expected Output +```json +{ + "data": [], + "pagination": { ... } +} +``` + +### Verification +- No errors +- JSON response (even if empty data array) + +--- + +## Recipe 2: Create Custodial Wallet and Address + +Create a Custodial wallet and generate an address for deposits. + +### Prerequisites +- Completed Recipe 1 (working API auth) + +### Permissions Required +- Wallet - Read, Write +- Address - Write + +### Steps + +**Tip:** Before building the request, run `cobo doc /wallets` to see exact parameter names and types for this endpoint. + +```bash +# 1. Create Custodial Asset wallet +cobo post /wallets \ + --wallet_type Custodial \ + --wallet_subtype Asset \ + --name "My Treasury Wallet" + +# Response includes wallet_id - save it! +# Example: "wallet_id": "f47ac10b-58cc-..." +``` + +```bash +# 2. Store wallet_id +WALLET_ID="" + +# 3. Check supported chains for this wallet (IMPORTANT!) +cobo get /wallets/$WALLET_ID/chains +# Dev/Sandbox: Custodial uses COBO_ETH, not ETH +# Production: Uses standard chain IDs (ETH, BTC, etc.) + +# 4. Create an address (use correct chain_id for your environment) +cobo post /wallets/$WALLET_ID/addresses \ + --chain_id COBO_ETH \ # For dev/sandbox + # --chain_id ETH \ # For production + --count 1 + +# Response includes address and address_id + +# 5. Verify address was created +cobo get /wallets/$WALLET_ID/addresses +``` + +### Expected Output + +Wallet creation: +```json +{ + "wallet_id": "f47ac10b-...", + "wallet_type": "Custodial", + "wallet_subtype": "Asset", + "name": "My Treasury Wallet" +} +``` + +Address creation: +```json +{ + "address_id": "abc123...", + "address": "0x1234...", + "chain_id": "ETH" +} +``` + +### Verification +```bash +# List wallets - should include new wallet +cobo get /wallets --wallet_type Custodial + +# List addresses - should include new address +cobo get /wallets/$WALLET_ID/addresses +``` + +--- + +## Recipe 3: Create MPC Org-Controlled Wallet + +Create an MPC wallet with organization control. + +### Prerequisites +- Completed Recipe 1 +- MPC vault exists (created in Portal) + +### Permissions Required +- Wallet - Read, Write +- Address - Write +- MPC Vault - Read + +### Steps + +```bash +# 1. List available vaults (NOTE: endpoint is /wallets/mpc/vaults, not /vaults) +cobo get /wallets/mpc/vaults + +# Save vault_id from response +VAULT_ID="" + +# 2. Create MPC Org-Controlled wallet +cobo post /wallets \ + --wallet_type MPC \ + --wallet_subtype Org-Controlled \ + --name "MPC Treasury" \ + --vault_id $VAULT_ID + +# Save wallet_id +WALLET_ID="" + +# 3. Check supported chains (IMPORTANT!) +cobo get /wallets/$WALLET_ID/chains +# Dev/Sandbox: MPC uses ANVIL_SETH (Sepolia), not ETH +# Production: Uses standard chain IDs + +# 4. Create address (use correct chain_id for your environment) +cobo post /wallets/$WALLET_ID/addresses \ + --chain_id ANVIL_SETH \ # For dev/sandbox + # --chain_id ETH \ # For production + --count 1 + +# 5. Verify +cobo get /wallets/$WALLET_ID +cobo get /wallets/$WALLET_ID/addresses +``` + +### Common Issues + +**Vault not found (404 on /vaults):** +- Use `/wallets/mpc/vaults`, not `/vaults` + +**No vaults returned:** +1. Open Portal: `cobo open portal` +2. Navigate to MPC Wallets → Vaults +3. Create a new vault or use existing + +**Chain not enabled:** +- Check supported chains with `cobo get /wallets/wallets/enabled_chains --wallet_type MPC` +- MPC wallets in dev/sandbox typically use `ANVIL_SETH` + +--- + +## Recipe 4: Execute a Transfer + +Transfer tokens from one address to another. + +### Prerequisites +- Source wallet with funds +- Destination address +- Working API auth + +### Permissions Required +- Transaction - Write +- Wallet - Read + +> **Note**: The CLI has limitations with complex JSON bodies. For production automation, consider using the SDK instead (see `references/samples/python.md`). + +### Steps + +```bash +# 1. Set variables +SOURCE_WALLET_ID="" +DESTINATION_ADDRESS="0x..." +TOKEN_ID="ETH" +AMOUNT="0.01" + +# 2. Check source wallet balance +cobo get /wallets/$SOURCE_WALLET_ID/token_balances + +# 3. Validate destination address +cobo get /wallets/check_address_validity \ + --chain_id ETH \ + --address $DESTINATION_ADDRESS + +# 4. Estimate fees +cobo post /transactions/estimate_fee \ + --request_id "fee-$(date +%s)" \ + --source "{\"source_type\":\"Asset\",\"wallet_id\":\"$SOURCE_WALLET_ID\"}" \ + --token_id $TOKEN_ID \ + --destination "{\"destination_type\":\"Address\",\"account_output\":{\"address\":\"$DESTINATION_ADDRESS\",\"amount\":\"$AMOUNT\"}}" + +# 5. Create transfer with unique request_id +REQUEST_ID="tx-$(date +%s)-$(openssl rand -hex 4)" + +cobo post /transactions/transfer \ + --request_id $REQUEST_ID \ + --source "{\"source_type\":\"Asset\",\"wallet_id\":\"$SOURCE_WALLET_ID\"}" \ + --token_id $TOKEN_ID \ + --destination "{\"destination_type\":\"Address\",\"account_output\":{\"address\":\"$DESTINATION_ADDRESS\",\"amount\":\"$AMOUNT\"}}" + +# Save transaction_id from response +TRANSACTION_ID="" + +# 6. Check status +cobo get /transactions/$TRANSACTION_ID +``` + +### Transaction States +Use [Transaction Statuses](https://www.cobo.com/developers/v2/guides/transactions/status.md) - primary reference for transaction statuses + +### Common Issues + +**Insufficient balance:** +```bash +cobo get /wallets/$SOURCE_WALLET_ID/token_balances +# Ensure balance > amount + estimated fee +``` + +**Pending authorization:** +```bash +# Open Portal to approve +cobo open portal +# Navigate to Transactions → Pending +``` + +--- + +## Recipe 5: Monitor Transaction Status + +Poll for transaction completion. + +### Prerequisites +- Active transaction_id + +### Steps + +```bash +TRANSACTION_ID="" + +# Single status check +cobo get /transactions/$TRANSACTION_ID + +# Polling script +while true; do + STATUS=$(cobo get /transactions/$TRANSACTION_ID | jq -r '.status') + echo "Status: $STATUS" + + if [[ "$STATUS" == "Completed" || "$STATUS" == "Failed" || "$STATUS" == "Rejected" ]]; then + echo "Transaction finished with status: $STATUS" + break + fi + + sleep 10 +done + +# Get full transaction details after completion +cobo get /transactions/$TRANSACTION_ID +``` + +### Alternative: Webhooks + +Instead of polling, set up webhooks: + +```bash +# 1. Create webhook endpoint +cobo post /webhooks/endpoints \ + --url "https://your-server.com/webhook" \ + --events '["wallets.transaction.succeeded","wallets.transaction.failed"]' + +# 2. Listen for events (for testing) +cobo webhook listen --events "transaction.completed" +``` + +--- + +## Recipe 6: Debug a Failed Transaction + +Diagnose why a transaction failed. + +### Prerequisites +- Failed transaction_id + +### Steps + +```bash +TRANSACTION_ID="" + +# 1. Get full transaction details +cobo get /transactions/$TRANSACTION_ID + +# Look for: +# - status: "Failed" or "Rejected" +# - fail_reason: explains the failure +# - sub_status: more detail + +# 2. Check common causes + +# Insufficient balance? +WALLET_ID=$(cobo get /transactions/$TRANSACTION_ID | jq -r '.source.wallet_id') +cobo get /wallets/$WALLET_ID/token_balances + +# Invalid destination? +DEST_ADDRESS=$(cobo get /transactions/$TRANSACTION_ID | jq -r '.destination.address') +CHAIN_ID=$(cobo get /transactions/$TRANSACTION_ID | jq -r '.chain_id') +cobo get /wallets/check_address_validity --chain_id $CHAIN_ID --address $DEST_ADDRESS + +# 3. Check API logs +cobo logs tail + +# 4. Enable debug for more details +cobo --enable-debug get /transactions/$TRANSACTION_ID +``` + +### Common Failure Reasons + +| Reason | Cause | Fix | +|--------|-------|-----| +| `insufficient_balance` | Not enough funds | Fund wallet | +| `invalid_address` | Bad destination | Validate address | +| `policy_rejected` | Risk policy blocked | Review in Portal | +| `signing_failed` | MPC signing issue | Check MPC setup | + +--- + +## Recipe 7: Set Up Webhook Listener + +Receive real-time notifications for events. + +### Prerequisites +- Working API auth +- Publicly accessible URL (for production) or local listener (for testing) + +### Permissions Required +- Webhook - Read + +### Steps + +#### For Testing (Local Listener) + +```bash +# 1. Start local webhook listener +cobo webhook listen --events "wallets.transaction.succeeded,wallets.transaction.failed" + +# This creates a temporary tunnel and registers the webhook + +# 2. Trigger a test event +cobo webhook trigger wallets.transaction.succeeded + +# 3. Observe the event in the listener output +``` + +#### For Production + +```bash +# 1. List available event types +cobo webhook events +# Or via API: cobo get /webhooks/event_definitions + +# 2. Create webhook endpoint +cobo post /webhooks/endpoints \ + --url "https://your-server.com/webhook" \ + --events '["wallets.transaction.created","wallets.transaction.succeeded","wallets.transaction.failed"]' + +# Save endpoint_id +ENDPOINT_ID="" + +# 3. Verify endpoint +cobo get /webhooks/endpoints/$ENDPOINT_ID + +# 4. Test with a transaction +# (perform a transfer and watch for webhook) +``` + +### Webhook Payload Example + +```json +{ + "event_type": "wallets.transaction.succeeded", + "event_id": "evt_...", + "created_timestamp": 1234567890, + "data": { + "transaction_id": "...", + "wallet_id": "...", + "status": "Completed" + } +} +``` + +## Quick Reference Table + +| Task | Recipe | +|------|--------| +| First-time setup | Recipe 1 | +| Create Custodial wallet | Recipe 2 | +| Create MPC wallet | Recipe 3 | +| Transfer tokens | Recipe 4 | +| Monitor transaction | Recipe 5 | +| Debug failure | Recipe 6 | +| Set up webhooks | Recipe 7 | diff --git a/skills/cobo-waas/references/sdk-patterns.md b/skills/cobo-waas/references/sdk-patterns.md new file mode 100644 index 0000000..573f13d --- /dev/null +++ b/skills/cobo-waas/references/sdk-patterns.md @@ -0,0 +1,440 @@ +# SDK Patterns: Vibe Coding Guide + +Guide for generating SDK code quickly. Helps agents translate CLI operations to SDK code. + +## Code generation rules (must follow) +**Must be kept when generating code:** use the **template** of the corresponding language as the single source of truth. Template path: 'assets/templates/python.md', 'ts-node.md, 'go.md', 'java.md'. The code structure and style must be consistent with the template (Setup is used briefly for one operation). No additional encapsulation functions, no guessing parameters, and no forced serialization to dict/JSON are required unless explicitly requested by the user. For details, see SKILL.md "SDK Code Generation Specification 」. + +## When to Use CLI vs SDK + +| Scenario | Recommendation | Reason | +|----------|----------------|--------| +| Quick exploration | CLI | Faster iteration | +| One-off operations | CLI | No code needed | +| Automation scripts | SDK | Better error handling | +| Production services | SDK | Type safety, retries | +| Complex workflows | SDK | State management | +| Webhook handlers | SDK | Server integration | + +**Rule of thumb**: Use CLI for exploration and testing, SDK for production code. + +## Environment Setup Patterns + +All SDKs should read configuration from environment variables for security. + +### Environment Variables + +| Variable | Purpose | +|----------|---------| +| `COBO_API_SECRET` | Private key (hex string) | +| `COBO_API_KEY` | Public key (hex string, optional) | +| `COBO_ENV` | Environment: `dev`, `prod`, `sandbox` | +| `COBO_API_HOST` | Custom API host (optional) | + +### Python Setup + +```python +import os + +# Required +api_secret = os.environ.get("COBO_API_SECRET") +if not api_secret: + raise ValueError("COBO_API_SECRET not set") + +# Optional with default +env = os.environ.get("COBO_ENV", "dev") +host_map = { + "dev": "https://api.dev.cobo.com/v2", + "prod": "https://api.cobo.com/v2", + "sandbox": "https://api.sandbox.cobo.com/v2", +} +host = os.environ.get("COBO_API_HOST", host_map.get(env)) +``` + +### Node.js Setup + +```javascript +const apiSecret = process.env.COBO_API_SECRET; +if (!apiSecret) { + throw new Error("COBO_API_SECRET not set"); +} + +const env = process.env.COBO_ENV || "dev"; +``` + +### Go Setup + +```go +apiSecret := os.Getenv("COBO_API_SECRET") +if apiSecret == "" { + log.Fatal("COBO_API_SECRET not set") +} + +env := os.Getenv("COBO_ENV") +if env == "" { + env = "dev" +} +``` + +## CLI to SDK Translation + +### List Wallets + +**CLI:** +```bash +cobo get /wallets --wallet_type Custodial --limit 10 +``` + +**Python:** +```python +from cobo_waas2.api import WalletsApi + +wallets_api = WalletsApi(api_client) +response = wallets_api.list_wallets( + wallet_type="Custodial", + limit=10 +) +for wallet in response.data: + print(wallet.wallet_id, wallet.name) +``` + +**Node.js:** +```javascript +const walletsApi = new CoboWaas2.WalletsApi(); +const response = await walletsApi.listWallets({ + walletType: "Custodial", + limit: 10 +}); +response.data.forEach(wallet => { + console.log(wallet.wallet_id, wallet.name); +}); +``` + +### Create Custodial Wallet + +**CLI:** +```bash +cobo post /wallets --wallet_type Custodial --wallet_subtype Asset --name "My Wallet" +``` + +**Python:** +```python +from cobo_waas2.models import CreateWalletParams, CreateCustodialWalletParams, WalletType, WalletSubtype + +params = CreateWalletParams(actual_instance=CreateCustodialWalletParams( + name="My Wallet", + wallet_type=WalletType.CUSTODIAL, + wallet_subtype=WalletSubtype.ASSET +)) +response = wallets_api.create_wallet(create_wallet_params=params) +wallet = response.actual_instance +print(f"Created: {wallet.wallet_id}") +``` + +**Node.js:** +```javascript +const params = { + name: "My Wallet", + wallet_type: "Custodial", + wallet_subtype: "Asset" +}; +const wallet = await walletsApi.createWallet(params); +console.log(`Created: ${wallet.wallet_id}`); +``` + +### Create Address + +**CLI:** +```bash +cobo post /wallets/{wallet_id}/addresses --chain_id ETH +``` + +**Python:** +```python +from cobo_waas2.models import CreateAddressRequest + +response = wallets_api.create_address( + wallet_id=wallet_id, + create_address_request=CreateAddressRequest(chain_id="ETH", count=1) +) +address = response.data[0] # Response is a list +print(f"Address: {address.address}") +``` + +### Create Transfer + +**CLI:** +```bash +cobo post /transactions/transfer \ + --request_id "tx-123" \ + --source '{"source_type":"Wallet","wallet_id":"..."}' \ + --token_id "ETH" \ + --destination '{"destination_type":"Address","account_output":{"address":"0x...","amount":"0.01"}}' +``` + +**Python:** +```python +import time +import os +from cobo_waas2.api import TransactionsApi +from cobo_waas2.models import TransferParams, TransferSource, TransferDestination, WalletSubtype, CustodialTransferSource, AddressTransferDestination, AddressTransferDestinationAccountOutput + +tx_api = TransactionsApi(api_client) + +params = TransferParams( + request_id=f"tx-{int(time.time())}-{os.urandom(4).hex()}", + source=TransferSource( + actual_instance=CustodialTransferSource( + source_type=WalletSubtype.ASSET, + wallet_id=wallet_id + ) + ), + token_id="ETH", + destination=TransferDestination( + actual_instance=AddressTransferDestination( + destination_type="Address", + account_output=AddressTransferDestinationAccountOutput( + address="0x...", + amount="0.01" + ) + ) + ) +) + +response = tx_api.create_transfer_transaction(transfer_params=params) +tx = response.actual_instance +print(f"TX: {tx.transaction_id}, Status: {tx.status}") +``` + +### Get Transaction Status + +**CLI:** +```bash +cobo get /transactions/{transaction_id} +``` + +**Python:** +```python +tx = transactions_api.get_transaction(transaction_id=transaction_id) +print(f"Status: {tx.status}") +``` + +## Error Handling Patterns + +### Python + +```python +from cobo_waas2.exceptions import ApiException + +try: + wallet = wallets_api.get_wallet_by_id(wallet_id="invalid") +except ApiException as e: + print(f"Error {e.status}: {e.reason}") + print(f"Body: {e.body}") + + if e.status == 401: + print("Authentication failed - check API key") + elif e.status == 404: + print("Resource not found") + elif e.status == 429: + print("Rate limited - wait and retry") +``` + +### Node.js + +```javascript +try { + const wallet = await walletsApi.getWalletById("invalid"); +} catch (error) { + if (error.response) { + console.log(`Error ${error.response.status}: ${error.response.statusText}`); + console.log(`Body: ${JSON.stringify(error.response.data)}`); + + switch (error.response.status) { + case 401: + console.log("Authentication failed - check API key"); + break; + case 404: + console.log("Resource not found"); + break; + case 429: + console.log("Rate limited - wait and retry"); + break; + } + } else { + console.log(`Network error: ${error.message}`); + } +} +``` + +### Go + +```go +wallet, _, err := client.WalletsAPI.GetWalletById(ctx, "invalid").Execute() +if err != nil { + if apiErr, ok := err.(*cobo_waas2.GenericOpenAPIError); ok { + fmt.Printf("Error: %s\n", apiErr.Error()) + fmt.Printf("Body: %s\n", string(apiErr.Body())) + } + return +} +``` + +## Async Patterns for Long Operations + +Transactions can take time to complete. Use polling or webhooks. + +### Polling Pattern (Python) + +```python +import time + +def wait_for_transaction(transaction_id, timeout=300, interval=10): + """Poll until transaction reaches terminal state.""" + terminal_states = {"Completed", "Failed", "Rejected", "Cancelled"} + start = time.time() + + while time.time() - start < timeout: + tx = transactions_api.get_transaction(transaction_id=transaction_id) + print(f"Status: {tx.status}") + + if tx.status in terminal_states: + return tx + + time.sleep(interval) + + raise TimeoutError(f"Transaction {transaction_id} did not complete") + +# Usage +tx = wait_for_transaction(transaction_id) +if tx.status == "Completed": + print("Success!") +else: + print(f"Failed: {tx.fail_reason}") +``` + +### Polling Pattern (Node.js) + +```javascript +async function waitForTransaction(transactionId, timeout = 300000, interval = 10000) { + const terminalStates = new Set(["Completed", "Failed", "Rejected", "Cancelled"]); + const start = Date.now(); + + while (Date.now() - start < timeout) { + const tx = await transactionsApi.getTransaction(transactionId); + console.log(`Status: ${tx.status}`); + + if (terminalStates.has(tx.status)) { + return tx; + } + + await new Promise(resolve => setTimeout(resolve, interval)); + } + + throw new Error(`Transaction ${transactionId} did not complete`); +} +``` + +## Webhook Handler Pattern + +### Python (Flask) + +```python +from flask import Flask, request, jsonify +import hmac +import hashlib + +app = Flask(__name__) +WEBHOOK_SECRET = os.environ.get("COBO_WEBHOOK_SECRET") + +@app.route("/webhook", methods=["POST"]) +def handle_webhook(): + # Verify signature + signature = request.headers.get("X-Cobo-Signature") + body = request.get_data() + + expected = hmac.new( + WEBHOOK_SECRET.encode(), + body, + hashlib.sha256 + ).hexdigest() + + if not hmac.compare_digest(signature, expected): + return jsonify({"error": "Invalid signature"}), 401 + + # Process event + event = request.json + event_type = event.get("event_type") + + if event_type == "wallets.transaction.succeeded": + tx_id = event["data"]["transaction_id"] + print(f"Transaction {tx_id} completed") + # Handle completion + + elif event_type == "wallets.transaction.failed": + tx_id = event["data"]["transaction_id"] + print(f"Transaction {tx_id} failed") + # Handle failure + + return jsonify({"status": "ok"}) +``` + +### Node.js (Express) + +```javascript +const express = require("express"); +const crypto = require("crypto"); + +const app = express(); +app.use(express.json()); + +const WEBHOOK_SECRET = process.env.COBO_WEBHOOK_SECRET; + +app.post("/webhook", (req, res) => { + // Verify signature + const signature = req.headers["x-cobo-signature"]; + const body = JSON.stringify(req.body); + + const expected = crypto + .createHmac("sha256", WEBHOOK_SECRET) + .update(body) + .digest("hex"); + + if (signature !== expected) { + return res.status(401).json({ error: "Invalid signature" }); + } + + // Process event + const { event_type, data } = req.body; + + switch (event_type) { + case "wallets.transaction.succeeded": + console.log(`Transaction ${data.transaction_id} completed`); + break; + case "wallets.transaction.failed": + console.log(`Transaction ${data.transaction_id} failed`); + break; + } + + res.json({ status: "ok" }); +}); +``` + +## Common SDK Operations Matrix + +| Operation | Python | Node.js | Go | +|-----------|--------|---------|-----| +| List wallets | `wallets_api.list_wallets()` | `walletsApi.listWallets()` | `client.WalletsAPI.ListWallets()` | +| Get wallet | `wallets_api.get_wallet_by_id(id)` | `walletsApi.getWalletById(id)` | `client.WalletsAPI.GetWalletById(id)` | +| Create wallet | `wallets_api.create_wallet(params)` | `walletsApi.createWallet(params)` | `client.WalletsAPI.CreateWallet(params)` | +| List addresses | `wallets_api.list_addresses(id)` | `walletsApi.listAddresses(id)` | `client.WalletsAPI.ListAddresses(id)` | +| Create address | `wallets_api.create_address(id, params)` | `walletsApi.createAddress(id, params)` | `client.WalletsAPI.CreateAddress(id, params)` | +| Create transfer | `transactions_api.create_transfer(params)` | `transactionsApi.createTransfer(params)` | `client.TransactionsAPI.CreateTransfer(params)` | +| Get transaction | `transactions_api.get_transaction(id)` | `transactionsApi.getTransaction(id)` | `client.TransactionsAPI.GetTransaction(id)` | + +## SDK Reference Links + +- **Python**: See `samples/python.md` for complete examples +- **Node.js**: See `samples/ts-node.md` for complete examples +- **Go**: See `samples/go.md` for complete examples +- **Java**: See `samples/java.md` for complete examples diff --git a/skills/cobo-waas/references/security.md b/skills/cobo-waas/references/security.md new file mode 100644 index 0000000..b22a72c --- /dev/null +++ b/skills/cobo-waas/references/security.md @@ -0,0 +1,236 @@ +# Credential Handling and Security + +This document provides security guardrails for AI agents working with Cobo CLI and WaaS APIs. + +## Never-Do List + +**NEVER do any of the following:** + +1. **Echo secrets to console or logs** + ```bash + # WRONG - exposes secret + echo $COBO_API_SECRET + cat ~/.cobo/config.toml | grep api_secret + + # RIGHT - only reference existence + cobo config list | grep -q api_secret && echo "Secret is set" + ``` + +2. **Store secrets in git** + ```bash + # WRONG - secrets in repo + git add .cobo/config.toml + git add .env + + # RIGHT - use .gitignore + echo ".cobo/" >> .gitignore + echo ".env" >> .gitignore + ``` + +3. **Include secrets in agent output** + ```json + // WRONG + {"api_secret": "abc123..."} + + // RIGHT + {"api_secret": "[REDACTED]"} + ``` + +4. **Pass secrets as command-line arguments** + ```bash + # WRONG - visible in process list + cobo --api-secret abc123 get /wallets + + # RIGHT - use config file or env vars + export COBO_API_SECRET=abc123 + cobo get /wallets + ``` + +5. **Log full config contents** + ```bash + # WRONG + cobo --enable-debug get /wallets 2>&1 | tee debug.log + + # RIGHT - review debug output before sharing + cobo --enable-debug get /wallets + ``` + +## Config File Locations + +| File | Location | Contains | +|------|----------|----------| +| Main config | `~/.cobo/config.toml` | environment, auth_method, api_key, api_secret | +| SDK config | Various | Depends on SDK | + +### Config File Permissions + +```bash +# Ensure config is readable only by owner +chmod 600 ~/.cobo/config.toml +chmod 600 .env + +# Verify permissions +ls -la ~/.cobo/config.toml +# Expected: -rw------- (600) +``` + +## Secure Key Generation + +### Using CLI (Recommended) + +```bash +# Generate and store automatically +cobo keys generate --key-type API +``` + +The CLI: +- Generates a cryptographically secure Ed25519 keypair +- Stores keys in `~/.cobo/config.toml` +- Displays keys only once during generation + +### Manual Generation + +```bash +# If you need to generate keys manually +openssl genpkey -algorithm ed25519 -outform DER | xxd -p -c 64 +``` + +## Environment Variables + +### Supported Variables (CLI) + +| Variable | Purpose | Example | +|----------|---------|---------| +| `COBO_API_SECRET` | API private key (hex) | `abc123...` (64 chars) | +| `COBO_API_KEY` | API public key (hex) | `def456...` (64 chars) | +| `COBO_ENVIRONMENT` | Environment | `dev`, `prod`, `sandbox` | +| `COBO_AUTH_METHOD` | Auth method | `apikey`, `user`, `org`, `none` | +| `COBO_API_HOST` | Custom API host | `https://api.dev.cobo.com` | + +### Setting Environment Variables + +```bash +# For current session +export COBO_API_SECRET="" +export COBO_ENVIRONMENT="dev" + +# For persistent use (add to shell profile) +echo 'export COBO_API_SECRET=""' >> ~/.bashrc + +# Note: cobo-cli does not auto-load `.env` for CLI config. +# Use environment variables or `cobo config set` instead. +``` + +### Precedence Order + +1. Environment variables (highest) +2. `~/.cobo/config.toml` (lowest) + +## Output Redaction Rules + +When generating output for users, agents MUST redact: + +| Field | Action | Example | +|-------|--------|---------| +| `api_secret` | Full redact | `[REDACTED]` | +| `api_key` | Partial show | `abc1...xyz9` | +| `private_key` | Full redact | `[REDACTED]` | +| `mnemonic` | Full redact | `[REDACTED]` | +| `password` | Full redact | `[REDACTED]` | + +### Redaction Example + +```json +// Input (from cobo config list) +{ + "env": "dev", + "api_key": "abc123def456...", + "api_secret": "secret789xyz..." +} + +// Output (agent response) +{ + "env": "dev", + "api_key": "abc1...6789", + "api_secret": "[REDACTED]" +} +``` + +## Secure Workflow Patterns + +### Pattern 1: Fresh Setup + +```bash +# 1. Generate keys (only time secret is shown) +cobo keys generate --key-type API +# Copy and securely store the secret! + +# 2. Set auth +cobo auth apikey + +# 3. Verify without exposing +cobo config list | grep -E "^(env|auth_method)=" +``` + +### Pattern 2: Script Configuration + +```bash +#!/bin/bash +# Load from environment, never hardcode +if [ -z "$COBO_API_SECRET" ]; then + echo "Error: COBO_API_SECRET not set" + exit 1 +fi + +cobo get /wallets --limit 10 +``` + +### Pattern 3: CI/CD + +```yaml +# GitHub Actions example +env: + COBO_API_SECRET: ${{ secrets.COBO_API_SECRET }} + COBO_ENV: dev + +steps: + - run: cobo get /wallets --limit 1 +``` + +## Security Checklist + +Before any operation: + +- [ ] Secrets are not in command-line arguments +- [ ] Config file permissions are restricted (600) +- [ ] `.gitignore` includes `.cobo/` and `.env` +- [ ] Agent outputs redact sensitive fields +- [ ] Environment is confirmed (dev vs prod) +- [ ] Debug logs are not shared without review + +## Incident Response + +If a secret is exposed: + +1. **Immediately revoke the key** + ```bash + cobo open developer + # Delete the compromised API key in Portal + ``` + +2. **Generate new keys** + ```bash + cobo keys generate --key-type API + # Register new public key in Portal + ``` + +3. **Audit recent activity** + ```bash + cobo logs tail + # Review for unauthorized operations + ``` + +4. **Update all references** + - Update CI/CD secrets + - Update environment variables + - Update any scripts using the old key diff --git a/skills/cobo-waas/references/troubleshooting.md b/skills/cobo-waas/references/troubleshooting.md new file mode 100644 index 0000000..688eaff --- /dev/null +++ b/skills/cobo-waas/references/troubleshooting.md @@ -0,0 +1,177 @@ +# Troubleshooting Decision Tree + +Visual troubleshooting guide organized by symptom. Follow the tree to diagnose and resolve issues. + +--- + +## CLI Not Working + +``` +cobo command fails? +│ +├── "command not found: cobo" +│ └── CLI not installed +│ → pip install cobo-cli +│ → Verify: cobo version +│ +├── "ModuleNotFoundError" +│ └── Broken installation +│ → pip uninstall cobo-cli +│ → pip install cobo-cli +│ +├── "Permission denied" +│ └── Installation permission issue +│ → pip install --user cobo-cli +│ → Or use virtualenv +│ +└── Version mismatch or old version + └── Update CLI + → pip install --upgrade cobo-cli +``` + +--- + +## Configuration Issues + +``` +cobo config list shows problems? +│ +├── Empty or no output +│ └── Fresh installation +│ → cobo env dev +│ → cobo keys generate --key-type API +│ → cobo auth apikey +│ +├── Missing env +│ └── Environment not set +│ → cobo env dev # or prod, sandbox +│ +├── Missing auth_method +│ └── Auth method not set +│ → cobo auth apikey +│ +├── Missing api_key or api_secret +│ └── Keys not generated +│ → cobo keys generate --key-type API +│ → Save the output! +│ +└── Wrong environment + └── Pointing to wrong env + → cobo env + → Register keys in that environment's Portal +``` + +--- + +## API Calls Failing + +``` +cobo get/post returns error? +│ +├── Connection error / timeout +│ ├── No internet +│ │ → Check network connectivity +│ │ +│ ├── VPN/proxy issue +│ │ → Disable VPN or configure proxy +│ │ +│ └── Firewall blocking +│ → Allow api.cobo.com, api.dev.cobo.com +│ +├── Status 400 Bad Request +│ ├── Missing required parameter +│ │ → cobo /path --describe +│ │ +│ ├── Invalid parameter value +│ │ → cobo /path --describe --param_name +│ │ +│ └── Malformed JSON +│ → Check JSON syntax in complex params +│ → Use single quotes around JSON +│ +├── Status 401 Unauthorized +│ ├── No keys in config +│ │ → cobo config list +│ │ → cobo keys generate --key-type API +│ │ +│ ├── Keys not registered in Portal +│ │ → cobo open developer +│ │ → Add public key as API key +│ │ +│ ├── Auth method not set +│ │ → cobo auth apikey +│ │ +│ └── Wrong environment for key +│ → Key registered in dev but calling prod? +│ → cobo env +│ +├── Status 403 Forbidden +│ ├── Insufficient key permissions +│ │ → cobo open developer +│ │ → Edit key → Add permissions +│ │ +│ ├── Resource not owned by org +│ │ → Check resource belongs to your org +│ │ +│ └── Operation not allowed +│ → Check if wallet type supports operation +│ +├── Status 404 Not Found +│ ├── Wrong endpoint path +│ │ → cobo get --list | grep +│ │ +│ ├── Resource doesn't exist +│ │ → cobo get /wallets (list resources) +│ │ +│ └── Wrong environment +│ → Resource in dev but querying prod? +│ +├── Status 409 Conflict +│ └── Duplicate request_id +│ → Use unique request_id: +│ --request_id "tx-$(date +%s)-$(openssl rand -hex 4)" +│ +├── Status 429 Rate Limited +│ └── Too many requests +│ → Wait and retry +│ → Add delays between requests +│ +└── Status 500/502/503 + └── Server issue + → Wait 30-60 seconds + → Retry request + → Check Cobo status page [Error codes](https://www.cobo.com/developers/v2/guides/overview/error-codes.md) +``` + +## Quick Diagnosis Commands + +Run these to quickly assess state: + +```bash +# Full diagnostic +echo "=== CLI ===" && cobo version +echo "=== Config ===" && cobo config list +echo "=== Connectivity ===" && cobo get /wallets --limit 1 2>&1 | head -3 + +# Specific checks +cobo config list | grep env # Check environment +cobo config list | grep auth_method # Check auth +cobo get --list | head -10 # Verify API access +``` + +--- + +## When to Escalate + +Contact Cobo support if: + +1. **500 errors persist** after retries +4. **Unexplained 403** with correct permissions +5. **Data inconsistency** between API and Portal + +Include in support request: +- Environment (dev/prod/sandbox) +- Command with `--enable-debug` output +- Error message +- Organization Id +- Transaction/wallet IDs (if applicable)