Metadata-Version: 2.4
Name: axiomatic_proofkit
Version: 0.1.4
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: py-algorand-sdk<3.0,>=2.6

**Axiomatic ProofKit (Python)** is the client SDK for building and publishing **p1** Proof-of-Valuation (PoVal) attestations on Algorand.

The goal is a simple, trust-minimized flow:

1. Build a canonical **p1** payload (JCS/ACJ-style JSON).
2. Publish it on-chain as the note of a **0-ALGO self-transaction**.
3. Keep signing and key management **on the client side**.
4. Allow anyone to independently recompute and verify what was published.

This SDK does **not** depend on any Axiomatic backend.

---

## Who is this for?

Use `axiomatic_proofkit` if you:

- Build Python back-ends that need to **issue** PoVal attestations on Algorand.
- Integrate model-based valuations into RWA / tokenization / lending flows.
- Want deterministic, auditable `p1` payloads aligned with the Node SDKs.

If you only need to **verify** on-chain notes, see [`axiomatic_verifier`](https://pypi.org/project/axiomatic_verifier/).

---

## Typical use cases

1. **Real estate tokenization**
   - Compute a fair value `v` and interval `u` for a property.
   - Build a `p1` attestation with `axiomatic_proofkit`.
   - Anchor the PoVal proof on Algorand TestNet/MainNet as a 0-ALGO self-tx.

2. **On-chain collateral / lending**
   - Issue PoVal attestations for assets posted as collateral.
   - Use the on-chain note + txid as input for your credit/risk policies.

3. **Back-office & audit tooling**
   - Periodically issue updated valuations.
   - Keep a verifiable trail of model versions, hashes and timestamps.

---

## Installation

Requires **Python 3.10+**.

```bash
pip install axiomatic_proofkit
pip install py-algorand-sdk
```

---

## Exposed API

From `axiomatic_proofkit`:

* `build_p1(...)`
* `canonical_note_bytes_p1(p1)`
* `assert_note_size_ok(p1, max_bytes=...)`
* `build_canonical_input(raw_input, allowed_keys=...)`
* `compute_input_hash(canonical_input, allowed_keys=...)`
* `publish_p1(p1, from_addr=..., sign=..., network=..., algod=..., wait_rounds=...)`
* `PublishError`

These are the only functions you need for typical integrations.

---

## p1 structure

`build_p1` returns a dict of the form:

```python
p1 = {
    "s": "p1",          # schema marker
    "a": "re:EUR",      # asset tag (e.g. "re:EUR")
    "mv": "v2",         # model version
    "mh": "",           # model hash (hex, optional)
    "ih": "...",        # input hash (hex, required)
    "v": 550000.0,      # point estimate (e.g. value)
    "u": [520000.0, 580000.0],  # uncertainty range [low, high]
    "ts": 1762609210,   # unix epoch seconds
}
```

The object is normalized and serialized using a JCS/ACJ-style canonical JSON encoder for stable hashing and cross-language parity.

---

## Quickstart: publish a p1 to Algorand TestNet

The following example mirrors the internal smoke test used to validate the SDK.

### 1. Environment

Create a `.env` file next to your script:

```env
ALGORAND_MNEMONIC=your 25-word mnemonic for a TestNet account
ALGORAND_NETWORK=testnet
# Optional: override default node
# ALGOD_URL=https://testnet-api.algonode.cloud
```

For production, use a secure signing setup (wallet, KMS, HSM, etc.).
This example uses a mnemonic **only** for demonstration.

### 2. Example script (`publish_p1_example.py`)

```python
import os
import sys
import json
import base64
from pathlib import Path

from algosdk import account, mnemonic, encoding, transaction
from algosdk.v2client.algod import AlgodClient

from axiomatic_proofkit import (
    build_p1,
    canonical_note_bytes_p1,
    assert_note_size_ok,
    build_canonical_input,
    compute_input_hash,
    publish_p1,
)

ROOT = Path(__file__).resolve().parent


def load_env_from_file(env_path: Path) -> None:
    try:
        for line in env_path.read_text(encoding="utf-8").splitlines():
            if not line or line.strip().startswith("#") or "=" not in line:
                continue
            k, v = line.split("=", 1)
            k, v = k.strip(), v.strip()
            if k and (k not in os.environ):
                os.environ[k] = v
    except FileNotFoundError:
        pass


load_env_from_file(ROOT / ".env")

MNEMONIC = os.getenv("ALGORAND_MNEMONIC")
NETWORK = (os.getenv("ALGORAND_NETWORK") or "testnet").strip()
ALGOD_URL = (
    os.getenv("ALGOD_URL")
    or (
        "https://mainnet-api.algonode.cloud"
        if NETWORK == "mainnet"
        else "https://testnet-api.algonode.cloud"
    )
).strip()

if not MNEMONIC:
    print("❌ ALGORAND_MNEMONIC missing.", file=sys.stderr)
    sys.exit(1)


def compute_input_hash_from_golden() -> str:
    """
    If golden/p1_input.json exists, compute its canonical ACJ/JCS hash.
    Otherwise return a deterministic placeholder for the smoke test.
    """
    golden_dir = ROOT / "golden"
    inp_path = golden_dir / "p1_input.json"

    if not inp_path.is_file():
        return "0" * 64

    try:
        raw = json.loads(inp_path.read_text(encoding="utf-8"))
    except Exception as e:
        print(f"⚠️ Unable to read golden/p1_input.json: {e}")
        return "0" * 64

    allowed_keys = list(raw.keys())
    cin = build_canonical_input(raw, allowed_keys=allowed_keys)
    ih = compute_input_hash(cin, allowed_keys=allowed_keys)
    print(f"✅ Golden input hash: {ih}")
    return ih


def main() -> None:
    # 1) Derive private key and address from mnemonic (demo only)
    sk = mnemonic.to_private_key(MNEMONIC)
    addr = account.address_from_private_key(sk)

    # 2) Compute input hash (from golden file if available)
    ih = compute_input_hash_from_golden()

    # 3) Build canonical p1
    p1 = build_p1(
        asset_tag="re:EUR",
        model_version="v2",
        model_hash_hex="",
        input_hash_hex=ih,
        value_eur=550_000,
        uncertainty_low_eur=520_000,
        uncertainty_high_eur=580_000,
        timestamp_epoch=None,  # use "now"
    )

    # 4) Inspect note size and hash
    note_bytes, note_sha, note_len = canonical_note_bytes_p1(p1)
    assert note_bytes is not None  # just to show it's a bytes-like object
    assert_note_size_ok(p1)
    print(f"P1 note size: {note_len} bytes | sha256: {note_sha}")

    # 5) Client-side signer: keep your keys local
    def sign(unsigned_bytes: bytes) -> bytes:
        """
        unsigned_bytes: msgpack-encoded UnsignedTransaction.
        Return: msgpack bytes of the SignedTransaction.
        """
        tx_dict = encoding.msgpack.unpackb(unsigned_bytes)
        txn = transaction.Transaction.undictify(tx_dict)
        stx = txn.sign(sk)
        stx_b64 = encoding.msgpack_encode(stx)   # base64(msgpack signed)
        return base64.b64decode(stx_b64)         # raw msgpack bytes

    # 6) Algod client (Algonode by default)
    algod = AlgodClient("", ALGOD_URL)

    # 7) Publish p1 as a 0-ALGO self-transaction note
    res = publish_p1(
        p1,
        network=NETWORK,
        algod=algod,
        from_addr=addr,
        sign=sign,
        wait_rounds=4,
    )

    print("PUBLISHED:")
    print(json.dumps(res, indent=2))


if __name__ == "__main__":
    main()
```

Run:

```bash
python publish_p1_example.py
```

You should see a JSON result with:

* `txid`
* `explorer_url`
* `note_sha256`
* `note_size`
* `network`

You can open the explorer URL and inspect the on-chain note.

---

## Publish → verify (end-to-end)

To verify a published `p1` attestation from Python, use [`axiomatic_verifier`](https://pypi.org/project/axiomatic_verifier/):

```python
from axiomatic_verifier import verify_tx

res = verify_tx(
    txid="YOUR_TXID",
    network="testnet",
    # or your own indexer:
    # indexer_url="https://testnet-idx.algonode.cloud",
    max_skew_past_sec=3600,
    max_skew_future_sec=300,
)

print(res["verified"], res.get("reason"))
```

This checks:

* that the note is structurally valid,
* that canonicalization and `sha256` match,
* and that the timestamp `ts` is within your allowed time window.

---

## Security notes

* The `sign` function is fully controlled by you.
* For real integrations, replace inline mnemonic signing with:

  * wallet connectors,
  * custodial services,
  * HSM/KMS or cloud KMS.
* The SDK never sends your secrets anywhere and does not rely on Axiomatic servers.

This is an early-access SDK: feedback and issues are very welcome.
