# Cobo - Wallet Custody Integration (Waas 2.0)

## Available To

Free for the following exchange plans:

* **On-Premise Plans:**
  * Enterprise Unlimited

{% hint style="info" %}
This is a **wallet custody** plugin and is only available on **On-Premise** **(Unlimited)** plans. It is not offered on Cloud plans because custody must be controlled by the exchange operator's own infrastructure and Cobo API credentials.
{% endhint %}

## What Is It?

The **Cobo Plugin** connects a HollaEx exchange to [Cobo Wallet-as-a-Service 2.0](https://www.cobo.com/developers/v2) using a single shared **Custodial Asset Wallet** as the custody backend.

It exposes server-side routes only:

* Generates per-user deposit addresses inside the shared Cobo wallet.
* Credits are deposited when Cobo emits a `transactions.*` webhook, **and** the canonical state is reconfirmed via a signed `GET /v2/transactions/{id}` API call.
* Dispatches pending exchange withdrawals to Cobo every minute via `POST /v2/transactions/transfer`.
* Approves only those Cobo callback requests whose `request_id` matches a pending withdrawal it previously dispatched (auto-deny otherwise).

## Who Needs It?

This plugin is suitable for exchange operators that:

* Use Cobo as their custody provider.
* Want a single shared Custodial Asset Wallet with per-user deposit addresses across many chains.
* Want pending exchange withdrawals submitted to Cobo automatically.
* Need defense-in-depth on deposit credits: every webhook is re-verified against Cobo's authenticated API before any mint is created or updated.

## How to Use It?

Install the plugin from the **Plugins** section inside the Operator Control, then configure the plugin meta with your Cobo API credentials and shared wallet ID.

#### 1. Generate a Cobo API key pair

Cobo authenticates every API request with an Ed25519 key pair.

1. Generate a key pair using either the [Cobo CLI](https://www.cobo.com/developers/v2/guides/overview/cobo-auth#generate-an-api-key-and-an-api-secret), OpenSSL, or any Ed25519 library. The output you need is two 32-byte hex strings:
   * **API key** - the public key in hex (64 hex characters).
   * **API secret** - the private key in hex (64 hex characters).
2. Register the **public** key on Cobo Portal under **API Management -> Register an API key**, scoped at minimum to:
   * `wallet.read`, `wallet.create_address`
   * `transaction.read`, `transaction.withdraw`
   * `webhook.read`, `webhook.edit`, `callback.read`

#### 2. Create a shared Custodial Asset Wallet

1. In Cobo Portal, create a new wallet:
   * `wallet_type`: `Custodial`
   * `wallet_subtype`: `Asset`
2. Copy the wallet ID. This is the single shared wallet that will hold per-user deposit addresses for every supported chain. The plugin does **not** create one wallet per exchange user.

#### 3. Configure the plugin

Open the plugin in the Operator Control and set the following fields:

| Field                | Required | Default                       | Description                                                                                                                                                                                                                                     |
| -------------------- | -------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api_url`            | **yes**  | `https://api.dev.cobo.com/v2` | Cobo WaaS 2.0 base URL. Use `https://api.cobo.com/v2` for production.                                                                                                                                                                           |
| `api_key`            | **yes**  | empty                         | Your Cobo API public key (hex, 64 chars).                                                                                                                                                                                                       |
| `api_secret`         | **yes**  | empty                         | Your Cobo API private key (hex, 64 chars). Used to sign every outbound API request.                                                                                                                                                             |
| `wallet_id`          | **yes**  | empty                         | ID of the shared Custodial Asset Wallet.                                                                                                                                                                                                        |
| `webhook_public_key` | **yes**  | dev key                       | Cobo's Ed25519 public key (hex) used to verify inbound webhooks and callbacks. Production: `8d4a482641adb2a34b726f05827dba9a9653e5857469b8749052bf4458a86729`. Development: `a04ea1d5fa8da71f1dcfccf972b9c4eba0a2d8aba1f6da26f49977b08a0d2718`. |

The plugin will not initialize unless all required values are present.

#### 4. Register the webhook endpoint on Cobo Portal

In Cobo Portal -> **Developers -> Webhook Endpoints**, register:

```
POST https://<your exchange url>/plugins/cobo/webhook
```

Subscribe, at minimum, to:

* `transactions.created`
* `transactions.updated`

Cobo signs every event with `Biz-Resp-Signature` and `Biz-Timestamp` request headers. The plugin verifies the signature and then **re-fetches the canonical transaction** via `GET /v2/transactions/{id}` so the on-disk state, not the webhook body, is what drives the deposit credit or withdrawal status update. Forged or stale webhooks are dropped.

#### 5. Register the callback endpoint on Cobo Portal

Cobo asks an external endpoint to approve every initiated withdrawal before it is signed and broadcast. In Cobo Portal -> **Developers -> Callback Endpoints**, register:

```
POST https://<your exchange url>/plugins/cobo/callback
```

The plugin replies in plain text with `ok` or `deny`:

* `ok` only when the callback's `request_id` matches a pending exchange withdrawal that the plugin previously dispatched and is in `waiting=true, status=false, dismissed=false, rejected=false` state.
* `deny` for everything else, including missing/invalid signature, unknown `request_id`, or burns that are no longer in the dispatched state.

#### 6. User-facing address creation

Authenticated users request a deposit address with:

```
GET https://<your exchange url>/plugins/cobo/create-address?crypto=<currency>&network=<network>
Authorization: Bearer <user token>
```

The plugin:

1. Returns `400` if the user already has a wallet for this currency+network.
2. Reuses any existing same-network address the user already has, if one exists.
3. Otherwise calls `POST /v2/wallets/{wallet_id}/addresses` with `{ chain_id, count: 1 }` and registers the returned address with `toolsLib.wallet.createUserWalletByKitId`.

#### 7. Withdrawal dispatch

Two cron jobs run every minute:

* `markPendingWithdrawalsProcessing` flips eligible pending burns to `processing: true`. A burn is eligible when it is **not** completed, dismissed, rejected, waiting, processing, or on hold, **and** its currency maps to a Cobo chain.
* `dispatchPendingWithdrawals` picks up `processing` burns, marks them `waiting: true, processing: false`, then calls `POST /v2/transactions/transfer` with:
  * `request_id` set to the exchange's burn `transaction_id` (used for callback matching and webhook reconciliation).
  * `source: { source_type: "Asset", wallet_id }`.
  * `token_id` resolved via `GET /v2/wallets/{wallet_id}/tokens?chain_ids=...` (cached in memory).
  * `destination.account_output: { address, amount }`.

Final completion is recorded later by the webhook (which re-verifies via the Cobo API) and writes `status: true` plus the on-chain hash to the exchange burn record.

To trigger dispatch manually as an admin:

```
POST https://<your exchange url>/plugins/cobo/dispatch-withdrawals
Authorization: Bearer <admin token>
```

#### Health check

```
GET https://<your exchange url>/plugins/cobo/health
```

### Supported networks

Supported chains are loaded from Cobo at startup via `GET /v2/wallets/chains?wallet_type=Custodial&wallet_subtype=Asset` and cached in memory. The plugin maps Cobo chain names to exchange currency symbols. Built-in mappings include Bitcoin, Bitcoin Cash, Litecoin, Dogecoin, Dash, Ethereum, Tron, Solana, Polygon, Avalanche C-Chain, Arbitrum, Optimism, Base, BNB Smart Chain, XRP, Cosmos, Algorand, Cardano, Polkadot, Stellar, Tezos, TON, NEAR, Aptos, and Sui. Other chains returned by Cobo are best-effort matched by name.

Assets without a mapped Cobo chain are skipped by the withdrawal dispatcher and cannot derive deposit addresses until the mapping is added.

***

#### **Defense-in-Depth**

Every completed credit deposit, and withdrawal, goes through two checks before the kit ledger is touched:

1. The inbound webhook signature is verified with Cobo's published Ed25519 public key.
2. The plugin then makes its own signed `GET /v2/transactions/{id}` call and uses **that** response (status, amount, destination address, transaction hash, request\_id) to decide what to credit or update.

Even if the webhook signature were ever bypassed or the public key were misconfigured, no funds move unless Cobo's authenticated API confirms the same transaction.

***

## **Benefits for HollaEx Operators**

The Cobo plugin lets an exchange keep custody operations inside Cobo while preserving the normal HollaEx wallet flow for users. Deposit addresses are derived from a shared Cobo Custodial Asset Wallet; deposit credits depend on signed Cobo webhooks **and** Cobo API confirmation, outgoing withdrawals are queued and dispatched on a cron, and Cobo callbacks for those withdrawals are auto-approved only when they match a known pending exchange burn. This keeps custody integration centralized, auditable, and aligned with the exchange's existing pending transaction workflow.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.hollaex.com/plugins/use-plugins/cobo-wallet-custody-integration-waas-2.0.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
