sol mainnet payouts tasks
This commit is contained in:
@@ -92,7 +92,7 @@ The gateway receives a client request, asks the tracker for an inference route (
|
|||||||
7. Begin accepting inference connections
|
7. Begin accepting inference connections
|
||||||
|
|
||||||
### Payment flow (ADR-0015 supersedes ADR-0002 settlement mechanics)
|
### Payment flow (ADR-0015 supersedes ADR-0002 settlement mechanics)
|
||||||
Clients pre-fund an API key with USDT. The tracker meters each request against the off-chain ledger. Periodic settlement batches USDT payouts from the custodial treasury to node operators proportional to work units. Fraud penalties forfeit pending balance (ADR-0018); strike/ban state persists in the tracker registry. TAI token emission remains deferred (ADR-0002 roadmap).
|
Clients pre-fund an API key with USDT. The tracker meters each request against the off-chain ledger. Periodic settlement batches USDT payouts from the custodial treasury to node operators proportional to work units (default: every 24 h or when pending ≥ 5 USDT). Fraud penalties forfeit pending balance (ADR-0018); strike/ban state persists in the tracker registry. TAI reward accrual is deferred — see ADR-0025 for reserved-mint / off-chain phase B/C; ADR-0002 roadmap for public listing.
|
||||||
|
|
||||||
### Fraud detection (ADR-0018; historical ADR-0003)
|
### Fraud detection (ADR-0018; historical ADR-0003)
|
||||||
Validators re-run ~5% of completed requests with TOPLOC activation verification. Caught cheaters forfeit pending balance and receive strikes; three strikes bans the wallet. Probation (first N unpaid jobs) remains the anti-sybil re-entry cost.
|
Validators re-run ~5% of completed requests with TOPLOC activation verification. Caught cheaters forfeit pending balance and receive strikes; three strikes bans the wallet. Probation (first N unpaid jobs) remains the anti-sybil re-entry cost.
|
||||||
|
|||||||
52
docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md
Normal file
52
docs/adr/0025-tai-off-chain-accrual-and-reserved-mint.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# ADR-0025: TAI reserved mint and off-chain accrual (phase B/C)
|
||||||
|
|
||||||
|
## Status: Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
ADR-0015 chose **USDT-direct custodial settlement** for alpha and near-term production. Clients pay USDT; nodes receive batched USDT SPL payouts. ADR-0002's TAI reward token, revenue-backed floor, and open-market listing gates remain the long-term design but are **not** the live payment path.
|
||||||
|
|
||||||
|
The owner wants TAI to exist without the cost and legal surface of a public launch: no AMM, no open listing, no client-facing TAI, no on-chain stake machinery.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Phase B — Reserved mainnet mint (cheap, optional early)
|
||||||
|
|
||||||
|
- Create a fixed-supply TAI SPL mint on **mainnet** when treasury work happens (~0.002 SOL).
|
||||||
|
- Entire initial supply sits in a **team-controlled** wallet (same custody posture as the USDT treasury today).
|
||||||
|
- **No public emission, no market, no client UX.** Mint exists for name reservation and future programmatic rewards only.
|
||||||
|
- Document mint address in operator config; do not advertise to users.
|
||||||
|
|
||||||
|
### Phase C — Off-chain TAI accrual alongside USDT (before automatic on-chain TAI payouts)
|
||||||
|
|
||||||
|
- Extend the billing ledger with **`tai_pending[wallet]`** accrued from completed inference work using a simple rule (e.g. USDT node share × configurable TAI-per-USDT rate, or fixed TAI per work unit).
|
||||||
|
- TAI accrual is **display-only + ledger-persisted** initially; nodes see pending TAI in dashboard/CLI.
|
||||||
|
- **Clients never pay or hold TAI.** USDT remains the only client-facing asset.
|
||||||
|
- Optional manual or scheduled **TAI SPL batch transfers** from the team wallet (same batching pattern as USDT `send_payouts`) — operator-triggered until automatic emission is justified by volume.
|
||||||
|
- The existing **10% protocol USDT cut** continues to accumulate as future TAI liquidity per ADR-0015/0002; do not redirect it until a deliberate liquidity event.
|
||||||
|
|
||||||
|
### Explicit non-goals (this ADR)
|
||||||
|
|
||||||
|
- Open-market listing, AMM, or DEX liquidity
|
||||||
|
- Buyback floor endpoint or backing-price oracle (ADR-0002 machinery)
|
||||||
|
- On-chain stake deposits or slash contracts
|
||||||
|
- Paying clients rebates or accepting TAI for inference
|
||||||
|
- Replacing USDT node payouts with TAI-only payouts before volume gates in ADR-0002 pass
|
||||||
|
|
||||||
|
## Relation to ADR-0002 listing gates
|
||||||
|
|
||||||
|
Public TAI listing stays gated on **$50k cumulative USDT volume** and **25+ nodes / 15+ wallets**. Phase B/C may proceed **below** those gates because they do not create a public market — only reserved supply and off-chain accounting.
|
||||||
|
|
||||||
|
Securities review remains required before any **public** distribution or listing; off-chain accrual to hired/known operators with manual SPL transfers is an operator discretion, not a product promise.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- USDT mainnet pilot (two-wallet setup) is unblocked without TAI complexity.
|
||||||
|
- TAI narrative is preserved at minimal cost (mint + ledger column + optional manual transfers).
|
||||||
|
- Automatic TAI emission can later reuse the US-033 settlement loop shape with a second mint and separate pending bucket.
|
||||||
|
- Dashboard and APIs must label TAI balances as **non-withdrawable** until an on-chain payout batch confirms.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- USDT settlement tests remain authoritative for production payouts (`tests/test_settlement_loop.py`).
|
||||||
|
- When phase C lands: ledger tests for `tai_pending` accrual, idempotent gossip replication, and optional TAI batch payout adapter tests mirroring USDT.
|
||||||
101
docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md
Normal file
101
docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
Status: ready-for-agent
|
||||||
|
|
||||||
|
# US-049 — Mainnet USDT cutover: two-wallet pilot checklist
|
||||||
|
|
||||||
|
Priority: High (first real-money friends test)
|
||||||
|
Stage: Operator runbook + config verification
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Move from **Solana devnet + mock-USDT** to **Solana mainnet + real USDT** for a minimal pilot: **one client wallet** (inference payer) and **one node-operator wallet** (payout recipient). Treasury holds USDT and pays SOL fees. TAI stays phase B/C per [ADR-0025](../adr/0025-tai-off-chain-accrual-and-reserved-mint.md).
|
||||||
|
|
||||||
|
## Wallet roles
|
||||||
|
|
||||||
|
| Role | Keypair | On-chain use |
|
||||||
|
|---|---|---|
|
||||||
|
| **Treasury** | Operator `treasury-keypair.json` (multisig when ready) | Holds USDT float + SOL for fees; sends batched node payouts |
|
||||||
|
| **Client** | Your inference-user wallet | SPL USDT → treasury; bound to API key for ledger credit |
|
||||||
|
| **Node** | Your node-operator wallet | Receives USDT payout batches from treasury |
|
||||||
|
|
||||||
|
The node process already creates/loads a Solana wallet at startup; the client wallet is bound via accounts/dashboard (`POST /v1/wallet/register` or US-041 flows).
|
||||||
|
|
||||||
|
## Pre-flight (devnet smoke — do not skip)
|
||||||
|
|
||||||
|
- [ ] Tracker with `--solana-rpc-url https://api.devnet.solana.com`, mock mint, treasury keypair
|
||||||
|
- [ ] `--settle-period 60 --payout-threshold 0` — confirm payout appears on dashboard **Settlement history** with explorer link
|
||||||
|
- [ ] Run `python -m pytest tests/test_settlement_loop.py -q` — includes prod 24h/5 USDT gate tests
|
||||||
|
- [ ] One inference request → node pending → settlement tx → node wallet balance increases
|
||||||
|
|
||||||
|
## Mainnet config change (config-only cutover)
|
||||||
|
|
||||||
|
Replace devnet values; **no code deploy required** beyond what is already on the branch.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example — use your mainnet RPC provider
|
||||||
|
meshnet-tracker start \
|
||||||
|
--solana-rpc-url https://api.mainnet-beta.solana.com \
|
||||||
|
--usdt-mint EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v \
|
||||||
|
--treasury-keypair ~/.config/solana/meshnet-treasury-mainnet.json \
|
||||||
|
--settle-period 86400 \
|
||||||
|
--payout-threshold 5.0 \
|
||||||
|
--payout-dust-floor 0.01 \
|
||||||
|
--starting-credit 0 \
|
||||||
|
--devnet-topup 0
|
||||||
|
```
|
||||||
|
|
||||||
|
| Flag | Devnet (test) | Mainnet (pilot) |
|
||||||
|
|---|---|---|
|
||||||
|
| RPC | `api.devnet.solana.com` | Mainnet RPC (Helius/QuickNode/etc.) |
|
||||||
|
| `--usdt-mint` | mock mint from `devnet_setup.py` | Real USDT mint (`EPjF…` on Solana) |
|
||||||
|
| `--settle-period` | `60` (fast verify) | `86400` (24 h) |
|
||||||
|
| `--payout-threshold` | `0` | `5.0` USDT |
|
||||||
|
| `--starting-credit` | `1.0` (optional) | `0` |
|
||||||
|
| `--devnet-topup` | `1.0` | `0` |
|
||||||
|
|
||||||
|
## Treasury funding
|
||||||
|
|
||||||
|
- [ ] Fund treasury wallet with **SOL** for fees (~0.1–0.5 SOL to start; ~$0.001 per daily batch + ~$0.30 once per new node ATA)
|
||||||
|
- [ ] Fund treasury with **USDT** for node payouts (your float — e.g. first week of expected node earnings)
|
||||||
|
- [ ] Client wallet holds USDT; send a test SPL transfer to treasury ATA; confirm deposit watcher credits API key within one poll interval
|
||||||
|
|
||||||
|
## Two-wallet pilot steps
|
||||||
|
|
||||||
|
1. **Start tracker** on mainnet config above (single settlement tracker per ADR-0016).
|
||||||
|
2. **Client path:** register account → create API key → bind **client wallet** → deposit USDT to treasury → verify ledger balance on dashboard.
|
||||||
|
3. **Node path:** start `meshnet-node` with **node wallet** keypair → register → serve inference.
|
||||||
|
4. **Inference:** client sends `POST /v1/chat/completions` with API key; verify 402 before deposit, success after.
|
||||||
|
5. **Accrual:** confirm node **pending USDT** on dashboard rises; client balance debits.
|
||||||
|
6. **Payout (24 h):** wait for `--settle-period` **or** temporarily lower to `300` for first pilot verification, then restore `86400`.
|
||||||
|
7. **Threshold path:** alternatively, accumulate ≥ `5` USDT pending in one session to trigger immediate batch without waiting 24 h.
|
||||||
|
8. **Verify on-chain:** settlement history shows mainnet tx signature; node wallet USDT ATA balance increased; pending zeroed.
|
||||||
|
|
||||||
|
## Safety checks
|
||||||
|
|
||||||
|
- [ ] `--devnet-topup 0` — no faucet on mainnet
|
||||||
|
- [ ] `--starting-credit 0` — no free inference credit
|
||||||
|
- [ ] Treasury keypair not committed to git; file mode 600
|
||||||
|
- [ ] Plan multisig migration before large float ([alpha runbook](../../.scratch/alpha-hardening/runbooks/02-treasury-key-rotation.md) intent)
|
||||||
|
- [ ] Issue **21** (TOPLOC calibration) before production audit thresholds on untrusted nodes
|
||||||
|
|
||||||
|
## Cost estimate (this pilot)
|
||||||
|
|
||||||
|
- **SOL fees:** pennies per day at 1 batch × 1 node
|
||||||
|
- **USDT:** whatever clients deposit and nodes earn (treasury is passthrough for payouts)
|
||||||
|
- **TAI mint (optional, ADR-0025 phase B):** ~0.002 SOL one-time — defer if not needed for pilot
|
||||||
|
|
||||||
|
## Acceptance criteria
|
||||||
|
|
||||||
|
- [ ] Devnet checklist completed once
|
||||||
|
- [ ] Mainnet tracker serves dashboard; billing enabled
|
||||||
|
- [ ] Client wallet deposit → ledger credit → inference → debit
|
||||||
|
- [ ] Node wallet receives ≥1 confirmed USDT payout batch on mainnet
|
||||||
|
- [ ] 24 h period enforced: sub-threshold pending not paid before period (covered by `tests/test_settlement_loop.py`)
|
||||||
|
- [ ] ≥5 USDT pending triggers payout without waiting full period (covered by tests)
|
||||||
|
- [ ] Rollback documented: switch RPC + mint back to devnet if needed
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- ADR-0015 (USDT custodial settlement)
|
||||||
|
- ADR-0025 (TAI reserved mint / off-chain accrual — not blocking this pilot)
|
||||||
|
- US-033 / US-032 (settlement + deposits)
|
||||||
|
- `scripts/devnet_setup.py` (devnet only)
|
||||||
@@ -1051,10 +1051,31 @@
|
|||||||
"US-046"
|
"US-046"
|
||||||
],
|
],
|
||||||
"completionNotes": "Skip auto-join when model explicit; sequential source try with progress; 30 s model-source timeout; assignment_vram_mb for CPU; per-file /v1/model-files/download with manifest and retries. Remaining: live Windows two-machine retest."
|
"completionNotes": "Skip auto-join when model explicit; sequential source try with progress; 30 s model-source timeout; assignment_vram_mb for CPU; per-file /v1/model-files/download with manifest and retries. Remaining: live Windows two-machine retest."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "US-049",
|
||||||
|
"title": "49 — Mainnet USDT cutover: two-wallet pilot checklist",
|
||||||
|
"description": "Operator runbook to move from Solana devnet + mock-USDT to mainnet + real USDT for a minimal pilot: one client wallet (deposits USDT, pays for inference) and one node wallet (receives batched payouts). Treasury holds USDT float and SOL for fees. TAI deferred per ADR-0025.",
|
||||||
|
"acceptanceCriteria": [
|
||||||
|
"Devnet smoke completed: settlement loop pays with --settle-period 60 and mock mint",
|
||||||
|
"python -m pytest tests/test_settlement_loop.py -q passes (includes 24 h / 5 USDT gate tests)",
|
||||||
|
"Mainnet tracker configured: real USDT mint, --starting-credit 0, --devnet-topup 0, --settle-period 86400",
|
||||||
|
"Client wallet deposit credits API key ledger; inference debits balance",
|
||||||
|
"Node wallet receives at least one confirmed mainnet USDT payout batch",
|
||||||
|
"Sub-threshold pending not paid before 24 h; ≥5 USDT pending triggers immediate payout"
|
||||||
|
],
|
||||||
|
"priority": 49,
|
||||||
|
"status": "open",
|
||||||
|
"notes": "Source issue: docs/issues/49-mainnet-usdt-cutover-two-wallet-pilot.md. ADR-0025 covers optional TAI mint; not blocking this pilot.",
|
||||||
|
"dependsOn": [
|
||||||
|
"US-032",
|
||||||
|
"US-033",
|
||||||
|
"US-039"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"updatedAt": "2026-07-13T16:15:00.000Z",
|
"updatedAt": "2026-07-13T16:40:00.000Z",
|
||||||
"statusVocabulary": {
|
"statusVocabulary": {
|
||||||
"open": "Not started",
|
"open": "Not started",
|
||||||
"in-design": "Decisions pending before implementation can begin",
|
"in-design": "Decisions pending before implementation can begin",
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import uuid
|
|||||||
DEFAULT_PRICE_PER_1K_TOKENS = 0.02 # USDT
|
DEFAULT_PRICE_PER_1K_TOKENS = 0.02 # USDT
|
||||||
DEFAULT_STARTING_CREDIT = 0.0 # USDT; accounts must be funded before inference
|
DEFAULT_STARTING_CREDIT = 0.0 # USDT; accounts must be funded before inference
|
||||||
DEFAULT_BILLING_DB_PATH = "billing.sqlite"
|
DEFAULT_BILLING_DB_PATH = "billing.sqlite"
|
||||||
|
DEFAULT_SETTLE_PERIOD = 86400.0 # seconds — max time between node payouts (24 h prod)
|
||||||
|
DEFAULT_PAYOUT_THRESHOLD = 5.0 # USDT — immediate payout when pending exceeds this
|
||||||
|
DEFAULT_PAYOUT_DUST_FLOOR = 0.01 # USDT — never pay less than this
|
||||||
NODE_REVENUE_SHARE = 0.90 # nodes 90% / protocol cut 10% (ADR-0015)
|
NODE_REVENUE_SHARE = 0.90 # nodes 90% / protocol cut 10% (ADR-0015)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .accounts import DEFAULT_ACCOUNTS_DB_PATH
|
from .accounts import DEFAULT_ACCOUNTS_DB_PATH
|
||||||
from .billing import DEFAULT_BILLING_DB_PATH
|
from .billing import (
|
||||||
|
DEFAULT_BILLING_DB_PATH,
|
||||||
|
DEFAULT_PAYOUT_DUST_FLOOR,
|
||||||
|
DEFAULT_PAYOUT_THRESHOLD,
|
||||||
|
DEFAULT_SETTLE_PERIOD,
|
||||||
|
)
|
||||||
from .capability import ALL_POLICIES as ALL_CAPABILITY_POLICIES
|
from .capability import ALL_POLICIES as ALL_CAPABILITY_POLICIES
|
||||||
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH
|
from .hf_pricing import DEFAULT_HF_PRICING_LOG_DB_PATH
|
||||||
from .logging_setup import (
|
from .logging_setup import (
|
||||||
@@ -237,19 +242,19 @@ def main() -> None:
|
|||||||
common.add_argument(
|
common.add_argument(
|
||||||
"--settle-period",
|
"--settle-period",
|
||||||
type=float,
|
type=float,
|
||||||
default=86400.0,
|
default=DEFAULT_SETTLE_PERIOD,
|
||||||
help="Max seconds between payouts to a node (dev: 60, prod: 86400)",
|
help="Max seconds between payouts to a node (dev: 60, prod: 86400)",
|
||||||
)
|
)
|
||||||
common.add_argument(
|
common.add_argument(
|
||||||
"--payout-threshold",
|
"--payout-threshold",
|
||||||
type=float,
|
type=float,
|
||||||
default=5.0,
|
default=DEFAULT_PAYOUT_THRESHOLD,
|
||||||
help="Pending USDT that triggers an immediate payout (dev: 0)",
|
help="Pending USDT that triggers an immediate payout (dev: 0)",
|
||||||
)
|
)
|
||||||
common.add_argument(
|
common.add_argument(
|
||||||
"--payout-dust-floor",
|
"--payout-dust-floor",
|
||||||
type=float,
|
type=float,
|
||||||
default=0.01,
|
default=DEFAULT_PAYOUT_DUST_FLOOR,
|
||||||
help="Never pay out less than this many USDT",
|
help="Never pay out less than this many USDT",
|
||||||
)
|
)
|
||||||
common.add_argument(
|
common.add_argument(
|
||||||
|
|||||||
@@ -12,7 +12,12 @@ import urllib.request
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from meshnet_contracts import LocalSolanaContracts
|
from meshnet_contracts import LocalSolanaContracts
|
||||||
from meshnet_tracker.billing import BillingLedger
|
from meshnet_tracker.billing import (
|
||||||
|
BillingLedger,
|
||||||
|
DEFAULT_PAYOUT_DUST_FLOOR,
|
||||||
|
DEFAULT_PAYOUT_THRESHOLD,
|
||||||
|
DEFAULT_SETTLE_PERIOD,
|
||||||
|
)
|
||||||
from meshnet_tracker.server import TrackerServer
|
from meshnet_tracker.server import TrackerServer
|
||||||
|
|
||||||
MODEL = "stub-model"
|
MODEL = "stub-model"
|
||||||
@@ -172,3 +177,115 @@ def test_non_leader_never_signs():
|
|||||||
assert ledger.get_node_pending("wallet-a") > 0
|
assert ledger.get_node_pending("wallet-a") > 0
|
||||||
finally:
|
finally:
|
||||||
tracker.stop()
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
|
# Production defaults — sourced from billing.py (cli.py uses the same constants)
|
||||||
|
PROD_SETTLE_PERIOD = DEFAULT_SETTLE_PERIOD
|
||||||
|
PROD_PAYOUT_THRESHOLD = DEFAULT_PAYOUT_THRESHOLD
|
||||||
|
PROD_DUST_FLOOR = DEFAULT_PAYOUT_DUST_FLOOR
|
||||||
|
|
||||||
|
|
||||||
|
def _set_pending_since(ledger: BillingLedger, wallet: str, ts: float) -> None:
|
||||||
|
with ledger._lock:
|
||||||
|
ledger._pending_since[wallet] = ts
|
||||||
|
|
||||||
|
|
||||||
|
def test_prod_settlement_constants():
|
||||||
|
"Production settlement policy is 24 h period, 5 USDT threshold, 0.01 dust floor.\n\nTags: billing, settlement"
|
||||||
|
assert DEFAULT_SETTLE_PERIOD == 86400.0
|
||||||
|
assert DEFAULT_PAYOUT_THRESHOLD == 5.0
|
||||||
|
assert DEFAULT_PAYOUT_DUST_FLOOR == 0.01
|
||||||
|
|
||||||
|
|
||||||
|
def test_payables_24h_period_not_due_before_elapsed():
|
||||||
|
"Sub-threshold pending is not payable until the 24 h period elapses.\n\nTags: billing, settlement"
|
||||||
|
ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02)
|
||||||
|
ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)])
|
||||||
|
assert ledger.get_node_pending("node-wallet") == pytest.approx(0.018)
|
||||||
|
|
||||||
|
t0 = 1_700_000_000.0
|
||||||
|
_set_pending_since(ledger, "node-wallet", t0)
|
||||||
|
due_early = ledger.payables(
|
||||||
|
threshold=PROD_PAYOUT_THRESHOLD,
|
||||||
|
max_period=PROD_SETTLE_PERIOD,
|
||||||
|
dust_floor=PROD_DUST_FLOOR,
|
||||||
|
now=t0 + PROD_SETTLE_PERIOD - 1.0,
|
||||||
|
)
|
||||||
|
assert due_early == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_payables_24h_period_triggers_at_boundary():
|
||||||
|
"Sub-threshold pending becomes payable exactly after the 24 h period.\n\nTags: billing, settlement"
|
||||||
|
ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02)
|
||||||
|
ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)])
|
||||||
|
pending = ledger.get_node_pending("node-wallet")
|
||||||
|
|
||||||
|
t0 = 1_700_000_000.0
|
||||||
|
_set_pending_since(ledger, "node-wallet", t0)
|
||||||
|
due = ledger.payables(
|
||||||
|
threshold=PROD_PAYOUT_THRESHOLD,
|
||||||
|
max_period=PROD_SETTLE_PERIOD,
|
||||||
|
dust_floor=PROD_DUST_FLOOR,
|
||||||
|
now=t0 + PROD_SETTLE_PERIOD,
|
||||||
|
)
|
||||||
|
assert due == [("node-wallet", pytest.approx(pending))]
|
||||||
|
|
||||||
|
|
||||||
|
def test_payables_threshold_triggers_before_24h_period():
|
||||||
|
"Pending ≥ 5 USDT pays immediately without waiting the 24 h period.\n\nTags: billing, settlement"
|
||||||
|
ledger = BillingLedger(starting_credit=1000.0, default_price_per_1k=0.02)
|
||||||
|
# cost = 300k * 0.02 / 1k = 6.0 USDT → node share 5.4 USDT
|
||||||
|
ledger.charge_request("client", MODEL, 300_000, [("node-wallet", 12)])
|
||||||
|
assert ledger.get_node_pending("node-wallet") >= PROD_PAYOUT_THRESHOLD
|
||||||
|
|
||||||
|
due = ledger.payables(
|
||||||
|
threshold=PROD_PAYOUT_THRESHOLD,
|
||||||
|
max_period=PROD_SETTLE_PERIOD,
|
||||||
|
dust_floor=PROD_DUST_FLOOR,
|
||||||
|
now=time.time(),
|
||||||
|
)
|
||||||
|
assert len(due) == 1
|
||||||
|
assert due[0][0] == "node-wallet"
|
||||||
|
|
||||||
|
|
||||||
|
def test_prod_defaults_hold_sub_threshold_pending():
|
||||||
|
"Tracker with prod 24 h / 5 USDT settings does not pay tiny balances early.\n\nTags: billing, settlement"
|
||||||
|
ledger = BillingLedger(starting_credit=100.0, default_price_per_1k=0.02)
|
||||||
|
ledger.charge_request("client", MODEL, 1000, [("node-wallet", 12)])
|
||||||
|
treasury = _FakePayoutTreasury()
|
||||||
|
tracker = _make_tracker(
|
||||||
|
ledger,
|
||||||
|
treasury,
|
||||||
|
threshold=PROD_PAYOUT_THRESHOLD,
|
||||||
|
period=PROD_SETTLE_PERIOD,
|
||||||
|
dust=PROD_DUST_FLOOR,
|
||||||
|
)
|
||||||
|
tracker.start()
|
||||||
|
try:
|
||||||
|
time.sleep(0.5)
|
||||||
|
assert treasury.batches == []
|
||||||
|
assert ledger.get_node_pending("node-wallet") > 0
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def test_prod_threshold_triggers_immediate_payout():
|
||||||
|
"Tracker with prod 24 h period still pays when pending ≥ 5 USDT.\n\nTags: billing, settlement"
|
||||||
|
ledger = BillingLedger(starting_credit=1000.0, default_price_per_1k=0.02)
|
||||||
|
ledger.charge_request("client", MODEL, 300_000, [("node-wallet", 12)])
|
||||||
|
treasury = _FakePayoutTreasury()
|
||||||
|
tracker = _make_tracker(
|
||||||
|
ledger,
|
||||||
|
treasury,
|
||||||
|
threshold=PROD_PAYOUT_THRESHOLD,
|
||||||
|
period=PROD_SETTLE_PERIOD,
|
||||||
|
dust=PROD_DUST_FLOOR,
|
||||||
|
)
|
||||||
|
tracker.start()
|
||||||
|
try:
|
||||||
|
assert _wait_for(lambda: treasury.batches)
|
||||||
|
assert ledger.get_node_pending("node-wallet") == pytest.approx(0.0)
|
||||||
|
assert treasury.batches[0][0][0] == "node-wallet"
|
||||||
|
assert treasury.batches[0][0][1] >= PROD_PAYOUT_THRESHOLD
|
||||||
|
finally:
|
||||||
|
tracker.stop()
|
||||||
|
|||||||
Reference in New Issue
Block a user