From 0395571e6b6bf558af165c58126b021cae4ec762 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 12 Oct 2026 20:26:53 +0200 Subject: [PATCH 1/2] fix: enable max quotes in the usdt qa fixture --- README.md | 65 +++++++++++++++++++++++++++++++++++++++++ usdt-fixture | 3 +- usdt/cli.cjs | 1 + usdt/max-smoke.cjs | 50 +++++++++++++++++++++++++++++++ usdt/pins.json | 4 +-- usdt/server.cjs | 6 ++++ usdt/wallet/Cargo.lock | 2 +- usdt/wallet/src/main.rs | 4 +++ 8 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 usdt/max-smoke.cjs diff --git a/README.md b/README.md index 3bee08c..d370538 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Run the shared Arbitrum fork, Alto and gateway without spending mainnet funds: export USDT_TEST_ENV_FILE=/absolute/path/to/private.env ./usdt-fixture up ./usdt-fixture check +./usdt-fixture max-smoke ./usdt-fixture smoke ./usdt-fixture bridge-smoke eval "$(./usdt-fixture env)" @@ -33,6 +34,7 @@ SOURCE_TX DELIVERED` updates a real USDT0 source message. See [USDT fixture](doc payment controls, proof verification, the optional Shop overlay and simulation limits. + ## Services - **Bitcoin Core** (regtest): Bitcoin node for development @@ -966,6 +968,69 @@ curl -s http://localhost:23000/health | jq '.lnd.balance' - Default credentials are used - All services are exposed on localhost only +## MAX quotes + +The fixture pins Core `0.8.0-rc4` and the gateway policy that allows only the +sender's USDT balance-slot override during gas estimation. `max-smoke` funds a +disposable wallet and requires an affordable MAX quote without submission or +changing its real balance. Use this `usdt` gateway for MAX; the older +`arbitrum-provider` fixture does not supply the required metadata and policy. +For a credential-free archive upstream, apply `bitkit-docker#27` to the same +fixture checkout before building; it supplies the existing public RPC option. + +The test run owns app builds, device installation and mobile deadline/MAX checks. + +For a seat whose service host cannot fetch private source, transfer the verified +images from a build host. Set `USDT_IMAGE_TAG` to the fixture build's reported +fingerprint, `FIXTURE_SOURCE_BOX` to that build host and `SERVICES_BOX` to the +seat's services host. This transfers images only: + +```sh +ssh "$FIXTURE_SOURCE_BOX" docker image save \ + "bitkit-docker/usdt-fixture:$USDT_IMAGE_TAG" \ + "bitkit-docker/usdt-gateway:$USDT_IMAGE_TAG" | ssh "$SERVICES_BOX" docker image load +``` + +The seat's own fixture checkout must contain this fix and `bitkit-docker#27`. +Use its existing Compose project, and set `SEAT_PORT_OFFSET` to the seat's +published Bitcoin RPC port minus `43782`. In that checkout on the services host: + +```sh +export USDT_PROJECT="$COMPOSE_PROJECT_NAME" +export USDT_GATEWAY_PORT=$((23453 + SEAT_PORT_OFFSET)) +export USDT_CONTROL_PORT=$((23454 + SEAT_PORT_OFFSET)) +./usdt-fixture up +./usdt-fixture check +./usdt-fixture max-smoke +./usdt-fixture status +curl -fsS "http://127.0.0.1:$USDT_GATEWAY_PORT/healthz" +``` + +The launcher computes the image fingerprint from the tracked fixture inputs; +require it to match the transferred tag. Require gateway `status=ok`, chain +`42161`, provider mode `healthy`, and an affordable MAX quote with +`submitted=false`. The smoke checks that an estimation override and final +paymaster authorization occurred, with unchanged real balances and no new +submission. Keep this chain alive throughout the app checks. + +`./usdt-fixture env` prints all four app endpoint overrides. Forward the gateway +and control ports through the test run's existing relays. For Android on the +services host, use its own seat-specific adb server and serial: + +```sh +adb -P "$ANDROID_ADB_SERVER_PORT" -s "$SERIAL" reverse tcp:23453 tcp:"$USDT_GATEWAY_PORT" +adb -P "$ANDROID_ADB_SERVER_PORT" -s "$SERIAL" reverse tcp:23454 tcp:"$USDT_CONTROL_PORT" +``` + +That Android build uses gateway `http://127.0.0.1:23453/v1/usdt`; iOS uses the +run's forwarded loopback gateway. Keep staging Bitcoin, Pubky and VSS settings. +Fund the app's displayed disposable Arbitrum account with +`./usdt-fixture fund ADDRESS 20`. Android Core `com.synonym:bitkit-core-android:0.8.0-rc4` +is published; build the run's rc4-pinned companion with `--refresh-dependencies` +and its existing Maven authentication. No local-AAR substitute is needed. +The test run owns app installation, MAX review and deadline-expiry proof on its +lane's devices. No device is launched on the coordinator. + ## Production Considerations Do not use for production. Bitkit Dev server is vibe-coded and not optimised. diff --git a/usdt-fixture b/usdt-fixture index ef8d2eb..45f99ff 100755 --- a/usdt-fixture +++ b/usdt-fixture @@ -75,7 +75,7 @@ ENV wallet) compose run --rm --no-deps -e USDT_WALLET_NAME="${USDT_WALLET_NAME:-sender}" usdt-tools wallet "$@" ;; - smoke|bridge-smoke|status|balance|fund|mine|reset|provider-mode|bundling|bundle|orchestra|layerzero|layerzero-mode) + smoke|max-smoke|bridge-smoke|status|balance|fund|mine|reset|provider-mode|bundling|bundle|orchestra|layerzero|layerzero-mode) compose run --rm --no-deps usdt-tools "$command" "$@" ;; help|-h|--help) @@ -87,6 +87,7 @@ Usage: ./usdt-fixture down Remove this project's containers, disposable wallets and fork cache env Print app/helper endpoint exports check Run fixture lint and protocol tests inside Docker (stack must be up) + max-smoke Check affordable MAX quotes without submission smoke Check real Core payments, fees, pending recovery and proofs bridge-smoke Check local bridge funding, receipts and refunds wallet ARGS Run the Core test wallet; read proof/binding files from /evidence diff --git a/usdt/cli.cjs b/usdt/cli.cjs index 3c45da8..9a02a1b 100644 --- a/usdt/cli.cjs +++ b/usdt/cli.cjs @@ -4,6 +4,7 @@ const { rpc } = require('./rpc.cjs'); const { state, urls } = require('./config.cjs'); async function main() { const [command = 'help', ...args] = process.argv.slice(2); + if (command === 'max-smoke') return require('./max-smoke.cjs').smoke(); if (command === 'smoke') return require('./smoke.cjs').smoke(); if (command === 'bridge-smoke') return require('./bridge-smoke.cjs').smoke(); if (command === 'wallet') { diff --git a/usdt/max-smoke.cjs b/usdt/max-smoke.cjs new file mode 100644 index 0000000..4c4b54b --- /dev/null +++ b/usdt/max-smoke.cjs @@ -0,0 +1,50 @@ +const assert = require('node:assert/strict'); +const { execFile } = require('node:child_process'); +const { promisify } = require('node:util'); +const { randomUUID } = require('node:crypto'); +const { mkdtempSync, rmSync } = require('node:fs'); +const path = require('node:path'); +const { rpc } = require('./rpc.cjs'); +const { state, urls } = require('./config.cjs'); +const exec = promisify(execFile); + +async function smoke() { + const directory = mkdtempSync(path.join(state, 'max-smoke-')); + const runId = randomUUID(); + const wallet = async (name, ...args) => { + const { stdout } = await exec('usdt-fixture-wallet', args, { + env: { + ...process.env, + USDT_WALLET_DIR: directory, + USDT_WALLET_NAME: `${runId}-${name}`, + }, + timeout: 120000, + }); + return JSON.parse(stdout); + }; + try { + assert.equal((await rpc(urls.control, 'status')).mode, 'healthy'); + const sender = (await wallet('sender', 'address')).address; + const recipient = (await wallet('recipient', 'address')).address; + await rpc(urls.control, 'fund', [sender, '20']); + const before = BigInt(await rpc(urls.control, 'balance', [sender])); + const callsBefore = (await rpc(urls.control, 'status')).max; + const quote = await wallet('sender', 'quote-max', recipient); + const callsAfter = (await rpc(urls.control, 'status')).max; + assert(callsAfter.estimatesWithOverride > callsBefore.estimatesWithOverride); + assert(callsAfter.paymasterAuthorizations > callsBefore.paymasterAuthorizations); + assert.equal(callsAfter.submissions, callsBefore.submissions); + assert(quote.amount > 0 && quote.maximum_fee > 0); + assert(BigInt(quote.amount) + BigInt(quote.maximum_fee) <= before); + assert.equal(BigInt(await rpc(urls.control, 'balance', [sender])), before); + assert.equal(BigInt(await rpc(urls.control, 'balance', [recipient])), 0n); + assert.equal(BigInt(await rpc(urls.node, 'eth_getBalance', [sender, 'latest'])), 0n); + assert.equal(await rpc(urls.node, 'eth_getCode', [sender, 'latest']), '0x'); + console.log( + JSON.stringify({ passed: true, balance: before.toString(), quote, submitted: false }), + ); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +} +module.exports = { smoke }; diff --git a/usdt/pins.json b/usdt/pins.json index bb1f614..c6a5b4e 100644 --- a/usdt/pins.json +++ b/usdt/pins.json @@ -5,6 +5,6 @@ "linux_arm64": "5bdce3dade8b6f0a65d4fccf6908426aee4905a988b08dce79d0a72fe7498310", "linux_amd64": "6c66ffcc55fa4249197721baa3098bc208014ea1d8aa04b2ed50ac6bccffb226" }, - "service": "3d8c0b625b02f0731ff0a19910752961ada3e6f2", - "core": "bdce512c9d1025ba6cd71baa8c2df1a39dcb3d02" + "service": "ada7f6a55e7f2478ebcc8404153c06f6433d9e9f", + "core": "1ebbd776d83acd1748cd9b32375d5aa420ebb4aa" } diff --git a/usdt/server.cjs b/usdt/server.cjs index 31ca68c..08d5d8b 100644 --- a/usdt/server.cjs +++ b/usdt/server.cjs @@ -62,6 +62,7 @@ async function run() { // Shop's verifier can read this fork over the private Compose network. servers.push(await serve(ports.reader, upstreamReader(urls.node), '0.0.0.0')); let mode = 'healthy'; + const max = { estimatesWithOverride: 0, paymasterAuthorizations: 0, submissions: 0 }; servers.push( await serve(ports.provider, async (method, params) => { if (mode === 'unavailable') @@ -79,8 +80,12 @@ async function run() { ], }; if (method === 'pm_getPaymasterStubData' || method === 'pm_getPaymasterData') { + if (method === 'pm_getPaymasterData') max.paymasterAuthorizations++; return paymasterData(urls.node, params, mode); } + if (method === 'eth_estimateUserOperationGas' && params[2] !== undefined) + max.estimatesWithOverride++; + if (method === 'eth_sendUserOperation') max.submissions++; return rpc(urls.alto, method, params); }), ); @@ -99,6 +104,7 @@ async function run() { chainId: 42161, forkBlock: pins.forkBlock, mode, + max: { ...max }, urls: { ...urls, gateway: `http://127.0.0.1:${process.env.USDT_GATEWAY_PORT ?? ports.gateway}`, diff --git a/usdt/wallet/Cargo.lock b/usdt/wallet/Cargo.lock index ea8813b..8b197df 100644 --- a/usdt/wallet/Cargo.lock +++ b/usdt/wallet/Cargo.lock @@ -974,7 +974,7 @@ dependencies = [ [[package]] name = "bitkitcore" -version = "0.8.0-rc2" +version = "0.8.0-rc4" dependencies = [ "alloy-primitives", "alloy-rlp", diff --git a/usdt/wallet/src/main.rs b/usdt/wallet/src/main.rs index a361aaf..16cf72f 100644 --- a/usdt/wallet/src/main.rs +++ b/usdt/wallet/src/main.rs @@ -61,6 +61,10 @@ async fn main() -> Result<()> { let result = match command { "address" => json!({"address":address}), "balance" => json!({"address":address,"balance":wallet.balance().await?}), + "quote-max" => { + let recipient = args.get(1).context("quote-max requires recipient")?; + json!(wallet.quote_max_transfer(recipient.clone(), UsdtDestination::Arbitrum).await?) + } "quote" | "send" => { let recipient = args.get(1).context("send requires recipient and atomic amount")?; let amount: u64 = args.get(2).context("send requires atomic amount")?.parse()?; From 0d7508c1d527abc4caff45dca23f54e55378bf0e Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Mon, 12 Oct 2026 22:25:02 +0200 Subject: [PATCH 2/2] fix: max-smoke asserts exact call counts and the full-balance quote --- docs/usdt-fixture.md | 3 +++ usdt/max-smoke.cjs | 22 +++++++++++++++++----- usdt/wallet/src/main.rs | 2 +- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/docs/usdt-fixture.md b/docs/usdt-fixture.md index a80045e..f6a9233 100644 --- a/docs/usdt-fixture.md +++ b/docs/usdt-fixture.md @@ -23,6 +23,7 @@ export USDT_TEST_ENV_FILE=/absolute/path/to/private.env ./usdt-fixture ps ./usdt-fixture check ./usdt-fixture smoke +./usdt-fixture max-smoke ./usdt-fixture bridge-smoke ./usdt-fixture fund 0xYOUR_TEST_WALLET 10 ./usdt-fixture balance 0xYOUR_TEST_WALLET @@ -43,6 +44,7 @@ eval "$(./usdt-fixture env)" | `wallet address` | Address of the disposable Core wallet | | `wallet send ADDRESS ATOMIC_AMOUNT [Destination]` | Quote and send through Core; optional destination uses `Polygon`, `Bsc`, etc. | | `wallet quote ADDRESS ATOMIC_AMOUNT [Destination]` | Inspect a quote without paying | +| `wallet quote-max ADDRESS` | Inspect a MAX quote (the balance less Core's fee reserve) without paying | | `wallet refresh ID` | Reconcile pending execution, including older receipts; Core may retry the identical signed operation | | `wallet sync`, then `wallet history` | Scan local payment history; repeat `sync` until `complete` is true | | `wallet proof ID BINDING_FILE` | Create the standard proof after execution | @@ -154,5 +156,6 @@ The overlay is infrastructure wiring; a paid USDT Shop journey and browser unloc ./usdt-fixture up ./usdt-fixture check ./usdt-fixture smoke +./usdt-fixture max-smoke ./usdt-fixture bridge-smoke ``` diff --git a/usdt/max-smoke.cjs b/usdt/max-smoke.cjs index 4c4b54b..679efa3 100644 --- a/usdt/max-smoke.cjs +++ b/usdt/max-smoke.cjs @@ -31,17 +31,29 @@ async function smoke() { const callsBefore = (await rpc(urls.control, 'status')).max; const quote = await wallet('sender', 'quote-max', recipient); const callsAfter = (await rpc(urls.control, 'status')).max; - assert(callsAfter.estimatesWithOverride > callsBefore.estimatesWithOverride); - assert(callsAfter.paymasterAuthorizations > callsBefore.paymasterAuthorizations); - assert.equal(callsAfter.submissions, callsBefore.submissions); + const calls = Object.fromEntries( + Object.keys(callsAfter).map((key) => [key, callsAfter[key] - callsBefore[key]]), + ); + // Core's MAX quote takes at most three fee rounds, each one override estimate and one + // paymaster authorization; the counters are the provider's, so nothing else may quote meanwhile. + assert( + calls.estimatesWithOverride >= 1 && calls.estimatesWithOverride <= 3, + JSON.stringify(calls), + ); + assert( + calls.paymasterAuthorizations >= 1 && calls.paymasterAuthorizations <= 3, + JSON.stringify(calls), + ); + assert.equal(calls.submissions, 0, JSON.stringify(calls)); assert(quote.amount > 0 && quote.maximum_fee > 0); - assert(BigInt(quote.amount) + BigInt(quote.maximum_fee) <= before); + // MAX quotes the whole balance: the amount is the balance less the fee reserve. + assert.equal(BigInt(quote.amount) + BigInt(quote.maximum_fee), before); assert.equal(BigInt(await rpc(urls.control, 'balance', [sender])), before); assert.equal(BigInt(await rpc(urls.control, 'balance', [recipient])), 0n); assert.equal(BigInt(await rpc(urls.node, 'eth_getBalance', [sender, 'latest'])), 0n); assert.equal(await rpc(urls.node, 'eth_getCode', [sender, 'latest']), '0x'); console.log( - JSON.stringify({ passed: true, balance: before.toString(), quote, submitted: false }), + JSON.stringify({ passed: true, balance: before.toString(), quote, calls, submitted: false }), ); } finally { rmSync(directory, { recursive: true, force: true }); diff --git a/usdt/wallet/src/main.rs b/usdt/wallet/src/main.rs index 16cf72f..450b315 100644 --- a/usdt/wallet/src/main.rs +++ b/usdt/wallet/src/main.rs @@ -124,7 +124,7 @@ async fn main() -> Result<()> { }).await?; verified.map(|p| json!({"payment_id":p.payment_id,"transfer_id":p.transfer_id,"sender":p.sender,"recipient":p.recipient,"amount":p.amount,"timestamp":p.timestamp})).unwrap_or(Value::Null) } - _ => bail!("use address, balance, quote/send RECIPIENT ATOMIC_AMOUNT [Destination], deposit, refresh ID, sync, history, proof ID BINDING_FILE, or verify BINDING_FILE PROOF_FILE"), + _ => bail!("use address, balance, quote/send RECIPIENT ATOMIC_AMOUNT [Destination], quote-max RECIPIENT, deposit, refresh ID, sync, history, proof ID BINDING_FILE, or verify BINDING_FILE PROOF_FILE"), }; println!("{}", serde_json::to_string_pretty(&result)?); Ok(())