Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.18.0] - 2026-09-11

### Fixed
- A raw `estimatesmartfee` result is clamped to a per-chain sanity ceiling, `FEE_ESTIMATE_SANITY_CEILING`.
- A two-phase reveal is prefunded at a package-aware fee rate.
- UTXO dedup is linearised and tracker freshness is unified into one classifier.
- The bufferutils patch converges with the SDK copy and is pinned by a twin guard.
- The Docker image no longer tries to bake a `.env` file, so the build succeeds on the legacy builder and configuration reaches the container as environment only.

## [0.17.0] - 2026-09-10

### Added
Expand Down
7 changes: 3 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,9 @@ RUN npm ci --omit=dev

COPY ./src /XChainEncoder/src
COPY ./docs /XChainEncoder/docs
# Glob pattern matches zero or one .env file so the build succeeds when
# the source has no .env (typical in CI / fresh clones). Mirrors the
# pattern used by xchain-indexer/Dockerfile.
COPY ./.en[v] /XChainEncoder/.env
# No .env is baked in: configuration reaches the container as environment
# (xchain-node at `docker run`, docker-compose.yml via env_file). An optional
# `COPY ./.en[v]` glob here builds only under BuildKit.

# Run node directly rather than through `npm run api` (which is this exact
# command). npm builds a three-process tree, npm -> sh -c -> node, and neither
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# XChain Platform Encoder

<p align="center">
<img src="https://img.shields.io/badge/version-0.16.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-1%2C683%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/version-0.17.0-blue" alt="Version">
<img src="https://img.shields.io/badge/tests-1%2C738%2B%20passing-brightgreen" alt="Tests">
<img src="https://img.shields.io/badge/node-%3E%3D22-green" alt="Node">
<img src="https://img.shields.io/badge/license-AGPL--3.0--or--later-blue" alt="License">
</p>
Expand Down Expand Up @@ -165,7 +165,7 @@ neither source sets one, so these defaults hold on an unconfigured box:
| `npm run build` | Production browser bundle (minified) -> `dist/xchain_encoder.min.js` |
| `npm run build:dev` | Development browser bundle (unminified) |
| `npm run smoke-test` | Smoke tests (~52 tests, <1s) |
| `npm run test:unit` | Unit tests (815 tests) |
| `npm run test:unit` | Unit tests (870 tests) |
| `npm run test:integration` | Integration tests (115 tests) |
| `npm run test:boundary` | Boundary condition tests (~101 tests) |
| `npm run test:security` | Security tests (63 tests) |
Expand All @@ -178,7 +178,7 @@ neither source sets one, so these defaults hold on an unconfigured box:
| `npm run bench` | Performance benchmarks |
| `npm run bench:full` | Extended benchmarks with JSON output |
| `npm run bench:soak` | Soak test (sustained load) |
| `npm test` | Unit tests (hermetic, no external services, 815 tests) |
| `npm test` | Unit tests (hermetic, no external services, 870 tests) |
| `npm run test:regtest` | Regtest integration tests (requires local bitcoind) |

## Test Suite
Expand Down
3 changes: 3 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ version: '3'
services:
xchain_encoder:
build: .
# The image bakes no .env (Dockerfile); the README's .env is handed to the
# container here instead.
env_file: .env
# HARD CONSTRAINT: exactly one encoder replica per endpoint. The UTXO
# outpoint-reservation double-spend guard, the recent-build duplicate
# refusal and the rate limiter are all in-process
Expand Down
5 changes: 3 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
{
"name": "xchain-encoder",
"description": "xchain-encoder encodes XChain Platform ACTION commands into blockchain transactions.",
"version": "0.17.0",
"version": "0.18.0",
"license": "AGPL-3.0-or-later",
"repository": {
"type": "git",
"url": "https://github.com/XChain-Platform/xchain-encoder.git"
},
"dependencies": {
"axios": "^1.18.1",
"bip174": "2.1.1",
"bip32": "4.0.0",
"bip39": "^3.1.0",
"bitcoinjs-lib": "6.1.7",
Expand Down
42 changes: 41 additions & 1 deletion src/BlockchainConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,36 @@ function noEstimateRelayMultiplier(){
return (Number.isFinite(raw) && raw > 0) ? raw : 10
}

// Absolute per-chain sanity ceiling (COIN/kB, the same unit estimatesmartfee
// returns) on the RAW non-regtest estimate before anything else sees it.
// getFeePerKilobyte's result is used two ways downstream: as the fee actually
// charged when a caller supplies none, AND as the anchor the caller-facing
// caps (MAX_FEE_RATE_KB, MAX_FEE_RATE_MULTIPLIER) derive their relative
// ceiling from - so a node returning a spiked or malformed estimate on
// testnet/mainnet lifts its own ceiling right along with it, and nothing
// downstream is positioned to catch that; those caps bound a CALLER-supplied
// rate, never the node's own report. Defaults are coin-scale, since fee rates
// run roughly 1000x higher in DOGE/kB than in BTC/kB or LTC/kB for an
// equivalent real-world cost; keyed off NETWORK's coin prefix
// (bitcoin-mainnet, litecoin-testnet, dogecoin-mainnet, ...). An unrecognized
// or missing prefix falls back to the bitcoin-scale default so an
// unconfigured deployment still gets a ceiling rather than none.
// FEE_ESTIMATE_SANITY_CEILING overrides in COIN/kB for every chain; a
// non-positive or unparseable value keeps the coin default (fail-safe: this
// guard can be tightened or loosened by env, never silently disabled).
const DEFAULT_FEE_ESTIMATE_SANITY_CEILING = {
bitcoin: 0.01, // 0.01 BTC/kB
litecoin: 0.01, // 0.01 LTC/kB
dogecoin: 10, // 10 DOGE/kB (~1000x the 0.01 DOGE/kB documented recommended rate)
}

function feeEstimateSanityCeiling(){
const override = parseFloat(process.env.FEE_ESTIMATE_SANITY_CEILING)
if (Number.isFinite(override) && override > 0) return override
const coin = String(process.env.NETWORK || '').split('-')[0].toLowerCase()
return DEFAULT_FEE_ESTIMATE_SANITY_CEILING[coin] || DEFAULT_FEE_ESTIMATE_SANITY_CEILING.bitcoin
}

// Sanitize an axios error before it is logged or re-thrown. RPC calls pass
// auth:{username,password} to axios, which attaches the request config to the
// thrown error, so logging the raw error serializes the node RPC password into
Expand Down Expand Up @@ -446,7 +476,17 @@ class BlockchainConnector {
// sentinel (feerate:-1, not enough data) is treated as an error rather
// than silently returned.
if (responseData.result && Number(responseData.result.feerate) > 0) {
return responseData.result.feerate;
const feerate = Number(responseData.result.feerate);
const ceiling = feeEstimateSanityCeiling();
if (feerate > ceiling) {
// Loud diagnostic: this is a money-affecting path and a clamp here
// is silent everywhere else unless it is logged at error level.
console.error('estimatesmartfee returned ' + feerate + '/kB on a non-regtest ' +
'chain, above the ' + ceiling + '/kB sanity ceiling; clamping to the ceiling. ' +
'Set FEE_ESTIMATE_SANITY_CEILING to override.');
return ceiling;
}
return feerate;
}
// No estimate at all (feerate:-1). A public TESTNET routinely has too few
// transactions to estimate from: DOGE testnet answers -1 with an empty
Expand Down
Loading
Loading