E2E Test Suite: Architecture
Source Files
| File | Role |
|---|---|
src/BlockchainConnector.js |
JSON-RPC client for coin node (axios, Basic Auth). Methods: getNetworkInfo, broadcastTx, waitForTx, getTransactionHex, getFeePerKilobyte, plus reorg primitives (invalidateBlock, reconsiderBlock, generateBlock, etc.) |
src/XChainUtxoTrackerConnector.js |
JSON-RPC client for UTXO tracker (axios). Methods: ping, getSyncStatus, getQuiescentStatus, quiesce, getUtxosFromAddress, waitForUtxos |
src/XChainEncoderConnector.js |
JSON-RPC client for encoder (axios). Methods: ping, createTx (13 parameters) |
src/XChainDecoderConnector.js |
JSON-RPC client for decoder (axios). Methods: ping, health |
src/XChainIndexerConnector.js |
JSON-RPC client for indexer (axios). Methods: ping, health, call, getCapabilityValidators, getStakeSourceByPubkey, waitForIndexedBlock |
src/XChainExplorerConnector.js |
JSON-RPC client for explorer (axios). Methods: ping |
src/XChainHubConnector.js |
Multi-endpoint failover hub client (axios). Methods: ping, getAllConfig, _call. Static: parseEndpoints |
src/RegtestMinerConnector.js |
JSON-RPC client for regtest miner (axios). Methods: ping, sendFunds, setMiningTime, setDefaultMiningTime, pauseMining, resumeMining, generateBlocks |
src/db.js |
MariaDB client with connection pooling and 44 waitFor*/check* polling methods |
src/CryptoNetworks.js |
Static network config provider. Returns bitcoinjs-lib network objects for all 9 coin/network combinations |
test/cryptoHelper.js |
BIP39/BIP32 wallet generation, address derivation, funded address creation |
test/transactionHelper.js |
PSBT construction, signing, broadcast, P2SH two-step handling, UTXO verification cache |
test/initialCheck.test.js |
Mocha root hooks (beforeAll/afterAll): bootstrap sequence, teardown, gas token creation |
test/perf/perfCollector.js |
Global singleton for bootstrap phase timing and poll metric collection |
test/reporters/performance-reporter.js |
Custom Mocha reporter capturing per-test timing, memory usage, and poll metrics |
Bootstrap Sequence
The initialCheck.test.js beforeAll hook executes five named phases, each instrumented via perfCollector.phase():
flowchart TD
P1["Phase 1: env-resolution<br>- Read .env via dotenv<br>- If direct env vars missing → query Hub for service config<br>- Set global COIN, NETWORK, COIN_CODE, NETWORK_OBJECT"]
P2["Phase 2: connector-init<br>- Instantiate 8 connectors as globals<br>- Create MariaDB connection pool (limit 10)"]
P3["Phase 3: service-pings<br>- Ping all 8 services (node, tracker, encoder, decoder, indexer, explorer, DB, miner); throw on failure<br>- Configure mining: setMiningTime(1000, 1000)"]
P4["Phase 4: native-fee-price-seed<br>- Seed XCHAIN/USD and {COIN}/USD prices via nativeFeeHelper<br>- Ensures oracle prices are fresh before any action test runs"]
P5["Phase 5: gas-token-check<br>- checkIssue({ tick: 'XCHAIN', status: 'valid' })<br>- If not found: fund address, ISSUE XCHAIN token<br>- If found: skip (idempotent)"]
P1 --> P2 --> P3 --> P4 --> P5
Transaction Flow
Standard OP_RETURN Path
flowchart TD
GNFA["cryptoHelper.getNewFundedAddress()"]
GNA["getNewAddress()<br>→ BIP39 mnemonic → BIP32 derivation → P2PKH address"]
SF["regtestMinerConnector.sendFunds(address, amount)"]
WFT["nodeConnector.waitForTx(txid)<br>→ poll until confirmed"]
WFU["utxoTrackerConnector.waitForUtxos(address)<br>→ poll until indexed"]
CAST["transactionHelper.createAndSendTransaction(addressInfo, message)"]
CTX["encoderConnector.createTx(utxos, pubkey, ..., data, ..., changeAddress)<br>→ returns { encoding: 'opreturn', psbt: hex }"]
PSBT["Psbt.fromHex() → signInput() → finalizeAllInputs() → extractTransaction()"]
BTX["nodeConnector.broadcastTx(txHex)"]
WFT2["nodeConnector.waitForTx(txHash, 60000)"]
GUFA["utxoTrackerConnector.getUtxosFromAddress()<br>→ filter confirmed → cache"]
WFI["indexerDatabase.waitForIssue({ source, tick, txHash, status: 'valid' })"]
POLL["polls every 1s for up to 30s → returns row or null"]
GNFA --> GNA
GNFA --> SF
GNFA --> WFT
GNFA --> WFU
GNFA -->|next| CAST
CAST --> CTX
CAST --> PSBT
CAST --> BTX
CAST --> WFT2
CAST --> GUFA
CAST -->|next| WFI
WFI --> POLL
P2SH Two-Step Path
When the encoder returns encoding: "P2SH", transactionHelper automatically handles the two-transaction flow:
- First PSBT: creates the P2SH output (fund transaction)
- Sign with standard
finalizeAllInputs() - Broadcast first transaction
- Second PSBT: calls
encoderConnector.createTx()again withp2shHashandp2shHexfrom the first transaction - Sign with
xchainP2shFinalizer(custom finalizer applying witness/redeem scripts) - Broadcast second transaction
- Wait for both transactions to confirm
- Return the second transaction’s hash (the one the indexer processes)
sequenceDiagram
participant TH as transactionHelper
participant ENC as encoder
participant NODE as coin node
TH->>ENC: createTx() (first PSBT, funds the P2SH output)
ENC-->>TH: unsigned PSBT
TH->>TH: sign with finalizeAllInputs()
TH->>NODE: broadcast first transaction
NODE-->>TH: confirmation
TH->>ENC: createTx() again with p2shHash and p2shHex (second PSBT)
ENC-->>TH: unsigned PSBT
TH->>TH: sign with xchainP2shFinalizer
TH->>NODE: broadcast second transaction
NODE-->>TH: confirmation
Note over TH: returns the second transaction's hash, the one the indexer processes
UTXO Verification Cache
transactionHelper maintains a per-address cache of confirmed UTXOs:
_verifiedUtxos: array of confirmed UTXOs from the last transaction
_verifiedUtxosAddress: address the cache belongs to
Cache lifecycle:
- After broadcasting, poll
getUtxosFromAddress()and filter toconfirmations > 0 - If any UTXO matches the broadcast
txHash, save the confirmed set to cache - On the next
createAndSendTransaction()for the same address, pass cached UTXOs directly to the encoder (bypassing the tracker fetch) - Cache is consumed (cleared) after use
This prevents the encoder from selecting stale mempool entries that may persist for up to 60 seconds in the UTXO tracker’s cleanup cycle.
Polling Architecture
Database Class (db.js)
The Database class maintains a MariaDB connection pool and provides two methods for each ACTION type:
| Method Pattern | Behavior |
|---|---|
checkIssue(filterObj) |
Single query. Builds parameterized WHERE clause from non-null filter fields. Returns first row or null. Releases connection. |
waitForIssue(filterObj, timeMax) |
Polls checkIssue() every 1 second until a row is found or timeMax (default 30s) is exceeded. Records performance metrics. |
WHERE clause construction:
- Each non-null field in the filter object adds a
column = ?clause - Values are passed as parameterized query parameters (SQL injection safe)
isNullOrNullString(value)uses loose equality (== null || == "") to skip null-like fields
44 polling methods cover: issues, sends, credits, debits, mints, broadcasts, lists, airdrops, dispensers, dispenser statuses, dispenses, address options, destroys, messages, prices, files, sleeps, sweeps, dividends, callbacks, orders, order matches, swaps, swap matches, batches, links, coinpays, coinpay obligations, contracts, executions, deposits, withdrawals, stakes, unstakes, delegations, stake-key revocations, contract-stakes, contract-unstakes, contract-delegations, slash events, attestation requests, attestation responses, reward claims.
Hub Discovery
When direct environment variables are not set, the bootstrap sequence discovers service endpoints from xchain-hub:
- Parse hub endpoints from
HUB_VALIDATORS(comma-separated) orHUB_URL/HUB_PORT - Instantiate
XChainHubConnectorwith the endpoint array - Call
getAllConfig()→ returnsconfig[coin][network][service][param] - Extract host/port for each service from the hub config
- Override all URLs to
"localhost"(Docker Compose convention, services are accessed via Docker network, not hub-reported hostnames)
The XChainHubConnector._call() method implements multi-endpoint failover: it tries each URL in order, moving to the next on connection failure.
Wallet Management
cryptoHelper.js manages test wallets through a global cache (global.wallets):
| Operation | Behavior |
|---|---|
getWallet(label) |
Returns cached wallet or creates a new skeleton ({ mnemonic: null, seed: null, coin: null, network: null, addresses: [] }) |
getNewAddress(label, coin, network, mnemonic, addressType, addressIndex) |
Generates BIP39 mnemonic (if not cached), derives BIP32 path m/44'/0'/0'/0/{index}, returns { mnemonic, privateKey, publicKey, address } |
getNewFundedAddress(...) |
Calls getNewAddress, then funds via regtest miner, waits for transaction confirmation and UTXO indexing |
Memory cleanup: During afterAll, all wallet seeds and private keys are zeroed via Buffer.fill(0), then global.wallets is set to {}.
Test File Organization
xchain-e2e-test/
├── src/ # Service connector classes (10 files)
├── test/
│ ├── initialCheck.test.js # Mocha root hooks (beforeAll/afterAll)
│ ├── cryptoHelper.js # BIP39/BIP32 wallet management
│ ├── transactionHelper.js # PSBT construction, signing, broadcast
│ ├── actions/ # 64 action test files (live, ordered)
│ ├── helpers/ # 40 modules (action helpers + federation/fee/utility helpers)
│ ├── unit/ # 350+ unit tests (stubbed, no services)
│ ├── integration/ # 150+ integration tests (stubbed I/O)
│ │ ├── fixtures/ # mockMariadb, services, dbRows, hub
│ │ ├── setup/ # Bootstrap, teardown tests
│ │ ├── pipeline/ # Funding flow, tx flow tests
│ │ ├── helpers/ # Action helper pipeline tests
│ │ ├── database/ # Connection pool, polling tests
│ │ ├── errors/ # Error handling tests
│ │ └── state/ # UTXO cache, wallet cache tests
│ ├── e2e/ # 35+ E2E tests (live services)
│ ├── smoke/ # 15+ smoke tests (quick checks)
│ ├── boundary/ # 140+ boundary tests
│ ├── chaos/ # 75+ chaos tests
│ ├── fuzz/ # 50+ fuzz tests (fast-check)
│ ├── regression/ # 120+ regression tests (P0/P1/P2 tagged)
│ ├── perf/ # Performance collector singleton
│ └── reporters/ # Custom Mocha reporter
├── scripts/ # Mutation report, perf gate
├── stryker.config.mjs # Phase 1: mutation testing (unit)
└── stryker.phase2.config.mjs # Phase 2: mutation testing (unit + integration)
Copyright © 2025–2026 Dankest, LLC
Based on XChain Platform by Dankest, LLC – https://dankest.llc
Licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-or-later) with a commercial license available for proprietary use.
You may use, modify, and distribute this material under the terms of the License. See LICENSE and NOTICE for full terms. See the licensing overview.