feat(non-evm): operator binding config, planner hints, and validation
- Add config/non-evm-operator-binding.json (public hints only; no secrets). - Extend .env.master.example with XRPL/Tron/Solana/TRONGRID overrides. - Wire solana-gru-bridge-lineup refs; refresh non-evm lane stubs from binding. - Teach liquidity-gap planner to read binding; validate JSON in validate-config-files.sh. - Document handoff in CWUSDC_NON_MANUAL_PROVIDER_TASKS; cross-link GRU spec. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -93,6 +93,24 @@ else
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
if [[ -f "$PROJECT_ROOT/config/non-evm-operator-binding.json" ]]; then
|
||||
log_ok "Found: config/non-evm-operator-binding.json"
|
||||
if command -v jq &>/dev/null; then
|
||||
if jq -e '
|
||||
(.version | type == "string")
|
||||
and (.xrpl | type == "object")
|
||||
and (.tron | type == "object")
|
||||
and (.minimumFundingTargets | type == "object")
|
||||
' "$PROJECT_ROOT/config/non-evm-operator-binding.json" &>/dev/null; then
|
||||
log_ok "non-evm-operator-binding.json: structure valid"
|
||||
else
|
||||
log_err "non-evm-operator-binding.json: invalid structure"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
log_warn "Optional config/non-evm-operator-binding.json missing; skipping"
|
||||
fi
|
||||
if [[ -f "$PROJECT_ROOT/config/public-routing-coverage-matrix.json" ]]; then
|
||||
log_ok "Found: config/public-routing-coverage-matrix.json"
|
||||
if command -v jq &>/dev/null; then
|
||||
|
||||
@@ -13,6 +13,16 @@ ROOT = Path(__file__).resolve().parents[2]
|
||||
CONFIG_OUT = ROOT / "config/non-evm-lane-requirements.json"
|
||||
REPORT_JSON = ROOT / "reports/status/non-evm-lane-requirements-latest.json"
|
||||
REPORT_MD = ROOT / "reports/status/non-evm-lane-requirements-latest.md"
|
||||
OPERATOR_BINDING = ROOT / "config/non-evm-operator-binding.json"
|
||||
|
||||
|
||||
def load_operator_binding_hints() -> dict[str, Any]:
|
||||
if not OPERATOR_BINDING.exists():
|
||||
return {}
|
||||
try:
|
||||
return json.loads(OPERATOR_BINDING.read_text())
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return {}
|
||||
|
||||
LANES: list[dict[str, Any]] = [
|
||||
{
|
||||
@@ -91,11 +101,36 @@ def table(headers: list[str], rows: list[list[Any]]) -> str:
|
||||
|
||||
def main() -> int:
|
||||
generated_at = datetime.now(timezone.utc).isoformat()
|
||||
bind = load_operator_binding_hints()
|
||||
hints = bind.get("minimumFundingTargets") or {}
|
||||
|
||||
def min_tgt(network: str) -> str:
|
||||
m = hints.get(network) if hints else None
|
||||
if isinstance(m, dict) and m:
|
||||
parts = [f"{k}={v}" for k, v in m.items() if k != "note" and v]
|
||||
note = m.get("note")
|
||||
base = "; ".join(parts) if parts else "TBD"
|
||||
return f"{base} ({note})" if note else base
|
||||
return "TBD"
|
||||
|
||||
lanes_out = []
|
||||
for lane in LANES:
|
||||
row = dict(lane)
|
||||
net = row["network"]
|
||||
if net == "solana":
|
||||
row["minimumFundingTarget"] = min_tgt("solana")
|
||||
elif net == "tron":
|
||||
row["minimumFundingTarget"] = min_tgt("tron")
|
||||
elif net == "xrpl":
|
||||
row["minimumFundingTarget"] = min_tgt("xrpl")
|
||||
lanes_out.append(row)
|
||||
|
||||
payload = {
|
||||
"schema": "non-evm-lane-requirements/v1",
|
||||
"generatedAt": generated_at,
|
||||
"status": "stubs_bound_repo_side",
|
||||
"lanes": LANES,
|
||||
"lanes": lanes_out,
|
||||
"operatorBindingConfig": str(OPERATOR_BINDING.relative_to(ROOT)),
|
||||
"validationRule": "A lane is claimable only after canonicalWallet, asset IDs, native gas/reserve target, venue target, and evidence source are non-TBD.",
|
||||
}
|
||||
for path in (CONFIG_OUT, REPORT_JSON):
|
||||
@@ -120,7 +155,7 @@ def main() -> int:
|
||||
lane["minimumFundingTarget"],
|
||||
lane["claimBoundary"],
|
||||
]
|
||||
for lane in LANES
|
||||
for lane in lanes_out
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -18,6 +18,15 @@ const repoRoot = resolve(new URL("../..", import.meta.url).pathname);
|
||||
const readinessPath = resolve(repoRoot, "reports/status/token-aggregation-adoption-readiness-live-20260509.json");
|
||||
const nonEvmHealthPath = resolve(repoRoot, "reports/status/non-evm-network-health-latest.json");
|
||||
const nonEvmLaneStatusPath = resolve(repoRoot, "reports/status/non-evm-lane-status-latest.json");
|
||||
const operatorNonEvmBindingPath = resolve(repoRoot, "config/non-evm-operator-binding.json");
|
||||
|
||||
function readOperatorNonEvmBinding() {
|
||||
try {
|
||||
return JSON.parse(readFileSync(operatorNonEvmBindingPath, "utf8"));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
const jsonOut = resolve(repoRoot, "reports/status/token-aggregation-liquidity-gap-funding-plan-latest.json");
|
||||
const mdOut = resolve(repoRoot, "reports/status/token-aggregation-liquidity-gap-funding-plan-latest.md");
|
||||
const deployer = (process.env.DEPLOYER_ADDRESS || process.env.DEPLOYER || "0x4A666F96fC8764181194447A7dFdb7d471b301C8").trim();
|
||||
@@ -298,7 +307,9 @@ function solanaWalletFromConfig() {
|
||||
return { address: "", source: "missing" };
|
||||
}
|
||||
|
||||
function tronWalletFromConfig() {
|
||||
function tronWalletFromConfig(binding) {
|
||||
const override = binding?.tron?.addressOverride?.trim();
|
||||
if (override) return { address: override, source: "config_non_evm_operator_binding_tron_override" };
|
||||
const explicit = readEnvValue("TRON_DEPLOYER_ADDRESS", "TRON_WALLET_ADDRESS", "TRON_PUBLIC_ADDRESS", "TRON_ACCOUNT_ADDRESS");
|
||||
if (explicit) return { address: explicit, source: "env_tron_address" };
|
||||
const ethAddress = deployer.replace(/^0x/i, "");
|
||||
@@ -310,9 +321,26 @@ function tronWalletFromConfig() {
|
||||
return { address: "", source: "missing" };
|
||||
}
|
||||
|
||||
function xrplWalletFromConfig() {
|
||||
function xrplWalletFromConfig(binding) {
|
||||
const explicit = readEnvValue("XRPL_DEPLOYER_ADDRESS", "XRP_DEPLOYER_ADDRESS", "XRPL_WALLET_ADDRESS", "XRP_WALLET_ADDRESS", "XRPL_ACCOUNT");
|
||||
return explicit ? { address: explicit, source: "env_xrpl_address" } : { address: "", source: "missing" };
|
||||
if (explicit?.trim()) return { address: explicit.trim(), source: "env_xrpl_address" };
|
||||
const cfg = binding?.xrpl?.canonicalAccount?.trim();
|
||||
if (cfg) return { address: cfg, source: "config_non_evm_operator_binding" };
|
||||
return { address: "", source: "missing" };
|
||||
}
|
||||
|
||||
function requiredFundingFor(network, binding) {
|
||||
const m = binding?.minimumFundingTargets;
|
||||
if (network === "Solana" && m?.solana?.minSolOperationalHint) {
|
||||
return `${m.solana.minSolOperationalHint} SOL (repo hint; operator confirms venue targets)`;
|
||||
}
|
||||
if (network === "Tron" && m?.tron?.minTrxOperationalHint) {
|
||||
return `${m.tron.minTrxOperationalHint} TRX (repo hint; operator confirms energy/inventory)`;
|
||||
}
|
||||
if (network === "XRPL" && m?.xrpl?.minXrpReserveHint) {
|
||||
return `${m.xrpl.minXrpReserveHint} XRP (repo reserve hint; align trustlines/issuer policy)`;
|
||||
}
|
||||
return "TBD";
|
||||
}
|
||||
|
||||
async function solanaNativeBalance(address) {
|
||||
@@ -501,15 +529,17 @@ async function buildNonEvmFundingRequirements() {
|
||||
const health = readJsonIfExists(nonEvmHealthPath, null);
|
||||
const laneStatus = readJsonIfExists(nonEvmLaneStatusPath, null);
|
||||
const lanes = laneStatus?.lanes ?? {};
|
||||
const binding = readOperatorNonEvmBinding();
|
||||
const solanaWallet = solanaWalletFromConfig();
|
||||
const tronWallet = tronWalletFromConfig();
|
||||
const xrplWallet = xrplWalletFromConfig();
|
||||
const tronWallet = tronWalletFromConfig(binding);
|
||||
const xrplWallet = xrplWalletFromConfig(binding);
|
||||
const tronCanon = binding?.tron?.canonicalDeployerConfirmed === true;
|
||||
const [solanaBalance, tronBalance, xrplBalance] = await Promise.all([
|
||||
solanaNativeBalance(solanaWallet.address),
|
||||
tronNativeBalance(tronWallet.address),
|
||||
xrplNativeBalance(xrplWallet.address),
|
||||
]);
|
||||
return [
|
||||
const requirements = [
|
||||
{
|
||||
network: "Solana",
|
||||
target: "mainnet-beta",
|
||||
@@ -520,7 +550,7 @@ async function buildNonEvmFundingRequirements() {
|
||||
currentBalanceRaw: solanaBalance.raw,
|
||||
nativeGasAsset: "SOL",
|
||||
bridgeOrWrappedAsset: lanes.solana?.destinationAsset?.symbol ?? "cWAUSDT",
|
||||
requiredFunding: "TBD",
|
||||
requiredFunding: requiredFundingFor("Solana", binding),
|
||||
status: solanaWallet.address ? "spl_mint_inventory_and_minimum_funding_targets_required" : "wallet_and_spl_mint_inventory_required",
|
||||
networkHealth: networkHealth(health, "Solana"),
|
||||
requirements: [
|
||||
@@ -540,11 +570,20 @@ async function buildNonEvmFundingRequirements() {
|
||||
currentBalanceRaw: tronBalance.raw,
|
||||
nativeGasAsset: "TRX",
|
||||
bridgeOrWrappedAsset: "TronAdapter relay inventory",
|
||||
requiredFunding: "TBD",
|
||||
status: tronWallet.source === "derived_from_evm_deployer_address" ? "derived_tron_wallet_needs_operator_confirmation_and_asset_inventory" : "native_tron_wallet_and_asset_inventory_required",
|
||||
requiredFunding: requiredFundingFor("Tron", binding),
|
||||
status:
|
||||
!tronWallet.address
|
||||
? "native_tron_wallet_and_asset_inventory_required"
|
||||
: tronWallet.source === "derived_from_evm_deployer_address" && !tronCanon
|
||||
? "derived_tron_wallet_needs_operator_confirmation_and_asset_inventory"
|
||||
: "native_tron_wallet_and_asset_inventory_required",
|
||||
networkHealth: networkHealth(health, "Tron"),
|
||||
requirements: [
|
||||
tronWallet.source === "derived_from_evm_deployer_address" ? "Confirm whether the EVM deployer-derived Tron address is the canonical native Tron deployer." : "Bind canonical Tron custody wallet address.",
|
||||
tronCanon
|
||||
? "Operator confirmed EVM-derived Tron deployer as canonical (config/non-evm-operator-binding.json tron.canonicalDeployerConfirmed)."
|
||||
: tronWallet.source === "derived_from_evm_deployer_address"
|
||||
? "Confirm whether the EVM deployer-derived Tron address is the canonical native Tron deployer (set tron.canonicalDeployerConfirmed when true)."
|
||||
: "Bind canonical Tron custody wallet address.",
|
||||
"Check TRX energy/bandwidth funding and any native TRC-20 inventory needed for relay settlement.",
|
||||
"Promote or document native Tron-side contracts/assets before treating Tron as liquidity-ready.",
|
||||
"Close Chain 138 TronAdapter source/publication evidence separately from native Tron funding.",
|
||||
@@ -560,15 +599,18 @@ async function buildNonEvmFundingRequirements() {
|
||||
currentBalanceRaw: xrplBalance.raw,
|
||||
nativeGasAsset: "XRP",
|
||||
bridgeOrWrappedAsset: lanes.xrpl?.wrappedAsset?.address ? `wXRP ${lanes.xrpl.wrappedAsset.address}` : "wXRP",
|
||||
requiredFunding: "TBD",
|
||||
requiredFunding: requiredFundingFor("XRPL", binding),
|
||||
status: xrplWallet.address ? "xrpl_reserve_trustline_and_bridge_inventory_required" : "xrpl_wallet_reserve_and_bridge_inventory_required",
|
||||
networkHealth: networkHealth(health, "XRPL"),
|
||||
requirements: [
|
||||
xrplWallet.address ? "Canonical XRPL account is bound for native XRP checks." : "Bind canonical XRPL account and optional destination tag policy.",
|
||||
binding?.xrpl?.destinationTagPolicy && binding.xrpl.destinationTagPolicy !== "none"
|
||||
? `Destination tag policy (from config): ${binding.xrpl.destinationTagPolicy}.`
|
||||
: null,
|
||||
"Check XRP reserve, transfer-fee cushion, and any trustline/issuer requirements.",
|
||||
"Check Chain 138 wXRP inventory and MintBurnController authorization readiness.",
|
||||
"Close Chain 138 XRPLAdapter/wXRP/MintBurnController source-publication evidence separately from XRPL funding.",
|
||||
],
|
||||
].filter(Boolean),
|
||||
},
|
||||
{
|
||||
network: "Other non-EVM majors",
|
||||
@@ -590,8 +632,17 @@ async function buildNonEvmFundingRequirements() {
|
||||
],
|
||||
},
|
||||
];
|
||||
return {
|
||||
requirements,
|
||||
bindingEcho: {
|
||||
configPath: "config/non-evm-operator-binding.json",
|
||||
xrplAccountBound: Boolean(xrplWallet.address),
|
||||
tronCanonicalDeployerConfirmed: tronCanon,
|
||||
tronWalletSource: tronWallet.source,
|
||||
solanaWalletSource: solanaWallet.source,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const readiness = JSON.parse(readFileSync(readinessPath, "utf8"));
|
||||
const details = readiness.blockerInventory?.liquidityMissingDetails ?? [];
|
||||
const rows = [];
|
||||
@@ -743,7 +794,8 @@ const counts = rows.reduce((acc, row) => {
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const nonEvmFundingRequirements = await buildNonEvmFundingRequirements();
|
||||
const nev = await buildNonEvmFundingRequirements();
|
||||
const nonEvmFundingRequirements = nev.requirements;
|
||||
const ethereumSourceInventory = await buildEthereumSourceInventory();
|
||||
|
||||
const payload = {
|
||||
@@ -778,6 +830,7 @@ const payload = {
|
||||
coffeeMoneyPlan,
|
||||
ethereumSourceInventory,
|
||||
nonEvmFundingRequirements,
|
||||
operatorNonEvmBindingEcho: nev.bindingEcho,
|
||||
};
|
||||
|
||||
const md = [
|
||||
@@ -896,7 +949,12 @@ const md = [
|
||||
"",
|
||||
"## Non-EVM Funding Requirements",
|
||||
"",
|
||||
"These networks are now part of funding scope. The planner resolves non-EVM deployer wallets where the repo can prove them, checks native gas balances where possible, and leaves funding amounts `TBD` until asset IDs and minimum venue targets are bound.",
|
||||
"These networks are now part of funding scope. The planner resolves non-EVM deployer wallets where the repo can prove them, checks native gas balances where possible, and applies `minimumFundingTargets` hints from `config/non-evm-operator-binding.json` (operator must still confirm live venue targets).",
|
||||
"",
|
||||
table(
|
||||
["Field", "Value"],
|
||||
Object.entries(payload.operatorNonEvmBindingEcho || {}).map(([k, v]) => [k, typeof v === "object" ? JSON.stringify(v) : String(v)]),
|
||||
),
|
||||
"",
|
||||
table(
|
||||
["Network", "Target", "Wallet", "Source", "Native gas", "Current balance", "Required funding", "Status"],
|
||||
|
||||
Reference in New Issue
Block a user