@carbonx3/cx3-iife
The official browser script (CX3 IIFE) for CarbonX3 Green Chain.
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.
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 withcx3.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:
setRpcdropsAuthorizationandX-Api-Keyheaders 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:
<script src="https://cdn.jsdelivr.net/npm/@carbonx3/[email protected]/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/[email protected]/min/cx3-0.2.9.min.js.
Self-hosted
Download cx3-0.2.9.min.js, put it next to your page and load it:
<script src="cx3-0.2.9.min.js"></script>
With a bundler
npm install @carbonx3/cx3-iife
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()resolvesfalse). - 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
setRpcthe wallet asksGET <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
<button type="button" id="connect">Connect wallet</button>
<p id="address"></p>
<script src="https://cdn.jsdelivr.net/npm/@carbonx3/[email protected]/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.
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
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.
statusis"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).successstaystrue, andtx.wait()follows it. - Dropped sends. Until
finalAfter(ms epoch, about 12 minutes after the signature) another validator may still include a dropped send, sotx.wait()keeps waiting until then by default. After it the status saysfailedandtx.wait()rejects withTX_FAILED. TX_TRACK_TIMEOUTmeans 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.
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:
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 insessionStorage, this tab only), or"". - When the user removes your site in the app: the script checks the link every few seconds and calls your
accountsChangedlisteners 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/…andcontract-send/…paths in the table above, with theX-CX3-Pair-KeyandX-CX3-Link-Keyheaders the script sends, andDELETEforlinkandlink/sign/{id}. The pairing'soriginshould 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
- Application integration guide
- CX3js SDK for Node.js
- CX3Script developer site
- CarbonX3 Green Chain
License
UNLICENSED: proprietary software of Mersolution Technology Ltd. Use is subject to the CX3Script Terms of Use (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, Copyright (c) 2009 Kazuhiko Arase, MIT license; its notice stays in the file. "QR Code" is a registered trademark of DENSO WAVE INCORPORATED.