From 9839106ee831c5042868a489cb18335acaecdc29 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Tue, 4 Aug 2026 16:14:41 +0300 Subject: [PATCH 01/24] security: make Casper settlement atomic and fail closed --- .github/workflows/ci.yml | 76 +++++++--------- .github/workflows/codeql.yml | 8 +- CHANGELOG.md | 26 ++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 34 +++---- SECURITY.md | 11 ++- SUPPORTED.md | 7 +- demo/.env.example | 7 ++ demo/agent-compute-demo.js | 23 ++--- demo/casper-mcp.mjs | 26 +++--- demo/compute-bridge.js | 125 ++++++++++++-------------- demo/demo-mainnet.js | 45 ++++------ demo/demo.js | 34 +++++-- demo/deploy-mainnet.js | 29 ++++-- demo/deploy.js | 36 +++++--- demo/package.json | 3 +- demo/settlement-verifier.js | 68 ++++++++++++++ demo/test/settlement-verifier.test.js | 75 ++++++++++++++++ demo/trusted-contract.js | 27 ++++++ deployments/casper-v2.json | 12 +++ docs/DEPLOYMENT.md | 57 +++++++++--- src/main.rs | 72 +++++++++++++-- 23 files changed, 572 insertions(+), 233 deletions(-) create mode 100644 demo/settlement-verifier.js create mode 100644 demo/test/settlement-verifier.test.js create mode 100644 demo/trusted-contract.js create mode 100644 deployments/casper-v2.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69835fb..38db083 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,77 +19,61 @@ jobs: name: Contract · build (Wasm) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Install Rust (pinned toolchain) - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2025-02-04 - targets: wasm32-unknown-unknown - - - name: Cache cargo - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }} + run: | + rustup toolchain install nightly-2025-02-04 --profile minimal + rustup target add wasm32-unknown-unknown --toolchain nightly-2025-02-04 - name: Build Wasm (release) - run: cargo build --release + run: cargo +nightly-2025-02-04 build --release --locked --target wasm32-unknown-unknown - name: Upload contract artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 with: name: aifinpay-casper-wasm path: target/wasm32-unknown-unknown/release/*.wasm - if-no-files-found: warn + if-no-files-found: error quality: - name: Contract · fmt · clippy (non-blocking) + name: Contract · fmt · clippy runs-on: ubuntu-latest - continue-on-error: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Install Rust with components - uses: dtolnay/rust-toolchain@master - with: - toolchain: nightly-2025-02-04 - components: rustfmt, clippy - targets: wasm32-unknown-unknown + run: | + rustup toolchain install nightly-2025-02-04 --profile minimal --component rustfmt --component clippy + rustup target add wasm32-unknown-unknown --toolchain nightly-2025-02-04 - name: Format check - run: cargo fmt --all -- --check - continue-on-error: true + run: cargo +nightly-2025-02-04 fmt --all -- --check - name: Clippy - run: cargo clippy --all-targets -- -D warnings - continue-on-error: true + run: cargo +nightly-2025-02-04 clippy --bin aifinpay_casper --target wasm32-unknown-unknown --locked -- -D warnings demo: - name: Demo / SDK · lint · format (non-blocking) + name: Demo / SDK · tests runs-on: ubuntu-latest - continue-on-error: true defaults: run: working-directory: demo steps: - - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 20 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Install dependencies - run: npm install --no-audit --no-fund - - - name: Prettier format check - run: npx --yes prettier --check "**/*.{js,mjs,json,md}" - continue-on-error: true - - - name: ESLint - run: npx --yes eslint . --ext .js,.mjs - continue-on-error: true + run: npm ci --no-audit --no-fund + + - name: Settlement verification tests + run: npm test + + - name: Syntax checks + run: | + node --check settlement-verifier.js + node --check trusted-contract.js + node --check compute-bridge.js + node --check agent-compute-demo.js + node --check demo.js + node --check demo-mainnet.js + node --check casper-mcp.mjs diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index f60353d..d7bc95c 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -22,18 +22,18 @@ jobs: matrix: language: ["javascript-typescript"] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@e60ea984bd3baa95954f2856bcf24f9eaba46637 with: language: ${{ matrix.language }} queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@e60ea984bd3baa95954f2856bcf24f9eaba46637 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@e60ea984bd3baa95954f2856bcf24f9eaba46637 with: category: "/language:${{ matrix.language }}" diff --git a/CHANGELOG.md b/CHANGELOG.md index c25c806..dbdf73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,32 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +## [2.0.0] - 2026-08-04 + +### Security +- Changed `pay_agent` from receipt-only bookkeeping to an atomic native CSPR + transfer followed by an immutable settlement record and event. +- Bound every agent registration to `runtime::get_caller()` and authorize a + payment only when the caller owns `from_agent`; reject zero-value, + self-payment, malformed identifier/wallet, duplicate request and counter + overflow cases. +- Replaced permissive bridge verification with exact, fail-closed checks of + execution success, contract hash, entry point and all quoted payment terms. +- Added request expiry, bounded pending state, replay/in-flight protection and + retry-safe upstream failure handling to the HTTP 402 bridge. +- Quarantined all demo/MCP payment entry points until a complete, reviewed v2 + deployment manifest has `status: verified`. Environment variables cannot + override the trusted contract. +- Removed the legacy mainnet demo's second native transfer and require the + provider to self-register with a distinct funded key. + +### Tests +- Added 13 Node regression/negative tests for exact settlement verification, + failed/pending deploys, malformed sessions, amount mismatches and deployment + quarantine. +- Made Rust formatting, Clippy, locked Wasm build, artifact presence, Node + tests and syntax checks blocking in CI; pinned third-party GitHub actions. + ### Added - World-class repository structure for the Casper Agentic Buildathon final round: full README with architecture (Mermaid) and payment-lifecycle diagrams, badges, diff --git a/Cargo.lock b/Cargo.lock index 31f2e6a..951b192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "aifinpay-casper" -version = "1.0.0" +version = "2.0.0" dependencies = [ "casper-contract", "casper-types", diff --git a/Cargo.toml b/Cargo.toml index b28552e..9ee8a61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aifinpay-casper" -version = "1.0.0" +version = "2.0.0" edition = "2021" [[bin]] diff --git a/README.md b/README.md index ab251f9..286b202 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Autonomous AI agents need to pay each other and pay for services — compute, da ## Introduction -AiFinPay is payment infrastructure for the machine economy. As autonomous AI agents begin to buy compute, data, and API access on their own, they need a way to **pay and be paid** with a verifiable, non-custodial settlement record. AiFinPay provides that as a protocol layer over [HTTP 402](https://en.wikipedia.org/wiki/HTTP_402) (x402), and **this repository implements the Casper settlement backend**: a Rust → Wasm smart contract that gives every agent an on-chain identity and records every payment permanently. +AiFinPay is payment infrastructure for the machine economy. This repository contains the Casper settlement backend: a Rust → Wasm contract in which agents self-register and `pay_agent` atomically transfers native CSPR before writing an immutable receipt. ## Problem @@ -75,14 +75,14 @@ Card rails and custodial wallets assume a human and a browser. Agents need progr AiFinPay closes the loop: - **x402 protocol** — a service returns `HTTP 402 Payment Required`; the agent settles on-chain and retries with proof. -- **Casper settlement contract** — agents `register_agent` for an on-chain identity, then `pay_agent` to settle. Every settlement emits a `PaymentSettled` event. +- **Casper settlement contract** — agents self-register an on-chain identity, then `pay_agent` atomically transfers CSPR and emits `PaymentSettled`. - **Idempotent settlement** — payments are keyed by `request_id`, so retries are safe and double-settlement is impossible. - **MCP server** — AI agents (e.g. Claude via Claude Code / Claude Desktop) settle on Casper as a native tool call. ## Features -- 🧾 **On-chain agent registry** — `register_agent(agent_id, wallet)` with an `AgentRegistered` event. -- 💸 **Verifiable settlement** — `pay_agent(from, to, amount, request_id)` emits `PaymentSettled`, permanently recorded on Casper. +- 🧾 **Caller-bound registry** — `register_agent(agent_id, wallet)` accepts only the caller's account hash. +- 💸 **Atomic settlement** — `pay_agent(from, to, amount, request_id)` moves CSPR and records the exact terms in one transaction. - 🔁 **Idempotent by design** — duplicate `request_id` is rejected (no double spend). - 🌐 **x402 bridge** — a reference compute gate that enforces `HTTP 402` and verifies settlement on-chain before releasing a resource. - 🤖 **MCP integration** — drive settlements directly from an AI agent runtime. @@ -210,7 +210,7 @@ rustup target add wasm32-unknown-unknown cd demo && npm install && cp .env.example .env ``` -Set `CONTRACT_HASH` in `demo/.env` after deploying (or use the live hash below). +Payment clients also require a reviewed `deployments/casper-v2.json`; an environment variable alone cannot enable an unverified contract. ## Local Development @@ -231,11 +231,11 @@ make build # target/wasm32-unknown-unknown/release/aifinpay_casper.wasm make deploy # deploys via demo/deploy.js, prints the new CONTRACT_HASH ``` -Save the printed `CONTRACT_HASH` into `demo/.env`. Full walkthrough: [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). +Do not enable payment routes until the deployed Wasm/source provenance has been independently checked and the v2 manifest is committed with `status: verified`. Full walkthrough: [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md). -## 🟢 Casper Mainnet Deployment +## Historical Casper deployments — quarantined -The settlement contract is **live on Casper Mainnet** — not only testnet. Every entry point below has been exercised on mainnet with real CSPR. +The hashes below are retained as historical evidence only. They are v1 deployments: `pay_agent` recorded an amount but did not transfer it, and a separate transfer produced the old demo balance change. They are not valid payment proof and all current clients reject them. | Field | Value | |-------|-------| @@ -245,9 +245,9 @@ The settlement contract is **live on Casper Mainnet** — not only testnet. Ever | **Explorer** | [cspr.live mainnet](https://cspr.live/contract/9903a5e3948e799196df54b17270bc6769338ac1cc36c9eb47e113f88d23f019) | | **Install deploy** | [`0d560c62…`](https://cspr.live/deploy/0d560c62679d109525ee8b2b1ce1a275cba7deff50a90352f8b4aabf4f070386) | -### Live mainnet settlement (real value moved) +### Historical mainnet demonstration -A full agent-to-agent settlement executed on mainnet — two agents registered, a payment settled on-chain, and real CSPR delivered to the provider's wallet: +The following two independent operations were previously described as one settlement. The contract call recorded a receipt; the later native transfer moved value. | Action | Deploy | Explorer | |--------|--------|----------| @@ -256,9 +256,9 @@ A full agent-to-agent settlement executed on mainnet — two agents registered, | **PaymentSettled** (`pay_agent`) | `80df5895…` | [view](https://cspr.live/deploy/80df58959f81d99d717027cdc069e95a3464d867150184b0f05312de6c6eb6d7) | | Value transfer (2.5 CSPR → provider) | `564f19be…` | [view](https://cspr.live/deploy/564f19be2c89140a6dda9e97e4440d49890cf8df5b678b00bd0c625c6d975f3a) | -Reproduce on mainnet with a funded key at `demo/keys-mainnet/secret_key.pem`: `node demo/deploy-mainnet.js` then `node demo/demo-mainnet.js`. +Do not reproduce this v1 flow. The v2 mainnet script requires separately controlled buyer/provider keys and performs no second transfer. -## Casper Testnet Deployment +## Historical Casper Testnet Deployment | Field | Value | |-------|-------| @@ -271,8 +271,8 @@ Reproduce on mainnet with a funded key at `demo/keys-mainnet/secret_key.pem`: `n | Entry Point | Args | Description | |-------------|------|-------------| -| `register_agent` | `agent_id: String, wallet: String` | Register an AI agent on-chain → `AgentRegistered` | -| `pay_agent` | `from_agent: String, to_agent: String, amount: U512, request_id: String` | Settle a payment → `PaymentSettled` | +| `register_agent` | `agent_id: String, wallet: String` | Self-register caller wallet → `AgentRegistered` | +| `pay_agent` | `from_agent: String, to_agent: String, amount: U512, request_id: String` | Transfer CSPR and record settlement → `PaymentSettled` | | `get_payment_count` | — | Total settled payments | ### Events @@ -290,7 +290,7 @@ hash-47df409829ddf0612617460293ba591a19b26fa0c06918878204088d3eb9b78a ## Sample Transactions -Real transactions from the agent-compute demo on `casper-test`: +Historical v1 transactions from the agent-compute demo on `casper-test` (not valid settlement proof): | Action | Deploy | Explorer | |--------|--------|----------| @@ -321,7 +321,9 @@ Configure in `demo/.env` (see [`demo/.env.example`](demo/.env.example)): | `NODE_URL` | `https://node.testnet.casper.network/rpc` | Casper RPC endpoint | | `NETWORK_NAME` | `casper-test` | Casper network name | | `KEYS_DIR` | `./keys` | Directory holding the signing key | -| `CONTRACT_HASH` | — | Deployed contract hash (`hash-…`) | +| `CONTRACT_HASH` | — | Must match the verified v2 deployment manifest | +| `PROVIDER_AGENT_ID` | — | Merchant identity self-registered by its own wallet | +| `PROVIDER_KEYS_DIR` | — | Separate funded merchant key for two-party demos only | | `COMPUTE_UPSTREAM_URL` | _(optional)_ | Real OpenAI-compatible compute endpoint | | `COMPUTE_API_KEY` | _(optional)_ | API key for the upstream provider | diff --git a/SECURITY.md b/SECURITY.md index 20d24d4..f521e49 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,7 +4,8 @@ | Version | Supported | Network | |---------|-----------|---------| -| 1.x | ✅ | Casper Testnet (`casper-test`) | +| 2.x | Source fixed; not deployed | None until manifest verification | +| 1.x | ❌ vulnerable / quarantined | Historical testnet and mainnet deployments | See [SUPPORTED.md](SUPPORTED.md) for the full support matrix. @@ -44,3 +45,11 @@ the public Casper testnet infrastructure. This repository runs **CodeQL** static analysis, **Dependabot** dependency alerts and updates, and **secret scanning with push protection**. All High or greater severity alerts are triaged and resolved before release. + +## Deployment safety state + +Version 1.x recorded a settlement without transferring CSPR and did not bind +the claimed payer or registered wallet to the caller. It must not be used as a +payment proof. Version 2.0 fixes these defects in source, but clients remain +fail-closed until `deployments/casper-v2.json` contains independently checked +deployment, bytecode and source provenance with `status: verified`. diff --git a/SUPPORTED.md b/SUPPORTED.md index d4551f1..df9abf2 100644 --- a/SUPPORTED.md +++ b/SUPPORTED.md @@ -4,14 +4,15 @@ | Version | Status | Notes | |---------|-------------|-------| -| 1.x | ✅ Active | Current line. Bug fixes + security patches. | +| 2.x | Source candidate | Payment routes stay quarantined until verified deployment. | +| 1.x | ❌ Unsupported | Receipt-only settlement and missing caller binding. | ## Networks | Network | Chain name | Status | Contract | |----------------------------|---------------|----------------|----------| -| Casper Testnet | `casper-test` | ✅ Live | `hash-47df409829ddf0612617460293ba591a19b26fa0c06918878204088d3eb9b78a` | -| Casper Mainnet | `casper` | 🔜 Planned | — | +| Casper Testnet | `casper-test` | ⚠️ Legacy v1 only | Quarantined | +| Casper Mainnet | `casper` | ⚠️ Legacy v1 only | Quarantined | ## Toolchain diff --git a/demo/.env.example b/demo/.env.example index 41dff2a..a264507 100644 --- a/demo/.env.example +++ b/demo/.env.example @@ -1,5 +1,12 @@ NODE_URL=https://node.testnet.casper.network/rpc NETWORK_NAME=casper-test KEYS_DIR=./keys +# Separately controlled, funded merchant account used by demo.js. +PROVIDER_KEYS_DIR=./provider-keys # Set after deployment: CONTRACT_HASH=hash-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +# Merchant identity, registered on-chain by the merchant's own account. +PROVIDER_AGENT_ID=aifinpay-compute-provider +PRICE_MOTES=100000000 +ORDER_TTL_MS=600000 +MAX_PENDING_ORDERS=10000 diff --git a/demo/agent-compute-demo.js b/demo/agent-compute-demo.js index 441e951..61c8fbd 100644 --- a/demo/agent-compute-demo.js +++ b/demo/agent-compute-demo.js @@ -3,7 +3,7 @@ * * An autonomous AI agent buys LLM compute and SETTLES THE PAYMENT ON CASPER: * - * 1. agent + provider register on-chain (register_agent) + * 1. agent registers; provider is pre-registered by its own wallet * 2. agent asks the bridge for compute → HTTP 402 (pay_casper) * 3. agent settles on Casper → pay_agent (REAL testnet tx) * 4. bridge verifies the settlement on-chain → returns the compute result @@ -24,6 +24,7 @@ require('dotenv').config(); const { CasperClient, DeployUtil, Keys, CLValueBuilder, RuntimeArgs } = require('casper-js-sdk'); const { spawn } = require('child_process'); const path = require('path'); +const { assertTrustedContract } = require('./trusted-contract'); const NODE_URL = process.env.NODE_URL || 'https://node.testnet.casper.network/rpc'; const NETWORK = process.env.NETWORK_NAME || 'casper-test'; @@ -31,6 +32,7 @@ const KEYS_DIR = process.env.KEYS_DIR || path.join(__dirname, 'keys') const CONTRACT_HASH = process.env.CONTRACT_HASH; const BRIDGE_PORT = parseInt(process.env.BRIDGE_PORT || '4055', 10); const PRICE_MOTES = process.env.PRICE_MOTES || '100000000'; // 0.1 CSPR / call +const PROVIDER = process.env.PROVIDER_AGENT_ID; const GAS_CALL = '5000000000'; // 5 CSPR per entry-point call const PROMPT = process.env.PROMPT || @@ -40,6 +42,11 @@ if (!CONTRACT_HASH) { console.error('❌ CONTRACT_HASH not set in .env — run `node deploy.js` first.'); process.exit(1); } +if (!PROVIDER) { + console.error('❌ PROVIDER_AGENT_ID is required and must already be registered by the provider wallet.'); + process.exit(1); +} +assertTrustedContract(CONTRACT_HASH); const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); @@ -124,7 +131,6 @@ async function main() { const nonce = Date.now().toString(36); const BUYER = `aifinpay-buyer-${nonce}`; - const PROVIDER = `aifinpay-provider-${nonce}`; console.log('🤖 AiFinPay × Casper — AI agent pays for compute, settled on Casper'); console.log('===================================================================='); @@ -139,19 +145,14 @@ async function main() { try { await waitForBridge(BRIDGE_URL); - // ── 1. Register both agents on-chain ────────────────────────────────────── - console.log('📝 Step 1: Registering agents on Casper...'); + // ── 1. Register the payer. The merchant must self-register separately. ─── + console.log('📝 Step 1: Registering buyer on Casper...'); const r1 = await callEntry(client, keypair, 'register_agent', RuntimeArgs.fromMap({ agent_id: CLValueBuilder.string(BUYER), wallet: CLValueBuilder.string(accountHash), })); console.log(' buyer register tx:', r1, '→', explorer(r1)); await waitForSuccess(client, r1, 'register buyer'); - const r2 = await callEntry(client, keypair, 'register_agent', RuntimeArgs.fromMap({ - agent_id: CLValueBuilder.string(PROVIDER), wallet: CLValueBuilder.string(accountHash), - })); - console.log(' provider register tx:', r2, '→', explorer(r2)); - await waitForSuccess(client, r2, 'register provider'); - console.log(' ✅ both agents registered\n'); + console.log(' ✅ buyer registered; provider is pre-registered by its own wallet\n'); // ── 2. Ask the bridge for compute → expect HTTP 402 ─────────────────────── console.log('💡 Step 2: Agent requests compute →', JSON.stringify(PROMPT)); @@ -197,7 +198,7 @@ async function main() { console.log(''); console.log('On-chain settlement:'); console.log(' register buyer: ', explorer(r1)); - console.log(' register provider:', explorer(r2)); + console.log(' provider agent: ', PROVIDER, '(pre-registered)'); console.log(' PaymentSettled: ', explorer(pay)); console.log(' contract state: https://testnet.cspr.live/contract/' + CONTRACT_HASH.replace('hash-', '')); console.log(''); diff --git a/demo/casper-mcp.mjs b/demo/casper-mcp.mjs index ad47107..25508a6 100644 --- a/demo/casper-mcp.mjs +++ b/demo/casper-mcp.mjs @@ -19,7 +19,8 @@ * node casper-mcp.mjs (usually launched by Claude Desktop, not by hand) * * Config (demo/.env): CONTRACT_HASH (required), NODE_URL, NETWORK_NAME, KEYS_DIR, - * PRICE_MOTES, and optional COMPUTE_UPSTREAM_URL + COMPUTE_API_KEY for a real + * PRICE_MOTES, PROVIDER_AGENT_ID (pre-registered by the provider), and optional + * COMPUTE_UPSTREAM_URL + COMPUTE_API_KEY for a real * LLM answer instead of the labelled demo mock. */ import { Server } from '@modelcontextprotocol/sdk/server/index.js'; @@ -29,8 +30,10 @@ import { fileURLToPath } from 'node:url'; import path from 'node:path'; import dotenv from 'dotenv'; import casper from 'casper-js-sdk'; +import trustedContract from './trusted-contract.js'; const { CasperClient, DeployUtil, Keys, CLValueBuilder, RuntimeArgs } = casper; +const { assertTrustedContract } = trustedContract; const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.join(__dirname, '.env') }); @@ -44,6 +47,7 @@ const KEYS_DIR_RAW = process.env.KEYS_DIR || 'keys'; const KEYS_DIR = path.isAbsolute(KEYS_DIR_RAW) ? KEYS_DIR_RAW : path.join(__dirname, KEYS_DIR_RAW); const CONTRACT_HASH = process.env.CONTRACT_HASH; const PRICE_MOTES = process.env.PRICE_MOTES || '100000000'; // 0.1 CSPR / call +const PROVIDER = process.env.PROVIDER_AGENT_ID; const GAS_CALL = '5000000000'; // 5 CSPR per entry-point call const UPSTREAM_URL = process.env.COMPUTE_UPSTREAM_URL || ''; const UPSTREAM_KEY = process.env.COMPUTE_API_KEY || ''; @@ -58,6 +62,11 @@ if (!CONTRACT_HASH) { log('FATAL: CONTRACT_HASH not set in demo/.env (the deployed settlement contract).'); process.exit(1); } +if (!PROVIDER) { + log('FATAL: PROVIDER_AGENT_ID is required and must be registered by the provider wallet.'); + process.exit(1); +} +assertTrustedContract(CONTRACT_HASH); // ── Casper plumbing (same pattern as agent-compute-demo.js) ─────────────────── const keypair = Keys.Ed25519.loadKeyPairFromPrivateFile(path.join(KEYS_DIR, 'secret_key.pem')); @@ -104,28 +113,23 @@ async function waitForSuccess(deployHash, label, maxWait = 180000) { // ── Session state ───────────────────────────────────────────────────────────── const SESSION = Math.random().toString(36).slice(2, 8); const BUYER = `claude-agent-${SESSION}`; -const PROVIDER = `aifinpay-compute-${SESSION}`; const orders = new Map(); // request_id -> { from, to, amount, prompt } const settled = new Map(); // request_id -> deployHash let reqSeq = 0; -// Both agents must exist on-chain before pay_agent. Register once, lazily, and -// cache the promise so concurrent/later calls reuse the same registration. +// The buyer self-registers once. The merchant is a separately-owned identity +// and must have been registered by its own wallet before this server starts. let registrationPromise = null; function ensureRegistered() { if (!registrationPromise) { registrationPromise = (async () => { - log(`registering agents on-chain: ${BUYER} + ${PROVIDER} (one-time, ~30-60s)...`); + log(`registering buyer on-chain: ${BUYER} (one-time, ~30-60s)...`); const r1 = await callEntry('register_agent', RuntimeArgs.fromMap({ agent_id: CLValueBuilder.string(BUYER), wallet: CLValueBuilder.string(accountHash), })); await waitForSuccess(r1, 'register buyer'); - const r2 = await callEntry('register_agent', RuntimeArgs.fromMap({ - agent_id: CLValueBuilder.string(PROVIDER), wallet: CLValueBuilder.string(accountHash), - })); - await waitForSuccess(r2, 'register provider'); - log(`agents registered (buyer ${explorer(r1)} · provider ${explorer(r2)})`); - return { buyer: r1, provider: r2 }; + log(`buyer registered (${explorer(r1)}); provider ${PROVIDER} is pre-registered`); + return { buyer: r1 }; })().catch((e) => { registrationPromise = null; throw e; }); } return registrationPromise; diff --git a/demo/compute-bridge.js b/demo/compute-bridge.js index e6ebea6..b923dab 100644 --- a/demo/compute-bridge.js +++ b/demo/compute-bridge.js @@ -19,14 +19,17 @@ require('dotenv').config(); const http = require('http'); -const { CasperClient } = require('casper-js-sdk'); +const { validateExecutedSettlement } = require('./settlement-verifier'); +const { assertTrustedContract } = require('./trusted-contract'); const PORT = parseInt(process.env.BRIDGE_PORT || '4055', 10); const NODE_URL = process.env.NODE_URL || 'https://node.testnet.casper.network/rpc'; const NETWORK = process.env.NETWORK_NAME || 'casper-test'; const CONTRACT_HASH = process.env.CONTRACT_HASH || ''; -const PROVIDER_AGENT = process.env.PROVIDER_AGENT_ID || 'aifinpay-compute-provider'; +const PROVIDER_AGENT = process.env.PROVIDER_AGENT_ID || ''; const PRICE_MOTES = process.env.PRICE_MOTES || '100000000'; // 0.1 CSPR / call +const ORDER_TTL_MS = parseInt(process.env.ORDER_TTL_MS || '600000', 10); +const MAX_PENDING = parseInt(process.env.MAX_PENDING_ORDERS || '10000', 10); // Optional real upstream (OpenAI-compatible). If unset, a labelled demo mock runs. const UPSTREAM_URL = process.env.COMPUTE_UPSTREAM_URL || ''; const UPSTREAM_KEY = process.env.COMPUTE_API_KEY || ''; @@ -36,10 +39,15 @@ if (!CONTRACT_HASH) { console.error('[bridge] FATAL: CONTRACT_HASH not set (the deployed Casper settlement contract).'); process.exit(1); } +if (!PROVIDER_AGENT) { + console.error('[bridge] FATAL: PROVIDER_AGENT_ID is required and must be registered by the provider wallet.'); + process.exit(1); +} +assertTrustedContract(CONTRACT_HASH); -const client = new CasperClient(NODE_URL); -const orders = new Map(); // request_id -> { from_agent, to_agent, amount_motes } -const consumed = new Set(); // request_id already fulfilled (replay guard) +const orders = new Map(); // request_id -> quote terms + creation time +const consumed = new Map(); // request_id -> fulfillment time +const inflight = new Set(); // verified requests currently computing let seq = 0; function newRequestId() { @@ -55,8 +63,19 @@ function send(res, code, obj) { // 402 challenge — tells the agent exactly how to settle on Casper. function challenge(res, fromAgent) { + const now = Date.now(); + for (const [id, order] of orders) if (now - order.created_at > ORDER_TTL_MS) orders.delete(id); + for (const [id, timestamp] of consumed) if (now - timestamp > ORDER_TTL_MS) consumed.delete(id); + if (orders.size >= MAX_PENDING) { + return send(res, 503, { error: 'payment_capacity_exceeded' }); + } const request_id = newRequestId(); - orders.set(request_id, { from_agent: fromAgent, to_agent: PROVIDER_AGENT, amount_motes: PRICE_MOTES }); + orders.set(request_id, { + from_agent: fromAgent, + to_agent: PROVIDER_AGENT, + amount_motes: PRICE_MOTES, + created_at: now, + }); return send(res, 402, { error: 'Payment Required', protocol: 'AiFinPay-x402', @@ -80,25 +99,17 @@ function challenge(res, fromAgent) { }); } -// Pull an arg's parsed value out of a getDeploy raw response (StoredContractByHash). -function readSessionArgs(raw) { - const s = raw && raw.deploy && raw.deploy.session; - const sc = s && (s.StoredContractByHash || s.StoredVersionedContractByHash); - if (!sc || !Array.isArray(sc.args)) return null; - const out = { entry_point: sc.entry_point }; - for (const pair of sc.args) { - if (!Array.isArray(pair) || pair.length < 2) continue; - const [name, clv] = pair; - out[name] = clv && (clv.parsed !== undefined ? clv.parsed : clv); - } - return out; -} - // Verify the agent's Casper deploy actually settled THIS order. async function verifySettlement(deployHash, request_id) { const order = orders.get(request_id); if (!order) return { ok: false, reason: 'unknown_or_expired_request_id' }; - if (consumed.has(request_id)) return { ok: false, reason: 'request_id_already_fulfilled' }; + if (Date.now() - order.created_at > ORDER_TTL_MS) { + orders.delete(request_id); + return { ok: false, reason: 'unknown_or_expired_request_id' }; + } + if (consumed.has(request_id) || inflight.has(request_id)) { + return { ok: false, reason: 'request_id_already_fulfilled' }; + } // Casper 2.0: read info_get_deploy directly (casper-js-sdk 2.15.4 parses the // legacy execution_results, empty on a 2.0 node). @@ -112,36 +123,13 @@ async function verifySettlement(deployHash, request_id) { } catch (e) { return { ok: false, reason: `info_get_deploy failed: ${e.message || e}` }; } - if (!rpc) return { ok: false, reason: 'deploy_not_found' }; - const er = rpc.execution_info && rpc.execution_info.execution_result; - if (!er) return { ok: false, reason: 'deploy_not_executed_yet' }; - if (er.Version2 && er.Version2.error_message) return { ok: false, reason: `deploy_failed_on_chain: ${er.Version2.error_message}` }; - if (er.Version1 && er.Version1.Failure) return { ok: false, reason: `deploy_failed_on_chain: ${er.Version1.Failure.error_message || 'unknown'}` }; - if (!er.Version2 && !er.Version1) return { ok: false, reason: 'deploy_not_successful' }; - - // Strict check: confirm it was pay_agent for THIS request_id / recipient / amount. - const args = readSessionArgs(rpc); - if (args) { - if (args.entry_point && args.entry_point !== 'pay_agent') { - return { ok: false, reason: `wrong_entry_point: ${args.entry_point}` }; - } - if (args.request_id != null && String(args.request_id) !== String(request_id)) { - return { ok: false, reason: `request_id_mismatch: ${args.request_id}` }; - } - if (args.to_agent != null && String(args.to_agent) !== String(order.to_agent)) { - return { ok: false, reason: `recipient_mismatch: ${args.to_agent}` }; - } - if (args.amount != null) { - try { - if (BigInt(String(args.amount)) < BigInt(String(order.amount_motes))) { - return { ok: false, reason: `underpaid: ${args.amount} < ${order.amount_motes}` }; - } - } catch { /* non-numeric parsed amount — skip strict amount check */ } - } - } else { - console.warn('[bridge] could not parse session args — accepting on execution success only'); - } - return { ok: true }; + return validateExecutedSettlement(rpc, { + contract_hash: CONTRACT_HASH, + request_id, + from_agent: order.from_agent, + to_agent: order.to_agent, + amount_motes: order.amount_motes, + }); } // The actual compute. Real OpenAI-compatible upstream if configured, else a @@ -197,23 +185,26 @@ const server = http.createServer((req, res) => { const v = await verifySettlement(String(deployHash), String(reqId)); if (!v.ok) return send(res, 402, { error: 'payment_verification_failed', detail: v.reason }); - consumed.add(String(reqId)); const order = orders.get(String(reqId)); - const compute = await runCompute(body.prompt); - return send(res, 200, { - ok: true, - settlement: { - chain: 'casper', - contract_hash: CONTRACT_HASH, - request_id: reqId, - from_agent: order && order.from_agent, - to_agent: order && order.to_agent, - amount_motes: order && order.amount_motes, - deploy: deployHash, - explorer: `https://testnet.cspr.live/deploy/${deployHash}`, - }, - compute, - }); + inflight.add(String(reqId)); + try { + const compute = await runCompute(body.prompt); + consumed.set(String(reqId), Date.now()); + return send(res, 200, { + ok: true, + settlement: { + chain: 'casper', contract_hash: CONTRACT_HASH, request_id: reqId, + from_agent: order && order.from_agent, to_agent: order && order.to_agent, + amount_motes: order && order.amount_motes, deploy: deployHash, + explorer: `https://testnet.cspr.live/deploy/${deployHash}`, + }, + compute, + }); + } catch (error) { + return send(res, 502, { error: 'compute_upstream_failed', detail: error.message || String(error) }); + } finally { + inflight.delete(String(reqId)); + } }); }); diff --git a/demo/demo-mainnet.js b/demo/demo-mainnet.js index 3c0527c..7239721 100644 --- a/demo/demo-mainnet.js +++ b/demo/demo-mainnet.js @@ -1,23 +1,27 @@ /** * demo-mainnet.js — live AiFinPay settlement demo on Casper MAINNET. - * 1. register_agent (buyer + provider) on the live contract - * 2. pay_agent records the settlement on-chain (event + idempotency) - * 3. a real native CSPR transfer moves value to the provider's wallet - * Proves the provider balance went 0 -> N CSPR. Payee key is SAVED so funds aren't lost. + * 1. buyer and provider self-register using distinct funded accounts + * 2. pay_agent atomically moves native CSPR and records the settlement + * Proves the provider balance increases by the exact settlement amount. */ const { DeployUtil, Keys, CLValueBuilder, RuntimeArgs } = require('casper-js-sdk'); const fetch = require('node-fetch'); -const fs = require('fs'); const path = require('path'); +const { assertTrustedContract } = require('./trusted-contract'); const NODE_URL = 'https://node.cspr.cloud/rpc'; const API = 'https://api.cspr.cloud'; const KEY = process.env.CSPR_API_KEY || ''; const NETWORK = 'casper'; const KEYS_DIR = path.join(__dirname, 'keys-mainnet'); -const CONTRACT = process.env.CONTRACT || 'contract-9903a5e3948e799196df54b17270bc6769338ac1cc36c9eb47e113f88d23f019'; +const PROVIDER_KEYS_DIR = process.env.PROVIDER_KEYS_DIR; +const CONTRACT = process.env.CONTRACT_HASH; const GAS_CALL = '3000000000'; // 3 CSPR per contract call -const AMOUNT = '2500000000'; // 2.5 CSPR (native-transfer minimum) +const AMOUNT = '2500000000'; // 2.5 CSPR +if (!CONTRACT || !PROVIDER_KEYS_DIR) { + throw new Error('CONTRACT_HASH and PROVIDER_KEYS_DIR are required'); +} +assertTrustedContract(CONTRACT); async function rpc(method, params) { const r = await fetch(NODE_URL, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': KEY }, body: JSON.stringify({ jsonrpc: '2.0', id: Date.now(), method, params }) }); @@ -48,14 +52,10 @@ function callContract(kp, ep, args) { async function main() { const kp = Keys.Ed25519.loadKeyPairFromPrivateFile(path.join(KEYS_DIR, 'secret_key.pem')); + const payee = Keys.Ed25519.loadKeyPairFromPrivateFile(path.join(PROVIDER_KEYS_DIR, 'secret_key.pem')); const payerHash = kp.publicKey.toAccountHashStr(); - // provider wallet — SAVE the key so the transferred CSPR is recoverable - const payee = Keys.Ed25519.new(); - const payeeDir = path.join(__dirname, 'keys-mainnet-demo-payee'); - fs.mkdirSync(payeeDir, { recursive: true }); - fs.writeFileSync(path.join(payeeDir, 'secret_key.pem'), payee.exportPrivateKeyInPem()); - fs.writeFileSync(path.join(payeeDir, 'public_key.pem'), payee.exportPublicKeyInPem()); - fs.writeFileSync(path.join(payeeDir, 'public_key_hex.txt'), payee.publicKey.toHex()); + const providerHash = payee.publicKey.toAccountHashStr(); + if (providerHash === payerHash) throw new Error('buyer and provider accounts must be distinct'); console.log('AiFinPay x Casper — LIVE MAINNET agent settlement'); console.log('contract', CONTRACT); @@ -63,10 +63,6 @@ async function main() { console.log('provider(agent-002):', payee.publicKey.toAccountHashStr().slice(0, 28) + '...'); console.log(''); - const before = await balanceCSPR(payee.publicKey.toHex()); - console.log(`provider balance BEFORE: ${before} CSPR`); - console.log(''); - const SUF = String(process.pid) + '' + payerHash.slice(-4); const A1 = `aifinpay-buyer-${SUF}`, A2 = `aifinpay-provider-${SUF}`, REQ = `req-${SUF}`; const out = {}; @@ -77,21 +73,17 @@ async function main() { if (!r.ok) process.exit(1); console.log(`2) register_agent ${A2} ...`); - h = await submit(callContract(kp, 'register_agent', RuntimeArgs.fromMap({ agent_id: CLValueBuilder.string(A2), wallet: CLValueBuilder.string(payee.publicKey.toAccountHashStr()) }))); + h = await submit(callContract(payee, 'register_agent', RuntimeArgs.fromMap({ agent_id: CLValueBuilder.string(A2), wallet: CLValueBuilder.string(providerHash) }))); r = await wait(h); console.log(r.ok ? ` ok ${h}` : ` FAIL ${r.err}`); out.register_provider = h; if (!r.ok) process.exit(1); - console.log(`3) pay_agent settle 2.5 CSPR ${REQ} ...`); + const before = await balanceCSPR(payee.publicKey.toHex()); + console.log(`provider balance BEFORE settlement: ${before} CSPR`); + console.log(`3) pay_agent atomically settles 2.5 CSPR ${REQ} ...`); h = await submit(callContract(kp, 'pay_agent', RuntimeArgs.fromMap({ from_agent: CLValueBuilder.string(A1), to_agent: CLValueBuilder.string(A2), amount: CLValueBuilder.u512(AMOUNT), request_id: CLValueBuilder.string(REQ) }))); r = await wait(h); console.log(r.ok ? ` settled ${h}` : ` FAIL ${r.err}`); out.settle = h; if (!r.ok) process.exit(1); - console.log('4) native transfer 2.5 CSPR buyer -> provider ...'); - const dp = new DeployUtil.DeployParams(kp.publicKey, NETWORK, 1, 1800000); - const xfer = DeployUtil.ExecutableDeployItem.newTransfer(AMOUNT, payee.publicKey, null, 1); - h = await submit(DeployUtil.signDeploy(DeployUtil.makeDeploy(dp, xfer, DeployUtil.standardPayment('100000000')), kp)); - r = await wait(h); console.log(r.ok ? ` moved ${h}` : ` FAIL ${r.err}`); out.transfer = h; - const after = await balanceCSPR(payee.publicKey.toHex()); console.log(''); console.log(`provider balance AFTER: ${after} CSPR`); @@ -100,6 +92,5 @@ async function main() { console.log('=== RESULTS (mainnet) ==='); for (const [k, v] of Object.entries(out)) console.log(`${k}: https://cspr.live/deploy/${v}`); console.log('provider pubkey:', payee.publicKey.toHex()); - fs.writeFileSync(path.join(__dirname, 'demo-mainnet.out.json'), JSON.stringify({ contract: CONTRACT, ...out, provider_pubkey: payee.publicKey.toHex() }, null, 2)); } main().catch(e => { console.error('ERR', e.message || e); process.exit(1); }); diff --git a/demo/demo.js b/demo/demo.js index 3d6874e..6b09b86 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -1,7 +1,7 @@ /** * demo.js — full AiFinPay x Casper demo flow: * 1. Register AI Agent A (aifinpay-agent-001) - * 2. Register AI Agent B (aifinpay-agent-002) + * 2. Provider self-registers AI Agent B (aifinpay-agent-002) * 3. Agent A pays Agent B (settle 2.5 CSPR, request ID: req-001) * 4. Query payment count → confirm on-chain * @@ -14,10 +14,12 @@ const { CasperClient, DeployUtil, Keys, CLValueBuilder, RuntimeArgs } = require('casper-js-sdk'); const path = require('path'); +const { assertTrustedContract } = require('./trusted-contract'); const NODE_URL = process.env.NODE_URL || 'https://node.testnet.casper.network/rpc'; const NETWORK = process.env.NETWORK_NAME || 'casper-test'; const KEYS_DIR = process.env.KEYS_DIR || path.join(__dirname, 'keys'); +const PROVIDER_KEYS_DIR = process.env.PROVIDER_KEYS_DIR; const CONTRACT_HASH = process.env.CONTRACT_HASH; const GAS_CALL = '5000000000'; // 5 CSPR per call @@ -27,6 +29,11 @@ if (!CONTRACT_HASH) { console.error('❌ CONTRACT_HASH not set in .env — run node deploy.js first'); process.exit(1); } +if (!PROVIDER_KEYS_DIR) { + console.error('❌ PROVIDER_KEYS_DIR is required; buyer and provider must use distinct funded accounts'); + process.exit(1); +} +assertTrustedContract(CONTRACT_HASH); async function callEntry(client, keypair, entryPoint, args) { const hashBytes = Buffer.from(CONTRACT_HASH.replace('hash-', ''), 'hex'); @@ -47,8 +54,20 @@ async function waitForDeploy(client, deployHash, maxWait = 120000) { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const result = await client.getDeploy(deployHash); - if (result[1].execution_results.length > 0) return result; + const response = await fetch(NODE_URL, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'info_get_deploy', params: { deploy_hash: deployHash } }), + }); + const result = await response.json(); + const er = result && result.result && result.result.execution_info && result.result.execution_info.execution_result; + if (er && er.Version2) { + if (er.Version2.error_message) throw new Error(`deploy failed: ${er.Version2.error_message}`); + return result; + } + if (er && er.Version1) { + if (er.Version1.Failure) throw new Error(`deploy failed: ${er.Version1.Failure.error_message || 'unknown'}`); + return result; + } } catch (_) {} await new Promise(r => setTimeout(r, 3000)); } @@ -59,7 +78,12 @@ async function main() { const keypair = Keys.Ed25519.loadKeyPairFromPrivateFile( require('path').join(KEYS_DIR, 'secret_key.pem') ); + const providerKeypair = Keys.Ed25519.loadKeyPairFromPrivateFile( + path.join(PROVIDER_KEYS_DIR, 'secret_key.pem') + ); const accountHash = keypair.publicKey.toAccountHashStr(); + const providerHash = providerKeypair.publicKey.toAccountHashStr(); + if (providerHash === accountHash) throw new Error('buyer and provider accounts must be distinct'); const client = new CasperClient(NODE_URL); console.log('🤖 AiFinPay x Casper — Demo Flow'); @@ -81,9 +105,9 @@ async function main() { // ── Step 2: Register Agent B ───────────────────────────────────────────── console.log('📝 Step 2: Registering aifinpay-agent-002...'); - const tx2 = await callEntry(client, keypair, 'register_agent', RuntimeArgs.fromMap({ + const tx2 = await callEntry(client, providerKeypair, 'register_agent', RuntimeArgs.fromMap({ agent_id: CLValueBuilder.string('aifinpay-agent-002'), - wallet: CLValueBuilder.string('account-hash-0000000000000000000000000000000000000000000000000000000000000002'), + wallet: CLValueBuilder.string(providerHash), })); console.log(' Deploy hash:', tx2); console.log(' Explorer: ', `https://testnet.cspr.live/deploy/${tx2}`); diff --git a/demo/deploy-mainnet.js b/demo/deploy-mainnet.js index 19e6865..71779c1 100644 --- a/demo/deploy-mainnet.js +++ b/demo/deploy-mainnet.js @@ -14,6 +14,7 @@ require('dotenv').config({ path: require('path').join(__dirname, '.env.mainnet') const { DeployUtil, Keys, RuntimeArgs } = require('casper-js-sdk'); const fetch = require('node-fetch'); const fs = require('fs'); +const crypto = require('crypto'); const path = require('path'); // Mainnet defaults — override in .env.mainnet if the cspr.cloud key isn't mainnet-enabled. @@ -21,7 +22,7 @@ const NODE_URL = process.env.NODE_URL || 'https://node.mainnet.cspr.clou const CSPR_API_KEY = process.env.CSPR_API_KEY || ''; const NETWORK = process.env.NETWORK_NAME || 'casper'; const KEYS_DIR = process.env.KEYS_DIR || path.join(__dirname, 'keys-mainnet'); -const WASM_PATH = path.join(__dirname, '..', 'aifinpay_casper.wasm'); +const WASM_PATH = path.join(__dirname, '..', 'target', 'wasm32-unknown-unknown', 'release', 'aifinpay_casper.wasm'); const GAS_INSTALL = process.env.GAS_INSTALL || '200000000000'; // 200 CSPR async function rpc(method, params) { @@ -46,8 +47,18 @@ async function waitForDeploy(deployHash, maxWait = 240000) { while (Date.now() - start < maxWait) { try { const result = await rpc('info_get_deploy', { deploy_hash: deployHash }); - if (result.execution_results && result.execution_results.length > 0) return result; - } catch (_) {} + const er = result.execution_info && result.execution_info.execution_result; + if (er && er.Version2) { + if (er.Version2.error_message) throw new Error(`install failed: ${er.Version2.error_message}`); + return result; + } + if (er && er.Version1) { + if (er.Version1.Failure) throw new Error(`install failed: ${er.Version1.Failure.error_message || 'unknown'}`); + return result; + } + } catch (error) { + if (/install failed/.test(error.message || '')) throw error; + } await new Promise(r => setTimeout(r, 5000)); process.stdout.write('.'); } @@ -55,6 +66,9 @@ async function waitForDeploy(deployHash, maxWait = 240000) { } async function main() { + if (process.env.ALLOW_MAINNET_DEPLOY !== 'I_UNDERSTAND_THIS_SPENDS_REAL_CSPR') { + throw new Error('Set ALLOW_MAINNET_DEPLOY=I_UNDERSTAND_THIS_SPENDS_REAL_CSPR for an intentional mainnet install'); + } const keyPath = path.join(KEYS_DIR, 'secret_key.pem'); if (!fs.existsSync(keyPath)) { console.error('❌ No mainnet keypair found. Run: node keygen-mainnet.js'); @@ -70,6 +84,7 @@ async function main() { } const wasm = new Uint8Array(fs.readFileSync(WASM_PATH)); console.log(`📦 Wasm: ${(wasm.length / 1024).toFixed(1)} KB`); + console.log(`🔒 Wasm SHA-256: ${crypto.createHash('sha256').update(wasm).digest('hex')}`); // Verify connection + that we're really on mainnet const status = await rpc('info_get_status', {}); @@ -104,12 +119,7 @@ async function main() { console.log('🔗 Explorer: ', `https://cspr.live/deploy/${deployHash}`); console.log('\n⏳ Waiting for execution'); - const execResult = await waitForDeploy(deployHash); - const outcome = execResult.execution_results[0]?.result; - if (outcome?.Failure) { - console.error('\n❌ Deploy failed:', outcome.Failure.error_message); - process.exit(1); - } + await waitForDeploy(deployHash); console.log('\n\n🔍 Fetching contract hash from account named keys...'); const accountResult = await rpc('state_get_account_info', { public_key: keypair.publicKey.toHex() }); @@ -130,6 +140,7 @@ async function main() { `NODE_URL=${NODE_URL}\nNETWORK_NAME=${NETWORK}\nKEYS_DIR=./keys-mainnet\nCONTRACT_HASH=${contractHash}\n` ); console.log('\n📝 Saved contract hash to .env.mainnet.out'); + console.log('Release remains quarantined until deployments/casper-v2.json is independently verified.'); } main().catch(err => { diff --git a/demo/deploy.js b/demo/deploy.js index 4b5ba11..699f42b 100644 --- a/demo/deploy.js +++ b/demo/deploy.js @@ -12,6 +12,7 @@ require('dotenv').config(); const { CasperClient, DeployUtil, Keys, RuntimeArgs } = require('casper-js-sdk'); const fs = require('fs'); +const crypto = require('crypto'); const path = require('path'); const NODE_URL = process.env.NODE_URL || 'https://node.testnet.casper.network/rpc'; @@ -37,7 +38,9 @@ async function main() { process.exit(1); } const wasm = new Uint8Array(fs.readFileSync(WASM_PATH)); + const wasmSha256 = crypto.createHash('sha256').update(wasm).digest('hex'); console.log(`📦 Wasm size: ${(wasm.length / 1024).toFixed(1)} KB`); + console.log(`🔒 Wasm SHA-256: ${wasmSha256}`); const client = new CasperClient(NODE_URL); @@ -60,13 +63,7 @@ async function main() { // Wait for inclusion console.log('\n⏳ Waiting for execution (~60s)...'); - const result = await waitForDeploy(client, deployHash); - const execResult = result[1].execution_results[0]?.result; - - if (execResult?.Failure) { - console.error('❌ Deploy failed:', execResult.Failure.error_message); - process.exit(1); - } + await waitForDeploy(deployHash); // Retrieve contract hash from account named keys console.log('\n🔍 Fetching contract hash...'); @@ -88,17 +85,30 @@ async function main() { console.log('📝 Add to .env:'); console.log(` CONTRACT_HASH=${contractHash}`); - // Save to file - fs.appendFileSync('.env', `\nCONTRACT_HASH=${contractHash}\n`); + console.log('Release remains quarantined until deployments/casper-v2.json is independently verified.'); } -async function waitForDeploy(client, deployHash, maxWait = 120000) { +async function waitForDeploy(deployHash, maxWait = 120000) { const start = Date.now(); while (Date.now() - start < maxWait) { try { - const result = await client.getDeploy(deployHash); - if (result[1].execution_results.length > 0) return result; - } catch (_) {} + const response = await fetch(NODE_URL, { + method: 'POST', headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'info_get_deploy', params: { deploy_hash: deployHash } }), + }); + const result = await response.json(); + const er = result && result.result && result.result.execution_info && result.result.execution_info.execution_result; + if (er && er.Version2) { + if (er.Version2.error_message) throw new Error(`install failed: ${er.Version2.error_message}`); + return result; + } + if (er && er.Version1) { + if (er.Version1.Failure) throw new Error(`install failed: ${er.Version1.Failure.error_message || 'unknown'}`); + return result; + } + } catch (error) { + if (/install failed/.test(error.message || '')) throw error; + } await new Promise(r => setTimeout(r, 3000)); } throw new Error('Deploy timed out after 2 minutes'); diff --git a/demo/package.json b/demo/package.json index 756ab7b..e2a536b 100644 --- a/demo/package.json +++ b/demo/package.json @@ -9,7 +9,8 @@ "agent-demo": "node agent-compute-demo.js", "dashboard": "node serve-dashboard.js", "mcp": "node casper-mcp.mjs", - "mcp:test": "node test-mcp.mjs" + "mcp:test": "node test-mcp.mjs", + "test": "node --test test/*.test.js" }, "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", diff --git a/demo/settlement-verifier.js b/demo/settlement-verifier.js new file mode 100644 index 0000000..760a476 --- /dev/null +++ b/demo/settlement-verifier.js @@ -0,0 +1,68 @@ +'use strict'; + +function normalizeHash(value) { + return typeof value === 'string' ? value.toLowerCase().replace(/^hash-/, '') : null; +} + +function readSessionArgs(raw) { + const session = raw && raw.deploy && raw.deploy.session; + const stored = session && (session.StoredContractByHash || session.StoredVersionedContractByHash); + if (!stored || !Array.isArray(stored.args)) return null; + const out = { entry_point: stored.entry_point, contract_hash: stored.hash }; + for (const pair of stored.args) { + if (!Array.isArray(pair) || pair.length < 2) continue; + const [name, clv] = pair; + out[name] = clv && (clv.parsed !== undefined ? clv.parsed : clv); + } + return out; +} + +function validateExecutedSettlement(rpc, expected) { + if (!rpc) return { ok: false, reason: 'deploy_not_found' }; + const result = rpc.execution_info && rpc.execution_info.execution_result; + if (!result) return { ok: false, reason: 'deploy_not_executed_yet' }; + if (result.Version2) { + if (result.Version2.error_message) { + return { ok: false, reason: `deploy_failed_on_chain: ${result.Version2.error_message}` }; + } + } else if (result.Version1) { + if (result.Version1.Failure) { + return { + ok: false, + reason: `deploy_failed_on_chain: ${result.Version1.Failure.error_message || 'unknown'}`, + }; + } + if (!result.Version1.Success) return { ok: false, reason: 'deploy_not_successful' }; + } else { + return { ok: false, reason: 'deploy_not_successful' }; + } + + const args = readSessionArgs(rpc); + if (!args) return { ok: false, reason: 'unparseable_session_args' }; + if (normalizeHash(args.contract_hash) !== normalizeHash(expected.contract_hash)) { + return { ok: false, reason: 'contract_hash_mismatch' }; + } + if (args.entry_point !== 'pay_agent') return { ok: false, reason: 'wrong_entry_point' }; + for (const key of ['request_id', 'from_agent', 'to_agent', 'amount']) { + if (args[key] == null) return { ok: false, reason: `missing_${key}` }; + } + if (String(args.request_id) !== String(expected.request_id)) { + return { ok: false, reason: 'request_id_mismatch' }; + } + if (String(args.from_agent) !== String(expected.from_agent)) { + return { ok: false, reason: 'payer_mismatch' }; + } + if (String(args.to_agent) !== String(expected.to_agent)) { + return { ok: false, reason: 'recipient_mismatch' }; + } + try { + if (BigInt(String(args.amount)) !== BigInt(String(expected.amount_motes))) { + return { ok: false, reason: 'amount_mismatch' }; + } + } catch { + return { ok: false, reason: 'amount_invalid' }; + } + return { ok: true }; +} + +module.exports = { readSessionArgs, validateExecutedSettlement }; diff --git a/demo/test/settlement-verifier.test.js b/demo/test/settlement-verifier.test.js new file mode 100644 index 0000000..cac50a6 --- /dev/null +++ b/demo/test/settlement-verifier.test.js @@ -0,0 +1,75 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { validateExecutedSettlement } = require('../settlement-verifier'); +const { assertTrustedContract } = require('../trusted-contract'); + +const expected = { + contract_hash: 'hash-aabbcc', + request_id: 'order-1', + from_agent: 'buyer-1', + to_agent: 'merchant-1', + amount_motes: '100000000', +}; + +function rpc(patch = {}) { + const values = { ...expected, amount: expected.amount_motes, ...patch }; + return { + execution_info: { execution_result: { Version2: { error_message: null } } }, + deploy: { + session: { + StoredContractByHash: { + hash: values.contract_hash, + entry_point: values.entry_point || 'pay_agent', + args: [ + ['request_id', { parsed: values.request_id }], + ['from_agent', { parsed: values.from_agent }], + ['to_agent', { parsed: values.to_agent }], + ['amount', { parsed: values.amount }], + ].filter(([name]) => !values.omit || values.omit !== name), + }, + }, + }, + }; +} + +test('accepts only the exact successful settlement', () => { + assert.deepEqual(validateExecutedSettlement(rpc(), expected), { ok: true }); +}); + +for (const [name, patch, reason] of [ + ['contract', { contract_hash: 'hash-deadbeef' }, 'contract_hash_mismatch'], + ['entry point', { entry_point: 'register_agent' }, 'wrong_entry_point'], + ['request', { request_id: 'order-2' }, 'request_id_mismatch'], + ['payer', { from_agent: 'attacker' }, 'payer_mismatch'], + ['recipient', { to_agent: 'attacker' }, 'recipient_mismatch'], + ['underpayment', { amount: '99999999' }, 'amount_mismatch'], + ['overpayment', { amount: '100000001' }, 'amount_mismatch'], + ['invalid amount', { amount: 'not-a-number' }, 'amount_invalid'], +]) { + test(`rejects wrong ${name}`, () => { + assert.equal(validateExecutedSettlement(rpc(patch), expected).reason, reason); + }); +} + +test('rejects missing required arguments instead of accepting execution success', () => { + assert.equal(validateExecutedSettlement(rpc({ omit: 'amount' }), expected).reason, 'missing_amount'); +}); + +test('rejects an unparseable session instead of accepting execution success', () => { + const value = rpc(); + value.deploy.session = { ModuleBytes: { module_bytes: '', args: [] } }; + assert.equal(validateExecutedSettlement(value, expected).reason, 'unparseable_session_args'); +}); + +test('rejects failed and pending deploys', () => { + const failed = rpc(); + failed.execution_info.execution_result.Version2.error_message = 'revert'; + assert.match(validateExecutedSettlement(failed, expected).reason, /deploy_failed_on_chain/); + assert.equal(validateExecutedSettlement({ deploy: rpc().deploy }, expected).reason, 'deploy_not_executed_yet'); +}); + +test('payment entry points remain quarantined until the v2 manifest is verified', () => { + assert.throws(() => assertTrustedContract('hash-aabbcc'), /payments are quarantined/); +}); diff --git a/demo/trusted-contract.js b/demo/trusted-contract.js new file mode 100644 index 0000000..27b6c1a --- /dev/null +++ b/demo/trusted-contract.js @@ -0,0 +1,27 @@ +'use strict'; + +// Payment routes consume the reviewed deployment manifest. Environment +// variables cannot override trust. Deploying alone is insufficient. +const deployment = require('../deployments/casper-v2.json'); +const CASPER_V2_CONTRACT_HASH = deployment.contractHash; + +function normalize(value) { + return typeof value === 'string' ? value.toLowerCase().replace(/^(hash|contract)-/, '') : ''; +} + +function assertTrustedContract(candidate) { + const complete = deployment.status === 'verified' + && deployment.contractVersion === '2.0.0' + && /^(hash-|contract-)?[0-9a-f]{64}$/i.test(deployment.contractHash || '') + && /^[0-9a-f]{64}$/i.test(deployment.deployHash || '') + && /^[0-9a-f]{64}$/i.test(deployment.wasmSha256 || '') + && /^[0-9a-f]{40}$/i.test(deployment.sourceCommit || '') + && Boolean(deployment.deployedAt && deployment.verifiedAt); + if (!complete || normalize(candidate) !== normalize(CASPER_V2_CONTRACT_HASH)) { + throw new Error( + 'Casper payments are quarantined until the audited v2 deployment manifest is complete and verified.', + ); + } +} + +module.exports = { CASPER_V2_CONTRACT_HASH, assertTrustedContract, deployment }; diff --git a/deployments/casper-v2.json b/deployments/casper-v2.json new file mode 100644 index 0000000..2627ebb --- /dev/null +++ b/deployments/casper-v2.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": 1, + "contractVersion": "2.0.0", + "network": "casper", + "status": "source_only", + "contractHash": null, + "deployHash": null, + "wasmSha256": null, + "sourceCommit": null, + "deployedAt": null, + "verifiedAt": null +} diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md index 4ad9111..45f72bc 100644 --- a/docs/DEPLOYMENT.md +++ b/docs/DEPLOYMENT.md @@ -1,24 +1,31 @@ -# Deployment Guide — AiFinPay × Casper +# Deployment Guide — AiFinPay × Casper v2 + +> **Release gate:** existing v1 testnet/mainnet hashes are quarantined. A new +> deployment is not trusted until its Wasm hash, source commit, deploy result, +> contract hash and independent verification are committed to +> `deployments/casper-v2.json` with `status: verified`. ## Prerequisites ```bash -# Rust + wasm32 target +# The CI/release toolchain is intentionally pinned. curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -rustup target add wasm32-unknown-unknown +rustup toolchain install nightly-2025-02-04 --profile minimal +rustup target add wasm32-unknown-unknown --toolchain nightly-2025-02-04 -# Node.js 18+ +# Node.js 24+ node --version # Install demo dependencies -cd demo && npm install +cd demo && npm ci ``` ## Step 1 — Build the Wasm ```bash cd aifinpay-casper/ -cargo build --release +cargo +nightly-2025-02-04 build --release --locked --target wasm32-unknown-unknown +sha256sum target/wasm32-unknown-unknown/release/aifinpay_casper.wasm ``` Output: `target/wasm32-unknown-unknown/release/aifinpay_casper.wasm` (~55KB) @@ -30,7 +37,9 @@ cd demo/ node keygen.js ``` -This creates: +Generate two separately controlled and funded accounts. Each agent may only +register the account that signed the deploy. Never use one key for buyer and +provider. Key generation creates: - `keys/secret_key.pem` — private key (keep secret, never commit) - `keys/public_key.pem` — public key - `keys/public_key_hex.txt` — hex public key @@ -64,7 +73,11 @@ Contract hash: hash-xxxxxxxx... Explorer: https://testnet.cspr.live/contract/xxxxxxxx... ``` -The contract hash is auto-appended to `.env`. +Deployment alone does not enable payment traffic. Record the final successful +deploy hash, contract hash, exact Wasm SHA-256, 40-character source commit and +UTC deployment time in `deployments/casper-v2.json`. Independently query +`info_get_deploy`, compare the installed Wasm/source build, then add +`verifiedAt` and change `status` to `verified` in a reviewed commit. ## Step 5 — Run Demo Flow @@ -72,10 +85,10 @@ The contract hash is auto-appended to `.env`. node demo.js ``` -This will: +Set `PROVIDER_KEYS_DIR` to the separately funded provider key. This will: 1. Register `aifinpay-agent-001` on-chain -2. Register `aifinpay-agent-002` on-chain -3. Settle a payment (2.5 CSPR, request ID: `req-001`) +2. Have the provider self-register `aifinpay-agent-002` +3. Atomically transfer and record 2.5 CSPR (`req-001`) 4. Print all transaction hashes + explorer links ## Step 6 — Verify On-Chain @@ -122,4 +135,24 @@ Paste the contract hash → click Connect → live data loads from Casper RPC. **"Wasm not found"** — Run `cargo build --release` from the root directory first. -**Contract hash not in named keys** — Wait 30s after deploy hash confirms, then re-run `node deploy.js` (it won't re-deploy, just fetches the hash). +**Payments are quarantined** — this is expected until the v2 manifest is +complete, independently verified and committed. `CONTRACT_HASH` cannot bypass +the gate. + +**Contract hash not in named keys** — inspect the successful deploy/account +state directly. Do not rerun the deploy script blindly because it creates a new +contract. + +## Production sign-off + +Before changing the manifest to `verified`, all of the following must be true: + +- blocking CI succeeded for the exact source commit and its uploaded Wasm; +- the artifact SHA-256 equals `wasmSha256` in the manifest; +- `info_get_deploy` reports success on the expected chain; +- `contractHash` belongs to that install deploy and exposes the v2 entry points; +- buyer and provider use distinct accounts and self-registration was tested; +- zero amount, forged payer, duplicate request, self-payment and malformed ID + calls revert; a valid payment changes recipient balance by the exact amount; +- bridge regression tests pass and the SDK/MCP address comes from the reviewed + manifest. diff --git a/src/main.rs b/src/main.rs index dd44990..027157b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,10 +10,11 @@ use alloc::{ }; use casper_contract::{ - contract_api::{runtime, storage}, + contract_api::{runtime, storage, system}, unwrap_or_revert::UnwrapOrRevert, }; use casper_types::{ + account::AccountHash, CLType, CLValue, EntityEntryPoint, EntryPointAccess, EntryPointPayment, EntryPointType, EntryPoints, Key, Parameter, URef, U512, api_error::ApiError, @@ -47,6 +48,13 @@ const ERR_MISSING_KEY: u16 = 1; const ERR_ALREADY_REGISTERED: u16 = 100; const ERR_AGENT_NOT_FOUND: u16 = 101; const ERR_ALREADY_SETTLED: u16 = 102; +const ERR_UNAUTHORIZED: u16 = 103; +const ERR_INVALID_WALLET: u16 = 104; +const ERR_INVALID_IDENTIFIER: u16 = 105; +const ERR_INVALID_AMOUNT: u16 = 106; +const ERR_SELF_PAYMENT: u16 = 107; +const ERR_TRANSFER_FAILED: u16 = 108; +const ERR_OVERFLOW: u16 = 109; // ── Helpers ─────────────────────────────────────────────────────────────────── @@ -67,6 +75,30 @@ fn write_u64(key: &str, value: u64) { storage::write(get_uref(key), value); } +fn checked_increment(value: u64) -> u64 { + value + .checked_add(1) + .unwrap_or_revert_with(ApiError::User(ERR_OVERFLOW)) +} + +fn valid_identifier(value: &str) -> bool { + !value.is_empty() + && value.len() <= 64 + && value.bytes().all(|byte| { + byte.is_ascii_alphanumeric() + || byte == b'-' + || byte == b'_' + || byte == b'.' + || byte == b':' + }) +} + +fn require_identifier(value: &str) { + if !valid_identifier(value) { + runtime::revert(ApiError::User(ERR_INVALID_IDENTIFIER)); + } +} + fn emit_event(event_type: &str, payload: &str) { let seed = get_uref(KEY_EVENTS); let idx = read_u64(KEY_EVENT_COUNT); @@ -75,7 +107,7 @@ fn emit_event(event_type: &str, payload: &str) { &format!("evt_{}", idx), format!("{{\"type\":\"{}\",\"payload\":{}}}", event_type, payload), ); - write_u64(KEY_EVENT_COUNT, idx + 1); + write_u64(KEY_EVENT_COUNT, checked_increment(idx)); } // ── Entry points ────────────────────────────────────────────────────────────── @@ -86,6 +118,12 @@ fn emit_event(event_type: &str, payload: &str) { pub extern "C" fn register_agent() { let agent_id: String = runtime::get_named_arg(ARG_AGENT_ID); let wallet: String = runtime::get_named_arg(ARG_WALLET); + require_identifier(&agent_id); + + let caller_wallet = runtime::get_caller().to_formatted_string(); + if wallet != caller_wallet { + runtime::revert(ApiError::User(ERR_UNAUTHORIZED)); + } let seed = get_uref(KEY_AGENTS); @@ -113,19 +151,38 @@ pub extern "C" fn pay_agent() { let to_agent: String = runtime::get_named_arg(ARG_TO_AGENT); let amount: U512 = runtime::get_named_arg(ARG_AMOUNT); let request_id: String = runtime::get_named_arg(ARG_REQUEST_ID); + require_identifier(&from_agent); + require_identifier(&to_agent); + require_identifier(&request_id); + if amount.is_zero() { + runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); + } + if from_agent == to_agent { + runtime::revert(ApiError::User(ERR_SELF_PAYMENT)); + } let agents_seed = get_uref(KEY_AGENTS); let payments_seed = get_uref(KEY_PAYMENTS); // Both agents must be registered - let _: String = storage::dictionary_get(agents_seed, &from_agent) + let from_wallet: String = storage::dictionary_get(agents_seed, &from_agent) .unwrap_or_revert() .unwrap_or_revert_with(ApiError::User(ERR_AGENT_NOT_FOUND)); - let _: String = storage::dictionary_get(agents_seed, &to_agent) + let to_wallet: String = storage::dictionary_get(agents_seed, &to_agent) .unwrap_or_revert() .unwrap_or_revert_with(ApiError::User(ERR_AGENT_NOT_FOUND)); + let caller_wallet = runtime::get_caller().to_formatted_string(); + if from_wallet != caller_wallet { + runtime::revert(ApiError::User(ERR_UNAUTHORIZED)); + } + if from_wallet == to_wallet { + runtime::revert(ApiError::User(ERR_SELF_PAYMENT)); + } + let destination = AccountHash::from_formatted_str(&to_wallet) + .unwrap_or_revert_with(ApiError::User(ERR_INVALID_WALLET)); + // Idempotent — reject duplicate request IDs let existing: Option = storage::dictionary_get(payments_seed, &request_id).unwrap_or_revert(); @@ -133,6 +190,11 @@ pub extern "C" fn pay_agent() { runtime::revert(ApiError::User(ERR_ALREADY_SETTLED)); } + // Atomic economic settlement. If the transfer fails, contract execution + // reverts before a receipt or event can be written. + system::transfer_to_account(destination, amount, None) + .unwrap_or_revert_with(ApiError::User(ERR_TRANSFER_FAILED)); + // Record settlement on-chain let record = format!( "{{\"from\":\"{}\",\"to\":\"{}\",\"amount\":\"{}\",\"request_id\":\"{}\",\"status\":\"SETTLED\"}}", @@ -141,7 +203,7 @@ pub extern "C" fn pay_agent() { storage::dictionary_put(payments_seed, &request_id, record); let count = read_u64(KEY_PAYMENT_COUNT); - write_u64(KEY_PAYMENT_COUNT, count + 1); + write_u64(KEY_PAYMENT_COUNT, checked_increment(count)); // Emit PaymentSettled event emit_event( From a7120cc98ee67bd5218d6647f4c951662b0f7c60 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Tue, 4 Aug 2026 16:16:28 +0300 Subject: [PATCH 02/24] fix: handle Casper account parsing explicitly --- src/main.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main.rs b/src/main.rs index 027157b..ae66243 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,11 +14,9 @@ use casper_contract::{ unwrap_or_revert::UnwrapOrRevert, }; use casper_types::{ - account::AccountHash, - CLType, CLValue, EntityEntryPoint, EntryPointAccess, EntryPointPayment, - EntryPointType, EntryPoints, Key, Parameter, URef, U512, - api_error::ApiError, - contracts::NamedKeys, + account::AccountHash, api_error::ApiError, contracts::NamedKeys, CLType, CLValue, + EntityEntryPoint, EntryPointAccess, EntryPointPayment, EntryPointType, EntryPoints, Key, + Parameter, URef, U512, }; // ── Storage keys ───────────────────────────────────────────────────────────── @@ -180,8 +178,10 @@ pub extern "C" fn pay_agent() { if from_wallet == to_wallet { runtime::revert(ApiError::User(ERR_SELF_PAYMENT)); } - let destination = AccountHash::from_formatted_str(&to_wallet) - .unwrap_or_revert_with(ApiError::User(ERR_INVALID_WALLET)); + let destination = match AccountHash::from_formatted_str(&to_wallet) { + Ok(account_hash) => account_hash, + Err(_) => runtime::revert(ApiError::User(ERR_INVALID_WALLET)), + }; // Idempotent — reject duplicate request IDs let existing: Option = From e4309b1f08782c1ac1772490cd0c3ecfb779c6d0 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 03:54:33 +0300 Subject: [PATCH 03/24] feat(casper): replace chain-local identity with canonical route settlement v3 --- src/main.rs | 327 ++++++++++++++++++++++++++++++++++------------------ 1 file changed, 215 insertions(+), 112 deletions(-) diff --git a/src/main.rs b/src/main.rs index ae66243..128114d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,32 +19,45 @@ use casper_types::{ Parameter, URef, U512, }; +// AiFinPay Casper settlement v3 is a VALUE SETTLEMENT contract only. +// Global AIFP-3 Agent Passport identity (@username + immutable Agent ID + signed +// wallet bindings) lives above the chain and MUST NOT be re-created here. + // ── Storage keys ───────────────────────────────────────────────────────────── -const KEY_AGENTS: &str = "agents"; const KEY_PAYMENTS: &str = "payments"; const KEY_EVENTS: &str = "events"; const KEY_PAYMENT_COUNT: &str = "payment_count"; const KEY_EVENT_COUNT: &str = "event_count"; -const KEY_CONTRACT_HASH: &str = "aifinpay_casper_hash"; -const KEY_CONTRACT_VERSION: &str = "aifinpay_casper_version"; +const KEY_ADMIN: &str = "admin"; +const KEY_TREASURY: &str = "treasury"; +const KEY_PAUSED: &str = "paused"; +const KEY_CONTRACT_HASH: &str = "aifinpay_casper_v3_hash"; +const KEY_CONTRACT_VERSION: &str = "aifinpay_casper_v3_version"; -// ── Entry point names ───────────────────────────────────────────────────────── -const EP_REGISTER_AGENT: &str = "register_agent"; -const EP_PAY_AGENT: &str = "pay_agent"; +// ── Entry points ────────────────────────────────────────────────────────────── +const EP_PAY: &str = "pay"; +const EP_SET_PAUSED: &str = "set_paused"; +const EP_SET_TREASURY: &str = "set_treasury"; +const EP_SET_ADMIN: &str = "set_admin"; const EP_GET_PAYMENT_COUNT: &str = "get_payment_count"; -// ── Argument names ──────────────────────────────────────────────────────────── -const ARG_AGENT_ID: &str = "agent_id"; -const ARG_WALLET: &str = "wallet"; -const ARG_FROM_AGENT: &str = "from_agent"; -const ARG_TO_AGENT: &str = "to_agent"; -const ARG_AMOUNT: &str = "amount"; +// ── Arguments ──────────────────────────────────────────────────────────────── +const ARG_ROUTE: &str = "route"; +const ARG_MERCHANT: &str = "merchant"; +const ARG_GROSS_AMOUNT: &str = "gross_amount"; const ARG_REQUEST_ID: &str = "request_id"; +const ARG_VALID_UNTIL_MS: &str = "valid_until_ms"; +const ARG_PAUSED: &str = "paused"; +const ARG_TREASURY: &str = "treasury"; +const ARG_ADMIN: &str = "admin"; + +// ── Canonical economic profiles ───────────────────────────────────────────── +const ROUTE_AIFP1: u8 = 1; // merchant traffic monetisation: 99/1/0 FROM gross +const ROUTE_AIFP2: u8 = 2; // x402 agent payment: 100/0/0 +const EXPIRY_MAX_AHEAD_MS: u64 = 20 * 60 * 1000; -// ── Error codes ─────────────────────────────────────────────────────────────── +// ── Error codes (stable for SDK/E2E assertions) ────────────────────────────── const ERR_MISSING_KEY: u16 = 1; -const ERR_ALREADY_REGISTERED: u16 = 100; -const ERR_AGENT_NOT_FOUND: u16 = 101; const ERR_ALREADY_SETTLED: u16 = 102; const ERR_UNAUTHORIZED: u16 = 103; const ERR_INVALID_WALLET: u16 = 104; @@ -53,8 +66,11 @@ const ERR_INVALID_AMOUNT: u16 = 106; const ERR_SELF_PAYMENT: u16 = 107; const ERR_TRANSFER_FAILED: u16 = 108; const ERR_OVERFLOW: u16 = 109; - -// ── Helpers ─────────────────────────────────────────────────────────────────── +const ERR_INVALID_ROUTE: u16 = 110; +const ERR_PAUSED: u16 = 111; +const ERR_EXPIRED: u16 = 112; +const ERR_EXPIRY_TOO_FAR: u16 = 113; +const ERR_FEE_ROUNDS_TO_ZERO: u16 = 114; fn get_uref(name: &str) -> URef { match runtime::get_key(name).unwrap_or_revert_with(ApiError::User(ERR_MISSING_KEY)) { @@ -73,6 +89,26 @@ fn write_u64(key: &str, value: u64) { storage::write(get_uref(key), value); } +fn read_string(key: &str) -> String { + storage::read::(get_uref(key)) + .unwrap_or_revert() + .unwrap_or_revert_with(ApiError::User(ERR_MISSING_KEY)) +} + +fn write_string(key: &str, value: String) { + storage::write(get_uref(key), value); +} + +fn read_bool(key: &str) -> bool { + storage::read::(get_uref(key)) + .unwrap_or_revert() + .unwrap_or(true) +} + +fn write_bool(key: &str, value: bool) { + storage::write(get_uref(key), value); +} + fn checked_increment(value: u64) -> u64 { value .checked_add(1) @@ -81,7 +117,7 @@ fn checked_increment(value: u64) -> u64 { fn valid_identifier(value: &str) -> bool { !value.is_empty() - && value.len() <= 64 + && value.len() <= 128 && value.bytes().all(|byte| { byte.is_ascii_alphanumeric() || byte == b'-' @@ -97,6 +133,18 @@ fn require_identifier(value: &str) { } } +fn parse_account(value: &str) -> AccountHash { + AccountHash::from_formatted_str(value) + .unwrap_or_else(|_| runtime::revert(ApiError::User(ERR_INVALID_WALLET))) +} + +fn require_admin() { + let caller = runtime::get_caller().to_formatted_string(); + if caller != read_string(KEY_ADMIN) { + runtime::revert(ApiError::User(ERR_UNAUTHORIZED)); + } +} + fn emit_event(event_type: &str, payload: &str) { let seed = get_uref(KEY_EVENTS); let idx = read_u64(KEY_EVENT_COUNT); @@ -108,130 +156,158 @@ fn emit_event(event_type: &str, payload: &str) { write_u64(KEY_EVENT_COUNT, checked_increment(idx)); } -// ── Entry points ────────────────────────────────────────────────────────────── - -/// Register an AI agent with the settlement layer. -/// Args: agent_id (String), wallet (String) -#[no_mangle] -pub extern "C" fn register_agent() { - let agent_id: String = runtime::get_named_arg(ARG_AGENT_ID); - let wallet: String = runtime::get_named_arg(ARG_WALLET); - require_identifier(&agent_id); - - let caller_wallet = runtime::get_caller().to_formatted_string(); - if wallet != caller_wallet { - runtime::revert(ApiError::User(ERR_UNAUTHORIZED)); +fn split_gross(route: u8, gross: U512) -> (U512, U512) { + if gross.is_zero() { + runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); } - - let seed = get_uref(KEY_AGENTS); - - let existing: Option = storage::dictionary_get(seed, &agent_id).unwrap_or_revert(); - if existing.is_some() { - runtime::revert(ApiError::User(ERR_ALREADY_REGISTERED)); + match route { + ROUTE_AIFP1 => { + // Exact 1% = floor(gross / 100); no multiplication overflow path. + let treasury = gross / U512::from(100u64); + if treasury.is_zero() { + runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)); + } + let merchant = gross - treasury; + (merchant, treasury) + } + ROUTE_AIFP2 => (gross, U512::zero()), + _ => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), } - - storage::dictionary_put(seed, &agent_id, wallet.clone()); - - emit_event( - "AgentRegistered", - &format!( - "{{\"agent_id\":\"{}\",\"wallet\":\"{}\"}}", - agent_id, wallet - ), - ); } -/// Settle a payment between two registered AI agents and emit PaymentSettled. -/// Args: from_agent (String), to_agent (String), amount (U512 motes), request_id (String) -#[no_mangle] -pub extern "C" fn pay_agent() { - let from_agent: String = runtime::get_named_arg(ARG_FROM_AGENT); - let to_agent: String = runtime::get_named_arg(ARG_TO_AGENT); - let amount: U512 = runtime::get_named_arg(ARG_AMOUNT); - let request_id: String = runtime::get_named_arg(ARG_REQUEST_ID); - require_identifier(&from_agent); - require_identifier(&to_agent); - require_identifier(&request_id); - if amount.is_zero() { - runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); +fn validate_expiry(valid_until_ms: u64) { + let now_ms = runtime::get_blocktime().value(); + if valid_until_ms < now_ms { + runtime::revert(ApiError::User(ERR_EXPIRED)); } - if from_agent == to_agent { - runtime::revert(ApiError::User(ERR_SELF_PAYMENT)); + let max = now_ms + .checked_add(EXPIRY_MAX_AHEAD_MS) + .unwrap_or_revert_with(ApiError::User(ERR_OVERFLOW)); + if valid_until_ms > max { + runtime::revert(ApiError::User(ERR_EXPIRY_TOO_FAR)); } +} - let agents_seed = get_uref(KEY_AGENTS); - let payments_seed = get_uref(KEY_PAYMENTS); +// ── Settlement ─────────────────────────────────────────────────────────────── + +/// Canonical CSPR settlement. +/// +/// Args: +/// - route: 1=AIFP-1 (99/1/0), 2=AIFP-2 (100/0/0) +/// - merchant: formatted `account-hash-...` +/// - gross_amount: payer total in motes +/// - request_id: unique idempotency/payment id +/// - valid_until_ms: block-time expiry, max 20 minutes ahead +/// +/// The caller is the payer. No caller-supplied `from_agent` is accepted. +#[no_mangle] +pub extern "C" fn pay() { + if read_bool(KEY_PAUSED) { + runtime::revert(ApiError::User(ERR_PAUSED)); + } - // Both agents must be registered - let from_wallet: String = storage::dictionary_get(agents_seed, &from_agent) - .unwrap_or_revert() - .unwrap_or_revert_with(ApiError::User(ERR_AGENT_NOT_FOUND)); + let route: u8 = runtime::get_named_arg(ARG_ROUTE); + let merchant_raw: String = runtime::get_named_arg(ARG_MERCHANT); + let gross: U512 = runtime::get_named_arg(ARG_GROSS_AMOUNT); + let request_id: String = runtime::get_named_arg(ARG_REQUEST_ID); + let valid_until_ms: u64 = runtime::get_named_arg(ARG_VALID_UNTIL_MS); - let to_wallet: String = storage::dictionary_get(agents_seed, &to_agent) - .unwrap_or_revert() - .unwrap_or_revert_with(ApiError::User(ERR_AGENT_NOT_FOUND)); + require_identifier(&request_id); + validate_expiry(valid_until_ms); - let caller_wallet = runtime::get_caller().to_formatted_string(); - if from_wallet != caller_wallet { - runtime::revert(ApiError::User(ERR_UNAUTHORIZED)); - } - if from_wallet == to_wallet { + let payer = runtime::get_caller(); + let merchant = parse_account(&merchant_raw); + if payer == merchant { runtime::revert(ApiError::User(ERR_SELF_PAYMENT)); } - let destination = match AccountHash::from_formatted_str(&to_wallet) { - Ok(account_hash) => account_hash, - Err(_) => runtime::revert(ApiError::User(ERR_INVALID_WALLET)), - }; - // Idempotent — reject duplicate request IDs + let treasury_raw = read_string(KEY_TREASURY); + let treasury = parse_account(&treasury_raw); + let (merchant_amount, treasury_amount) = split_gross(route, gross); + + // Replay is checked BEFORE any transfer. Casper execution reverts atomically + // on a later transfer failure, so no receipt survives an unsuccessful pay. + let payments_seed = get_uref(KEY_PAYMENTS); let existing: Option = storage::dictionary_get(payments_seed, &request_id).unwrap_or_revert(); if existing.is_some() { runtime::revert(ApiError::User(ERR_ALREADY_SETTLED)); } - // Atomic economic settlement. If the transfer fails, contract execution - // reverts before a receipt or event can be written. - system::transfer_to_account(destination, amount, None) + system::transfer_to_account(merchant, merchant_amount, None) .unwrap_or_revert_with(ApiError::User(ERR_TRANSFER_FAILED)); + if !treasury_amount.is_zero() { + system::transfer_to_account(treasury, treasury_amount, None) + .unwrap_or_revert_with(ApiError::User(ERR_TRANSFER_FAILED)); + } - // Record settlement on-chain + let payer_raw = payer.to_formatted_string(); let record = format!( - "{{\"from\":\"{}\",\"to\":\"{}\",\"amount\":\"{}\",\"request_id\":\"{}\",\"status\":\"SETTLED\"}}", - from_agent, to_agent, amount, request_id + "{{\"route\":{},\"payer\":\"{}\",\"merchant\":\"{}\",\"gross_amount\":\"{}\",\"merchant_amount\":\"{}\",\"treasury_amount\":\"{}\",\"creator_amount\":\"0\",\"request_id\":\"{}\",\"valid_until_ms\":{},\"status\":\"SETTLED\"}}", + route, + payer_raw, + merchant_raw, + gross, + merchant_amount, + treasury_amount, + request_id, + valid_until_ms ); - storage::dictionary_put(payments_seed, &request_id, record); - + storage::dictionary_put(payments_seed, &request_id, record.clone()); let count = read_u64(KEY_PAYMENT_COUNT); write_u64(KEY_PAYMENT_COUNT, checked_increment(count)); + emit_event("PaymentSettled", &record); +} + +#[no_mangle] +pub extern "C" fn set_paused() { + require_admin(); + let paused: bool = runtime::get_named_arg(ARG_PAUSED); + write_bool(KEY_PAUSED, paused); + emit_event("PausedChanged", &format!("{{\"paused\":{}}}", paused)); +} + +#[no_mangle] +pub extern "C" fn set_treasury() { + require_admin(); + let treasury: String = runtime::get_named_arg(ARG_TREASURY); + let parsed = parse_account(&treasury); + write_string(KEY_TREASURY, parsed.to_formatted_string()); + emit_event( + "TreasuryChanged", + &format!("{{\"treasury\":\"{}\"}}", parsed.to_formatted_string()), + ); +} - // Emit PaymentSettled event +#[no_mangle] +pub extern "C" fn set_admin() { + require_admin(); + let admin: String = runtime::get_named_arg(ARG_ADMIN); + let parsed = parse_account(&admin); + write_string(KEY_ADMIN, parsed.to_formatted_string()); emit_event( - "PaymentSettled", - &format!( - "{{\"from\":\"{}\",\"to\":\"{}\",\"amount\":\"{}\",\"request_id\":\"{}\"}}", - from_agent, to_agent, amount, request_id - ), + "AdminChanged", + &format!("{{\"admin\":\"{}\"}}", parsed.to_formatted_string()), ); } -/// Returns total settled payments count. #[no_mangle] pub extern "C" fn get_payment_count() { let count = read_u64(KEY_PAYMENT_COUNT); runtime::ret(CLValue::from_t(count).unwrap_or_revert()); } -// ── Contract installation ───────────────────────────────────────────────────── - fn build_entry_points() -> EntryPoints { let mut eps = EntryPoints::new(); eps.add_entry_point(EntityEntryPoint::new( - EP_REGISTER_AGENT, + EP_PAY, vec![ - Parameter::new(ARG_AGENT_ID, CLType::String), - Parameter::new(ARG_WALLET, CLType::String), + Parameter::new(ARG_ROUTE, CLType::U8), + Parameter::new(ARG_MERCHANT, CLType::String), + Parameter::new(ARG_GROSS_AMOUNT, CLType::U512), + Parameter::new(ARG_REQUEST_ID, CLType::String), + Parameter::new(ARG_VALID_UNTIL_MS, CLType::U64), ], CLType::Unit, EntryPointAccess::Public, @@ -240,13 +316,26 @@ fn build_entry_points() -> EntryPoints { )); eps.add_entry_point(EntityEntryPoint::new( - EP_PAY_AGENT, - vec![ - Parameter::new(ARG_FROM_AGENT, CLType::String), - Parameter::new(ARG_TO_AGENT, CLType::String), - Parameter::new(ARG_AMOUNT, CLType::U512), - Parameter::new(ARG_REQUEST_ID, CLType::String), - ], + EP_SET_PAUSED, + vec![Parameter::new(ARG_PAUSED, CLType::Bool)], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Called, + EntryPointPayment::Caller, + )); + + eps.add_entry_point(EntityEntryPoint::new( + EP_SET_TREASURY, + vec![Parameter::new(ARG_TREASURY, CLType::String)], + CLType::Unit, + EntryPointAccess::Public, + EntryPointType::Called, + EntryPointPayment::Caller, + )); + + eps.add_entry_point(EntityEntryPoint::new( + EP_SET_ADMIN, + vec![Parameter::new(ARG_ADMIN, CLType::String)], CLType::Unit, EntryPointAccess::Public, EntryPointType::Called, @@ -265,28 +354,38 @@ fn build_entry_points() -> EntryPoints { eps } -/// Called once on deploy — installs the contract and initialises storage. +/// New install only. The installer becomes admin; treasury is explicit and +/// validated. Settlement starts PAUSED until deployment evidence and E2E are +/// reviewed. This avoids carrying unsafe v1/v2 state into the canonical route. #[no_mangle] pub extern "C" fn call() { - let agents_uref = storage::new_dictionary(KEY_AGENTS).unwrap_or_revert(); + let treasury_arg: String = runtime::get_named_arg(ARG_TREASURY); + let treasury = parse_account(&treasury_arg).to_formatted_string(); + let admin = runtime::get_caller().to_formatted_string(); + let payments_uref = storage::new_dictionary(KEY_PAYMENTS).unwrap_or_revert(); let events_uref = storage::new_dictionary(KEY_EVENTS).unwrap_or_revert(); let payment_count_uref: URef = storage::new_uref(0u64); let event_count_uref: URef = storage::new_uref(0u64); + let admin_uref: URef = storage::new_uref(admin.clone()); + let treasury_uref: URef = storage::new_uref(treasury.clone()); + let paused_uref: URef = storage::new_uref(true); let mut named_keys = NamedKeys::new(); - named_keys.insert(KEY_AGENTS.to_string(), Key::URef(agents_uref)); named_keys.insert(KEY_PAYMENTS.to_string(), Key::URef(payments_uref)); named_keys.insert(KEY_EVENTS.to_string(), Key::URef(events_uref)); named_keys.insert(KEY_PAYMENT_COUNT.to_string(), Key::URef(payment_count_uref)); named_keys.insert(KEY_EVENT_COUNT.to_string(), Key::URef(event_count_uref)); + named_keys.insert(KEY_ADMIN.to_string(), Key::URef(admin_uref)); + named_keys.insert(KEY_TREASURY.to_string(), Key::URef(treasury_uref)); + named_keys.insert(KEY_PAUSED.to_string(), Key::URef(paused_uref)); let (contract_hash, contract_version) = storage::new_contract( build_entry_points(), Some(named_keys), Some(KEY_CONTRACT_HASH.to_string()), Some(KEY_CONTRACT_VERSION.to_string()), - None, // no message topics + None, ); runtime::put_key(KEY_CONTRACT_HASH, Key::Hash(contract_hash.value())); @@ -294,4 +393,8 @@ pub extern "C" fn call() { KEY_CONTRACT_VERSION, Key::URef(storage::new_uref(contract_version)), ); + + // Installation event is not written inside the new contract context by this + // session function; deployment evidence must record installer/admin/treasury + // and contract hash externally and then read named keys before unpausing. } From 01e9fe8d5352fbd9e76c62a41e03528764ad536e Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:22:55 +0300 Subject: [PATCH 04/24] chore(casper): align package version with settlement v3 RC --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 9ee8a61..c188e78 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "aifinpay-casper" -version = "2.0.0" +version = "3.0.0-rc.1" edition = "2021" [[bin]] From 130489211d4e325aceca54045da0266eb901becc Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:23:42 +0300 Subject: [PATCH 05/24] chore(casper): add quarantined v3 deployment manifest --- deployments/casper-v3.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 deployments/casper-v3.json diff --git a/deployments/casper-v3.json b/deployments/casper-v3.json new file mode 100644 index 0000000..019c57a --- /dev/null +++ b/deployments/casper-v3.json @@ -0,0 +1,19 @@ +{ + "schemaVersion": 2, + "contractVersion": "3.0.0-rc.1", + "network": "casper", + "routeModel": { + "AIFP-1": { "route": 1, "treasuryBps": 100, "creatorBps": 0, "grossInclusive": true }, + "AIFP-2": { "route": 2, "treasuryBps": 0, "creatorBps": 0, "grossInclusive": true } + }, + "status": "source_only", + "contractHash": null, + "deployHash": null, + "wasmSha256": null, + "sourceCommit": null, + "treasuryAccountHash": null, + "adminAccountHash": null, + "deployedAt": null, + "verifiedAt": null, + "e2eEvidence": null +} From a81ec0761b5f28c977e4ec543e4861f2c8ad076c Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:23:54 +0300 Subject: [PATCH 06/24] fix(casper): trust only verified v3 deployment manifest --- demo/trusted-contract.js | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/demo/trusted-contract.js b/demo/trusted-contract.js index 27b6c1a..11f355a 100644 --- a/demo/trusted-contract.js +++ b/demo/trusted-contract.js @@ -1,9 +1,9 @@ 'use strict'; -// Payment routes consume the reviewed deployment manifest. Environment -// variables cannot override trust. Deploying alone is insufficient. -const deployment = require('../deployments/casper-v2.json'); -const CASPER_V2_CONTRACT_HASH = deployment.contractHash; +// Production payment routes consume the reviewed v3 deployment manifest. +// Environment variables cannot override trust. Deploying alone is insufficient. +const deployment = require('../deployments/casper-v3.json'); +const CASPER_V3_CONTRACT_HASH = deployment.contractHash; function normalize(value) { return typeof value === 'string' ? value.toLowerCase().replace(/^(hash|contract)-/, '') : ''; @@ -11,17 +11,23 @@ function normalize(value) { function assertTrustedContract(candidate) { const complete = deployment.status === 'verified' - && deployment.contractVersion === '2.0.0' + && deployment.contractVersion === '3.0.0-rc.1' + && deployment.routeModel?.['AIFP-1']?.treasuryBps === 100 + && deployment.routeModel?.['AIFP-1']?.creatorBps === 0 + && deployment.routeModel?.['AIFP-2']?.treasuryBps === 0 + && deployment.routeModel?.['AIFP-2']?.creatorBps === 0 && /^(hash-|contract-)?[0-9a-f]{64}$/i.test(deployment.contractHash || '') && /^[0-9a-f]{64}$/i.test(deployment.deployHash || '') && /^[0-9a-f]{64}$/i.test(deployment.wasmSha256 || '') && /^[0-9a-f]{40}$/i.test(deployment.sourceCommit || '') - && Boolean(deployment.deployedAt && deployment.verifiedAt); - if (!complete || normalize(candidate) !== normalize(CASPER_V2_CONTRACT_HASH)) { + && /^account-hash-[0-9a-f]{64}$/i.test(deployment.treasuryAccountHash || '') + && /^account-hash-[0-9a-f]{64}$/i.test(deployment.adminAccountHash || '') + && Boolean(deployment.deployedAt && deployment.verifiedAt && deployment.e2eEvidence); + if (!complete || normalize(candidate) !== normalize(CASPER_V3_CONTRACT_HASH)) { throw new Error( - 'Casper payments are quarantined until the audited v2 deployment manifest is complete and verified.', + 'Casper payments are quarantined until the canonical v3 deployment manifest, artifact hash, governance and paid E2E evidence are complete and verified.', ); } } -module.exports = { CASPER_V2_CONTRACT_HASH, assertTrustedContract, deployment }; +module.exports = { CASPER_V3_CONTRACT_HASH, assertTrustedContract, deployment }; From c08a70feca3209967810f47909a3dbe60eaf71c1 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:24:10 +0300 Subject: [PATCH 07/24] fix(casper): verify canonical v3 pay entry point and arguments --- demo/settlement-verifier.js | 69 +++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 15 deletions(-) diff --git a/demo/settlement-verifier.js b/demo/settlement-verifier.js index 760a476..8e8ff85 100644 --- a/demo/settlement-verifier.js +++ b/demo/settlement-verifier.js @@ -1,7 +1,7 @@ 'use strict'; function normalizeHash(value) { - return typeof value === 'string' ? value.toLowerCase().replace(/^hash-/, '') : null; + return typeof value === 'string' ? value.toLowerCase().replace(/^(hash|contract)-/, '') : null; } function readSessionArgs(raw) { @@ -17,7 +17,7 @@ function readSessionArgs(raw) { return out; } -function validateExecutedSettlement(rpc, expected) { +function executionSucceeded(rpc) { if (!rpc) return { ok: false, reason: 'deploy_not_found' }; const result = rpc.execution_info && rpc.execution_info.execution_result; if (!result) return { ok: false, reason: 'deploy_not_executed_yet' }; @@ -25,43 +25,82 @@ function validateExecutedSettlement(rpc, expected) { if (result.Version2.error_message) { return { ok: false, reason: `deploy_failed_on_chain: ${result.Version2.error_message}` }; } - } else if (result.Version1) { + return { ok: true }; + } + if (result.Version1) { if (result.Version1.Failure) { return { ok: false, reason: `deploy_failed_on_chain: ${result.Version1.Failure.error_message || 'unknown'}`, }; } - if (!result.Version1.Success) return { ok: false, reason: 'deploy_not_successful' }; - } else { - return { ok: false, reason: 'deploy_not_successful' }; + return result.Version1.Success ? { ok: true } : { ok: false, reason: 'deploy_not_successful' }; } + return { ok: false, reason: 'deploy_not_successful' }; +} + +/** + * Verify an executed canonical Casper settlement v3 call. + * + * expected fields: + * - contract_hash + * - route: 1 (AIFP-1) or 2 (AIFP-2) + * - merchant: account-hash-... + * - gross_amount_motes + * - request_id + * - valid_until_ms + * - payer_public_key (optional; if supplied it must match deploy.header.account) + */ +function validateExecutedSettlement(rpc, expected) { + const executed = executionSucceeded(rpc); + if (!executed.ok) return executed; const args = readSessionArgs(rpc); if (!args) return { ok: false, reason: 'unparseable_session_args' }; if (normalizeHash(args.contract_hash) !== normalizeHash(expected.contract_hash)) { return { ok: false, reason: 'contract_hash_mismatch' }; } - if (args.entry_point !== 'pay_agent') return { ok: false, reason: 'wrong_entry_point' }; - for (const key of ['request_id', 'from_agent', 'to_agent', 'amount']) { + if (args.entry_point !== 'pay') return { ok: false, reason: 'wrong_entry_point' }; + + for (const key of ['route', 'merchant', 'gross_amount', 'request_id', 'valid_until_ms']) { if (args[key] == null) return { ok: false, reason: `missing_${key}` }; } + + const route = Number(args.route); + if (!Number.isInteger(route) || (route !== 1 && route !== 2)) { + return { ok: false, reason: 'route_invalid' }; + } + if (route !== Number(expected.route)) { + return { ok: false, reason: 'route_mismatch' }; + } if (String(args.request_id) !== String(expected.request_id)) { return { ok: false, reason: 'request_id_mismatch' }; } - if (String(args.from_agent) !== String(expected.from_agent)) { - return { ok: false, reason: 'payer_mismatch' }; + if (String(args.merchant).toLowerCase() !== String(expected.merchant).toLowerCase()) { + return { ok: false, reason: 'merchant_mismatch' }; } - if (String(args.to_agent) !== String(expected.to_agent)) { - return { ok: false, reason: 'recipient_mismatch' }; + try { + if (BigInt(String(args.gross_amount)) !== BigInt(String(expected.gross_amount_motes))) { + return { ok: false, reason: 'gross_amount_mismatch' }; + } + } catch { + return { ok: false, reason: 'gross_amount_invalid' }; } try { - if (BigInt(String(args.amount)) !== BigInt(String(expected.amount_motes))) { - return { ok: false, reason: 'amount_mismatch' }; + if (BigInt(String(args.valid_until_ms)) !== BigInt(String(expected.valid_until_ms))) { + return { ok: false, reason: 'valid_until_mismatch' }; } } catch { - return { ok: false, reason: 'amount_invalid' }; + return { ok: false, reason: 'valid_until_invalid' }; } + + if (expected.payer_public_key != null) { + const payer = rpc?.deploy?.header?.account; + if (!payer || String(payer).toLowerCase() !== String(expected.payer_public_key).toLowerCase()) { + return { ok: false, reason: 'payer_signer_mismatch' }; + } + } + return { ok: true }; } From e11f6c57b10965c346602552916b65ad838e0fb6 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:24:27 +0300 Subject: [PATCH 08/24] test(casper): cover canonical v3 settlement verifier --- demo/test/settlement-verifier.test.js | 54 +++++++++++++++++---------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/demo/test/settlement-verifier.test.js b/demo/test/settlement-verifier.test.js index cac50a6..7bdfdb6 100644 --- a/demo/test/settlement-verifier.test.js +++ b/demo/test/settlement-verifier.test.js @@ -6,27 +6,35 @@ const { validateExecutedSettlement } = require('../settlement-verifier'); const { assertTrustedContract } = require('../trusted-contract'); const expected = { - contract_hash: 'hash-aabbcc', + contract_hash: 'hash-' + 'aa'.repeat(32), + route: 1, + merchant: 'account-hash-' + 'bb'.repeat(32), + gross_amount_motes: '100000000', request_id: 'order-1', - from_agent: 'buyer-1', - to_agent: 'merchant-1', - amount_motes: '100000000', + valid_until_ms: '1900000000000', + payer_public_key: '01' + 'cc'.repeat(32), }; function rpc(patch = {}) { - const values = { ...expected, amount: expected.amount_motes, ...patch }; + const values = { + ...expected, + gross_amount: expected.gross_amount_motes, + ...patch, + }; return { execution_info: { execution_result: { Version2: { error_message: null } } }, deploy: { + header: { account: values.payer_public_key }, session: { StoredContractByHash: { hash: values.contract_hash, - entry_point: values.entry_point || 'pay_agent', + entry_point: values.entry_point || 'pay', args: [ + ['route', { parsed: values.route }], + ['merchant', { parsed: values.merchant }], + ['gross_amount', { parsed: values.gross_amount }], ['request_id', { parsed: values.request_id }], - ['from_agent', { parsed: values.from_agent }], - ['to_agent', { parsed: values.to_agent }], - ['amount', { parsed: values.amount }], + ['valid_until_ms', { parsed: values.valid_until_ms }], ].filter(([name]) => !values.omit || values.omit !== name), }, }, @@ -34,27 +42,33 @@ function rpc(patch = {}) { }; } -test('accepts only the exact successful settlement', () => { +test('accepts only the exact successful canonical v3 settlement', () => { assert.deepEqual(validateExecutedSettlement(rpc(), expected), { ok: true }); }); for (const [name, patch, reason] of [ - ['contract', { contract_hash: 'hash-deadbeef' }, 'contract_hash_mismatch'], - ['entry point', { entry_point: 'register_agent' }, 'wrong_entry_point'], + ['contract', { contract_hash: 'hash-' + 'dd'.repeat(32) }, 'contract_hash_mismatch'], + ['entry point', { entry_point: 'pay_agent' }, 'wrong_entry_point'], + ['route', { route: 2 }, 'route_mismatch'], ['request', { request_id: 'order-2' }, 'request_id_mismatch'], - ['payer', { from_agent: 'attacker' }, 'payer_mismatch'], - ['recipient', { to_agent: 'attacker' }, 'recipient_mismatch'], - ['underpayment', { amount: '99999999' }, 'amount_mismatch'], - ['overpayment', { amount: '100000001' }, 'amount_mismatch'], - ['invalid amount', { amount: 'not-a-number' }, 'amount_invalid'], + ['merchant', { merchant: 'account-hash-' + 'ee'.repeat(32) }, 'merchant_mismatch'], + ['underpayment', { gross_amount: '99999999' }, 'gross_amount_mismatch'], + ['overpayment', { gross_amount: '100000001' }, 'gross_amount_mismatch'], + ['invalid amount', { gross_amount: 'not-a-number' }, 'gross_amount_invalid'], + ['expiry', { valid_until_ms: '1900000000001' }, 'valid_until_mismatch'], + ['payer signer', { payer_public_key: '01' + 'ff'.repeat(32) }, 'payer_signer_mismatch'], ]) { test(`rejects wrong ${name}`, () => { assert.equal(validateExecutedSettlement(rpc(patch), expected).reason, reason); }); } +test('rejects invalid route values', () => { + assert.equal(validateExecutedSettlement(rpc({ route: 3 }), expected).reason, 'route_invalid'); +}); + test('rejects missing required arguments instead of accepting execution success', () => { - assert.equal(validateExecutedSettlement(rpc({ omit: 'amount' }), expected).reason, 'missing_amount'); + assert.equal(validateExecutedSettlement(rpc({ omit: 'gross_amount' }), expected).reason, 'missing_gross_amount'); }); test('rejects an unparseable session instead of accepting execution success', () => { @@ -70,6 +84,6 @@ test('rejects failed and pending deploys', () => { assert.equal(validateExecutedSettlement({ deploy: rpc().deploy }, expected).reason, 'deploy_not_executed_yet'); }); -test('payment entry points remain quarantined until the v2 manifest is verified', () => { - assert.throws(() => assertTrustedContract('hash-aabbcc'), /payments are quarantined/); +test('payment entry points remain quarantined until the v3 manifest is verified', () => { + assert.throws(() => assertTrustedContract(expected.contract_hash), /v3 deployment manifest/); }); From 656ffe49b872235a32e0627f25bc9675cc0b4f49 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:25:00 +0300 Subject: [PATCH 09/24] fix(casper): make mainnet deploy script match v3 installer and evidence model --- demo/deploy-mainnet.js | 139 ++++++++++++++++++++++++----------------- 1 file changed, 81 insertions(+), 58 deletions(-) diff --git a/demo/deploy-mainnet.js b/demo/deploy-mainnet.js index 71779c1..d51e664 100644 --- a/demo/deploy-mainnet.js +++ b/demo/deploy-mainnet.js @@ -1,29 +1,46 @@ /** - * deploy-mainnet.js — install the AiFinPay settlement contract on Casper MAINNET. + * deploy-mainnet.js — install canonical AiFinPay settlement v3 on Casper MAINNET. * - * Same logic as deploy.js but targets the live network: - * NETWORK = 'casper' (mainnet chain name, vs 'casper-test') - * NODE_URL = mainnet RPC (override via .env.mainnet if needed) - * keys = ./keys-mainnet/ (dedicated mainnet key, not the testnet demo key) - * explorer = https://cspr.live (vs testnet.cspr.live) - * - * Requires the mainnet account to be funded with real CSPR first (~250 CSPR). + * This script is fail-closed: it requires an explicit treasury account-hash, + * a clean reviewed git commit, real mainnet confirmation, and records the + * deployment as deployed_unverified. It never marks a payment route verified + * or live; paid E2E evidence is a separate release gate. */ require('dotenv').config({ path: require('path').join(__dirname, '.env.mainnet') }); -const { DeployUtil, Keys, RuntimeArgs } = require('casper-js-sdk'); +const { DeployUtil, Keys, RuntimeArgs, CLValueBuilder } = require('casper-js-sdk'); const fetch = require('node-fetch'); const fs = require('fs'); const crypto = require('crypto'); const path = require('path'); +const { execFileSync } = require('child_process'); -// Mainnet defaults — override in .env.mainnet if the cspr.cloud key isn't mainnet-enabled. const NODE_URL = process.env.NODE_URL || 'https://node.mainnet.cspr.cloud/rpc'; const CSPR_API_KEY = process.env.CSPR_API_KEY || ''; const NETWORK = process.env.NETWORK_NAME || 'casper'; const KEYS_DIR = process.env.KEYS_DIR || path.join(__dirname, 'keys-mainnet'); const WASM_PATH = path.join(__dirname, '..', 'target', 'wasm32-unknown-unknown', 'release', 'aifinpay_casper.wasm'); +const MANIFEST_PATH = path.join(__dirname, '..', 'deployments', 'casper-v3.json'); const GAS_INSTALL = process.env.GAS_INSTALL || '200000000000'; // 200 CSPR +const TREASURY_ACCOUNT_HASH = process.env.TREASURY_ACCOUNT_HASH || ''; + +const ACCOUNT_HASH_RE = /^account-hash-[0-9a-f]{64}$/i; +const COMMIT_RE = /^[0-9a-f]{40}$/i; + +function git(args) { + return execFileSync('git', args, { cwd: path.join(__dirname, '..'), encoding: 'utf8' }).trim(); +} + +function reviewedSourceCommit() { + const dirty = git(['status', '--porcelain']); + if (dirty) throw new Error('Refusing deployment from a dirty working tree'); + const head = git(['rev-parse', 'HEAD']); + const expected = process.env.SOURCE_COMMIT || head; + if (!COMMIT_RE.test(expected) || expected.toLowerCase() !== head.toLowerCase()) { + throw new Error(`SOURCE_COMMIT must equal the checked-out reviewed HEAD (${head})`); + } + return head; +} async function rpc(method, params) { const headers = { 'Content-Type': 'application/json' }; @@ -54,7 +71,7 @@ async function waitForDeploy(deployHash, maxWait = 240000) { } if (er && er.Version1) { if (er.Version1.Failure) throw new Error(`install failed: ${er.Version1.Failure.error_message || 'unknown'}`); - return result; + if (er.Version1.Success) return result; } } catch (error) { if (/install failed/.test(error.message || '')) throw error; @@ -69,78 +86,84 @@ async function main() { if (process.env.ALLOW_MAINNET_DEPLOY !== 'I_UNDERSTAND_THIS_SPENDS_REAL_CSPR') { throw new Error('Set ALLOW_MAINNET_DEPLOY=I_UNDERSTAND_THIS_SPENDS_REAL_CSPR for an intentional mainnet install'); } - const keyPath = path.join(KEYS_DIR, 'secret_key.pem'); - if (!fs.existsSync(keyPath)) { - console.error('❌ No mainnet keypair found. Run: node keygen-mainnet.js'); - process.exit(1); + if (!ACCOUNT_HASH_RE.test(TREASURY_ACCOUNT_HASH)) { + throw new Error('TREASURY_ACCOUNT_HASH must be an explicit formatted account-hash-<64 hex> value'); } - const keypair = Keys.Ed25519.loadKeyPairFromPrivateFile(keyPath); - console.log('🔑 Deployer (mainnet):', keypair.publicKey.toHex()); - console.log(' Account hash :', keypair.publicKey.toAccountHashStr()); - if (!fs.existsSync(WASM_PATH)) { - console.error('❌ Wasm not found at:', WASM_PATH); - process.exit(1); + const sourceCommit = reviewedSourceCommit(); + const keyPath = path.join(KEYS_DIR, 'secret_key.pem'); + if (!fs.existsSync(keyPath)) throw new Error('No mainnet keypair found in KEYS_DIR'); + const keypair = Keys.Ed25519.loadKeyPairFromPrivateFile(keyPath); + const adminAccountHash = keypair.publicKey.toAccountHashStr(); + if (adminAccountHash.toLowerCase() === TREASURY_ACCOUNT_HASH.toLowerCase()) { + console.warn('⚠️ Admin and treasury are the same account. This is allowed by the contract but should be an explicit governance decision.'); } + + if (!fs.existsSync(WASM_PATH)) throw new Error(`Wasm not found at ${WASM_PATH}`); const wasm = new Uint8Array(fs.readFileSync(WASM_PATH)); - console.log(`📦 Wasm: ${(wasm.length / 1024).toFixed(1)} KB`); - console.log(`🔒 Wasm SHA-256: ${crypto.createHash('sha256').update(wasm).digest('hex')}`); + const wasmSha256 = crypto.createHash('sha256').update(wasm).digest('hex'); - // Verify connection + that we're really on mainnet const status = await rpc('info_get_status', {}); - console.log(`🌐 Connected: ${status.chainspec_name} | Block: ${status.last_added_block_info.height}`); if (status.chainspec_name !== NETWORK) { - console.error(`❌ Connected chain "${status.chainspec_name}" != expected "${NETWORK}". Aborting.`); - process.exit(1); + throw new Error(`Connected chain "${status.chainspec_name}" != expected "${NETWORK}"`); } + await rpc('state_get_account_info', { public_key: keypair.publicKey.toHex() }); - // Confirm the account is funded before spending gas - try { - const bal = await rpc('state_get_account_info', { public_key: keypair.publicKey.toHex() }); - if (!bal || !bal.account) { - console.error('❌ Account not on-chain yet — fund it with CSPR and wait a couple minutes.'); - process.exit(1); - } - } catch (e) { - console.error('❌ Account not found on mainnet — fund it first.', e.message); - process.exit(1); - } + console.log('AiFinPay Casper settlement v3 mainnet deployment'); + console.log('sourceCommit=', sourceCommit); + console.log('admin=', adminAccountHash); + console.log('treasury=', TREASURY_ACCOUNT_HASH); + console.log('wasmSha256=', wasmSha256); const deployParams = new DeployUtil.DeployParams(keypair.publicKey, NETWORK, 1, 1800000); - const session = DeployUtil.ExecutableDeployItem.newModuleBytes(wasm, RuntimeArgs.fromMap({})); + const session = DeployUtil.ExecutableDeployItem.newModuleBytes( + wasm, + RuntimeArgs.fromMap({ treasury: CLValueBuilder.string(TREASURY_ACCOUNT_HASH) }), + ); const payment = DeployUtil.standardPayment(GAS_INSTALL); const deploy = DeployUtil.makeDeploy(deployParams, session, payment); const signed = DeployUtil.signDeploy(deploy, keypair); - console.log('\n🚀 Submitting MAINNET deploy...'); const result = await putDeploy(signed); const deployHash = result.deploy_hash; - console.log('\n✅ Deploy hash:', deployHash); - console.log('🔗 Explorer: ', `https://cspr.live/deploy/${deployHash}`); - - console.log('\n⏳ Waiting for execution'); + console.log('Deploy hash:', deployHash); + console.log('Explorer:', `https://cspr.live/deploy/${deployHash}`); await waitForDeploy(deployHash); - console.log('\n\n🔍 Fetching contract hash from account named keys...'); const accountResult = await rpc('state_get_account_info', { public_key: keypair.publicKey.toHex() }); - const contractKey = accountResult.account.named_keys.find(k => k.name === 'aifinpay_casper_hash'); - if (!contractKey) { - console.log('⚠️ Named key not found yet — re-run in 30s. Deploy:', `https://cspr.live/deploy/${deployHash}`); - process.exit(0); + const contractKey = accountResult.account.named_keys.find(k => k.name === 'aifinpay_casper_v3_hash'); + const versionKey = accountResult.account.named_keys.find(k => k.name === 'aifinpay_casper_v3_version'); + if (!contractKey || !versionKey) { + throw new Error('v3 named keys not found after successful install'); } const contractHash = contractKey.key; - console.log('\n🎉 =========================================='); - console.log(' CONTRACT LIVE ON CASPER MAINNET'); - console.log('=========================================='); - console.log('Deploy hash: ', deployHash); - console.log('Contract hash:', contractHash); - console.log('Explorer: ', `https://cspr.live/contract/${contractHash.replace('hash-','')}`); + + const manifest = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')); + const deployedAt = new Date().toISOString(); + const updated = { + ...manifest, + contractVersion: '3.0.0-rc.1', + status: 'deployed_unverified', + contractHash, + deployHash, + wasmSha256, + sourceCommit, + treasuryAccountHash: TREASURY_ACCOUNT_HASH, + adminAccountHash, + deployedAt, + verifiedAt: null, + e2eEvidence: null, + }; + fs.writeFileSync(MANIFEST_PATH, JSON.stringify(updated, null, 2) + '\n'); fs.writeFileSync(path.join(__dirname, '.env.mainnet.out'), - `NODE_URL=${NODE_URL}\nNETWORK_NAME=${NETWORK}\nKEYS_DIR=./keys-mainnet\nCONTRACT_HASH=${contractHash}\n` + `NODE_URL=${NODE_URL}\nNETWORK_NAME=${NETWORK}\nKEYS_DIR=./keys-mainnet\nCONTRACT_HASH=${contractHash}\nTREASURY_ACCOUNT_HASH=${TREASURY_ACCOUNT_HASH}\nSOURCE_COMMIT=${sourceCommit}\n` ); - console.log('\n📝 Saved contract hash to .env.mainnet.out'); - console.log('Release remains quarantined until deployments/casper-v2.json is independently verified.'); + + console.log('Contract hash:', contractHash); + console.log('Manifest updated:', MANIFEST_PATH); + console.log('STATUS=deployed_unverified — DO NOT unpause or enable payments yet.'); + console.log('Next: independent manifest review + config readback + paid AIFP-1/AIFP-2 E2E + replay/expiry negatives.'); } main().catch(err => { From 1c6e72c93ca96346634be655ac8b60dfa7b6c883 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:31:42 +0300 Subject: [PATCH 10/24] ci(casper): deterministically sync v3 Cargo.lock root package --- .github/workflows/sync-v3-lock.yml | 45 ++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 .github/workflows/sync-v3-lock.yml diff --git a/.github/workflows/sync-v3-lock.yml b/.github/workflows/sync-v3-lock.yml new file mode 100644 index 0000000..e38b611 --- /dev/null +++ b/.github/workflows/sync-v3-lock.yml @@ -0,0 +1,45 @@ +name: sync-v3-lock-once + +on: + push: + branches: + - release/settlement-v3-production-rc + +permissions: + contents: write + +jobs: + sync-lock: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: release/settlement-v3-production-rc + - name: Update local package metadata only + shell: bash + run: | + set -euo pipefail + cp Cargo.lock /tmp/Cargo.lock.before + cargo check >/tmp/cargo-check.log 2>&1 || { cat /tmp/cargo-check.log; exit 1; } + python3 - <<'PY' + from pathlib import Path + before = Path('/tmp/Cargo.lock.before').read_text() + after = Path('Cargo.lock').read_text() + expected = before.replace('name = "aifinpay-casper"\nversion = "2.0.0"', 'name = "aifinpay-casper"\nversion = "3.0.0-rc.1"', 1) + if before == expected: + raise SystemExit('expected v2 root package stanza was not present') + if after != expected: + Path('/tmp/Cargo.lock.actual').write_text(after) + raise SystemExit('Cargo changed more than the single local package version; refusing automatic lock update') + PY + - name: Commit deterministic lock update + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Cargo.lock + git diff --cached --exit-code && exit 0 + git commit -m "chore(casper): sync Cargo.lock for v3 RC" + git push origin HEAD:release/settlement-v3-production-rc From 634aac855f24e2b1e41af03cdea90fffcab086b1 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 07:32:11 +0000 Subject: [PATCH 11/24] chore(casper): sync Cargo.lock for v3 RC --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 951b192..ceacc07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "aifinpay-casper" -version = "2.0.0" +version = "3.0.0-rc.1" dependencies = [ "casper-contract", "casper-types", From 1bd44705e897ad5754cb34c8749b962ef7aacca4 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:34:07 +0300 Subject: [PATCH 12/24] chore(casper): remove one-shot Cargo.lock sync workflow --- .github/workflows/sync-v3-lock.yml | 45 ------------------------------ 1 file changed, 45 deletions(-) delete mode 100644 .github/workflows/sync-v3-lock.yml diff --git a/.github/workflows/sync-v3-lock.yml b/.github/workflows/sync-v3-lock.yml deleted file mode 100644 index e38b611..0000000 --- a/.github/workflows/sync-v3-lock.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: sync-v3-lock-once - -on: - push: - branches: - - release/settlement-v3-production-rc - -permissions: - contents: write - -jobs: - sync-lock: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - ref: release/settlement-v3-production-rc - - name: Update local package metadata only - shell: bash - run: | - set -euo pipefail - cp Cargo.lock /tmp/Cargo.lock.before - cargo check >/tmp/cargo-check.log 2>&1 || { cat /tmp/cargo-check.log; exit 1; } - python3 - <<'PY' - from pathlib import Path - before = Path('/tmp/Cargo.lock.before').read_text() - after = Path('Cargo.lock').read_text() - expected = before.replace('name = "aifinpay-casper"\nversion = "2.0.0"', 'name = "aifinpay-casper"\nversion = "3.0.0-rc.1"', 1) - if before == expected: - raise SystemExit('expected v2 root package stanza was not present') - if after != expected: - Path('/tmp/Cargo.lock.actual').write_text(after) - raise SystemExit('Cargo changed more than the single local package version; refusing automatic lock update') - PY - - name: Commit deterministic lock update - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.lock - git diff --cached --exit-code && exit 0 - git commit -m "chore(casper): sync Cargo.lock for v3 RC" - git push origin HEAD:release/settlement-v3-production-rc From 99aa3730d5517fcbd75448eaa6b2c8ca02f149bb Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:38:02 +0300 Subject: [PATCH 13/24] test(casper): add pure canonical route economics library --- src/lib.rs | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/lib.rs diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..38fb38d --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,71 @@ +#![no_std] + +use casper_types::U512; + +pub const ROUTE_AIFP1: u8 = 1; +pub const ROUTE_AIFP2: u8 = 2; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SplitError { + ZeroAmount, + FeeRoundsToZero, + InvalidRoute, +} + +/// Canonical immutable product economics shared by the v3 contract and host tests. +/// AIFP-1: merchant 99%, treasury 1%, creator 0%, all from the gross payer amount. +/// AIFP-2: merchant/provider 100%, treasury 0%, creator 0%. +pub fn split_gross(route: u8, gross: U512) -> Result<(U512, U512), SplitError> { + if gross.is_zero() { + return Err(SplitError::ZeroAmount); + } + match route { + ROUTE_AIFP1 => { + let treasury = gross / U512::from(100u64); + if treasury.is_zero() { + return Err(SplitError::FeeRoundsToZero); + } + Ok((gross - treasury, treasury)) + } + ROUTE_AIFP2 => Ok((gross, U512::zero())), + _ => Err(SplitError::InvalidRoute), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn aifp1_is_exactly_gross_inclusive_99_1_0() { + let (merchant, treasury) = split_gross(ROUTE_AIFP1, U512::from(10_000u64)).unwrap(); + assert_eq!(merchant, U512::from(9_900u64)); + assert_eq!(treasury, U512::from(100u64)); + assert_eq!(merchant + treasury, U512::from(10_000u64)); + } + + #[test] + fn aifp2_is_exactly_zero_percent() { + let (provider, treasury) = split_gross(ROUTE_AIFP2, U512::from(1u64)).unwrap(); + assert_eq!(provider, U512::from(1u64)); + assert_eq!(treasury, U512::zero()); + } + + #[test] + fn aifp1_rejects_when_one_percent_rounds_to_zero() { + assert_eq!( + split_gross(ROUTE_AIFP1, U512::from(99u64)), + Err(SplitError::FeeRoundsToZero) + ); + assert_eq!( + split_gross(ROUTE_AIFP1, U512::from(100u64)).unwrap(), + (U512::from(99u64), U512::from(1u64)) + ); + } + + #[test] + fn zero_and_unknown_routes_fail_closed() { + assert_eq!(split_gross(ROUTE_AIFP1, U512::zero()), Err(SplitError::ZeroAmount)); + assert_eq!(split_gross(3, U512::from(100u64)), Err(SplitError::InvalidRoute)); + } +} From 57649c53ca7ba9c38cbedbb795e567098f79d77f Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:38:17 +0300 Subject: [PATCH 14/24] ci(casper): wire tested economics library into v3 contract once --- .github/workflows/patch-v3-economics-once.yml | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/patch-v3-economics-once.yml diff --git a/.github/workflows/patch-v3-economics-once.yml b/.github/workflows/patch-v3-economics-once.yml new file mode 100644 index 0000000..2936257 --- /dev/null +++ b/.github/workflows/patch-v3-economics-once.yml @@ -0,0 +1,81 @@ +name: patch-v3-economics-once + +on: + push: + branches: + - release/settlement-v3-production-rc + +permissions: + contents: write + +jobs: + patch: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + ref: release/settlement-v3-production-rc + - name: Apply exact contract wiring patch + shell: bash + run: | + python3 - <<'PY' + from pathlib import Path + p = Path('src/main.rs') + s = p.read_text() + import_needle = 'extern crate alloc;\n\nuse alloc::{' + import_repl = 'extern crate alloc;\n\nuse aifinpay_casper::{split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2};\n\nuse alloc::{' + if s.count(import_needle) != 1: + raise SystemExit('unexpected import insertion point') + s = s.replace(import_needle, import_repl, 1) + const_block = '''const ROUTE_AIFP1: u8 = 1; // merchant traffic monetisation: 99/1/0 FROM gross +const ROUTE_AIFP2: u8 = 2; // x402 agent payment: 100/0/0 +''' + if s.count(const_block) != 1: + raise SystemExit('unexpected route constant block') + s = s.replace(const_block, '', 1) + old = '''fn split_gross(route: u8, gross: U512) -> (U512, U512) { + if gross.is_zero() { + runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); + } + match route { + ROUTE_AIFP1 => { + // Exact 1% = floor(gross / 100); no multiplication overflow path. + let treasury = gross / U512::from(100u64); + if treasury.is_zero() { + runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)); + } + let merchant = gross - treasury; + (merchant, treasury) + } + ROUTE_AIFP2 => (gross, U512::zero()), + _ => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), + } +} +''' + new = '''fn split_gross(route: u8, gross: U512) -> (U512, U512) { + canonical_split_gross(route, gross).unwrap_or_else(|error| match error { + SplitError::ZeroAmount => runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)), + SplitError::FeeRoundsToZero => runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)), + SplitError::InvalidRoute => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), + }) +} +''' + if s.count(old) != 1: + raise SystemExit('unexpected split_gross implementation') + s = s.replace(old, new, 1) + p.write_text(s) + PY + - name: Verify and commit + shell: bash + run: | + set -euo pipefail + cargo test --lib --locked + cargo build --release --target wasm32-unknown-unknown --locked + cargo fmt --check + cargo clippy --lib -- -D warnings + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/main.rs + git commit -m "fix(casper): use unit-tested immutable route economics" + git push origin HEAD:release/settlement-v3-production-rc From d85ccc35db7a0b47b52de6df943d02a1a76ae3cb Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:40:56 +0300 Subject: [PATCH 15/24] ci(casper): trigger one-shot economics wiring verifier --- .github/workflows/patch-v3-economics-once.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/patch-v3-economics-once.yml b/.github/workflows/patch-v3-economics-once.yml index 2936257..6c667a6 100644 --- a/.github/workflows/patch-v3-economics-once.yml +++ b/.github/workflows/patch-v3-economics-once.yml @@ -19,6 +19,7 @@ jobs: - name: Apply exact contract wiring patch shell: bash run: | + # One-shot deterministic source transformation; delete this workflow after its verified bot commit. python3 - <<'PY' from pathlib import Path p = Path('src/main.rs') From bfa3db4978b86eed38e8562d9ba65c5b6046fec6 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:44:03 +0300 Subject: [PATCH 16/24] style(casper): format canonical economics unit tests --- src/lib.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 38fb38d..6568983 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -65,7 +65,13 @@ mod tests { #[test] fn zero_and_unknown_routes_fail_closed() { - assert_eq!(split_gross(ROUTE_AIFP1, U512::zero()), Err(SplitError::ZeroAmount)); - assert_eq!(split_gross(3, U512::from(100u64)), Err(SplitError::InvalidRoute)); + assert_eq!( + split_gross(ROUTE_AIFP1, U512::zero()), + Err(SplitError::ZeroAmount) + ); + assert_eq!( + split_gross(3, U512::from(100u64)), + Err(SplitError::InvalidRoute) + ); } } From f2542eff96830e787991d47dfc3bdbdb100a458b Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:50:37 +0300 Subject: [PATCH 17/24] fix(casper): wire unit-tested immutable economics into v3 contract --- src/main.rs | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 128114d..c86b007 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,9 @@ extern crate alloc; +use aifinpay_casper::{ + split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2, +}; use alloc::{ format, string::{String, ToString}, @@ -51,9 +54,6 @@ const ARG_PAUSED: &str = "paused"; const ARG_TREASURY: &str = "treasury"; const ARG_ADMIN: &str = "admin"; -// ── Canonical economic profiles ───────────────────────────────────────────── -const ROUTE_AIFP1: u8 = 1; // merchant traffic monetisation: 99/1/0 FROM gross -const ROUTE_AIFP2: u8 = 2; // x402 agent payment: 100/0/0 const EXPIRY_MAX_AHEAD_MS: u64 = 20 * 60 * 1000; // ── Error codes (stable for SDK/E2E assertions) ────────────────────────────── @@ -157,22 +157,13 @@ fn emit_event(event_type: &str, payload: &str) { } fn split_gross(route: u8, gross: U512) -> (U512, U512) { - if gross.is_zero() { - runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); - } - match route { - ROUTE_AIFP1 => { - // Exact 1% = floor(gross / 100); no multiplication overflow path. - let treasury = gross / U512::from(100u64); - if treasury.is_zero() { - runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)); - } - let merchant = gross - treasury; - (merchant, treasury) + canonical_split_gross(route, gross).unwrap_or_else(|error| match error { + SplitError::ZeroAmount => runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)), + SplitError::FeeRoundsToZero => { + runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)) } - ROUTE_AIFP2 => (gross, U512::zero()), - _ => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), - } + SplitError::InvalidRoute => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), + }) } fn validate_expiry(valid_until_ms: u64) { From 5c8d1d2258afe7a990410a74ab70559d24f59139 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:50:54 +0300 Subject: [PATCH 18/24] chore(casper): remove superseded one-shot economics helper --- .github/workflows/patch-v3-economics-once.yml | 82 ------------------- 1 file changed, 82 deletions(-) delete mode 100644 .github/workflows/patch-v3-economics-once.yml diff --git a/.github/workflows/patch-v3-economics-once.yml b/.github/workflows/patch-v3-economics-once.yml deleted file mode 100644 index 6c667a6..0000000 --- a/.github/workflows/patch-v3-economics-once.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: patch-v3-economics-once - -on: - push: - branches: - - release/settlement-v3-production-rc - -permissions: - contents: write - -jobs: - patch: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - ref: release/settlement-v3-production-rc - - name: Apply exact contract wiring patch - shell: bash - run: | - # One-shot deterministic source transformation; delete this workflow after its verified bot commit. - python3 - <<'PY' - from pathlib import Path - p = Path('src/main.rs') - s = p.read_text() - import_needle = 'extern crate alloc;\n\nuse alloc::{' - import_repl = 'extern crate alloc;\n\nuse aifinpay_casper::{split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2};\n\nuse alloc::{' - if s.count(import_needle) != 1: - raise SystemExit('unexpected import insertion point') - s = s.replace(import_needle, import_repl, 1) - const_block = '''const ROUTE_AIFP1: u8 = 1; // merchant traffic monetisation: 99/1/0 FROM gross -const ROUTE_AIFP2: u8 = 2; // x402 agent payment: 100/0/0 -''' - if s.count(const_block) != 1: - raise SystemExit('unexpected route constant block') - s = s.replace(const_block, '', 1) - old = '''fn split_gross(route: u8, gross: U512) -> (U512, U512) { - if gross.is_zero() { - runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)); - } - match route { - ROUTE_AIFP1 => { - // Exact 1% = floor(gross / 100); no multiplication overflow path. - let treasury = gross / U512::from(100u64); - if treasury.is_zero() { - runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)); - } - let merchant = gross - treasury; - (merchant, treasury) - } - ROUTE_AIFP2 => (gross, U512::zero()), - _ => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), - } -} -''' - new = '''fn split_gross(route: u8, gross: U512) -> (U512, U512) { - canonical_split_gross(route, gross).unwrap_or_else(|error| match error { - SplitError::ZeroAmount => runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)), - SplitError::FeeRoundsToZero => runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)), - SplitError::InvalidRoute => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), - }) -} -''' - if s.count(old) != 1: - raise SystemExit('unexpected split_gross implementation') - s = s.replace(old, new, 1) - p.write_text(s) - PY - - name: Verify and commit - shell: bash - run: | - set -euo pipefail - cargo test --lib --locked - cargo build --release --target wasm32-unknown-unknown --locked - cargo fmt --check - cargo clippy --lib -- -D warnings - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/main.rs - git commit -m "fix(casper): use unit-tested immutable route economics" - git push origin HEAD:release/settlement-v3-production-rc From 1c313bc43eb8b76da618782dab55f8a43aabd911 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:51:14 +0300 Subject: [PATCH 19/24] ci(casper): run canonical Rust economics tests in release gate --- .github/workflows/ci.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38db083..d955a28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: if-no-files-found: error quality: - name: Contract · fmt · clippy + name: Contract · unit · fmt · clippy runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -47,10 +47,16 @@ jobs: rustup toolchain install nightly-2025-02-04 --profile minimal --component rustfmt --component clippy rustup target add wasm32-unknown-unknown --toolchain nightly-2025-02-04 + - name: Canonical economics unit tests + run: cargo +nightly-2025-02-04 test --lib --locked + - name: Format check run: cargo +nightly-2025-02-04 fmt --all -- --check - - name: Clippy + - name: Clippy library + run: cargo +nightly-2025-02-04 clippy --lib --locked -- -D warnings + + - name: Clippy Wasm contract run: cargo +nightly-2025-02-04 clippy --bin aifinpay_casper --target wasm32-unknown-unknown --locked -- -D warnings demo: From d1ddaf9822ae62e1765be68fc1f08b0b5c41a982 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:54:01 +0300 Subject: [PATCH 20/24] ci(casper): run economics unit tests on native host target --- .github/workflows/ci.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d955a28..2177eeb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,15 +46,16 @@ jobs: run: | rustup toolchain install nightly-2025-02-04 --profile minimal --component rustfmt --component clippy rustup target add wasm32-unknown-unknown --toolchain nightly-2025-02-04 + rustup target add x86_64-unknown-linux-gnu --toolchain nightly-2025-02-04 - - name: Canonical economics unit tests - run: cargo +nightly-2025-02-04 test --lib --locked + - name: Canonical economics unit tests (native host) + run: cargo +nightly-2025-02-04 test --lib --locked --target x86_64-unknown-linux-gnu - name: Format check run: cargo +nightly-2025-02-04 fmt --all -- --check - - name: Clippy library - run: cargo +nightly-2025-02-04 clippy --lib --locked -- -D warnings + - name: Clippy library (native host) + run: cargo +nightly-2025-02-04 clippy --lib --locked --target x86_64-unknown-linux-gnu -- -D warnings - name: Clippy Wasm contract run: cargo +nightly-2025-02-04 clippy --bin aifinpay_casper --target wasm32-unknown-unknown --locked -- -D warnings From e42d658d774ffedbfb98a68a496b757755d2eb39 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:56:07 +0300 Subject: [PATCH 21/24] ci(casper): apply deterministic rustfmt to v3 wiring once --- .github/workflows/format-v3-once.yml | 37 ++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/format-v3-once.yml diff --git a/.github/workflows/format-v3-once.yml b/.github/workflows/format-v3-once.yml new file mode 100644 index 0000000..b200a93 --- /dev/null +++ b/.github/workflows/format-v3-once.yml @@ -0,0 +1,37 @@ +name: format-v3-once + +on: + push: + branches: + - release/settlement-v3-production-rc + +permissions: + contents: write + +jobs: + format: + if: github.actor != 'github-actions[bot]' + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + with: + ref: release/settlement-v3-production-rc + - name: Install pinned formatter + run: rustup toolchain install nightly-2025-02-04 --profile minimal --component rustfmt + - name: Format and constrain diff + shell: bash + run: | + set -euo pipefail + cargo +nightly-2025-02-04 fmt --all + changed="$(git diff --name-only)" + test "$changed" = "src/main.rs" || { echo "unexpected rustfmt diff: $changed"; exit 1; } + cargo +nightly-2025-02-04 fmt --all -- --check + - name: Commit format-only patch + shell: bash + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add src/main.rs + git commit -m "style(casper): rustfmt v3 economics wiring" + git push origin HEAD:release/settlement-v3-production-rc From 1c3f01b84c92f3e37a1b87f0ee61f2116572cd73 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 16 Aug 2026 07:56:23 +0000 Subject: [PATCH 22/24] style(casper): rustfmt v3 economics wiring --- src/main.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main.rs b/src/main.rs index c86b007..0d3b6f5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,9 +3,7 @@ extern crate alloc; -use aifinpay_casper::{ - split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2, -}; +use aifinpay_casper::{split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2}; use alloc::{ format, string::{String, ToString}, @@ -159,9 +157,7 @@ fn emit_event(event_type: &str, payload: &str) { fn split_gross(route: u8, gross: U512) -> (U512, U512) { canonical_split_gross(route, gross).unwrap_or_else(|error| match error { SplitError::ZeroAmount => runtime::revert(ApiError::User(ERR_INVALID_AMOUNT)), - SplitError::FeeRoundsToZero => { - runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)) - } + SplitError::FeeRoundsToZero => runtime::revert(ApiError::User(ERR_FEE_ROUNDS_TO_ZERO)), SplitError::InvalidRoute => runtime::revert(ApiError::User(ERR_INVALID_ROUTE)), }) } From d81d4252a130ed558b2c23f3c6796b3da3b820a3 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 10:57:49 +0300 Subject: [PATCH 23/24] chore(casper): remove one-shot rustfmt helper --- .github/workflows/format-v3-once.yml | 37 ---------------------------- 1 file changed, 37 deletions(-) delete mode 100644 .github/workflows/format-v3-once.yml diff --git a/.github/workflows/format-v3-once.yml b/.github/workflows/format-v3-once.yml deleted file mode 100644 index b200a93..0000000 --- a/.github/workflows/format-v3-once.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: format-v3-once - -on: - push: - branches: - - release/settlement-v3-production-rc - -permissions: - contents: write - -jobs: - format: - if: github.actor != 'github-actions[bot]' - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v4 - with: - ref: release/settlement-v3-production-rc - - name: Install pinned formatter - run: rustup toolchain install nightly-2025-02-04 --profile minimal --component rustfmt - - name: Format and constrain diff - shell: bash - run: | - set -euo pipefail - cargo +nightly-2025-02-04 fmt --all - changed="$(git diff --name-only)" - test "$changed" = "src/main.rs" || { echo "unexpected rustfmt diff: $changed"; exit 1; } - cargo +nightly-2025-02-04 fmt --all -- --check - - name: Commit format-only patch - shell: bash - run: | - set -euo pipefail - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add src/main.rs - git commit -m "style(casper): rustfmt v3 economics wiring" - git push origin HEAD:release/settlement-v3-production-rc From 7d39021955b17c2906a0eadb77e72909d14f8d44 Mon Sep 17 00:00:00 2001 From: coinsecuritiescompany Date: Sun, 16 Aug 2026 11:02:31 +0300 Subject: [PATCH 24/24] fix(casper): remove unused route imports from wasm contract --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0d3b6f5..ea2b763 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ extern crate alloc; -use aifinpay_casper::{split_gross as canonical_split_gross, SplitError, ROUTE_AIFP1, ROUTE_AIFP2}; +use aifinpay_casper::{split_gross as canonical_split_gross, SplitError}; use alloc::{ format, string::{String, ToString},