# @carbonx3/cx3-iife

[![npm version](https://img.shields.io/npm/v/@carbonx3/cx3-iife.svg)](https://www.npmjs.com/package/@carbonx3/cx3-iife)
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/@carbonx3/cx3-iife/badge)](https://www.jsdelivr.com/package/npm/@carbonx3/cx3-iife)

The official browser script (**CX3 IIFE**) for [CarbonX3 Green Chain](https://carbonx3.com/).

One `<script>` file adds **`window.cx3`** (alias `window.CX3`) to a web page. The page connects the visitor's CX3 Wallet (the browser extension, or the phone app by QR code), has the wallet sign and send transactions, and reads chain data through a small proxy on your own server. There is no build step and no dependency.

> **Network status:** CarbonX3 Green Chain currently runs as a development network (devnet). Devnet CX3, tokens and contracts are test data with no monetary value and may be cleared when the devnet is reset.

For Node.js servers use the SDK instead: [`@carbonx3/cx3.js`](https://www.npmjs.com/package/@carbonx3/cx3.js).

## Features

- **Wallet connect and signing** through the CX3Wallet extension: the private key never reaches the page.
- **Phone wallet by QR code**: `cx3.connectPhone()` shows a QR code; the visitor scans it with the CX3 Wallet app and approves. From then on every send is approved and signed on the phone, and the key never leaves it. A built-in dialog is included, or draw the code in your own with `cx3.qrSvg()`.
- **Reads through your same-origin proxy**: balances, the token catalogue, transfers, a wallet's tokens and activity, CarbonFi pools, ABI and WIT.
- **Contract helper**: `cx3.contract(address).balanceOf(owner)`, `.transfer(to, amount)`, `.call(...)`, `.send(...)`.
- **Private tokens**: sender and recipient are checked before the wallet asks for approval.
- **Transaction tracking**: `tx.wait()` polls until the transaction is confirmed or failed for good. A send that is pending (queued or dropped from a block) is followed until it is final, so the page never asks for a second signature.
- **Wallet events**: `cx3.on("accountsChanged", fn)` tells the page when the user removes your site in the wallet, so it shows "Connect wallet" again without a reload.
- **No credentials in the browser**: `setRpc` drops `Authorization` and `X-Api-Key` headers and has no login option.

## Installation

### From a CDN (recommended)

Every version has its own fixed address. Load the exact version your page was tested with:

```html
<script src="https://cdn.jsdelivr.net/npm/@carbonx3/cx3-iife@0.2.9/min/cx3-0.2.9.min.js"
        integrity="sha384-erEAYMO5XonBabdhCYNtLWkrGwnht9Me0rKYwq0GlF31pNaOabEzWp645FAkQHtE"
        crossorigin="anonymous"></script>
```

The `integrity` value makes the browser refuse the file if a single byte differs. The same file is on unpkg: `https://unpkg.com/@carbonx3/cx3-iife@0.2.9/min/cx3-0.2.9.min.js`.

### Self-hosted

Download [`cx3-0.2.9.min.js`](https://cx3script.com/download/cx3-0.2.9.min.js), put it next to your page and load it:

```html
<script src="cx3-0.2.9.min.js"></script>
```

### With a bundler

```bash
npm install @carbonx3/cx3-iife
```

```javascript
import "@carbonx3/cx3-iife"; // sets window.cx3
```

The file is a plain browser script: it assigns `window.cx3` and exports nothing.

## Requirements

- A modern browser (`fetch`, `Promise`).
- **CX3Wallet** 0.1.74 or later for connecting, signing and `accountsChanged`. The extension connects on every https site and on your own machine during development; the user approves each site once, and every send or signature asks again. Iframes get no wallet (`cx3.wait()` resolves `false`).
- **CX3 Wallet app** (Android) 0.2.91 or later for the phone wallet (`connectPhone`).
- **A developer token** (your application's OAuth2 credentials, held by your proxy). While it is valid, every wallet can connect to your page; with a proxy set by `setRpc` the wallet asks `GET <proxy>/api/v1/whoami?address=…` on your origin when it connects, and a missing, invalid or revoked token stops the connect.
- For reads: a same-origin proxy on your server that holds your application's credentials (see below).

## Quick start

```html
<button type="button" id="connect">Connect wallet</button>
<p id="address"></p>

<script src="https://cdn.jsdelivr.net/npm/@carbonx3/cx3-iife@0.2.9/min/cx3-0.2.9.min.js"
        integrity="sha384-erEAYMO5XonBabdhCYNtLWkrGwnht9Me0rKYwq0GlF31pNaOabEzWp645FAkQHtE"
        crossorigin="anonymous"></script>
<script>
  const cx3 = window.cx3;
  cx3.setRpc(location.origin + "/my-dapp", { apiPrefix: "/api/v1" }); // your proxy

  document.getElementById("connect").onclick = async () => {
    if (!(await cx3.wait(2500))) {
      document.getElementById("address").textContent = "CX3Wallet not found";
      return;
    }
    const me = await cx3.connect();
    document.getElementById("address").textContent = me;

    const balance = await cx3.getBalance(me);
    const page = await cx3.listContracts({ limit: 50 });
    console.log(balance, page.contracts);
  };

  // The user removed this site in CX3Wallet (Settings → Connected Sites → Remove)
  cx3.on("accountsChanged", (accounts) => {
    if (accounts.length === 0) document.getElementById("address").textContent = "";
  });
</script>
```

## Your proxy (application credentials)

A page never holds credentials. Create an application in **CX3WebUI → Settings → API → Applications** and keep its client ID and client secret on your server. The proxy trades them at `POST https://cx3api.carbonx3.com/oauth/token` (`grant_type=client_credentials`) for a one-hour access token, caches it, and calls the gateway with `Authorization: Bearer <token>`. The page sees neither the secret nor the token.

The proxy answers under `apiPrefix` (default `/api/v1`) and forwards only the paths your page uses:

| `window.cx3` call | Proxy path | Gateway route |
|-------------------|------------|---------------|
| `getBalance` | `GET getBalanceAndNonce?address=` | `GET /wallet/getBalanceAndNonce?address=` |
| `listContracts` | `GET contracts?limit=&offset=` | `GET /contracts/list` |
| `listTransfers` | `GET contracts/{address}/transfers` | `GET /contracts/:address/transfers` |
| `getAbi` · `getContractInfo` | `GET contracts/{address}/abi` · `GET contract/{address}` | `GET /contracts/:address/abi` · `GET /contracts/:address` |
| `call`, `balanceOf` | `POST contract/call` | `POST /contracts/:address/call` |
| `bindApiKey` | `GET whoami?address=` | `GET /wallet/api-key/bind?address=` |
| `waitForTransaction` · `getStatus` | `GET tx/{hash}/status` · `GET getLastBlock` | `GET /tx/:hash/status` · `GET /getLastBlock` |
| `getWit` | `GET contracts/{address}/wit` | `GET /dapp/v1/contracts/:address/wit` |
| `getTokens` · `getActivity` | `GET accounts/{address}/tokens` · `/activity` | `GET /dapp/v1/accounts/:address/tokens` · `/activity` |
| `getDexPairs` · `getLpBalance` | `GET dex/pairs` · `dex/pairs/{id}/lp/{address}` | `GET /dapp/v1/dex/pairs` · `/dex/pairs/:id/lp/:address` |
| `connectPhone` | `POST link/pair` · `GET link/pair/{32 hex}` · `POST link/pair/{32 hex}/bind` | `POST /wallet-link/pair` · `GET /wallet-link/pair/:id` · `POST /wallet-link/pair/:id/bind` |
| `restorePhone`, `disconnect` (phone) | `GET link` · `DELETE link` | `GET /wallet-link/link` · `DELETE /wallet-link/link` |
| sends with the phone | `POST link/sign` · `GET link/sign/{24 hex}` · `DELETE link/sign/{24 hex}` | `POST /wallet-link/link/sign` · `GET` / `DELETE /wallet-link/link/sign/:id` |
| a contract send the phone signed | `POST contract-send/{address}` | `POST /dapp/v1/contracts/:address/send` |

The gateway's contract send waits up to 25 seconds for the block, and the script gives `contract-send` 35 seconds (other calls 12 seconds), so give that path a proxy timeout above 25 seconds and pass on the gateway's status code and JSON body unchanged (202, 409, 503 and 504 carry `requestId`, `txHash` and `hint`). Both status routes (`/tx/:hash/status`, `/dapp/v1/tx/:hash/status`) answer a dropped send as `pending` with `dropped` and `finalAfter`.

Writes with the extension never go through the proxy: the wallet signs and sends them. With the phone, the app signs and the relay carries the request (see below). A complete PHP proxy example is in the [CX3 IIFE guide](https://cx3script.com/docs?doc=cx3iife#app-proxy).

## API

| `window.cx3` | What it does |
|--------------|--------------|
| `VERSION` | The script version, e.g. `"0.2.9"` |
| `setRpc(url, { apiPrefix })` / `getRpc()` | Set / read your proxy's base URL |
| `wait(maxMs)` | Wait until CX3Wallet is ready (default 2500 ms); resolves `true` / `false` |
| `isAvailable()` / `wallet()` | Is the wallet there / the injected `window.carbonx3` |
| `connect()` / `accounts()` / `disconnect()` | Connect the extension (sets `cx3.address`), list accounts, drop the session (with the phone: ends its link) |
| `connectPhone({ onQr, signal, texts })` | Pair the CX3 Wallet app by QR code (built-in dialog, or `onQr(uri, expiresAt)` for your own); resolves with the phone's address |
| `restorePhone()` | After a reload: the phone linked earlier in this tab, if the link is still active; its address or `""` |
| `via` | Which wallet signs: `"extension"`, `"phone"` or `""` |
| `qrSvg(text, { size, margin, dark, light, label })` / `qrMatrix(text)` | A QR code as an `<svg>` element / as rows of booleans |
| `on("accountsChanged", fn)` / `off("accountsChanged", fn)` | `fn(accounts)` gets `[]` when the user removes your site in the wallet (Connected Sites → Remove), in the extension or in the phone app; `cx3.address` is cleared. Show "Connect wallet" again without a reload. Needs CX3Wallet 0.1.69+ |
| `address` | The connected address (lower-case) or `""` |
| `request(method, params)` | Raw wallet request (`cx3_*`) |
| `getBalance(address?)` | Native CX3 balance |
| `listContracts({ limit, offset, viewer, owner, standard, live })` | Token catalogue (limit up to 500) |
| `listTransfers(address, { limit, offset })` | A token's transfers |
| `getTokens(address?)` / `getActivity(address?, { limit, before })` | A wallet's tokens with balances / its history |
| `getDexPairs()` / `getLpBalance(pairId, address?)` | CarbonFi pools / a wallet's LP balance |
| `call(address, method, params, sender?)` | Read-only contract call |
| `getAbi(address)` / `getWit(address)` | A contract's ABI / the WIT of a CX3WASM contract |
| `getStatus()` | Chain head `{ height, hash, timestamp }` |
| `getContractInfo(address)` | Registry record |
| `isPrivateToken(address)` / `isTokenAuthorized(token, wallet)` | Private token checks |
| `contract(address)` | Helper: `balanceOf`, `transfer`, `call`, `send`, `transfers`, `isPrivate`, `isAuthorized` |
| `sendTransaction({ to, amount }, opts?)` | Native CX3 transfer, signed in the wallet; `amount` in CX3, at most 6 decimals (phone: `opts.onSeen()` when the app got the request) |
| `contractSend(address, method, params, opts?)` | Contract transaction, signed in the wallet (`opts.privateCheck`, `opts.onSeen`) |
| `waitForTransaction(hash, { timeoutMs, intervalMs, finalAfter })` / `tx.wait()` | Poll until confirmed or failed for good: by default 30 s, or until `finalAfter` plus 30 s for a dropped send; every 1 s (every 5 s while dropped). `timeoutMs` is your own limit |
| `bindApiKey(address?)` | Is the connected wallet your own, the one your application belongs to? For owner-only pages; visitors' wallets answer `wallet_mismatch` and may still use the app |

Parameters are named JSON, not ABI calldata: `contract(token).transfer(to, amount)` sends `{ to, amount }`. Amounts are the raw integer strings the contract expects (`"1000000000000000000"` is 1 token at 18 decimals).

## Sending

```javascript
await cx3.connect();

const tx = await cx3.contract("0x…token").transfer("0x…recipient", "1000000");
await tx.wait();                      // confirmed, or throws TX_FAILED / TX_TRACK_TIMEOUT

const native = await cx3.sendTransaction({ to: "0x…recipient", amount: "10" });
await native.wait();
```

The wallet shows the recipient, amount and fee, and the user approves or rejects every send.

A send answer has `txHash`, `status`, `dropped`, `finalAfter`, `requestId` and `hint`:

- **Pending is not failed.** `status` is `"pending"` (or `"submitted"` for a repeat of the same signed send) when the transaction is queued but not in a block yet: `TX_TIMEOUT` (the node's 25-second wait ended first) or a send the node trimmed from its block (HTTP 202, `dropped: true`). `success` stays `true`, and `tx.wait()` follows it.
- **Dropped sends.** Until `finalAfter` (ms epoch, about 12 minutes after the signature) another validator may still include a dropped send, so `tx.wait()` keeps waiting until then by default. After it the status says `failed` and `tx.wait()` rejects with `TX_FAILED`.
- **`TX_TRACK_TIMEOUT` means still pending**, not failed (`err.dropped`, `err.finalAfter`). Never ask the user to sign the same payment again while it can still run: a new signature is a second transaction.

## Phone wallet (QR code)

A visitor without the browser extension can use the CX3 Wallet app on their phone. The page shows a QR code, the user scans it in the app (QR scanner) and approves the connection. The pairing goes through your proxy to the CarbonX3 gateway's relay, so your developer token decides which app the phone sees: the app shows your application's name and your site.

```javascript
cx3.setRpc(location.origin + "/my-dapp", { apiPrefix: "/api/v1" }); // your proxy, with the link routes

// the built-in dialog: QR code, "Waiting For Approval In The App", Cancel
const me = await cx3.connectPhone();          // cx3.via === "phone", cx3.address === me

// sends now go to the phone: the user approves them in the app
const tx = await cx3.contract("0x…token").transfer("0x…recipient", "1000000", {
  onSeen: () => console.log("the app received the request")
});
await tx.wait();
```

Your own dialog instead of the built-in one:

```javascript
const box = document.getElementById("qr");
const stop = { cancelled: false };            // set stop.cancelled = true (or pass an AbortSignal) to give up
const me = await cx3.connectPhone({
  signal: stop,
  onQr: (uri, expiresAt) => {
    box.replaceChildren(cx3.qrSvg(uri, { size: 220 }));  // an <svg>: no images, no inline styles
  }
});
```

- **After a reload:** `await cx3.restorePhone()` gives the phone's address while its link is still active (the link key stays in `sessionStorage`, this tab only), or `""`.
- **When the user removes your site in the app:** the script checks the link every few seconds and calls your `accountsChanged` listeners with `[]`, as the extension does.
- **`cx3.disconnect()`** ends the link on the gateway too. **`cx3.connect()`** (the extension) takes over from a linked phone.
- **Built-in dialog texts:** `connectPhone({ texts: { title, hint, preparing, waiting, valid, cancel } })`, for example in another language. The dialog is styled through the DOM, so a Content-Security-Policy without `'unsafe-inline'` allows it.
- **Your proxy** forwards the `link/…` and `contract-send/…` paths in the table above, with the `X-CX3-Pair-Key` and `X-CX3-Link-Key` headers the script sends, and `DELETE` for `link` and `link/sign/{id}`. The pairing's `origin` should be set by your server (it is the site the app shows), not taken from the browser.

## Private tokens

Before the wallet asks for approval of a `transfer`, `transferFrom` or `mint` of a private token, the script asks the token whether the sender and the recipient are authorized. If one is not, the call rejects with `PRIVATE_TOKEN_SENDER` or `PRIVATE_TOKEN_RECIPIENT` and the wallet shows nothing. `{ privateCheck: false }` as the last argument skips the check; the contract still decides on the chain.

`getTokens()` lists public tokens only: the gateway gives a wallet's private tokens only with that wallet's own signed proof, which CX3Wallet sends for its own lists and never to a page.

## Errors

| Condition | Result |
|-----------|--------|
| No `setRpc` | `Error: RPC not set (CX3.setRpc)` |
| No wallet | `Error: CX3Wallet not found` |
| Proxy without valid credentials | HTTP 401 `oauth_required` (or `api_key_required`) |
| Application revoked | HTTP 401 `invalid_token` |
| Scope too small | HTTP 403 `insufficient_scope` |
| Not the application's wallet | `wallet_mismatch` |
| Method missing on the contract | `METHOD_NOT_FOUND` |
| `call` of a write method | `CALL_REQUIRES_VIEW` (use `contractSend`) |
| Private token, wallet not authorized | `PRIVATE_TOKEN_SENDER` / `PRIVATE_TOKEN_RECIPIENT` |
| Transaction failed for good (a dropped send only after its `finalAfter`) | `TX_FAILED` |
| `tx.wait()` stopped while the transaction was still pending (not a failure) | `TX_TRACK_TIMEOUT` |
| QR code expired (5 minutes) / dialog closed / bad relay answer | `PAIR_EXPIRED` / `PAIR_CANCELLED` / `PAIR_INVALID` |
| Rejected in the phone app / no answer in about 2 minutes | `USER_REJECTED` / `REQUEST_EXPIRED` |
| The app removed the site (the page is no longer connected) | `REQUEST_ENDED` |
| Phone send with a bad recipient or amount / more than 6 decimals (CX3 has 6) | `INVALID_RECIPIENT` / `INVALID_AMOUNT` / `INVALID_AMOUNT_PRECISION` |
| Phone contract send: queued, not in a block within the node's 25 s (with a hash it is pending, not an error) | `TX_TIMEOUT` |
| Phone contract send: the gateway could not record it (503); not sent, `err.retry()` sends the same signed request again later | `LEDGER_UNAVAILABLE` / `LEDGER_FULL` |
| Phone contract send: this signed request was already received (409); its outcome is open, never sign it again | `REPLAYED_REQUEST` |
| Phone contract send: no answer in 35 s / no connection (outcome open; `err.retry()`) | `RPC_TIMEOUT` / `NETWORK_ERROR` |
| The chain refused the transaction: its data is too large or nested too deeply, or its nonce is not the next one | `DATA_TOO_LARGE` / `DATA_TOO_DEEP` / `INVALID_NONCE_ORDER` |
| The chain refused the gas limit: below the minimum, or above a contract send's maximum | `GAS_LIMIT_TOO_LOW` / `GAS_LIMIT_EXCEEDED` |

Coded errors carry the code in `err.code`. A contract send's error, with the phone or the extension, also has `err.status` (HTTP), `err.requestId`, `err.hint`, `err.txHash` and `err.body` (the gateway's answer); a `TX_TIMEOUT` that names its hash is not an error on either path but a pending tx. `err.retry()` is there only where sending the very same signed request again is safe (not sent, or no answer): the gateway keys it by its request id, so it never becomes a second transaction, and it is accepted for a few minutes after the signature.

Show error text with `textContent`, never `innerHTML`.

## Documentation

- [CX3 IIFE guide](https://cx3script.com/docs?doc=cx3iife)
- [Application integration guide](https://cx3script.com/docs?doc=application-integration)
- [CX3js SDK for Node.js](https://www.npmjs.com/package/@carbonx3/cx3.js)
- [CX3Script developer site](https://cx3script.com)
- [CarbonX3 Green Chain](https://carbonx3.com/)

## License

`UNLICENSED`: proprietary software of Mersolution Technology Ltd. Use is subject to the [CX3Script Terms of Use](https://cx3script.com/terms) (item 03): you may load it in, and serve it unmodified with, applications, including commercial ones, that connect to CarbonX3 Green Chain, from your own server or a CDN at a pinned version with its `integrity` hash; you may not modify it, sell or republish it on its own, or use it with other networks.

Third-party code: the QR encoder is based on [QR Code Generator for JavaScript](https://github.com/kazuhikoarase/qrcode-generator), Copyright (c) 2009 Kazuhiko Arase, MIT license; its notice stays in the file. "QR Code" is a registered trademark of DENSO WAVE INCORPORATED.
