diff --git a/.ci-siblings b/.ci-siblings
index 59931117..059cdff6 100644
--- a/.ci-siblings
+++ b/.ci-siblings
@@ -9,9 +9,9 @@
# test/unit/sibling-coverage.test.js, which reports the surface this file leaves
# uncovered; the same gap applies to a sibling that is declared but missing.
#
-# ci-dispatch.sh ships each of these at its origin/master (falling back to local
-# HEAD) and REFUSES the push if one is not a checkout beside this repo, so a
-# declared sibling can never be silently absent.
+# ci-dispatch.sh ships each of these at the pushed branch (falling back to its
+# origin/master, then local HEAD) and REFUSES the push if one is not a checkout
+# beside this repo, so a declared sibling can never be silently absent.
# Indexer schema conformance, list-edit resolution and reorg DDL, the
# hub-connector contract, and federation read parity (which loads the indexer's
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c73053b3..bdd79918 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -28,7 +28,7 @@ jobs:
# sibling snapshot against a sibling still declaring the old version. That
# is unsatisfiable: the snapshot must move for the release, and develop
# must not. Empty everywhere else, which keeps the existing behaviour.
- siblings-ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || '' }}
+ siblings-ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || '' }}
# Override the Node version for a repo if ever needed:
# node-version: "20"
@@ -147,7 +147,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: XChain-Platform/xchain-indexer
- ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
+ ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
ssh-key: ${{ secrets.XCHAIN_INDEXER_DEPLOY_KEY }}
path: xchain-indexer
@@ -155,7 +155,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: XChain-Platform/xchain-hub
- ref: ${{ startsWith(github.head_ref, 'release/') && github.head_ref || (startsWith(github.head_ref, 'hotfix/') && 'master') || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
+ ref: ${{ startsWith(github.head_ref, 'release/') && github.head_ref || (startsWith(github.head_ref, 'hotfix/') && 'master') || github.base_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
ssh-key: ${{ secrets.XCHAIN_HUB_DEPLOY_KEY }}
path: xchain-hub
@@ -208,7 +208,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: XChain-Platform/xchain-hub
- ref: ${{ startsWith(github.head_ref, 'release/') && github.head_ref || (startsWith(github.head_ref, 'hotfix/') && 'master') || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
+ ref: ${{ startsWith(github.head_ref, 'release/') && github.head_ref || (startsWith(github.head_ref, 'hotfix/') && 'master') || github.base_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
ssh-key: ${{ secrets.XCHAIN_HUB_DEPLOY_KEY }}
path: xchain-hub
@@ -240,7 +240,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: XChain-Platform/xchain-indexer
- ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
+ ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || (github.ref == 'refs/heads/master' && 'master' || 'develop') }}
ssh-key: ${{ secrets.XCHAIN_INDEXER_DEPLOY_KEY }}
path: xchain-indexer
@@ -271,7 +271,7 @@ jobs:
# suite, so a sibling pinned at develop makes it measure the bundled
# snapshot against a version develop is never going to carry.
with:
- ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || '' }}
+ ref: ${{ (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && github.head_ref || github.base_ref || '' }}
- name: Use Node.js 22
uses: actions/setup-node@v4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb842b0b..6962ca77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
## [Unreleased]
+## [0.20.1] - 2026-09-23
+
+### Fixed
+- Re-slid BTC/DOGE testnet activations with LTC inert; canonical admission is non-throwing, and INERT legacy-signature mismatches stay on the invalid-row path.
+
+
+### Added
+- List rows mark what a reader cannot judge at a glance: a multi-recipient send, a multi-leg destroy and a batch carry a count badge that opens the legs or members under the row, and a batch member names its batch.
+
## [0.20.0] - 2026-09-17
### Changed
diff --git a/Dockerfile b/Dockerfile
index 92e5595e..21d85414 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,10 @@
-# Pinned to node:22-bookworm: the optional xchain-vm dependency's isolated-vm
-# native build only succeeds against Node 22 V8 headers (same pin as
-# xchain-indexer / xchain-utxo-tracker). The explorer itself runs on any >=22.
-FROM node:22-bookworm
+# Pinned by digest to the node:22.23.2-bookworm build whose V8/ICU/unicode/cldr
+# match xchain-vm's consensus runtime pin: the floating node:22-bookworm tag
+# moved to a Node patch whose V8/ICU no longer match, which fails that check.
+# The optional xchain-vm dependency's isolated-vm native build also needs
+# Node 22 V8 headers (same pin as xchain-indexer / xchain-utxo-tracker); the
+# explorer itself runs on any >=22.
+FROM node:22.23.2-bookworm@sha256:dd5847a04b0deee391fa145f1f4c6d214196668b6bcc7988ebed67249f226844
RUN mkdir /XChainExplorer/
# xchain-vm is staged into the build context by xchain-node's install path
diff --git a/README.md b/README.md
index 709df291..a9fa0c21 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
# XChain Platform Explorer
-
+
diff --git a/bin/ci-full.sh b/bin/ci-full.sh
index ca0978f5..b7f4b1e6 100755
--- a/bin/ci-full.sh
+++ b/bin/ci-full.sh
@@ -40,14 +40,11 @@
# which publishes the same port with the same fixture credentials. Docker is
# therefore REQUIRED for those tiers, never skipped, and the fixture is recycled
# between them so each tier meets a fresh database the way each GitHub job does.
-# The conformance tier is the one that reads env, and it goes to the SAME
-# fixture rather than to the venue's own MariaDB. It creates four databases of
-# its own (XChain_Conformance_*) and builds the real DDL inside them, which
-# needs a privileged user; the venue's `ci` user holds CREATE globally but ALL
-# only on ci_%, so pointing this at CI_DB_* got the databases created and then
-# failed mid-DDL with "INDEX command denied" (measured on DankServer). GitHub
-# gives this job its own throwaway container with root for exactly that reason,
-# and the fixture is that container. Nothing here echoes or logs a password.
+# The conformance tier uses the fixture selected by fixture-preflight.js. On a
+# venue that is the shared CI database identity and conformance uses disposable
+# ci_* schemas covered by its grants. Everywhere else it is the root identity
+# of the throwaway container. bin/run-conformance.js passes the password only
+# in the child environment.
#
# SKIPPED-BY-DESIGN: none. Every run-step in ci.yml has a twin below. The
# actions-only steps (actions/checkout, setup-node, npm ci, and the two
@@ -70,7 +67,37 @@ SELF="$(pwd)"
SIB="$(cd .. && pwd)"
FAILED=""
+# >>> ci-tier (generated block; re-run the tier wirer to update) >>>
+# Tier classes. A push grades the FAST tier only: the unit job, the pin and
+# drift guards, and the structure and hygiene checks the hook runs before it
+# dispatches. The tiers named below (coverage re-runs, perf scenarios) are
+# skipped when the gate sets CI_TIER=fast, and each skip is recorded so the
+# closing verdict can never claim a green it did not earn. Nothing stops
+# being graded: a scheduled sweep re-runs this same script with CI_TIER=full
+# on every repo every three hours and before any release or deploy, and a
+# red there is tracked down and fixed first. CI_TIER is unset for a hand
+# run, so a bare `npm run ci:full` still runs every tier as it always did.
+CI_TIER_FULL_ONLY=(
+ "db fixture for perf (mariadb on 3307)"
+ "perf (test:performance)"
+ "coverage ratchet (coverage:check)"
+)
+DEFERRED=""
+ci_tier_deferred() {
+ [ "${CI_TIER:-full}" = "fast" ] || return 1
+ local t
+ for t in ${CI_TIER_FULL_ONLY[@]+"${CI_TIER_FULL_ONLY[@]}"}; do
+ if [ "$t" = "$1" ]; then
+ DEFERRED="$DEFERRED [$1]"
+ echo; echo "ci:full ===== $1 DEFERRED (CI_TIER=fast, runs in the full sweep) ====="
+ return 0
+ fi
+ done
+ return 1
+}
+# <<< ci-tier <<<
run_tier() {
+ ci_tier_deferred "$1" && return 0 # ci-tier guard (generated)
local name="$1"; shift
echo; echo "ci:full ===== $name ====="
if "$@"; then
@@ -93,11 +120,6 @@ need_sib() {
done
}
-export CONFORMANCE_DB_HOST="${CONFORMANCE_DB_HOST:-127.0.0.1}"
-export CONFORMANCE_DB_PORT="${CONFORMANCE_DB_PORT:-3307}"
-export CONFORMANCE_DB_USER="${CONFORMANCE_DB_USER:-root}"
-export CONFORMANCE_DB_PASS="${CONFORMANCE_DB_PASS:-testpass}"
-
need_sib xchain-indexer xchain-vm xchain-sdk xchain-decoder xchain-documentation \
xchain-encoder xchain-hub
@@ -158,17 +180,10 @@ db_tier "integration (test:integration)" npm run test:integration
# --- job: conformance (needs: ci) ------------------------------------------
# Loads the REAL indexer, hub and decoder DDL from the sibling checkouts (the
# suite resolves them as ../xchain-*/src/sql, which is why the layout above is
-# not optional) into a real MariaDB. The fixture is recycled here too; when
-# CONFORMANCE_DB_* points somewhere else, the suite follows the env and simply
-# leaves the fresh fixture unused.
-# A run that points CONFORMANCE_DB_* somewhere other than the fixture address
-# does not need the fixture at all, so a failed bring-up must not stop it.
+# not optional) into a real MariaDB. The fixture is recycled here too, and the
+# launcher inherits the same identity selected by fixture-preflight.js.
run_tier "db fixture for conformance (mariadb on 3307)" db_fixture_reset
-if [ "$CONFORMANCE_DB_HOST" = "127.0.0.1" ] && [ "$CONFORMANCE_DB_PORT" = "3307" ]; then
- db_tier "conformance: schema canary (test:conformance)" npm run test:conformance
-else
- run_tier "conformance: schema canary (test:conformance)" npm run test:conformance
-fi
+db_tier "conformance: schema canary (test:conformance)" npm run test:conformance
npm run test:integration:down >/dev/null 2>&1
echo "ci:full: db fixture torn down (no DB tiers remain)"
@@ -193,8 +208,20 @@ run_tier "drift: hub-mirror-client byte-identity" sync_mirror_check
run_tier "coverage ratchet (coverage:check)" npm run coverage:check
echo
+# >>> ci-tier summary (generated) >>>
+echo "ci:full: tier class ${CI_TIER:-full}"
+if [ -n "${DEFERRED:-}" ]; then
+ echo "ci:full: DEFERRED to the full sweep:$DEFERRED"
+fi
+# <<< ci-tier summary <<<
if [ -n "$FAILED" ]; then
echo "ci:full: RED tiers:$FAILED"
exit 1
fi
-echo "ci:full: all tiers green (same set GitHub CI runs)"
+# >>> ci-tier verdict (generated) >>>
+if [ "${CI_TIER:-full}" = "fast" ]; then
+ echo "ci:full: all FAST tiers green; the DEFERRED tiers above were NOT graded here"
+else
+ echo "ci:full: all tiers green (same set GitHub CI runs)"
+fi
+# <<< ci-tier verdict <<<
diff --git a/bin/db-fixture.js b/bin/db-fixture.js
index 4138bde3..349c6067 100644
--- a/bin/db-fixture.js
+++ b/bin/db-fixture.js
@@ -41,6 +41,7 @@ const { spawnSync } = require('child_process');
const path = require('path');
const pre = require('../test/integration/helpers/fixture-preflight.js');
+const fixturePorts = require('./fixture-ports.js');
const REPO = path.join(__dirname, '..');
@@ -48,6 +49,7 @@ function compose(args, opts) {
return spawnSync('docker', ['compose', '-f', pre.COMPOSE_FILE, ...args], {
cwd: REPO,
encoding: 'utf8',
+ env: fixturePorts.composeEnvironment(pre.COMPOSE_FILE),
...(opts || {})
});
}
diff --git a/bin/fixture-ports.js b/bin/fixture-ports.js
new file mode 100644
index 00000000..b212e784
--- /dev/null
+++ b/bin/fixture-ports.js
@@ -0,0 +1,101 @@
+#!/usr/bin/env node
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const { spawnSync } = require('child_process');
+
+const REPO = path.join(__dirname, '..');
+const SPECS = Object.freeze({
+ 'test/integration/fixtures/docker-compose.test.yml': Object.freeze({
+ XCHAIN_EXPLORER_DB_PORT: 3307
+ }),
+ 'test/chaos/fixtures/docker-compose.chaos.yml': Object.freeze({
+ XCHAIN_EXPLORER_CHAOS_API_PORT: 8474,
+ XCHAIN_EXPLORER_CHAOS_DB_PORT: 3307
+ })
+});
+
+function integer(value, label) {
+ const text = String(value);
+ if (!/^(0|[1-9][0-9]*)$/.test(text)) {
+ throw new Error(`${label} must be a non-negative integer, got ${JSON.stringify(text)}`);
+ }
+ return Number(text);
+}
+
+function offset(env) {
+ const source = env || process.env;
+ return source.CI_PORT_OFFSET === undefined || source.CI_PORT_OFFSET === ''
+ ? 0
+ : integer(source.CI_PORT_OFFSET, 'CI_PORT_OFFSET');
+}
+
+function basePort(name) {
+ for (const spec of Object.values(SPECS)) {
+ if (Object.prototype.hasOwnProperty.call(spec, name)) return spec[name];
+ }
+ throw new Error(`unknown fixture port ${name}`);
+}
+
+function port(name, env) {
+ const source = env || process.env;
+ const value = source[name] === undefined || source[name] === ''
+ ? basePort(name) + offset(source)
+ : integer(source[name], name);
+ if (value < 1 || value > 65535) throw new Error(`${name} resolves outside the TCP port range: ${value}`);
+ return value;
+}
+
+function composeEnvironment(file, env) {
+ const spec = SPECS[file];
+ if (!spec) throw new Error(`unknown fixture compose file ${file}`);
+ const result = { ...(env || process.env) };
+ for (const name of Object.keys(spec)) result[name] = String(port(name, result));
+ return result;
+}
+
+function render(file, env) {
+ const spec = SPECS[file];
+ if (!spec) throw new Error(`unknown fixture compose file ${file}`);
+ let source = fs.readFileSync(path.join(REPO, file), 'utf8');
+ for (const [name, base] of Object.entries(spec)) {
+ source = source.split(`\${${name}:-${base}}`).join(String(port(name, env)));
+ }
+ return source;
+}
+
+function compose(file, args, env) {
+ const result = spawnSync('docker', ['compose', '-f', file, ...args], {
+ cwd: REPO,
+ env: composeEnvironment(file, env),
+ stdio: 'inherit'
+ });
+ if (result.error) throw result.error;
+ return result.status === null ? 1 : result.status;
+}
+
+function main(argv) {
+ const [command, name, ...args] = argv;
+ if (command === 'render' && name && args.length === 0) {
+ process.stdout.write(render(name));
+ return 0;
+ }
+ if (command === 'port' && name && args.length === 0) {
+ process.stdout.write(String(port(name)) + '\n');
+ return 0;
+ }
+ if (command === 'compose' && name && args.length > 0) return compose(name, args);
+ throw new Error('usage: fixture-ports.js ');
+}
+
+if (require.main === module) {
+ try {
+ process.exitCode = main(process.argv.slice(2));
+ } catch (err) {
+ console.error(err.message);
+ process.exitCode = 1;
+ }
+}
+
+module.exports = { SPECS, offset, port, composeEnvironment, render };
diff --git a/bin/pins/at1-explorer-identity.json b/bin/pins/at1-explorer-identity.json
index 0c3fdaca..57aa4e74 100644
--- a/bin/pins/at1-explorer-identity.json
+++ b/bin/pins/at1-explorer-identity.json
@@ -5,8 +5,8 @@
"twins": [
{
"path": "src/consensus/equivocation_header.js",
- "bytes": 4171,
- "sha256": "441998fc0d7cbf9cb94f79933224f1b5839981e79fb639bdda5eb0bacb07b46d"
+ "bytes": 4357,
+ "sha256": "33795d35b6133135745471fd00c93da001aef942dd1d894ff929a4ebc53fb81e"
},
{
"path": "src/consensus/gates/mirror_admission_gate.js",
@@ -30,8 +30,8 @@
},
{
"path": "src/consensus/stake_weighted_quorum.js",
- "bytes": 10112,
- "sha256": "ca074be59873a47b30da94c98a812771e3fb3252fea83fd5e56dbf2bd5b26ddf"
+ "bytes": 10294,
+ "sha256": "946191601217a7b2ac0e6c50d37eb6a111ba30544680aeff434f52b9f5c2d661"
}
],
"routes": {
diff --git a/bin/run-conformance.js b/bin/run-conformance.js
new file mode 100644
index 00000000..520c75bc
--- /dev/null
+++ b/bin/run-conformance.js
@@ -0,0 +1,46 @@
+#!/usr/bin/env node
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * Launch conformance with the identity selected by fixture preflight.
+ * The password is carried only in the child environment.
+ */
+
+'use strict';
+
+const { spawnSync } = require('child_process');
+const path = require('path');
+
+const pre = require('../test/integration/helpers/fixture-preflight.js');
+
+const REPO = path.join(__dirname, '..');
+
+function run() {
+ const result = spawnSync(process.execPath, [
+ require.resolve('mocha/bin/mocha.js'),
+ 'test/conformance/**/*.test.js',
+ '--timeout', '120000',
+ '--exit',
+ '--recursive'
+ ], {
+ cwd: REPO,
+ env: pre.conformanceEnvironment(),
+ stdio: 'inherit'
+ });
+ if (result.error) throw result.error;
+ return result.status === null ? 1 : result.status;
+}
+
+if (require.main === module) process.exit(run());
+
+module.exports = { run };
diff --git a/docs/openapi.build.js b/docs/openapi.build.js
index ec33aeff..13e96395 100644
--- a/docs/openapi.build.js
+++ b/docs/openapi.build.js
@@ -14,10 +14,15 @@
* either side must land together. The /{COIN}/explorer/* datatable routes are
* an internal surface for the web UI and are intentionally NOT documented.
*
+ * This is the one published spec: /openapi.json serves it and the interactive
+ * docs page (src/content/js/swagger-initializer.js) loads it. Typed 200 bodies
+ * come from docs/openapi.schemas.js, keyed by each route's db method.
+ *
* Run: node docs/openapi.build.js (regenerates docs/openapi.json in place)
*/
const fs = require('fs');
const path = require('path');
+const { COMPONENT_SCHEMAS, responseSchema } = require('./openapi.schemas.js');
const COINS = ['BTC', 'TBTC', 'RBTC', 'LTC', 'TLTC', 'RLTC', 'DOGE', 'TDOGE', 'RDOGE'];
@@ -595,6 +600,7 @@ function operation([p, method, types, tag, summary], opts) {
content: {
'application/json': {
schema: o.schema
+ || responseSchema(method)
|| { $ref: isList ? '#/components/schemas/ListResponse' : '#/components/schemas/ObjectResponse' },
},
},
@@ -672,7 +678,12 @@ const spec = {
+ 'LLM-friendly docs: https://docs.xchain.io/llms.txt',
license: { name: 'AGPL-3.0-or-later', url: 'https://docs.xchain.io/legal/licensing.md' },
},
- servers: [{ url: 'https://explorer.xchain.io' }],
+ // Relative first: the docs page ships with every explorer, so "Try it out" must
+ // call the instance that served it, not mainnet.
+ servers: [
+ { url: '/', description: 'This explorer instance' },
+ { url: 'https://explorer.xchain.io', description: 'Public mainnet explorer' },
+ ],
tags: [
{ name: 'Core' }, { name: 'Tokens' }, { name: 'Action history' }, { name: 'Markets' },
{ name: 'Dispensers' }, { name: 'Contracts' }, { name: 'Staking' }, { name: 'Prices' },
@@ -697,6 +708,7 @@ const spec = {
properties: {
total: { type: 'integer', description: 'Total matching rows' },
data: { type: 'array', items: { type: 'object' }, description: 'Result rows (fields vary per endpoint; amounts are decimal strings)' },
+ runtime: { type: 'string', description: 'Amount of time to execute and return the data' },
},
required: ['total', 'data'],
},
@@ -920,6 +932,11 @@ const spec = {
},
};
+// Refuse a typed body that would silently replace a hand-written one above.
+for (const name of Object.keys(COMPONENT_SCHEMAS))
+ if (spec.components.schemas[name]) throw new Error(`openapi.build: schema ${name} is declared twice`);
+Object.assign(spec.components.schemas, COMPONENT_SCHEMAS);
+
const out = path.join(__dirname, 'openapi.json');
fs.writeFileSync(out, JSON.stringify(spec, null, 2) + '\n');
console.log(`wrote ${out}: ${Object.keys(paths).length} paths (${ROUTES.length} table routes + ${SPECIAL.length} special)`);
diff --git a/docs/openapi.json b/docs/openapi.json
index b60fc536..57d3ab8e 100644
--- a/docs/openapi.json
+++ b/docs/openapi.json
@@ -11,7 +11,12 @@
},
"servers": [
{
- "url": "https://explorer.xchain.io"
+ "url": "/",
+ "description": "This explorer instance"
+ },
+ {
+ "url": "https://explorer.xchain.io",
+ "description": "Public mainnet explorer"
}
],
"tags": [
@@ -111,7 +116,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Address"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -178,7 +203,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Airdrop"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -244,7 +289,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Batch"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -310,7 +375,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Broadcast"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -377,7 +462,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Callback"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -444,7 +549,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Destroy"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -511,7 +636,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Dividend"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -581,7 +726,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Dispenser"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -647,7 +812,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DispenserCancel"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -713,7 +898,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DispenserClose"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -779,7 +984,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DispenserExpire"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -845,7 +1070,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/DispenserEdit"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -915,7 +1160,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Dispense"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -984,7 +1249,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Fee"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1052,7 +1337,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/File"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1119,7 +1424,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Issue"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1185,7 +1510,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Link"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1251,7 +1596,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/List"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1319,7 +1684,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1388,7 +1773,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Mint"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1455,7 +1860,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Order"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1521,7 +1946,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OrderExpire"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1587,7 +2032,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OrderEdit"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1653,7 +2118,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OrderCancel"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1718,7 +2203,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OrderMatch"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -1762,7 +2267,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/OrderMatch"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2029,7 +2554,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Send"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2096,7 +2641,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Sleep"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2163,7 +2728,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Swap"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2229,7 +2814,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SwapEdit"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2295,7 +2900,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SwapExpire"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2361,7 +2986,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SwapCancel"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2426,7 +3071,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SwapMatch"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2470,7 +3135,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/SwapMatch"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -2538,7 +3223,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Sweep"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8078,7 +8783,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/ExplorerStatus"
}
}
}
@@ -8122,7 +8827,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Action"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8166,7 +8891,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/ActionDetail"
}
}
}
@@ -8210,7 +8935,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/AddressInfo"
}
}
}
@@ -8263,7 +8988,34 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Balance"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The queried address, echoed when it is address-shaped"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8307,7 +9059,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/Block"
}
}
}
@@ -8547,7 +9299,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Credit"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8613,7 +9385,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Debit"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8679,7 +9471,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Escrow"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8747,7 +9559,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/History"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -8800,7 +9632,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "$ref": "#/components/schemas/Holder"
}
}
}
@@ -8865,7 +9697,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "$ref": "#/components/schemas/Holder"
}
}
}
@@ -9207,7 +10039,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/Token"
}
}
}
@@ -9275,7 +10107,27 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/TokenRow"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9495,7 +10347,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/Transaction"
}
}
}
@@ -9539,7 +10391,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Market"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9591,7 +10464,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Market"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9702,7 +10596,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MarketHistory"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9770,7 +10685,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MarketHistory"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9830,7 +10766,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MarketOrder"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9898,7 +10855,28 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ListResponse"
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "string",
+ "pattern": "^[0-9]+$",
+ "description": "Total number of records available, as a decimal string"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/MarketOrder"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ]
}
}
}
@@ -9949,7 +10927,7 @@
"content": {
"application/json": {
"schema": {
- "$ref": "#/components/schemas/ObjectResponse"
+ "$ref": "#/components/schemas/Orderbook"
}
}
}
@@ -11185,6 +12163,10 @@
"type": "object"
},
"description": "Result rows (fields vary per endpoint; amounts are decimal strings)"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
}
},
"required": [
@@ -11625,6 +12607,4553 @@
"required": [
"error"
]
+ },
+ "Action": {
+ "type": "object",
+ "description": "One row of the raw action feed",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Address": {
+ "type": "object",
+ "description": "ADDRESS action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "dispenser_preference": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Who may open a dispenser for this address (1=owner only, 2=anyone)"
+ },
+ "fee_preference": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "require_memo": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Airdrop": {
+ "type": "object",
+ "description": "AIRDROP action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "list_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Batch": {
+ "type": "object",
+ "description": "BATCH action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Broadcast": {
+ "type": "object",
+ "description": "BROADCAST action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "broadcast_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "fee": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "value": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "Callback": {
+ "type": "object",
+ "description": "CALLBACK action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "callback_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "callback_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Destroy": {
+ "type": "object",
+ "description": "DESTROY action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Dividend": {
+ "type": "object",
+ "description": "DIVIDEND action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "dividend_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Dispenser": {
+ "type": "object",
+ "description": "DISPENSER action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "current_status": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The dispenser's lifecycle status from its latest status row; `status` is the validity of the creating action only"
+ },
+ "escrow_remaining": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Amount of GIVE_TICK left in escrow now (create escrow + refills - payouts); \"0\" once the dispenser is closed, cancelled or expired; null when it cannot be derived"
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_escrow": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Amount of GIVE_TICK escrowed when the dispenser was created"
+ },
+ "give_ownership": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "give_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "oracle_address": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The ORACLE_ADDRESS a Mode B dispenser prices against; null otherwise"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "DispenserCancel": {
+ "type": "object",
+ "description": "DISPENSER_CANCEL action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "dispenser_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "DispenserClose": {
+ "type": "object",
+ "description": "DISPENSER_CLOSE action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "close_reason": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Why the dispenser closed: empty after an auto-drain, cancelled once the close delay elapses on a cancel"
+ },
+ "dispenser_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "dispenser_address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fiat": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fiat_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "oracle_address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "DispenserEdit": {
+ "type": "object",
+ "description": "DISPENSER_EDIT action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "dispenser_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "give_escrow": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "DispenserExpire": {
+ "type": "object",
+ "description": "DISPENSER_EXPIRE action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "dispenser_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Dispense": {
+ "type": "object",
+ "description": "DISPENSE action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "dispenser_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Coin attributed to this dispense. When one payment fills several dispenses in the same transaction, this is that dispense's share, not the payment's full amount"
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Fee": {
+ "type": "object",
+ "description": "Protocol fee payment data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fee_preference": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "fee_version": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "1=legacy, 2=unified gas"
+ },
+ "gas_cost": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "gas_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "method": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Legacy fee payment method (1=destroy, 2=donate)"
+ },
+ "native_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "native_coin_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "oracle_round": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "payment_mode": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "1=native coin, 2=XCHAIN balance"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "xchain_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "File": {
+ "type": "object",
+ "description": "FILE action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "encryption_method": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "gate_min_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "gate_ticker": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "key_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "Issue": {
+ "type": "object",
+ "description": "ISSUE action data (the fields as the action declared them)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "callback_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "callback_block": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "callback_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "decimals": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_callback": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_max_mint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_max_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_mint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_mint_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "lock_sleep": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "max_mint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "max_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "mint_address_max": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "mint_start_block": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "mint_stop_block": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "mint_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "transfer": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "transfer_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Link": {
+ "type": "object",
+ "description": "LINK action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "coin1": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "coin1_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "coin2": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "coin2_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "List": {
+ "type": "object",
+ "description": "LIST action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "edit": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "list_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "Message": {
+ "type": "object",
+ "description": "MESSAGE action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "coin": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Destination coin network for cross-chain messages"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "encrypted_message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "encryption_key": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "encryption_method": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "plaintext_message": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Mint": {
+ "type": "object",
+ "description": "MINT action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Order": {
+ "type": "object",
+ "description": "ORDER action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "get_address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_ownership": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "get_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_ownership": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "give_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "payout_legs": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "JSON [{to,bps}] split of seller proceeds applied at match; null when none"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "OrderCancel": {
+ "type": "object",
+ "description": "ORDER_CANCEL action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "order_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "OrderEdit": {
+ "type": "object",
+ "description": "ORDER_EDIT action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "order_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "OrderExpire": {
+ "type": "object",
+ "description": "ORDER_EXPIRE action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "order_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "OrderMatch": {
+ "type": "object",
+ "description": "ORDER_MATCH action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "get_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "settlement_type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Send": {
+ "type": "object",
+ "description": "SEND action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Sleep": {
+ "type": "object",
+ "description": "SLEEP action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "resume_block": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Swap": {
+ "type": "object",
+ "description": "SWAP action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "get_address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "get_ownership": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "get_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_ownership": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "give_tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "payout_legs": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "swap_status": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The swap's lifecycle status from its latest status row; `status` is the validity of the creating action only"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "SwapCancel": {
+ "type": "object",
+ "description": "SWAP_CANCEL action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "swap_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "SwapEdit": {
+ "type": "object",
+ "description": "SWAP_EDIT action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "allow_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_list": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "swap_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "SwapExpire": {
+ "type": "object",
+ "description": "SWAP_EXPIRE action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "swap_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "SwapMatch": {
+ "type": "object",
+ "description": "SWAP_MATCH action data (system-injected, so no transaction fields)",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "get_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "get_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "give_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "give_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Sweep": {
+ "type": "object",
+ "description": "SWEEP action data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "balances": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Nonzero when token balances are swept"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "dispensers": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Nonzero when open DISPENSERs are closed and their escrow credited to the destination"
+ },
+ "memo": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "orders": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Nonzero when open ORDERs are cancelled and their escrow credited to the destination"
+ },
+ "ownerships": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Nonzero when token ownerships are swept"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "swaps": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "Nonzero when open SWAPs are cancelled and their escrow credited to the destination"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Credit": {
+ "type": "object",
+ "description": "Ledger credit data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Debit": {
+ "type": "object",
+ "description": "Ledger debit data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Escrow": {
+ "type": "object",
+ "description": "Escrowed balance data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Balance": {
+ "type": "object",
+ "description": "Address token balance data",
+ "properties": {
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "coin_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "decimals": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "TokenRow": {
+ "type": "object",
+ "description": "One token from the token search",
+ "properties": {
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "decimals": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "id": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "lock_callback": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_description": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_max_mint": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_max_supply": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_mint": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_mint_supply": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "lock_sleep": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "max_mint": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "max_supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "supply": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "ActionDetail": {
+ "type": "object",
+ "description": "One action, fully decoded. Carries the fields its action type defines (the columns its list route serves plus any detail-only sections) and the ledger tail below, which every action carries.",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_format": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "credits": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "object",
+ "description": "One ledger row the action wrote",
+ "properties": {
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "description": "The credits this action wrote; null when none"
+ },
+ "debits": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "object",
+ "description": "One ledger row the action wrote",
+ "properties": {
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "description": "The debits this action wrote; null when none"
+ },
+ "escrows": {
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "type": "object",
+ "description": "One ledger row the action wrote",
+ "properties": {
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "description": "The escrows this action wrote; null when none"
+ },
+ "fee": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "object",
+ "description": "The protocol fee this action paid",
+ "properties": {
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "destination": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "fee_preference": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "fee_version": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "gas_cost": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "gas_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "method": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "native_coin": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "native_coin_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "oracle_round": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "payment_mode": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "xchain_amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ ],
+ "description": "Action fee information; null when the action paid none"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "tx_data": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The decoded action string of the carrying transaction; null for a system-injected action"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "AddressInfo": {
+ "type": "object",
+ "description": "Address information (type, native-coin balances and UTXO counts from this coin's UTXO tracker, controller bindings). Tracker fields stay null when no tracker is configured or it is unreachable.",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "balances": {
+ "type": "object",
+ "description": "Balances of COIN (BTC, LTC, DOGE, etc) as decimal strings",
+ "properties": {
+ "confirmed": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "pending": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "received": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "controllers": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ },
+ "description": "Controller bindings still gating this address's native actions; empty when none"
+ },
+ "estimated_value": {
+ "type": "object",
+ "description": "Estimated value of the confirmed balance in COIN and in USD",
+ "properties": {
+ "btc": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "usd": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "info": {
+ "type": "object",
+ "properties": {
+ "address": {
+ "type": "string"
+ },
+ "address_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "The address's deterministic index id, for ^ compaction; null when not deterministic"
+ }
+ }
+ },
+ "mempool_ready": {
+ "type": "boolean",
+ "description": "Present only when the tracker reports it"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "tracker_available": {
+ "type": "boolean"
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "Address Type (P2PKH, P2SH, Bech32, P2TR)"
+ },
+ "utxos": {
+ "type": "object",
+ "description": "Unspent Transaction Output counts (UTXOs)",
+ "properties": {
+ "confirmed": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "pending": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "required": [
+ "address",
+ "balances",
+ "utxos",
+ "estimated_value",
+ "tracker_available",
+ "controllers",
+ "info"
+ ]
+ },
+ "Block": {
+ "type": "object",
+ "description": "COIN block data",
+ "properties": {
+ "actions_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 of the block's actions data"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "contract_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 of the block's contract data"
+ },
+ "ledger_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 of the block's credits, debits, escrows and balances"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "state_hash": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "sha256 of in-place mutations and backdated credits (replication integrity only)"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ },
+ "required": [
+ "block_index",
+ "timestamp"
+ ]
+ },
+ "ExplorerStatus": {
+ "type": "object",
+ "description": "Explorer status data",
+ "properties": {
+ "available": {
+ "type": "object",
+ "description": "Coin networks this instance serves as CURRENT data, keyed by coin ticker. Starts from the configured availability map and then drops every coin the tip-age gate marks stale (see 'stale'), so this map varies per request and a delisted coin still appears in 'supported'. Read this to decide whether to query a coin; read 'supported' to learn which coins the instance knows about at all.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "hub_config_fetched_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "ISO-8601 timestamp of the last successful hub-config fetch. null until the first successful fetch.",
+ "format": "date-time"
+ },
+ "hub_config_age_seconds": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Seconds since the last successful hub-config fetch. A climbing value means the served hub-derived config is stale. null until the first successful fetch."
+ },
+ "last_block": {
+ "type": "object",
+ "description": "Most recent block index processed by the indexer, keyed by coin ticker (e.g. RBTC: 850). Value is null for a coin whose per-coin DB read failed (the outage this health endpoint exists to surface).",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "last_block_time": {
+ "type": "object",
+ "description": "Unix block_time of the most recent block processed by the indexer, keyed by coin ticker. Compare against the chain tip to detect indexer lag. Value is null for a coin whose per-coin DB read failed.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_tip": {
+ "type": "object",
+ "description": "Decoder's highest processed block index, keyed by coin ticker. null when the decoder tip is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_lag_blocks": {
+ "type": "object",
+ "description": "decoder_tip minus last_block (how far the indexer trails the decoder), keyed by coin ticker. null when the decoder tip is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "tip_age_seconds": {
+ "type": "object",
+ "description": "Wall-clock seconds between now and the block_time of the newest indexed block, keyed by coin ticker. Never negative: a tip dated ahead of this host clamps to 0 here and reports its skew in tip_future_seconds. null when that block_time is missing or unreadable. Unlike decoder_lag_blocks this sees a JOINT indexer+decoder freeze, because it measures against the local clock rather than against the other replica. Only coins this instance measures (those with a live pool) appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "tip_future_seconds": {
+ "type": "object",
+ "description": "Seconds the newest indexed block is dated AHEAD of this host's clock, keyed by coin ticker; 0 when the tip is not ahead, null when block_time is missing or unreadable. A non-zero value is host clock drift or a chain with lax timestamp rules, and it means tip_age_seconds is clamped rather than measured. Skew past the tolerance (EXPLORER_TIP_MAX_FUTURE_SKEW_S_, else EXPLORER_TIP_MAX_FUTURE_SKEW_S, default 7200; 0 disables the check) reads stale, so a future-dated tip cannot pass as fresh indefinitely. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "indexer_state": {
+ "type": "object",
+ "description": "Why the indexer trails the decoder, keyed by coin ticker. 'live' when decoder_lag_blocks is 0; 'future_block_wait' when the next block (last_block + 1) carries a block_time dated ahead of this host's clock, meaning no node on the chain may commit it yet and the pause is consensus rather than failure; 'behind' when that next block is already admissible and still uncommitted, which is the state worth alerting on; null when it cannot be determined. Read this field, NOT tip_future_seconds, to tell a deliberate wait apart from a wedge: tip_future_seconds describes the block the indexer has already committed, which is always past-dated, so it reads 0 for the whole duration of a future_block_wait. Chains with lax timestamp rules (Bitcoin testnet4 rides the 20-minute minimum-difficulty rule, stamping blocks about 1201s apart into the future) sit in future_block_wait as their steady state. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "next_block_time": {
+ "type": "object",
+ "description": "The decoder's recorded block_time (unix seconds) for the next block the indexer must commit (last_block + 1), keyed by coin ticker. Read from the decoder DB because the indexer has not committed that block yet. null when the indexer is at the decoder tip, the decoder DB is unreachable, or that block carries no usable timestamp. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "next_block_future_seconds": {
+ "type": "object",
+ "description": "Seconds the next block to be committed is dated AHEAD of this host's clock, keyed by coin ticker; 0 when it is not ahead, null when unknown. This is the honest measure of a consensus wait, and the value tip_future_seconds is often mistaken for. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "indexer_wait_clears_at": {
+ "type": "object",
+ "description": "ISO-8601 instant at which a 'future_block_wait' ends for that coin, i.e. when this host's clock reaches the next block's timestamp and the block becomes committable, keyed by coin ticker. null in every other state. Present so a client can render a countdown rather than leaving a confirmed-but-unindexed transaction looking lost. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "stale": {
+ "type": "object",
+ "description": "Tip-freshness verdict per coin ticker: true when tip_age_seconds has passed that coin's max tip age (EXPLORER_TIP_MAX_AGE_S_, else EXPLORER_TIP_MAX_AGE_S, default 21600; 0 disables the gate), and also true when tip_future_seconds has passed that coin's max future skew. Fails closed, so a missing or unreadable block_time also reads true. A true entry is removed from 'available' and left in 'supported'. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "replica_halted": {
+ "type": "object",
+ "description": "Durable consensus-divergence halt verdict per coin ticker, read from xchain-sync's sync_halt tables on the indexer and decoder replicas (true = either database has an active, uncleared halt row). A halted replica keeps reporting a small lag until its source mints past it, so this is detectable neither by 'stale' nor by tip_age_seconds; it composes with 'stale' rather than replacing it (immediate detection here, eventual removal from 'available' there). true/false only when both tables were read successfully; null when the signal could not be determined (table absent, e.g. a DB not built by the sync client, or the read failed) and is never coerced to false. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
+ },
+ "chain_tip": {
+ "type": "object",
+ "description": "Coin node's chain tip as the decoder reports it, keyed by coin ticker. null when the decoder health call is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "chain_lag_blocks": {
+ "type": "object",
+ "description": "Decoder's self-reported gap to the chain tip, keyed by coin ticker. null when the decoder health call is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_health": {
+ "type": "object",
+ "description": "Decoder self-reported status per coin ticker: 'healthy', 'unhealthy', 'node-stale' (the decoder's cached coin-node height is frozen, so chain_tip and chain_lag_blocks are nulled out), 'unconfigured' (no decoder URL set), or 'unreachable' (health call failed).",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "supported": {
+ "type": "object",
+ "description": "Every coin network the XChain platform defines for this instance, keyed by coin ticker with a display name as the value. Static per config: the tip-age gate never removes a coin from here, only from 'available'.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "History": {
+ "type": "object",
+ "description": "History data",
+ "properties": {
+ "action": {
+ "type": "string"
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "details": {
+ "type": [
+ "object",
+ "boolean"
+ ],
+ "description": "Summary fields of the action (tick, amount, destination and the like); false when the action carries none"
+ },
+ "parent_batch_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$",
+ "description": "The BATCH this action rode in; null outside a batch"
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ }
+ }
+ },
+ "Holder": {
+ "type": "object",
+ "properties": {
+ "total": {
+ "type": "integer",
+ "description": "Total number of records available"
+ },
+ "data": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/HolderRow"
+ }
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "coin_price": {
+ "type": "string",
+ "description": "Token price in COIN; absent when the token has no holders"
+ },
+ "decimals": {
+ "type": "integer",
+ "description": "Absent when the token has no holders"
+ },
+ "supply": {
+ "type": "string",
+ "description": "Absent when the token has no holders"
+ },
+ "tick": {
+ "type": "string",
+ "description": "Absent when the token has no holders"
+ }
+ },
+ "required": [
+ "total",
+ "data"
+ ],
+ "description": "Token holder data"
+ },
+ "HolderRow": {
+ "type": "object",
+ "description": "One holder of a token",
+ "properties": {
+ "address": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "Market": {
+ "type": "object",
+ "description": "DEX market data",
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "last_updated": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "tick1": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_24hr_change": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_24hr_high": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_24hr_low": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_24hr_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_24hr_volume": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_ask": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_bid": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick1_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_24hr_change": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_24hr_high": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_24hr_low": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_24hr_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_24hr_volume": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_ask": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_bid": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick2_price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "MarketHistory": {
+ "type": "object",
+ "description": "DEX market history data",
+ "properties": {
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": "string"
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "price": {
+ "type": "string"
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "buy",
+ "sell"
+ ]
+ }
+ }
+ },
+ "MarketOrder": {
+ "type": "object",
+ "description": "DEX market order data",
+ "properties": {
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "expiration": {
+ "type": [
+ "string",
+ "integer",
+ "null"
+ ],
+ "description": "Expiration block: a decimal string, or a number once an ORDER_EDIT has changed it"
+ },
+ "price": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "buy",
+ "sell"
+ ]
+ }
+ }
+ },
+ "Orderbook": {
+ "type": "object",
+ "description": "DEX market orderbook data",
+ "properties": {
+ "asks": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 2,
+ "maxItems": 2,
+ "description": "[price, amount]"
+ },
+ "description": "Open asks aggregated by price, lowest first"
+ },
+ "bids": {
+ "type": "array",
+ "items": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 2,
+ "maxItems": 2,
+ "description": "[price, amount]"
+ },
+ "description": "Open bids aggregated by price, highest first"
+ },
+ "market": {
+ "type": "string",
+ "description": "TICK1/TICK2; present only when the book has orders"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ }
+ },
+ "required": [
+ "asks",
+ "bids"
+ ]
+ },
+ "Token": {
+ "type": "object",
+ "description": "Token data. `locks.bridge` and the bridge fields of `info` appear only on a database that carries the bridge columns.",
+ "properties": {
+ "callback": {
+ "type": "object",
+ "properties": {
+ "amount": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "block": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "price": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ },
+ "controllers": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "info": {
+ "type": "object",
+ "properties": {
+ "bridge_chains": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "bridged": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "coin": {
+ "type": "string"
+ },
+ "decimals": {
+ "type": "integer"
+ },
+ "description": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "escrow_action_index": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "min_depth": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "pattern": "^[0-9]+$"
+ },
+ "owner": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tick_id": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Deterministic ticker id, for ^ compaction; null when not deterministic"
+ }
+ }
+ },
+ "linked_files": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "action_index": {
+ "type": "integer"
+ },
+ "block_index": {
+ "type": "integer"
+ },
+ "gated": {
+ "type": "boolean"
+ },
+ "name": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "title": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "type": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "lists": {
+ "type": "object",
+ "properties": {
+ "allow": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ },
+ "block": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ }
+ },
+ "locks": {
+ "type": "object",
+ "properties": {
+ "bridge": {
+ "type": "boolean"
+ },
+ "callback": {
+ "type": "boolean"
+ },
+ "description": {
+ "type": "boolean"
+ },
+ "max_mint": {
+ "type": "boolean"
+ },
+ "max_supply": {
+ "type": "boolean"
+ },
+ "mint": {
+ "type": "boolean"
+ },
+ "mint_supply": {
+ "type": "boolean"
+ },
+ "sleep": {
+ "type": "boolean"
+ }
+ }
+ },
+ "market": {
+ "type": "object",
+ "properties": {
+ "floor": {
+ "type": "number"
+ },
+ "price": {
+ "type": "number"
+ }
+ }
+ },
+ "mints": {
+ "type": "object",
+ "properties": {
+ "address_max": {
+ "type": "number"
+ },
+ "max": {
+ "type": "number"
+ },
+ "start_block": {
+ "type": "number"
+ },
+ "stop_block": {
+ "type": "number"
+ }
+ }
+ },
+ "open_polls": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "projects": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ },
+ "registry": {
+ "oneOf": [
+ {
+ "type": "null"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "link_action_index": {
+ "type": "integer"
+ },
+ "membership_action_index": {
+ "type": "integer"
+ },
+ "roster_action_index": {
+ "type": "integer"
+ },
+ "total": {
+ "type": "integer"
+ }
+ }
+ }
+ ]
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "supply": {
+ "type": "object",
+ "properties": {
+ "current": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "decimals": {
+ "type": "integer"
+ },
+ "max": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "required": [
+ "info",
+ "callback",
+ "market",
+ "lists",
+ "locks",
+ "mints",
+ "supply"
+ ]
+ },
+ "Transaction": {
+ "type": "object",
+ "description": "Transaction data. An unknown transaction answers only an empty actions list and a null tx_data.",
+ "properties": {
+ "actions": {
+ "type": "array",
+ "description": "Actions this transaction carried, newest first",
+ "items": {
+ "type": "object",
+ "description": "One action summary",
+ "properties": {
+ "action": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "action_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "details": {
+ "type": [
+ "object",
+ "boolean"
+ ],
+ "description": "Summary fields of the action (tick, amount, destination and the like); false when the action carries none"
+ },
+ "status": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ }
+ },
+ "block_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "runtime": {
+ "type": "string",
+ "description": "Amount of time to execute and return the data"
+ },
+ "source": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "timestamp": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ },
+ "tx_data": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "The decoded action string the transaction carries"
+ },
+ "tx_hash": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "tx_index": {
+ "type": "string",
+ "pattern": "^[0-9]+$"
+ }
+ },
+ "required": [
+ "actions",
+ "tx_data"
+ ]
}
},
"responses": {
diff --git a/docs/openapi.schemas.js b/docs/openapi.schemas.js
new file mode 100644
index 00000000..576c0965
--- /dev/null
+++ b/docs/openapi.schemas.js
@@ -0,0 +1,565 @@
+/*
+ * Copyright © 2025–2026 Dankest, LLC
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ * Licensed under the GNU Affero GPL v3.0 or later; see LICENSE.md.
+ * A commercial license is available - contact legal@dankest.llc.
+ *
+ * Typed 200 bodies for docs/openapi.build.js, keyed by the db method a route calls.
+ *
+ * Every row schema below lists exactly the columns its reader's query selects,
+ * typed as the response sink writes them: a BIGINT column (action and block
+ * indexes, block times, list references) leaves utility.jsonStringify as a
+ * decimal string, a TINYINT/INT column stays a JSON number, and text is a
+ * string. test/unit/http/openapi_response_schemas.test.js runs each reader and
+ * fails when a selected column and its row schema disagree, so a column added
+ * to a query lands here in the same change.
+ *
+ * Pure data plus two builders: no I/O, no requires, safe to load from a test.
+ */
+'use strict';
+
+// Column type codes. Upper case never leaves the sink as null; lower case can.
+const COLUMN_TYPES = {
+ S: { type: 'string' },
+ s: { type: ['string', 'null'] },
+ D: { type: 'string', pattern: '^[0-9]+$' },
+ d: { type: ['string', 'null'], pattern: '^[0-9]+$' },
+ i: { type: ['integer', 'null'] },
+};
+
+// Build a row object from 'name:code' pairs, sorted the way the API ksorts rows.
+function rowSchema(description, spec, notes, extra) {
+ const properties = {};
+ for (const field of spec.trim().split(/\s+/)) {
+ const [name, code] = field.split(':');
+ if (!COLUMN_TYPES[code]) throw new Error(`openapi.schemas: unknown column code ${code} on ${name}`);
+ properties[name] = Object.assign({}, COLUMN_TYPES[code],
+ notes && notes[name] ? { description: notes[name] } : {});
+ }
+ Object.assign(properties, extra || {});
+ const sorted = {};
+ for (const name of Object.keys(properties).sort()) sorted[name] = properties[name];
+ return { type: 'object', description, properties: sorted };
+}
+
+const TX_TAIL = 'block_index:D timestamp:D tx_hash:s tx_index:d';
+const ACTION_HEAD = 'action:S action_index:D action_format:i source:s';
+
+// [schema, db method, description, columns, per-column notes]
+const ROWS = [
+ ['Action', 'getActions', 'One row of the raw action feed',
+ 'action:S action_index:D action_format:i source:s ' + TX_TAIL],
+ ['Address', 'getAddresses', 'ADDRESS action data',
+ ACTION_HEAD + ' fee_preference:d require_memo:d dispenser_preference:d memo:s status:s ' + TX_TAIL,
+ { dispenser_preference: 'Who may open a dispenser for this address (1=owner only, 2=anyone)' }],
+ ['Airdrop', 'getAirdrops', 'AIRDROP action data',
+ ACTION_HEAD + ' tick:s list_action_index:d amount:s memo:s status:s ' + TX_TAIL],
+ ['Batch', 'getBatches', 'BATCH action data',
+ ACTION_HEAD + ' status:s ' + TX_TAIL],
+ ['Broadcast', 'getBroadcasts', 'BROADCAST action data',
+ ACTION_HEAD + ' message:s value:s fee:s broadcast_action_index:d memo:s status:s ' + TX_TAIL],
+ ['Callback', 'getCallbacks', 'CALLBACK action data',
+ ACTION_HEAD + ' tick:s callback_tick:s callback_amount:s memo:s status:s ' + TX_TAIL],
+ ['Destroy', 'getDestroys', 'DESTROY action data',
+ ACTION_HEAD + ' tick:s amount:s memo:s status:s ' + TX_TAIL],
+ ['Dividend', 'getDividends', 'DIVIDEND action data',
+ ACTION_HEAD + ' tick:s dividend_tick:s amount:s memo:s status:s ' + TX_TAIL],
+ ['Dispenser', 'getDispensers', 'DISPENSER action data',
+ ACTION_HEAD + ' address:s give_coin:s give_tick:s give_amount:s give_escrow:s give_ownership:i'
+ + ' get_coin:s get_tick:s get_amount:s oracle_address:s memo:s status:s ' + TX_TAIL
+ + ' escrow_remaining:s current_status:s',
+ { give_escrow: 'Amount of GIVE_TICK escrowed when the dispenser was created',
+ escrow_remaining: 'Amount of GIVE_TICK left in escrow now (create escrow + refills - payouts); '
+ + '"0" once the dispenser is closed, cancelled or expired; null when it cannot be derived',
+ current_status: 'The dispenser\'s lifecycle status from its latest status row; `status` is the validity of the creating action only',
+ oracle_address: 'The ORACLE_ADDRESS a Mode B dispenser prices against; null otherwise' }],
+ ['DispenserCancel', 'getDispenserCancels', 'DISPENSER_CANCEL action data',
+ ACTION_HEAD + ' dispenser_action_index:d memo:s status:s ' + TX_TAIL],
+ ['DispenserClose', 'getDispenserCloses', 'DISPENSER_CLOSE action data (system-injected, so no transaction fields)',
+ 'action:S action_index:D action_format:i dispenser_action_index:d dispenser_address:s give_coin:s give_tick:s'
+ + ' give_amount:s get_coin:s get_tick:s get_amount:s fiat:s fiat_amount:s oracle_address:s'
+ + ' block_index:D timestamp:D status:s close_reason:s',
+ { close_reason: 'Why the dispenser closed: empty after an auto-drain, cancelled once the close delay elapses on a cancel' }],
+ ['DispenserEdit', 'getDispenserEdits', 'DISPENSER_EDIT action data',
+ ACTION_HEAD + ' dispenser_action_index:d give_escrow:s expiration:d allow_list:d block_list:d memo:s status:s ' + TX_TAIL],
+ ['DispenserExpire', 'getDispenserExpires', 'DISPENSER_EXPIRE action data (system-injected, so no transaction fields)',
+ ACTION_HEAD + ' dispenser_action_index:d block_index:D timestamp:D status:s'],
+ ['Dispense', 'getDispenses', 'DISPENSE action data',
+ ACTION_HEAD + ' dispenser_action_index:d destination:s give_coin:s give_tick:s give_amount:s get_coin:s'
+ + ' get_tick:s get_amount:s status:s ' + TX_TAIL,
+ { get_amount: 'Coin attributed to this dispense. When one payment fills several dispenses in the same transaction, this is that dispense\'s share, not the payment\'s full amount' }],
+ ['Fee', 'getFees', 'Protocol fee payment data',
+ ACTION_HEAD + ' destination:s tick:s method:d amount:s gas_cost:d gas_price:s xchain_amount:s payment_mode:i'
+ + ' native_coin_amount:s native_coin:s oracle_round:d fee_preference:i fee_version:i ' + TX_TAIL,
+ { method: 'Legacy fee payment method (1=destroy, 2=donate)',
+ payment_mode: '1=native coin, 2=XCHAIN balance',
+ fee_version: '1=legacy, 2=unified gas' }],
+ ['File', 'getFiles', 'FILE action data',
+ ACTION_HEAD + ' name:s title:s type:s memo:s status:s gate_ticker:s gate_min_amount:s encryption_method:i key_hash:s ' + TX_TAIL],
+ ['Issue', 'getIssues', 'ISSUE action data (the fields as the action declared them)',
+ ACTION_HEAD + ' tick:s max_supply:s max_mint:s decimals:s description:s mint_supply:s transfer:s transfer_supply:s'
+ + ' lock_max_supply:s lock_mint:s lock_mint_supply:s lock_max_mint:s lock_description:s lock_sleep:s'
+ + ' lock_callback:s callback_block:s callback_tick:s callback_amount:s allow_list:d block_list:d'
+ + ' mint_address_max:s mint_start_block:s mint_stop_block:s memo:s status:s ' + TX_TAIL],
+ ['Link', 'getLinks', 'LINK action data',
+ ACTION_HEAD + ' coin1:s coin1_action_index:d coin2:s coin2_action_index:d memo:s status:s ' + TX_TAIL],
+ ['List', 'getLists', 'LIST action data',
+ ACTION_HEAD + ' type:s edit:s list_action_index:d memo:s status:s ' + TX_TAIL],
+ ['Message', 'getMessages', 'MESSAGE action data',
+ ACTION_HEAD + ' destination:s encryption_method:s encryption_key:s encrypted_message:s plaintext_message:s'
+ + ' status:s coin:s ' + TX_TAIL,
+ { coin: 'Destination coin network for cross-chain messages' }],
+ ['Mint', 'getMints', 'MINT action data',
+ ACTION_HEAD + ' destination:s tick:s amount:s memo:s status:s ' + TX_TAIL],
+ ['Order', 'getOrders', 'ORDER action data',
+ ACTION_HEAD + ' give_coin:s give_tick:s give_amount:s give_ownership:i get_coin:s get_tick:s get_amount:s'
+ + ' get_ownership:i get_address:s expiration:d allow_list:d block_list:d payout_legs:s memo:s status:s ' + TX_TAIL,
+ { payout_legs: 'JSON [{to,bps}] split of seller proceeds applied at match; null when none' }],
+ ['OrderCancel', 'getOrderCancels', 'ORDER_CANCEL action data',
+ ACTION_HEAD + ' order_action_index:d memo:s status:s ' + TX_TAIL],
+ ['OrderEdit', 'getOrderEdits', 'ORDER_EDIT action data',
+ ACTION_HEAD + ' order_action_index:d expiration:d allow_list:d block_list:d memo:s status:s ' + TX_TAIL],
+ ['OrderExpire', 'getOrderExpires', 'ORDER_EXPIRE action data (system-injected, so no transaction fields)',
+ ACTION_HEAD + ' order_action_index:d block_index:D timestamp:D status:s'],
+ ['OrderMatch', 'getOrderMatches', 'ORDER_MATCH action data (system-injected, so no transaction fields)',
+ 'action:S action_index:D action_format:i give_coin:s give_action_index:d give_amount:s get_coin:s'
+ + ' get_action_index:d get_amount:s settlement_type:s block_index:D timestamp:D status:s'],
+ ['Send', 'getSends', 'SEND action data',
+ ACTION_HEAD + ' destination:s tick:s amount:s memo:s status:s ' + TX_TAIL],
+ ['Sleep', 'getSleeps', 'SLEEP action data',
+ ACTION_HEAD + ' type:d tick:s resume_block:s memo:s status:s ' + TX_TAIL],
+ ['Swap', 'getSwaps', 'SWAP action data',
+ ACTION_HEAD + ' give_coin:s give_tick:s give_amount:s give_ownership:i get_coin:s get_tick:s get_amount:s'
+ + ' get_ownership:i get_address:s expiration:d allow_list:d block_list:d payout_legs:s memo:s status:s'
+ + ' swap_status:s ' + TX_TAIL,
+ { swap_status: 'The swap\'s lifecycle status from its latest status row; `status` is the validity of the creating action only' }],
+ ['SwapCancel', 'getSwapCancels', 'SWAP_CANCEL action data',
+ ACTION_HEAD + ' swap_action_index:d memo:s status:s ' + TX_TAIL],
+ ['SwapEdit', 'getSwapEdits', 'SWAP_EDIT action data',
+ ACTION_HEAD + ' swap_action_index:d expiration:d allow_list:d block_list:d memo:s status:s ' + TX_TAIL],
+ ['SwapExpire', 'getSwapExpires', 'SWAP_EXPIRE action data (system-injected, so no transaction fields)',
+ ACTION_HEAD + ' swap_action_index:d block_index:D timestamp:D status:s'],
+ ['SwapMatch', 'getSwapMatches', 'SWAP_MATCH action data (system-injected, so no transaction fields)',
+ 'action:S action_index:D action_format:i give_coin:s give_action_index:d get_coin:s get_action_index:d'
+ + ' block_index:D timestamp:D status:s'],
+ ['Sweep', 'getSweeps', 'SWEEP action data',
+ ACTION_HEAD + ' destination:s balances:d ownerships:d orders:d swaps:d dispensers:d memo:s status:s ' + TX_TAIL,
+ { balances: 'Nonzero when token balances are swept',
+ ownerships: 'Nonzero when token ownerships are swept',
+ orders: 'Nonzero when open ORDERs are cancelled and their escrow credited to the destination',
+ swaps: 'Nonzero when open SWAPs are cancelled and their escrow credited to the destination',
+ dispensers: 'Nonzero when open DISPENSERs are closed and their escrow credited to the destination' }],
+ ['Credit', 'getCredits', 'Ledger credit data',
+ 'action:S action_index:D address:s tick:s amount:s ' + TX_TAIL],
+ ['Debit', 'getDebits', 'Ledger debit data',
+ 'action:S action_index:D address:s tick:s amount:s ' + TX_TAIL],
+ ['Escrow', 'getEscrows', 'Escrowed balance data',
+ 'action:S action_index:D address:s tick:s amount:s ' + TX_TAIL],
+ ['Balance', 'getBalances', 'Address token balance data',
+ 'tick:s amount:s supply:s decimals:i coin_price:s'],
+ ['TokenRow', 'getTokens', 'One token from the token search',
+ 'id:d tick:s supply:s max_supply:s max_mint:s decimals:i lock_max_supply:i lock_mint:i lock_mint_supply:i'
+ + ' lock_max_mint:i lock_description:i lock_sleep:i lock_callback:i ' + TX_TAIL],
+];
+
+const SUMMARY_DETAILS = {
+ type: ['object', 'boolean'],
+ description: 'Summary fields of the action (tick, amount, destination and the like); false when the action carries none',
+};
+
+const HISTORY = rowSchema('History data', 'action:S action_index:D parent_batch_action_index:d status:s ' + TX_TAIL,
+ { parent_batch_action_index: 'The BATCH this action rode in; null outside a batch' },
+ { details: SUMMARY_DETAILS });
+
+const MARKET = rowSchema('DEX market data',
+ 'tick1:s tick1_price:s tick1_bid:s tick1_ask:s tick1_24hr_price:s tick1_24hr_high:s tick1_24hr_low:s'
+ + ' tick1_24hr_change:s tick1_24hr_volume:s tick2:s tick2_price:s tick2_bid:s tick2_ask:s tick2_24hr_price:s'
+ + ' tick2_24hr_high:s tick2_24hr_low:s tick2_24hr_change:s tick2_24hr_volume:s last_updated:d',
+ null, { id: { type: 'integer' } });
+
+const TRADE_SIDE = { type: 'string', enum: ['buy', 'sell'] };
+
+const MARKET_HISTORY = rowSchema('DEX market history data', 'action_index:D block_index:D timestamp:D', null, {
+ type: TRADE_SIDE, price: { type: 'string' }, amount: { type: 'string' } });
+
+const MARKET_ORDER = rowSchema('DEX market order data', 'action_index:D', null, {
+ type: TRADE_SIDE, price: { type: 'string' }, amount: { type: ['string', 'null'] },
+ expiration: { type: ['string', 'integer', 'null'],
+ description: 'Expiration block: a decimal string, or a number once an ORDER_EDIT has changed it' } });
+
+const LEDGER_EFFECT = rowSchema('One ledger row the action wrote', 'address:s tick:s amount:s');
+const LEDGER_EFFECTS = (what) => ({ type: ['array', 'null'], items: LEDGER_EFFECT, description: `The ${what} this action wrote; null when none` });
+
+const ACTION_FEE = rowSchema('The protocol fee this action paid',
+ 'source:s destination:s tick:s amount:s method:d gas_cost:d gas_price:s xchain_amount:s payment_mode:i'
+ + ' native_coin_amount:s native_coin:s oracle_round:d fee_preference:i fee_version:i');
+
+const RUNTIME = { type: 'string', description: 'Amount of time to execute and return the data' };
+const NULLABLE_STRING = { type: ['string', 'null'] };
+const NULLABLE_INTEGER = { type: ['integer', 'null'] };
+
+const ACTION_DETAIL = {
+ type: 'object',
+ description: 'One action, fully decoded. Carries the fields its action type defines (the columns its list route serves plus any detail-only sections) and the ledger tail below, which every action carries.',
+ properties: {
+ action: { type: 'string' },
+ action_format: NULLABLE_INTEGER,
+ action_index: COLUMN_TYPES.D,
+ block_index: COLUMN_TYPES.d,
+ credits: LEDGER_EFFECTS('credits'),
+ debits: LEDGER_EFFECTS('debits'),
+ escrows: LEDGER_EFFECTS('escrows'),
+ fee: { oneOf: [{ type: 'null' }, ACTION_FEE], description: 'Action fee information; null when the action paid none' },
+ runtime: RUNTIME,
+ source: NULLABLE_STRING,
+ status: NULLABLE_STRING,
+ timestamp: COLUMN_TYPES.d,
+ tx_data: { type: ['string', 'null'], description: 'The decoded action string of the carrying transaction; null for a system-injected action' },
+ tx_hash: NULLABLE_STRING,
+ tx_index: COLUMN_TYPES.d,
+ },
+};
+
+const BLOCK = {
+ type: 'object',
+ description: 'COIN block data',
+ properties: {
+ actions_hash: { type: ['string', 'null'], description: 'sha256 of the block\'s actions data' },
+ block_index: COLUMN_TYPES.D,
+ contract_hash: { type: ['string', 'null'], description: 'sha256 of the block\'s contract data' },
+ ledger_hash: { type: ['string', 'null'], description: 'sha256 of the block\'s credits, debits, escrows and balances' },
+ runtime: RUNTIME,
+ state_hash: { type: ['string', 'null'], description: 'sha256 of in-place mutations and backdated credits (replication integrity only)' },
+ timestamp: COLUMN_TYPES.D,
+ },
+ required: ['block_index', 'timestamp'],
+};
+
+const TRANSACTION = {
+ type: 'object',
+ description: 'Transaction data. An unknown transaction answers only an empty actions list and a null tx_data.',
+ properties: {
+ actions: {
+ type: 'array',
+ description: 'Actions this transaction carried, newest first',
+ items: rowSchema('One action summary', 'action:s action_index:D status:s', null, { details: SUMMARY_DETAILS }),
+ },
+ block_index: COLUMN_TYPES.D,
+ runtime: RUNTIME,
+ source: NULLABLE_STRING,
+ timestamp: COLUMN_TYPES.D,
+ tx_data: { type: ['string', 'null'], description: 'The decoded action string the transaction carries' },
+ tx_hash: NULLABLE_STRING,
+ tx_index: COLUMN_TYPES.D,
+ },
+ required: ['actions', 'tx_data'],
+};
+
+const AMOUNTS_OR_NULL = (names, schema) => Object.fromEntries(names.map((n) => [n, schema]));
+
+const ADDRESS_INFO = {
+ type: 'object',
+ description: 'Address information (type, native-coin balances and UTXO counts from this coin\'s UTXO tracker, controller bindings). Tracker fields stay null when no tracker is configured or it is unreachable.',
+ properties: {
+ address: { type: 'string' },
+ balances: { type: 'object', description: 'Balances of COIN (BTC, LTC, DOGE, etc) as decimal strings',
+ properties: AMOUNTS_OR_NULL(['confirmed', 'pending', 'received'], NULLABLE_STRING) },
+ controllers: { type: 'array', items: { type: 'object' }, description: 'Controller bindings still gating this address\'s native actions; empty when none' },
+ estimated_value: { type: 'object', description: 'Estimated value of the confirmed balance in COIN and in USD',
+ properties: AMOUNTS_OR_NULL(['btc', 'usd'], NULLABLE_STRING) },
+ info: { type: 'object', properties: {
+ address: { type: 'string' },
+ address_id: { type: ['integer', 'null'], description: 'The address\'s deterministic index id, for ^ compaction; null when not deterministic' } } },
+ mempool_ready: { type: 'boolean', description: 'Present only when the tracker reports it' },
+ runtime: RUNTIME,
+ tracker_available: { type: 'boolean' },
+ type: { type: ['string', 'null'], description: 'Address Type (P2PKH, P2SH, Bech32, P2TR)' },
+ utxos: { type: 'object', description: 'Unspent Transaction Output counts (UTXOs)',
+ properties: AMOUNTS_OR_NULL(['confirmed', 'pending'], NULLABLE_INTEGER) },
+ },
+ required: ['address', 'balances', 'utxos', 'estimated_value', 'tracker_available', 'controllers', 'info'],
+};
+
+const BOOL_MAP = (names) => Object.fromEntries(names.map((n) => [n, { type: 'boolean' }]));
+const NUMBER = { type: 'number' };
+
+const TOKEN = {
+ type: 'object',
+ description: 'Token data. `locks.bridge` and the bridge fields of `info` appear only on a database that carries the bridge columns.',
+ properties: {
+ callback: { type: 'object', properties: {
+ amount: { type: ['string', 'null'] }, block: COLUMN_TYPES.d, price: NULLABLE_STRING, tick: NULLABLE_STRING } },
+ controllers: { type: 'array', items: { type: 'object' } },
+ info: { type: 'object', properties: {
+ bridge_chains: NULLABLE_STRING, bridged: NULLABLE_INTEGER, coin: { type: 'string' },
+ decimals: { type: 'integer' }, description: NULLABLE_STRING, escrow_action_index: COLUMN_TYPES.d,
+ min_depth: COLUMN_TYPES.d, owner: NULLABLE_STRING, tick: NULLABLE_STRING,
+ tick_id: { type: ['integer', 'null'], description: 'Deterministic ticker id, for ^ compaction; null when not deterministic' } } },
+ linked_files: { type: 'array', items: { type: 'object', properties: {
+ action_index: { type: 'integer' }, block_index: { type: 'integer' }, gated: { type: 'boolean' },
+ name: NULLABLE_STRING, title: NULLABLE_STRING, type: NULLABLE_STRING } } },
+ lists: { type: 'object', properties: AMOUNTS_OR_NULL(['allow', 'block'], NULLABLE_INTEGER) },
+ locks: { type: 'object', properties: BOOL_MAP(['bridge', 'callback', 'description', 'max_mint', 'max_supply', 'mint', 'mint_supply', 'sleep']) },
+ market: { type: 'object', properties: { floor: NUMBER, price: NUMBER } },
+ mints: { type: 'object', properties: AMOUNTS_OR_NULL(['address_max', 'max', 'start_block', 'stop_block'], NUMBER) },
+ open_polls: { type: 'array', items: { type: 'object' } },
+ projects: { type: 'array', items: { type: 'object' } },
+ registry: { oneOf: [{ type: 'null' }, { type: 'object', properties: AMOUNTS_OR_NULL(
+ ['link_action_index', 'membership_action_index', 'roster_action_index', 'total'], { type: 'integer' }) }] },
+ runtime: RUNTIME,
+ supply: { type: 'object', properties: {
+ current: NULLABLE_STRING, decimals: { type: 'integer' }, max: NULLABLE_STRING } },
+ },
+ required: ['info', 'callback', 'market', 'lists', 'locks', 'mints', 'supply'],
+};
+
+const PRICE_LEVEL = { type: 'array', items: { type: 'string' }, minItems: 2, maxItems: 2, description: '[price, amount]' };
+
+const ORDERBOOK = {
+ type: 'object',
+ description: 'DEX market orderbook data',
+ properties: {
+ asks: { type: 'array', items: PRICE_LEVEL, description: 'Open asks aggregated by price, lowest first' },
+ bids: { type: 'array', items: PRICE_LEVEL, description: 'Open bids aggregated by price, highest first' },
+ market: { type: 'string', description: 'TICK1/TICK2; present only when the book has orders' },
+ runtime: RUNTIME,
+ },
+ required: ['asks', 'bids'],
+};
+
+// /status body, kept field-for-field with getStatus by the tip-freshness suite.
+const EXPLORER_STATUS = {
+ "type": "object",
+ "description": "Explorer status data",
+ "properties": {
+ "available": {
+ "type": "object",
+ "description": "Coin networks this instance serves as CURRENT data, keyed by coin ticker. Starts from the configured availability map and then drops every coin the tip-age gate marks stale (see 'stale'), so this map varies per request and a delisted coin still appears in 'supported'. Read this to decide whether to query a coin; read 'supported' to learn which coins the instance knows about at all.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "hub_config_fetched_at": {
+ "type": [
+ "string",
+ "null"
+ ],
+ "description": "ISO-8601 timestamp of the last successful hub-config fetch. null until the first successful fetch.",
+ "format": "date-time"
+ },
+ "hub_config_age_seconds": {
+ "type": [
+ "integer",
+ "null"
+ ],
+ "description": "Seconds since the last successful hub-config fetch. A climbing value means the served hub-derived config is stale. null until the first successful fetch."
+ },
+ "last_block": {
+ "type": "object",
+ "description": "Most recent block index processed by the indexer, keyed by coin ticker (e.g. RBTC: 850). Value is null for a coin whose per-coin DB read failed (the outage this health endpoint exists to surface).",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "last_block_time": {
+ "type": "object",
+ "description": "Unix block_time of the most recent block processed by the indexer, keyed by coin ticker. Compare against the chain tip to detect indexer lag. Value is null for a coin whose per-coin DB read failed.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_tip": {
+ "type": "object",
+ "description": "Decoder's highest processed block index, keyed by coin ticker. null when the decoder tip is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_lag_blocks": {
+ "type": "object",
+ "description": "decoder_tip minus last_block (how far the indexer trails the decoder), keyed by coin ticker. null when the decoder tip is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "tip_age_seconds": {
+ "type": "object",
+ "description": "Wall-clock seconds between now and the block_time of the newest indexed block, keyed by coin ticker. Never negative: a tip dated ahead of this host clamps to 0 here and reports its skew in tip_future_seconds. null when that block_time is missing or unreadable. Unlike decoder_lag_blocks this sees a JOINT indexer+decoder freeze, because it measures against the local clock rather than against the other replica. Only coins this instance measures (those with a live pool) appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "tip_future_seconds": {
+ "type": "object",
+ "description": "Seconds the newest indexed block is dated AHEAD of this host's clock, keyed by coin ticker; 0 when the tip is not ahead, null when block_time is missing or unreadable. A non-zero value is host clock drift or a chain with lax timestamp rules, and it means tip_age_seconds is clamped rather than measured. Skew past the tolerance (EXPLORER_TIP_MAX_FUTURE_SKEW_S_, else EXPLORER_TIP_MAX_FUTURE_SKEW_S, default 7200; 0 disables the check) reads stale, so a future-dated tip cannot pass as fresh indefinitely. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "indexer_state": {
+ "type": "object",
+ "description": "Why the indexer trails the decoder, keyed by coin ticker. 'live' when decoder_lag_blocks is 0; 'future_block_wait' when the next block (last_block + 1) carries a block_time dated ahead of this host's clock, meaning no node on the chain may commit it yet and the pause is consensus rather than failure; 'behind' when that next block is already admissible and still uncommitted, which is the state worth alerting on; null when it cannot be determined. Read this field, NOT tip_future_seconds, to tell a deliberate wait apart from a wedge: tip_future_seconds describes the block the indexer has already committed, which is always past-dated, so it reads 0 for the whole duration of a future_block_wait. Chains with lax timestamp rules (Bitcoin testnet4 rides the 20-minute minimum-difficulty rule, stamping blocks about 1201s apart into the future) sit in future_block_wait as their steady state. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "next_block_time": {
+ "type": "object",
+ "description": "The decoder's recorded block_time (unix seconds) for the next block the indexer must commit (last_block + 1), keyed by coin ticker. Read from the decoder DB because the indexer has not committed that block yet. null when the indexer is at the decoder tip, the decoder DB is unreachable, or that block carries no usable timestamp. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "next_block_future_seconds": {
+ "type": "object",
+ "description": "Seconds the next block to be committed is dated AHEAD of this host's clock, keyed by coin ticker; 0 when it is not ahead, null when unknown. This is the honest measure of a consensus wait, and the value tip_future_seconds is often mistaken for. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "indexer_wait_clears_at": {
+ "type": "object",
+ "description": "ISO-8601 instant at which a 'future_block_wait' ends for that coin, i.e. when this host's clock reaches the next block's timestamp and the block becomes committable, keyed by coin ticker. null in every other state. Present so a client can render a countdown rather than leaving a confirmed-but-unindexed transaction looking lost. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ },
+ "stale": {
+ "type": "object",
+ "description": "Tip-freshness verdict per coin ticker: true when tip_age_seconds has passed that coin's max tip age (EXPLORER_TIP_MAX_AGE_S_, else EXPLORER_TIP_MAX_AGE_S, default 21600; 0 disables the gate), and also true when tip_future_seconds has passed that coin's max future skew. Fails closed, so a missing or unreadable block_time also reads true. A true entry is removed from 'available' and left in 'supported'. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": "boolean"
+ }
+ },
+ "replica_halted": {
+ "type": "object",
+ "description": "Durable consensus-divergence halt verdict per coin ticker, read from xchain-sync's sync_halt tables on the indexer and decoder replicas (true = either database has an active, uncleared halt row). A halted replica keeps reporting a small lag until its source mints past it, so this is detectable neither by 'stale' nor by tip_age_seconds; it composes with 'stale' rather than replacing it (immediate detection here, eventual removal from 'available' there). true/false only when both tables were read successfully; null when the signal could not be determined (table absent, e.g. a DB not built by the sync client, or the read failed) and is never coerced to false. Only coins this instance measures appear here.",
+ "additionalProperties": {
+ "type": [
+ "boolean",
+ "null"
+ ]
+ }
+ },
+ "chain_tip": {
+ "type": "object",
+ "description": "Coin node's chain tip as the decoder reports it, keyed by coin ticker. null when the decoder health call is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "chain_lag_blocks": {
+ "type": "object",
+ "description": "Decoder's self-reported gap to the chain tip, keyed by coin ticker. null when the decoder health call is unavailable.",
+ "additionalProperties": {
+ "type": [
+ "integer",
+ "null"
+ ]
+ }
+ },
+ "decoder_health": {
+ "type": "object",
+ "description": "Decoder self-reported status per coin ticker: 'healthy', 'unhealthy', 'node-stale' (the decoder's cached coin-node height is frozen, so chain_tip and chain_lag_blocks are nulled out), 'unconfigured' (no decoder URL set), or 'unreachable' (health call failed).",
+ "additionalProperties": {
+ "type": "string"
+ }
+ },
+ "supported": {
+ "type": "object",
+ "description": "Every coin network the XChain platform defines for this instance, keyed by coin ticker with a display name as the value. Static per config: the tip-age gate never removes a coin from here, only from 'available'.",
+ "additionalProperties": {
+ "type": "string"
+ }
+ }
+ }
+};
+
+// Paged list body: the envelope plus this route's row type.
+function listBody(rowName, extra) {
+ const properties = Object.assign({
+ total: { type: 'integer', description: 'Total number of records available' },
+ data: { type: 'array', items: { $ref: '#/components/schemas/' + rowName } },
+ runtime: RUNTIME,
+ }, extra || {});
+ return { type: 'object', properties, required: ['total', 'data'] };
+}
+
+// The market readers hand back COUNT(*) untouched, so the sink writes it as a string.
+const STRING_TOTAL = { total: { type: 'string', pattern: '^[0-9]+$', description: 'Total number of records available, as a decimal string' } };
+
+const HOLDER = listBody('HolderRow', {
+ coin_price: { type: 'string', description: 'Token price in COIN; absent when the token has no holders' },
+ decimals: { type: 'integer', description: 'Absent when the token has no holders' },
+ supply: { type: 'string', description: 'Absent when the token has no holders' },
+ tick: { type: 'string', description: 'Absent when the token has no holders' },
+});
+HOLDER.description = 'Token holder data';
+
+const COMPONENT_SCHEMAS = Object.assign(
+ Object.fromEntries(ROWS.map(([name, , description, spec, notes]) => [name, rowSchema(description, spec, notes)])),
+ {
+ ActionDetail: ACTION_DETAIL, AddressInfo: ADDRESS_INFO, Block: BLOCK, ExplorerStatus: EXPLORER_STATUS,
+ History: HISTORY, Holder: HOLDER, HolderRow: rowSchema('One holder of a token', 'address:s amount:s'),
+ Market: MARKET, MarketHistory: MARKET_HISTORY, MarketOrder: MARKET_ORDER, Orderbook: ORDERBOOK,
+ Token: TOKEN, Transaction: TRANSACTION,
+ });
+
+// db method -> row schema of its paged list body.
+const ROW_SCHEMAS = Object.assign(Object.fromEntries(ROWS.map(([name, method]) => [method, name])), {
+ getHistory: 'History', getMarkets: 'Market', getMarketHistory: 'MarketHistory', getMarketOrders: 'MarketOrder',
+});
+
+// db method -> extra top-level fields its list body carries.
+const LIST_EXTRAS = {
+ getBalances: { address: { type: ['string', 'null'], description: 'The queried address, echoed when it is address-shaped' } },
+ getMarkets: STRING_TOTAL, getMarketHistory: STRING_TOTAL, getMarketOrders: STRING_TOTAL,
+};
+
+// db method -> component schema of its whole 200 body.
+const BODY_SCHEMAS = {
+ getAction: 'ActionDetail', getAddress: 'AddressInfo', getBlock: 'Block', getHolders: 'Holder',
+ getOrderbook: 'Orderbook', getStatus: 'ExplorerStatus', getToken: 'Token', getTransaction: 'Transaction',
+};
+
+// The typed 200 schema for a route calling `method`, or null for the generic one.
+function responseSchema(method) {
+ if (BODY_SCHEMAS[method]) return { $ref: '#/components/schemas/' + BODY_SCHEMAS[method] };
+ if (ROW_SCHEMAS[method]) return listBody(ROW_SCHEMAS[method], LIST_EXTRAS[method]);
+ return null;
+}
+
+module.exports = { COMPONENT_SCHEMAS, ROW_SCHEMAS, BODY_SCHEMAS, POST_PASS_COLUMNS: { getDispensers: ['escrow_remaining', 'current_status'] }, responseSchema };
diff --git a/package-lock.json b/package-lock.json
index eebd6362..53565160 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "xchain-explorer",
- "version": "0.20.0",
+ "version": "0.20.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xchain-explorer",
- "version": "0.20.0",
+ "version": "0.20.1",
"license": "AGPL-3.0-or-later",
"dependencies": {
"@fortawesome/fontawesome-free": "6.4.0",
@@ -6487,7 +6487,7 @@
}
},
"xchain-vm": {
- "version": "0.20.0",
+ "version": "0.20.1",
"license": "AGPL-3.0-or-later",
"optional": true,
"dependencies": {
diff --git a/package.json b/package.json
index 915b8a14..247cdda2 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "xchain-explorer",
"description": "xchain-explorer is a free open source token platform explorer and set of APIs to pull data from the XChain platform supported blockchains",
- "version": "0.20.0",
+ "version": "0.20.1",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
@@ -43,7 +43,7 @@
"test:explorer": "mocha test/unit/explorer/explorer_anchor_reward_attestations.test.js test/unit/explorer/explorer_attest_validator_stats.test.js test/unit/explorer/explorer_capability_snapshots.test.js test/unit/explorer/explorer_checkpoints.test.js test/unit/explorer/explorer_commitments.test.js test/unit/explorer/explorer_emissions.test.js test/unit/explorer/explorer_icon.test.js test/unit/explorer/explorer_mirror_gate.test.js test/unit/explorer/explorer_paging.test.js test/unit/explorer/explorer_relay.test.js test/unit/explorer/explorer_reorgs.test.js test/unit/explorer/explorer_response.test.js test/unit/explorer/explorer_routing.test.js test/unit/explorer/explorer_slash_proposals.test.js test/unit/explorer/explorer_vote_delegations.test.js --timeout 0 --exit",
"test:config": "mocha test/unit/config/config.test.js test/unit/connectors/hub_connector.test.js --timeout 0 --exit",
"test:integration": "bash bin/run-integration.sh",
- "test:conformance": "mocha test/conformance/**/*.test.js --timeout 120000 --exit --recursive",
+ "test:conformance": "node bin/run-conformance.js",
"test:e2e": "mocha test/e2e/**/*.test.js --timeout 30000 --exit --recursive",
"test:integration:up": "node bin/db-fixture.js up",
"test:integration:down": "node bin/db-fixture.js down",
@@ -64,8 +64,8 @@
"test:chaos:network": "node --expose-gc ./node_modules/.bin/mocha test/chaos/network_partition.test.js --timeout 0 --exit",
"test:chaos:resource": "node --expose-gc ./node_modules/.bin/mocha test/chaos/resource_saturation.test.js --timeout 0 --exit",
"test:chaos:external": "node --expose-gc ./node_modules/.bin/mocha test/chaos/external_deps.test.js --timeout 0 --exit",
- "test:chaos:up": "docker compose -f test/chaos/fixtures/docker-compose.chaos.yml up -d --wait",
- "test:chaos:down": "docker compose -f test/chaos/fixtures/docker-compose.chaos.yml down -v",
+ "test:chaos:up": "node bin/fixture-ports.js compose test/chaos/fixtures/docker-compose.chaos.yml up -d --wait",
+ "test:chaos:down": "node bin/fixture-ports.js compose test/chaos/fixtures/docker-compose.chaos.yml down -v",
"test:mutation": "npx stryker run test/mutation/stryker.config.mjs",
"test:mutation:utility": "npx stryker run test/mutation/stryker.config.mjs --mutate 'src/lib/utility.js'",
"test:mutation:db": "npx stryker run test/mutation/stryker.config.mjs --mutate 'src/db/index.js'",
diff --git a/src/XChainExplorer.js b/src/XChainExplorer.js
index b1844b30..90076962 100644
--- a/src/XChainExplorer.js
+++ b/src/XChainExplorer.js
@@ -46,6 +46,8 @@ const { isPreflightPostRequest, MAX_PREFLIGHT_PARAMS_LENGTH } = require('./explo
// other one.
const { PREFLIGHT_BODY_LIMIT } = require('./explorer/mount.js');
const { runRequest } = require('./explorer/request/index.js');
+const { newRequestState } = require('./explorer/request/state.js');
+const { resolveRoute } = require('./explorer/request/resolve_route.js');
// One logger for the whole service: getLogger() resolves to the shipper once api.js
// installs observability, and falls through to bare console before that.
@@ -180,9 +182,68 @@ class XChainExplorer {
// Serve one request end to end: match its URL, fetch any data it names, and
// send back the page or the JSON.
async processRequest(req, res){
+ if(await this.answerActionNotYetIndexed(req, res))
+ return;
+ if(await this.answerUnmatchedApiPath(req, res))
+ return;
return runRequest(this, req, res, { configEnv, stats });
}
+ // A /{COIN}/api/... path that no registered route claims would otherwise
+ // fall through to explorer/request/not_found.js's applyFallbacks, which
+ // answers every unmatched request the same way regardless of request type:
+ // an HTML 404 page. An API client parsing that body as JSON fails. Run the
+ // same route resolution runRequest is about to run, and answer JSON here
+ // only in the exact case applyFallbacks treats as unmatched (neither a file
+ // nor a data method was found), so a route that matched and chose to answer
+ // 404 itself (its own JSON, or a plain string) is left untouched.
+ async answerUnmatchedApiPath(req, res){
+ try {
+ let config = await this.configInfo.getConfig();
+ let st = newRequestState(this, req, config);
+ await resolveRoute(this, req, st);
+ if(st.cfg.type !== 'api' || !this.util.isNull(st.cfg.file) || !this.util.isNull(st.cfg.data.method))
+ return false;
+ res.set('Content-Type', 'application/json; charset=utf-8');
+ res.status(404).send(JSON.stringify({
+ error: 'The requested resource was not found.',
+ code: 'NOT_FOUND'
+ }));
+ return true;
+ } catch(_){
+ return false;
+ }
+ }
+
+ // An action_index above the highest one this instance has indexed is not a
+ // missing action, it is one the indexer has not reached: a client polling
+ // for confirmation of a just-broadcast action needs to tell that apart from
+ // an index that will never exist, so it gets its own code, the indexed
+ // high-water mark, and a Retry-After. An index at or below the mark falls
+ // through to the normal not-found answer. Any failure reading the mark
+ // falls through too, so this never turns a servable request into an error.
+ async answerActionNotYetIndexed(req, res){
+ try {
+ let m = /^\/([^/]+)\/api\/action\/([0-9]+)\/?$/.exec(String((req && req.path) || ''));
+ if(!m) return false;
+ let coin = m[1].toUpperCase();
+ if(!this.db || !this.db.pools || !this.db.pools[coin] || typeof this.db.getMaxActionIndex !== 'function')
+ return false;
+ let indexed = await this.db.getMaxActionIndex({ coin, data: {} });
+ if(BigInt(m[2]) <= indexed) return false;
+ res.set('Retry-After', '5');
+ res.set('Content-Type', 'application/json; charset=utf-8');
+ res.status(404).send(JSON.stringify({
+ error: 'This action has not been indexed yet.',
+ code: 'ACTION_NOT_YET_INDEXED',
+ indexed_through: indexed.toString()
+ }));
+ return true;
+ } catch(_){
+ return false;
+ }
+ }
+
static getSlowRequests() { return slowRequests; }
// Return p95 latency (ms) and total requests served, derived from the
diff --git a/src/action-detail/contracts.js b/src/action-detail/contracts.js
index 7db8b6c9..c0df61ea 100644
--- a/src/action-detail/contracts.js
+++ b/src/action-detail/contracts.js
@@ -136,3 +136,61 @@ module.exports = {
DEPOSIT: DEPOSIT_WITHDRAW,
WITHDRAW: DEPOSIT_WITHDRAW
};
+
+Object.defineProperty(module.exports, 'feeRows', {
+ value: function feeRows(){
+ return `(SELECT
+ m.action_index,
+ m.tick_id,
+ m.amount,
+ m.method,
+ m.destination_id,
+ m.gas_cost,
+ m.gas_price,
+ m.xchain_amount,
+ m.payment_mode,
+ m.native_coin_amount,
+ m.native_coin,
+ m.oracle_round,
+ m.fee_preference,
+ m.status_id,
+ m.fee_version,
+ NULL as source_id
+ FROM
+ fees m
+ UNION ALL
+ SELECT
+ ce.action_index,
+ CASE WHEN COUNT(DISTINCT d.tick_id)=1 THEN MIN(d.tick_id) ELSE NULL END as tick_id,
+ CASE WHEN COUNT(DISTINCT d.tick_id)=1 THEN SUM(CAST(d.amount AS DECIMAL(65,8))) ELSE NULL END as amount,
+ NULL as method,
+ NULL as destination_id,
+ ce.gas_used as gas_cost,
+ NULL as gas_price,
+ CASE WHEN COUNT(DISTINCT d.tick_id)=1 THEN SUM(CAST(d.amount AS DECIMAL(65,8))) ELSE NULL END as xchain_amount,
+ CASE WHEN COUNT(d.action_index)=0 THEN 1 ELSE 2 END as payment_mode,
+ NULL as native_coin_amount,
+ NULL as native_coin,
+ NULL as oracle_round,
+ NULL as fee_preference,
+ ce.status_id,
+ 2 as fee_version,
+ ce.caller_id as source_id
+ FROM
+ contract_executions ce
+ INNER JOIN actions ca ON (ca.action_index=ce.action_index)
+ INNER JOIN index_actions cia ON (cia.id=ca.action_id)
+ INNER JOIN index_statuses cis ON (cis.id=ce.status_id)
+ LEFT JOIN debits d ON (d.action_index=ce.action_index AND d.address_id=ce.caller_id)
+ WHERE
+ cia.action IN ('DEPLOY','EXECUTE')
+ AND ca.tx_index IS NOT NULL
+ AND ce.gas_used>0
+ AND cis.status NOT LIKE 'invalid%'
+ AND NOT EXISTS (
+ SELECT 1 FROM fees existing WHERE existing.action_index=ce.action_index
+ )
+ GROUP BY
+ ce.action_index, ce.gas_used, ce.status_id, ce.caller_id)`;
+ }
+});
diff --git a/src/action-detail/tokens.js b/src/action-detail/tokens.js
index 0edafbcc..ba246718 100644
--- a/src/action-detail/tokens.js
+++ b/src/action-detail/tokens.js
@@ -19,7 +19,25 @@
// Statement text lives under src/db/ with the rest of the explorer's SQL.
const sql = require('../db/action_detail/tokens_sql.js');
-const { tablesPresent, setTablesAbsent, isMissingTableError } = require('../db/schema_probe.js');
+const bridgeSql = require('../db/action_detail/xbridge_sql.js');
+const { tablesPresent, setTablesAbsent, isMissingTableError,
+ columnsPresent, setColumnsAbsent, isUnknownColumnError } = require('../db/schema_probe.js');
+
+// Read every leg of a multi-leg SEND or DESTROY, in wire order when the connected
+// replica records leg_ordinal. A replica without it takes the unsorted read, which
+// is exactly what every replica got before the column existed.
+async function readLegs({ db, config, action_index }, table, legsQuery){
+ const columns = [sql.LEG_ORDINAL_COLUMN];
+ const ordered = await columnsPresent(db, config, table, columns);
+ try {
+ return await db.doQuery(config, legsQuery(ordered), [action_index]);
+ } catch(e) {
+ // The net under a probe that answered wrong: record the real shape, read unsorted.
+ if(!ordered || !isUnknownColumnError(e)) throw e;
+ setColumnsAbsent(db, config, table, columns);
+ return db.doQuery(config, legsQuery(false), [action_index]);
+ }
+}
const AIRDROP = {
queries() {
@@ -41,11 +59,13 @@ const DESTROY = {
let query2 = null;
let query3 = null;
query = sql.DESTROY_QUERY;
- query2 = sql.DESTROY_QUERY2;
return { query, query2, query3 };
},
- afterQuery2(ctx, data, results) {
- data.destroys = results;
+ // Attach every burn leg; the leg read is probe-guarded, so it runs here rather
+ // than in the static query2 slot.
+ async afterMain(ctx, data) {
+ const legs = await readLegs(ctx, 'destroys', sql.destroyLegsQuery);
+ if(legs && legs.length) data.destroys = legs;
},
};
@@ -95,13 +115,13 @@ const SEND = {
let query2 = null;
let query3 = null;
query = sql.SEND_QUERY;
- query2 = sql.SEND_QUERY2;
return { query, query2, query3 };
},
// Add any SENDS to the send data, so a send to several recipients lists
- // every one of them
- afterQuery2(ctx, data, results) {
- data.sends = results;
+ // every one of them in the order it was broadcast
+ async afterMain(ctx, data) {
+ const legs = await readLegs(ctx, 'sends', sql.sendLegsQuery);
+ if(legs && legs.length) data.sends = legs;
},
};
@@ -125,11 +145,14 @@ const SWEEP = {
};
// XBRIDGE action (the cross-chain bridge leg: v0/v3 lock on BTC, v1/v4 burn off
-// BTC, v2/v5 the mirror-injected settle). There is no bespoke wire table for the
-// user-broadcast legs, so the baseline row (action, format, source, block, tx)
-// comes from getActionData's de-blank fallback and this handler only attaches
-// the bridge context that is knowable on THIS chain:
+// BTC, v2/v5 the mirror-injected settle). The baseline row (action, format,
+// source, block, tx) comes from getActionData's de-blank fallback and this
+// handler attaches the bridge context that is knowable on THIS chain:
//
+// - a user leg's own facts are its `xbridges` row, written at parse time on the
+// chain it was broadcast from: destination chain and address, the decimals and
+// minimum depth stamped at its block, its memo and its verdict. An injected
+// leg writes no such row;
// - an injected leg is keyed by its OWN action_index in bridge_settlements,
// the table the settle pass writes, so v2/v5 resolve their transfer_id,
// kind, the source leg they close and the destination they credited;
@@ -138,44 +161,66 @@ const SWEEP = {
// row for it and `bridge_settlement` is null until the far leg lands. That
// null is the in-flight state, not a missing record, which is why it is
// reported as an explicit `bridge_pending` flag rather than left blank;
-// - a replica that has not taken the indexer's bridge-tables migration holds no
-// bridge_settlements table at all, so this node knows NOTHING about the leg.
-// Every key above is then omitted rather than nulled: absence reads as unknown,
-// while `bridge_pending: true` would tell a holder their transfer is in flight
-// on the strength of a table that was never read.
+// - a replica that has not taken the indexer's bridge-tables migration holds
+// neither table, so this node knows NOTHING about the leg. Every key above is
+// then omitted rather than nulled: absence reads as unknown, while
+// `bridge_pending: true` would tell a holder their transfer is in flight on
+// the strength of a table that was never read.
//
// The amount moved is not re-read here: the shared ledger-effect step already
// attaches the credits and debits (the escrow credit on a lock, the escrow debit
// on an out-leg), which is the same ledger the consensus rule wrote.
+
+// The xbridges columns a user leg's detail card reads.
+const XBRIDGE_RECORD_KEYS = ['dest_chain', 'dest_address', 'decimals', 'min_depth', 'memo', 'status'];
+
+// Read one bridge table only when the connected schema carries it: on a replica that
+// never took the bridge migration the statement is error 1146 for the whole action
+// page, and the page is worth more than the section. Answers null when nothing was read.
+async function readBridgeTable({ db, config, action_index }, table, statement){
+ if(!await tablesPresent(db, config, [table])) return null;
+ try {
+ return await db.doQuery(config, statement, [action_index]);
+ } catch(e) {
+ // The net under a probe that answered wrong (it failed, or the table went
+ // away under a live explorer). A failure that is not a missing table is a
+ // real query failure and stays the caller's to handle.
+ if(!isMissingTableError(e)) throw e;
+ setTablesAbsent(db, config, [table]);
+ return null;
+ }
+}
+
+// Lift a user leg's own record to the top level; never overwrite a tick the baseline resolved.
+function attachXbridgeRecord(data, rows){
+ const row = (rows && rows.length) ? rows[0] : null;
+ if(!row) return;
+ for(const key of XBRIDGE_RECORD_KEYS)
+ data[key] = row[key];
+ data['tick'] = data['tick'] || row.tick || null;
+}
+
+// Lift the settle row's identifying fields so the card reads one shape whether the
+// leg was injected or broadcast; no row on a read table is the in-flight state.
+function attachXbridgeSettlement(data, settle){
+ let row = (settle && settle.length) ? settle[0] : null;
+ data['bridge_settlement'] = row;
+ data['transfer_id'] = (row) ? row.transfer_id : null;
+ data['bridge_kind'] = (row) ? row.kind : null;
+ data['tick'] = (row) ? row.tick : (data['tick'] || null);
+ data['bridge_pending'] = (row) ? false : true;
+}
+
const XBRIDGE = {
- // No detail table of its own; the de-blank baseline is the main row.
+ // No static detail query: both bridge reads are probe-guarded, so they run in
+ // afterMain, and the de-blank baseline is the main row.
queries() {
return { query: null, query2: null, query3: null };
},
- async afterMain({ db, config, action_index }, data) {
- // Ask the connected schema for the settle table before naming it: on a replica
- // that never took the bridge migration the statement below is error 1146 for the
- // whole action page, and the page is worth more than the section.
- if(!await tablesPresent(db, config, [sql.BRIDGE_SETTLEMENTS_TABLE])) return;
- let settle = null;
- try {
- settle = await db.doQuery(config, sql.XBRIDGE_SETTLEMENT, [action_index]);
- } catch(e) {
- // The net under a probe that answered wrong (it failed, or the table went
- // away under a live explorer). A failure that is not a missing table is a
- // real query failure and stays the caller's to handle.
- if(!isMissingTableError(e)) throw e;
- setTablesAbsent(db, config, [sql.BRIDGE_SETTLEMENTS_TABLE]);
- return;
- }
- let row = (settle && settle.length) ? settle[0] : null;
- data['bridge_settlement'] = row;
- // Lift the identifying fields to the top level so the detail card reads
- // one shape whether the leg was injected or broadcast.
- data['transfer_id'] = (row) ? row.transfer_id : null;
- data['bridge_kind'] = (row) ? row.kind : null;
- data['tick'] = (row) ? row.tick : (data['tick'] || null);
- data['bridge_pending'] = (row) ? false : true;
+ async afterMain(ctx, data) {
+ attachXbridgeRecord(data, await readBridgeTable(ctx, bridgeSql.XBRIDGES_TABLE, bridgeSql.XBRIDGE_RECORD));
+ const settle = await readBridgeTable(ctx, bridgeSql.BRIDGE_SETTLEMENTS_TABLE, bridgeSql.XBRIDGE_SETTLEMENT);
+ if(settle !== null) attachXbridgeSettlement(data, settle);
},
};
diff --git a/src/api.js b/src/api.js
index 2fcdba8d..c5c8edf2 100644
--- a/src/api.js
+++ b/src/api.js
@@ -64,6 +64,99 @@ const HUB_ENDPOINTS = xchainHubConnector.parseEndpoints();
const EXPLORER_API_PORT_HTTP = configInfo.env.EXPLORER_API_PORT_HTTP || 8080;
const EXPLORER_API_PORT_HTTPS = configInfo.env.EXPLORER_API_PORT_HTTPS || 8081;
+// Public reads used by token.html's bridge panels. They are mounted by api.js,
+// before XChainExplorer installs its wildcard page route, because one read is a
+// hub RPC and the other comes from the co-located hub-mirror schema rather than
+// the explorer's ordinary API-method table.
+function createBridgePanelHandlers(getExplorer, hubConnector){
+ const error = (res, status, message, code) =>
+ res.status(status).json({ error: message, code });
+
+ const nativeTickFor = (tick, chain) => {
+ const dot = tick.indexOf('.');
+ const root = dot > 0 ? tick.slice(0, dot).toUpperCase() : '';
+ return coins.ALLOWED_COINS.includes(root) && root !== String(chain || '').toUpperCase()
+ ? tick.slice(dot + 1) : tick;
+ };
+
+ const tickParam = (req, res) => {
+ const tick = String((req.params && req.params.tick) || '').trim();
+ if(!tick || tick.length > 250 || /[\x00-\x1f\x7f]/.test(tick)){
+ error(res, 400, 'Invalid token tick.', 'INVALID_TICK');
+ return null;
+ }
+ return tick;
+ };
+
+ return {
+ invariant: bridgeInvariantHandler(hubConnector, tickParam, error, nativeTickFor),
+ transfers: bridgeTransfersHandler(getExplorer, tickParam, error, nativeTickFor)
+ };
+}
+
+function bridgeInvariantHandler(hubConnector, tickParam, error, nativeTickFor){
+ return async function invariant(req, res){
+ const tick = tickParam(req, res);
+ if(tick === null) return;
+ if(!hubConnector)
+ return error(res, 503, 'Bridge invariant is unavailable.', 'BRIDGE_INVARIANT_UNAVAILABLE');
+ const routeCoin = String((req.params && req.params.coin) || '').toUpperCase();
+ const chain = coins.ALLOWED_COINS.find(coin => routeCoin.endsWith(coin)) || routeCoin;
+ const nativeTick = nativeTickFor(tick, chain);
+ try {
+ const result = await hubConnector.call({
+ jsonrpc: '2.0', method: 'getbridgeinvariant', params: { tick: nativeTick }, id: 1
+ }, { attempts: 1 });
+ if(result === null || result === undefined)
+ return error(res, 503, 'Bridge invariant is unavailable.', 'BRIDGE_INVARIANT_UNAVAILABLE');
+ // The renderer indexes the response by the displayed tick. Hub
+ // records use the native spelling, while a destination token page
+ // uses ORIGIN.TICK, so rename that one key at the HTTP boundary.
+ if(nativeTick !== tick && result && typeof result === 'object' &&
+ Object.prototype.hasOwnProperty.call(result, nativeTick))
+ return res.json({ [tick]: result[nativeTick] });
+ return res.json(result);
+ } catch(err){
+ log.warn('BRIDGE_INVARIANT_READ_FAILED', { err: err && err.message ? err.message : err });
+ return error(res, 503, 'Bridge invariant is unavailable.', 'BRIDGE_INVARIANT_UNAVAILABLE');
+ }
+ };
+}
+
+function bridgeTransfersHandler(getExplorer, tickParam, error, nativeTickFor){
+ return async function transfers(req, res){
+ const tick = tickParam(req, res);
+ if(tick === null) return;
+ const coin = String((req.params && req.params.coin) || '').toUpperCase();
+ const explorer = getExplorer();
+ const db = explorer && explorer.db;
+ const source = db && db.checkpointDb && db.checkpointDb[coin];
+ if(!db || typeof db.getBridgeTransfers !== 'function' || !source ||
+ !/^[A-Za-z0-9_$]+$/.test(String(source.name || '')))
+ return error(res, 503, 'Bridge transfers are unavailable.', 'BRIDGE_TRANSFERS_UNAVAILABLE');
+
+ // General-token rows store the native tick in the mirror. A bridged
+ // copy is displayed as ORIGIN.TICK, so remove a known foreign-chain
+ // root before binding the value. Native subassets retain their dot.
+ const nativeTick = nativeTickFor(tick, source.chain);
+
+ try {
+ const rows = await db.getBridgeTransfers({ coin, schema: source.name, network: source.network,
+ tick: nativeTick, chain: source.chain, limit: 100 });
+ return res.json(Array.isArray(rows) ? rows : []);
+ } catch(err){
+ log.warn('BRIDGE_TRANSFERS_READ_FAILED', { coin, err: err && err.message ? err.message : err });
+ return error(res, 503, 'Bridge transfers are unavailable.', 'BRIDGE_TRANSFERS_UNAVAILABLE');
+ }
+ };
+}
+
+function mountBridgePanelRoutes(app, getExplorer, hubConnector){
+ const handlers = createBridgePanelHandlers(getExplorer, hubConnector);
+ app.get('/:coin/api/bridge-invariant/:tick', (req, res) => handlers.invariant(req, res));
+ app.get('/:coin/api/bridge-transfers/:tick', (req, res) => handlers.transfers(req, res));
+}
+
// Everything the shutdown drain has to take down, published from startApi() as it
// is built. Module scope because the signal handler is registered at load, before
// startApi() has run: a SIGTERM that lands during startup finds the pieces that
@@ -194,6 +287,8 @@ async function startApi(){
// Declared here so the ping closure can reference it after explorer is created.
let explorer = null;
const jsonRpcController = createJsonRpcController(() => explorer, requestGate);
+ // Before XChainExplorer, whose constructor mounts the GET 404 wildcard.
+ mountBridgePanelRoutes(app, () => explorer, HUB_ENDPOINTS ? new xchainHubConnector(HUB_ENDPOINTS) : null);
const httpServer = startHttpListener(app);
// Secondary and optional. Both the drain and the WS upgrade below read
@@ -272,6 +367,8 @@ for (const sig of ['SIGTERM', 'SIGINT']) {
process.on(sig, () => shutdown(sig));
}
+module.exports = { createBridgePanelHandlers, mountBridgePanelRoutes };
+
startApi().catch(err => {
log.error('FATAL_STARTUP_ERROR', { err: err && err.message ? err.message : err, stack: err && err.stack });
process.exit(1);
diff --git a/src/consensus/equivocation_header.js b/src/consensus/equivocation_header.js
index 48e11991..c043bdec 100644
--- a/src/consensus/equivocation_header.js
+++ b/src/consensus/equivocation_header.js
@@ -18,11 +18,15 @@
* EQUIV|||||
*
* The canonical source of record is
- * xchain-documentation/protocol/reference-impl/equivocation_header.js; it is
- * vendored BYTE-IDENTICALLY into xchain-hub, xchain-indexer, xchain-explorer,
- * xchain-sdk and xchain-sync. Every PBFT/consensus engine prefixes its canonical
- * through here, the settlement gates (cross_settle, xexec, xcall, anchor, price,
- * attest) and the recovery verifier re-derive it to re-verify quorum signatures,
+ * xchain-indexer/src/consensus/equivocation_header.js; it is vendored
+ * BYTE-IDENTICALLY into xchain-hub, xchain-explorer, xchain-sdk, xchain-sync and
+ * xchain-documentation/protocol/reference-impl/consensus/equivocation_header.js.
+ * Edit the indexer copy only and re-run reconcile-twins.sh to re-vendor every
+ * other copy; never edit a vendored copy.
+ *
+ * Every PBFT/consensus engine prefixes its canonical through here, the settlement
+ * gates (cross_settle, xexec, xcall, anchor, price, attest) and the recovery
+ * verifier re-derive it to re-verify quorum signatures,
* and the SLASH v0 action verifies equivocation proofs against it. Adding ``
* makes equivocation provable WITHOUT false-positiving honest view changes (which
* re-sign different content for the same round under a different view). Equivocation
@@ -38,8 +42,9 @@
* the same anchor. The cross-service conformance suite
* (ConsensusPrimitiveConformance.test.js, driven by
* xchain-documentation/protocol/test-vectors) runs in every repo and asserts BOTH the
- * behavior (canonical vectors) AND byte-identity of the local copy to this canonical
- * source, so any unmirrored edit fails CI everywhere (a divergence forks the chain).
+ * behavior (canonical vectors) AND byte-identity of the local copy to the
+ * xchain-documentation copy, so any unmirrored edit fails CI everywhere (a
+ * divergence forks the chain).
*
********************************************************************/
diff --git a/src/consensus/gate_registry.js b/src/consensus/gate_registry.js
index 4c7845e9..8dffd67b 100644
--- a/src/consensus/gate_registry.js
+++ b/src/consensus/gate_registry.js
@@ -40,8 +40,9 @@
* RegistryMissError naming the key: a row a build lacks is a build defect and
* never a network state. Nothing may add a row after this module loads.
*
- * EXPLORER-ONLY GATES: none. Every twinned carrier's table is in the SHARED
- * block, so this repo registers no rows of its own.
+ * EXPLORER-ONLY GATES: ANCHOR_ACTIVATION controls how the browser labels
+ * anchors around the DOGE wire restart. It is registered after the shared
+ * block from gate_registry/local_rows.js.
*
********************************************************************/
@@ -49,6 +50,7 @@
const core = require('./gate_registry/core.js');
const { registerRows } = require('./gate_registry/shared_rows.js');
+const { registerLocalRows } = require('./gate_registry/local_rows.js');
const { env } = require('../config.js');
// The SHARED block, loaded for effect: each part queues its rows into
@@ -63,6 +65,7 @@ require('./gate_registry/shared_rows_5.js');
const { registry } = core;
registerRows(registry, env);
+registerLocalRows(registry);
module.exports = {
get: (key) => registry.get(key),
diff --git a/src/consensus/gate_registry/local_rows.js b/src/consensus/gate_registry/local_rows.js
new file mode 100644
index 00000000..672918e5
--- /dev/null
+++ b/src/consensus/gate_registry/local_rows.js
@@ -0,0 +1,31 @@
+/*********************************************************************
+ *
+ * Copyright © 2025-2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC - https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ *
+ * Explorer-owned activation rows that are not part of the shared gate block.
+ *
+ ********************************************************************/
+
+'use strict';
+
+// Register the DOGE height where the current ANCHOR wire set begins. Mainnet
+// and testnet retain older anchor history, while regtest starts from genesis.
+function registerLocalRows(registry) {
+ registry.addGate('anchor_activation.ANCHOR_ACTIVATION', 'height', {
+ mainnet: 6360000,
+ testnet: 67858600,
+ regtest: 0,
+ });
+}
+
+module.exports = { registerLocalRows };
diff --git a/src/consensus/gate_registry/shared_rows_1.js b/src/consensus/gate_registry/shared_rows_1.js
index bf311be0..568d8ff5 100644
--- a/src/consensus/gate_registry/shared_rows_1.js
+++ b/src/consensus/gate_registry/shared_rows_1.js
@@ -295,11 +295,11 @@ addGate('anchor_reward_activation.ANCHOR_ATTEST_ARRIVAL_MARGIN_S', 'constant', 6
// that window.
addGate('anchor_reward_activation.ANCHOR_ATTEST_BARRIER_ACTIVATION', 'height', {
mainnet: null, // INERT under the 2026-08-29 mainnet write hold
- // SIZED 2026-09-16 20:41Z, on the BTC clock because this member is BTC-only: the same
- // instant as the family's BTC CONSUMER height, so the one member that keeps BOTH
- // certificates gains them together rather than carrying a lone extra rule for 6 h.
- // Above the same roll and the same epoch close; the canon carries the measurement.
- testnet: 153266,
+ // SIZED 2026-09-16 20:41Z, RE-SLID 2026-09-19 and 2026-09-23, on the BTC clock because this
+ // member is BTC-only: the same instant as the family's BTC CONSUMER height, so the one
+ // member that keeps BOTH certificates gains them together rather than carrying a lone extra
+ // rule for 6 h. The canon carries the measurement.
+ testnet: 154291,
regtest: UNPINNED, // shares the family's arming seam so one venue lever arms both
});
diff --git a/src/consensus/gate_registry/shared_rows_2.js b/src/consensus/gate_registry/shared_rows_2.js
index c7fd765d..0063a165 100644
--- a/src/consensus/gate_registry/shared_rows_2.js
+++ b/src/consensus/gate_registry/shared_rows_2.js
@@ -216,9 +216,9 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_ACTIVATION', 'height', {
'BTC:mainnet': null,
'LTC:mainnet': null,
'DOGE:mainnet': null,
- 'BTC:testnet': 153222, // SIZED 2026-09-16 20:41Z: epoch close 153,216 + 6 buried; tip 152,756 + 466 at 498.7 s/blk, about 64.5 h
- 'LTC:testnet': 4891504, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 4,889,190 + 2314 at 82.5 s/blk
- 'DOGE:testnet': 67911796, // RE-CUT 2026-09-17 22:45Z onto that instant: tip 67,904,912 + 6884 at 27.7 s/blk
+ 'BTC:testnet': 154234, // RE-SLID 2026-09-23: train 154,074 + 160 blocks (17 h at the 383.04 s/blk bound, 25.6 h at the 575.89 s/blk 84 h trailing mean), the v0.20.1 patch reslide
+ 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train
+ 'DOGE:testnet': 67936053, // RE-SLID 2026-09-23: tip 67,924,397 at 17:48Z + 11656 blocks (83.8 h at 25.89 s/blk, the 84 h trailing mean, the same instant as the BTC producer), the v0.20.1 patch reslide
'BTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
'LTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
'DOGE:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
@@ -228,9 +228,9 @@ addGate('mirror_admission_activation.MIRROR_ADMISSION_CONSUMER_ACTIVATION', 'hei
'BTC:mainnet': null,
'LTC:mainnet': null,
'DOGE:mainnet': null,
- 'BTC:testnet': 153266, // its producer + 44 blocks, about 6 h: strictly above, never equal
- 'LTC:testnet': 4891766, // its producer + 262 blocks, about 6 h at 82.5 s/blk
- 'DOGE:testnet': 67912575, // its producer + 779 blocks, about 6 h at 27.7 s/blk
+ 'BTC:testnet': 154291, // RE-SLID 2026-09-23: its producer + 57 blocks (6 h at the 383.04 s/blk bound, 9.1 h at the 575.89 s/blk 84 h trailing mean), strictly above, never equal
+ 'LTC:testnet': null, // disabled for v0.20.1, 2026-09-18: LTC:testnet mirror admission ships null on this train; arms on a later train
+ 'DOGE:testnet': 67936888, // RE-SLID 2026-09-23: its producer + 835 blocks (6 h at 25.89 s/blk, the 84 h trailing mean), strictly above, never equal
'BTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
'LTC:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
'DOGE:regtest': UNPINNED, // ARMS by XC_MIRROR_ADMISSION_ACTIVATION at registration
diff --git a/src/consensus/gate_registry/shared_rows_5.js b/src/consensus/gate_registry/shared_rows_5.js
index cbf28976..fde6a96c 100644
--- a/src/consensus/gate_registry/shared_rows_5.js
+++ b/src/consensus/gate_registry/shared_rows_5.js
@@ -108,9 +108,16 @@ addGate('train_activation.TRAIN_ACTIVATION', 'ruleset', {
// the LTC leg of this family two days off its BTC counterpart a day after the first cut.
// That lead is the rolling-upgrade window the fleet roll must finish inside (24x the 90
// minute roll budget), and every testnet mirror-admission height sits above it on the same
- // BTC clock (the BTC producer at 153,222 is 106 blocks and about 17.0 h further up), so a
- // node lacking this rule set halts before it can grade an admission-stamped row.
- '0.20.0': { mainnet: 9999999999, testnet: 153116, regtest: 0 },
+ // BTC clock, so a node lacking this rule set halts before it can grade an admission-stamped
+ // row.
+ // RE-SLID 2026-09-23 for the v0.20.1 patch train, after the live tips overran the
+ // 2026-09-19 slide before the freeze: margin is 40 h to the nearest armed height, converted
+ // at each coin's fastest defensible cadence. Chain_tip TBTC 153,698 at 2026-09-23T15:55Z
+ // + 376 blocks, ceil(40 h / 383.04 s per block, the least-squares bound). The
+ // mirror-admission family below re-slides onto the same instant plus its own 17 h and 6 h
+ // offsets. LTC:testnet mirror admission ships disabled on this train and is
+ // untouched by this reslide; it arms on a later train.
+ '0.20.0': { mainnet: 9999999999, testnet: 154074, regtest: 0 },
});
// xchain_bridge_activation
diff --git a/src/consensus/gates/mirror_admission_gate.js b/src/consensus/gates/mirror_admission_gate.js
index 179bfb19..74029a89 100644
--- a/src/consensus/gates/mirror_admission_gate.js
+++ b/src/consensus/gates/mirror_admission_gate.js
@@ -3,17 +3,21 @@
const { get, copy, activeAt } = require('../gate_registry');
/*
- * mirror_admission_activation.js - admission by height for the mirror barrier family.
+ * mirror_admission_gate.js - admission by height for the mirror barrier family.
*
- * BYTE-IDENTICAL TWIN. This file exists at xchain-indexer/src/, xchain-hub/src/ and, once
- * vendored, xchain-explorer/src/. The three copies are held identical by
- * bin/sync-hub-mirror-client.sh --check (a cmp -s byte compare, not a digest) and the
- * exported constants are held value-identical to xchain-documentation/protocol/constants.js
- * by the activation-constants parity suite. A one-sided edit forks consensus at the boundary.
+ * BYTE-IDENTICAL TWIN. The canonical copy is xchain-indexer/src/; xchain-hub/src/ and
+ * xchain-explorer/src/ carry it byte for byte, so edit the indexer copy and copy it outward.
+ * reconcile-twins.sh --check grades both pairs. The explorer pair is also held by
+ * xchain-indexer/bin/sync-hub-mirror-client.sh --check (a cmp -s byte compare, not a digest),
+ * which never reads the hub copy; the hub pair is also held by the byte compare in the
+ * activation-constants parity suite, which skips without a sibling checkout unless
+ * XCHAIN_REQUIRE_SIBLINGS=1. That suite also holds the exported constants value-identical to
+ * xchain-documentation/protocol/constants.js. A one-sided edit forks consensus at the boundary.
*
- * ZERO REQUIRES, DELIBERATELY. This module is a CLIENT_FILES entry: it is vendored into the
- * explorer beside hub_db_sync.js, which can carry no dependency the explorer does not have.
- * price_batching_floor_activation.js is the precedent. The alternative considered and
+ * ONE REQUIRE, DELIBERATELY. This module is a DEP_FILES entry of sync-hub-mirror-client.sh: it
+ * is vendored into the explorer beside hub_db_sync.js, which can carry no dependency the
+ * explorer does not have, so it requires only ../gate_registry, which every consumer carries
+ * as its own. price_batching_floor_gate.js is the precedent. The alternative considered and
* rejected was threading an admission bound through eleven predicate signatures, their
* waiters and every call site.
*
@@ -364,20 +368,19 @@ function isAdmissionEra(network, eraBlock){
}
/**
- * The canonical tail for a row's admission map: '' below the activation, and '|' plus the
- * encoded map at or above it.
+ * The canonical tail for a row's admission map: '' unless both the row is at or above the
+ * activation and the map is present, and '|' plus the encoded map when both are true.
*
- * REFUSES IN BOTH DIRECTIONS, exactly as AttestationConsensus._buildCanonical does for
- * the mirror era. Building a legacy canonical for a modern row strands the row (its
- * signatures reproduce over bytes no verifier rebuilds); building a modern canonical for
- * a legacy row forks a from-genesis replay. Neither can be recovered from downstream, so
- * both throw where the caller that got it wrong is still on the stack.
+ * A version seam can expose either an admission map to an inert node or a legacy row to an
+ * armed node. Both cases take the legacy byte path so mixed versions can bind the row and
+ * let quorum verification decide whether its signatures match. Only an armed node with a
+ * present map emits admission bytes.
*
* No per-rail canonical VERSION field is minted for this, and none exists anywhere in the
* tree: this height-gated era check IS the versioning, and a version integer would
* duplicate the gate while giving a Byzantine leader a second field to disagree about.
*
- * @param {string} label the builder's canonical tag, for the refusal message
+ * @param {string} label the builder's canonical tag, retained for the stable caller API
* @param {string} network the row's network, half the activation key
* @param {number} eraBlock the ROW's own BTC block
* @param {object|null} map the row's admission map, or null for a legacy row
@@ -390,22 +393,16 @@ function admissionCanonicalField(label, network, eraBlock, map){
/**
* The admission map as a canonical VALUE rather than a pipe-appended tail: null below the
- * activation, the encoded map at or above it, with the same two refusals as the field
- * form. This is the spelling a JSON-shaped canonical (the PRICE batch) carries under its
- * own key, where a '|' tail would be a byte inside a string rather than a delimiter. One
- * era gate for both spellings, so the two carriers can never disagree about which era a
- * row is in.
+ * activation or when the map is absent, and the encoded map when the era is armed and the
+ * map is present. This is the spelling a JSON-shaped canonical (the PRICE batch) carries
+ * under its own key, where a '|' tail would be a byte inside a string rather than a
+ * delimiter. One era gate for both spellings keeps their boundary semantics identical.
*/
function admissionCanonicalValue(label, network, eraBlock, map){
let era = isAdmissionEra(network, eraBlock);
let has = (map !== null && map !== undefined);
- if(era && !has)
- throw new Error(label + ': admission-era row at block ' + String(eraBlock) + ' on ' + String(network) +
- ' has no admit_blocks; refusing to build a legacy canonical');
- if(!era && has)
- throw new Error(label + ': legacy-era row at block ' + String(eraBlock) + ' on ' + String(network) +
- ' was handed admit_blocks ' + JSON.stringify(map) + '; refusing to build an admission-era canonical');
if(!era) return null;
+ if(!has) return null;
return encodeAdmitBlocks(map);
}
diff --git a/src/consensus/stake_weighted_quorum.js b/src/consensus/stake_weighted_quorum.js
index 76fdd4ed..11665534 100644
--- a/src/consensus/stake_weighted_quorum.js
+++ b/src/consensus/stake_weighted_quorum.js
@@ -14,17 +14,20 @@
*
* THE single, CONSENSUS-CRITICAL implementation of the stake-weighted quorum
* predicate. The canonical source of record is
- * xchain-documentation/protocol/reference-impl/stake_weighted_quorum.js; it is
- * vendored BYTE-IDENTICALLY into xchain-hub, xchain-indexer, xchain-explorer,
- * xchain-sdk and xchain-sync. Every PBFT tally engine, every settlement gate
- * (cross_settle, xexec, xcall, anchor, price, attest), the recovery verifier and
- * the client/explorer checkpoint verifiers resolve through this predicate so they
- * can never drift; a divergence forks the chain.
+ * xchain-indexer/src/consensus/stake_weighted_quorum.js; it is vendored
+ * BYTE-IDENTICALLY into xchain-hub, xchain-explorer, xchain-sdk, xchain-sync and
+ * xchain-documentation/protocol/reference-impl/consensus/stake_weighted_quorum.js.
+ * Edit the indexer copy only and re-run reconcile-twins.sh to re-vendor every
+ * other copy; never edit a vendored copy. Every PBFT tally engine, every
+ * settlement gate (cross_settle, xexec, xcall, anchor, price, attest), the
+ * recovery verifier and the client/explorer checkpoint verifiers resolve through
+ * this predicate so they can never drift; a divergence forks the chain.
*
* The cross-service conformance suite (ConsensusPrimitiveConformance.test.js,
* driven by xchain-documentation/protocol/test-vectors) runs in every repo and
* asserts BOTH the behavior (canonical vectors) AND byte-identity of the local
- * copy to this canonical source, so any unmirrored edit fails CI everywhere.
+ * copy to the xchain-documentation copy, so any unmirrored edit fails CI
+ * everywhere.
*
* Self-contained on mathjs bignumber (exact, never a JS double): the predicate is
* a pure function with no injected utility instance, so it is identical in every
diff --git a/src/content/css/xchain.css b/src/content/css/xchain.css
index be134ebf..0ee43a9b 100644
--- a/src/content/css/xchain.css
+++ b/src/content/css/xchain.css
@@ -503,3 +503,22 @@ table.no-outer-borders tr:last-child {
.clear-both {
clear:both;
}
+
+/* The legs or members opened under a list row by its count badge
+ (action_markers.js). Indented and flat so it reads as part of the row
+ above it, not as a second table. */
+.xc-legs-child {
+ padding: var(--xc-legs-child-padding-y) var(--xc-space-none) var(--xc-legs-child-padding-y) var(--xc-legs-child-indent);
+}
+.xc-legs-table td,
+.xc-legs-table th {
+ font-size: var(--xc-legs-cell-font-size);
+ padding: var(--xc-legs-cell-padding-y) var(--xc-legs-cell-padding-x);
+}
+.xc-legs-toggle {
+ cursor: pointer;
+ text-decoration: none;
+}
+tr.xc-legs-open .xc-legs-toggle .fa-caret-down {
+ transform: rotate(180deg);
+}
diff --git a/src/content/html/action.html b/src/content/html/action.html
index 9f6208b5..bd565448 100644
--- a/src/content/html/action.html
+++ b/src/content/html/action.html
@@ -1927,6 +1927,10 @@
+
+ | Version |
+ |
+
', start);
+ if (end < 0) throw new Error('could not bound the #info-rollcall panel');
+ return ACTION_HTML.slice(start, end);
+}
+
+function render(data) {
+ const dom = new JSDOM('' + panelHtml() + '',
+ { runScripts: 'outside-only' });
+ dom.window.eval(fs.readFileSync(path.join(ROOT, 'src/content/js/jquery.min.js'), 'utf8'));
+ dom.window.eval(fs.readFileSync(path.join(ROOT, 'src/content/js/numeral.js'), 'utf8'));
+ dom.window.eval(`
+ function formatHash(h, len){ return String(h == null ? '' : h).substring(0, len); }
+ ${extractFn('isNull')}
+ ${extractFn('escapeHtml')}
+ `);
+ dom.window.eval(extractFn('showRollcallDetails'));
+ dom.window.showRollcallDetails(Object.assign({
+ epoch_height: 151400,
+ ledger_hash: 'ab'.repeat(32),
+ publisher: 'cd'.repeat(32),
+ signers: []
+ }, data));
+
+ const doc = dom.window.document;
+ const cell = (name) => doc.querySelector('#info-rollcall .' + name);
+ return {
+ version: cell('rollcall-version'),
+ gates: cell('rollcall-gates'),
+ gateBadges: [...cell('rollcall-gates').querySelectorAll('.badge')]
+ .map((badge) => badge.textContent)
+ };
+}
+
+function markupRows() {
+ const dom = new JSDOM('' + panelHtml() + '');
+ return [...dom.window.document.querySelectorAll('#info-rollcall > table:first-of-type tbody tr')]
+ .map((row) => ({
+ label: row.querySelector('th').textContent.trim(),
+ cell: row.querySelector('td').classList[0]
+ }));
+}
+
+describe('ROLLCALL detail version and consensus gates', function () {
+ it('renders v0 from action_format and shows no gate list', function () {
+ const out = render({ action_format: 0, gates: ['ignored.GATE'] });
+ expect(out.version.textContent.trim()).to.equal('ROLLCALL v0');
+ expect(out.version.querySelector('.badge')).to.not.equal(null);
+ expect(out.gates.textContent.trim()).to.equal('-');
+ expect(out.gateBadges).to.deep.equal([]);
+ });
+
+ it('renders v1 from action_format and every published gate as a badge', function () {
+ const out = render({
+ action_format: 1,
+ gates: ['alpha.FIRST', 'beta.SECOND', 'gamma.THIRD']
+ });
+ expect(out.version.textContent.trim()).to.equal('ROLLCALL v1');
+ expect(out.version.querySelector('.badge')).to.not.equal(null);
+ expect(out.gateBadges).to.deep.equal([
+ 'alpha.FIRST', 'beta.SECOND', 'gamma.THIRD'
+ ]);
+ });
+
+ it('preserves the published gate order', function () {
+ const gates = ['zeta.LAST', 'alpha.FIRST', 'middle.GATE'];
+ expect(render({ action_format: 1, gates }).gateBadges).to.deep.equal(gates);
+ });
+
+ it('escapes gate names before writing badge markup', function () {
+ const hostile = '
';
+ const out = render({ action_format: 1, gates: [hostile, 'safe.GATE'] });
+ expect(out.gateBadges).to.deep.equal([hostile, 'safe.GATE']);
+ expect(out.gates.querySelector('img')).to.equal(null);
+ expect(out.gates.innerHTML).to.contain('<img');
+ });
+
+ it('shows a dash when a v1 payload has no gates', function () {
+ expect(render({ action_format: 1, gates: [] }).gates.textContent.trim()).to.equal('-');
+ });
+
+ it('keeps the shipped markup and rollcall row layout exactly aligned', function () {
+ expect(markupRows()).to.deep.equal(CARDS.cards.rollcall.rows);
+ expect(markupRows().map((row) => row.cell)).to.deep.equal([
+ 'rollcall-version',
+ 'rollcall-epoch-height',
+ 'rollcall-ledger-hash',
+ 'rollcall-publisher',
+ 'rollcall-gates',
+ 'rollcall-signer-count'
+ ]);
+ });
+});
diff --git a/test/unit/content/lists/content_client_action_markers.test.js b/test/unit/content/lists/content_client_action_markers.test.js
new file mode 100644
index 00000000..96a5e715
--- /dev/null
+++ b/test/unit/content/lists/content_client_action_markers.test.js
@@ -0,0 +1,147 @@
+/*********************************************************************
+ *
+ * Copyright © 2025–2026 Dankest, LLC
+ * Based on XChain Platform by Dankest, LLC – https://dankest.llc
+ *
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ *
+ * This file is part of XChain Platform. Licensed under the GNU Affero
+ * General Public License v3.0 or later; see LICENSE.md. A commercial
+ * license (without AGPL source-disclosure terms) is available -
+ * contact legal@dankest.llc.
+ *
+ **********************************************************************
+ * The client half of the structure markers: getActionDetails appends a count
+ * toggle to a multi-leg send, a multi-destroy and a BATCH parent, an in-batch
+ * link to a member, and nothing to a plain row; the disclosure renderer lists
+ * legs and members from the full action payload with user text escaped.
+ * Runs the shipped functions sliced out of the page-loaded client parts.
+ */
+'use strict';
+const { srcText } = require('../../../helpers/source_text');
+const fs = require('fs');
+const path = require('path');
+const { JSDOM } = require('jsdom');
+const { expect } = require('chai');
+
+const SRC = srcText('src/content/js/xchain.js');
+
+function extractFn(name) {
+ const sig = 'function ' + name + '(';
+ const start = SRC.indexOf(sig);
+ if (start < 0) throw new Error('function not found in xchain.js: ' + name);
+ const braceStart = SRC.indexOf('{', start);
+ let depth = 0, i = braceStart;
+ for (; i < SRC.length; i++) {
+ const c = SRC[i];
+ if (c === '{') depth++;
+ else if (c === '}') { depth--; if (depth === 0) { i++; break; } }
+ }
+ return SRC.slice(start, i);
+}
+
+const RENDERERS = [
+ 'getActionDetails',
+ 'actionDetail_renderBasicActions', 'actionDetail_renderMarketActions', 'actionDetail_renderMessageActions',
+ 'actionDetail_renderContractActions', 'actionDetail_renderConsensusActions',
+ 'actionDetail_renderStructureMarkers', 'actionDetail_disclosureToggle',
+ 'actionDetail_renderDisclosure', 'actionDetail_statusClass',
+];
+
+function makeWindow() {
+ const dom = new JSDOM('', { runScripts: 'outside-only' });
+ dom.window.eval(fs.readFileSync(path.resolve(__dirname, '..', '..', '..', '..', 'src', 'content', 'js', 'formatters.js'), 'utf8'));
+ dom.window.eval(`
+ function formatAmount(v){ return String(v); }
+ function bcadd(a, b){ return String(Number(a) + Number(b)); }
+ `);
+ dom.window.XC = { coin: 'TDOGE', dispenser_preferences: {} };
+ for (const name of RENDERERS) dom.window.eval(extractFn(name));
+ return dom.window;
+}
+
+describe('structure markers client: getActionDetails', function () {
+ it('a multi-send row gets a recipients toggle after its leg-0 summary', function () {
+ const w = makeWindow();
+ const html = w.getActionDetails('SEND', { tick: 'P00P', amount: '1', destination: 'addrB', leg_count: 4, action_index: '2059' });
+ expect(html).to.include('addrB');
+ expect(html).to.include('xc-legs-toggle');
+ expect(html).to.include('data-action-index="2059"');
+ expect(html).to.include('4 recipients');
+ });
+
+ it('a multi-destroy row says legs, not recipients', function () {
+ const w = makeWindow();
+ const html = w.getActionDetails('DESTROY', { tick: 'P00P', amount: '1', leg_count: 2, action_index: '2062' });
+ expect(html).to.include('2 legs');
+ expect(html).to.not.include('recipients');
+ });
+
+ it('a BATCH parent shows its member count as a toggle', function () {
+ const w = makeWindow();
+ const html = w.getActionDetails('BATCH', { member_count: 3, action_index: '166' });
+ expect(html).to.include('Batch');
+ expect(html).to.include('3 actions');
+ expect(html).to.include('data-action-index="166"');
+ });
+
+ it('a member row links its parent batch', function () {
+ const w = makeWindow();
+ const html = w.getActionDetails('SEND', { tick: 'P00P', amount: '1', destination: 'addrB', parent_batch_action_index: 166 });
+ expect(html).to.include('xc-in-batch');
+ expect(html).to.include('/TDOGE/action/166');
+ expect(html).to.not.include('xc-legs-toggle');
+ });
+
+ it('a plain send row carries no marker at all', function () {
+ const w = makeWindow();
+ const html = w.getActionDetails('SEND', { tick: 'P00P', amount: '1', destination: 'addrB' });
+ expect(html).to.not.include('xc-legs-toggle');
+ expect(html).to.not.include('xc-in-batch');
+ });
+
+ it('a details value of false renders the summary without throwing', function () {
+ const w = makeWindow();
+ expect(w.getActionDetails('BATCH', false)).to.equal('Batch');
+ });
+});
+
+describe('structure markers client: actionDetail_renderDisclosure', function () {
+ it('lists every leg of a multi-send with the memo escaped', function () {
+ const w = makeWindow();
+ const html = w.actionDetail_renderDisclosure({
+ action: 'SEND',
+ sends: [
+ { destination: 'addrB', tick: 'P00P', amount: '1', memo: '', status: 'valid' },
+ { destination: 'addrC', tick: 'P00P', amount: '2', memo: '', status: 'invalid' },
+ ]
+ });
+ expect(html).to.include('addrB');
+ expect(html).to.include('addrC');
+ expect(html).to.include('<script>');
+ expect(html).to.not.include('