Metadata-Version: 2.5
Name: purechain-sdk
Version: 0.0.1
Summary: Client library for the PureChain network family (geth, besu, dag variants)
Project-URL: Homepage, https://github.com/isongjosiah/purechain-py
Project-URL: Repository, https://github.com/isongjosiah/purechain-py
Project-URL: Issues, https://github.com/isongjosiah/purechain-py/issues
Author: isongjosiah
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: blockchain,clique,ethereum,evm,purechain,web3,zero-gas
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: web3<9,>=7.0
Provides-Extra: dev
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest-cov>=6.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Description-Content-Type: text/markdown

# purechain

A client library for the PureChain network family. Three variants — `geth`,
`besu`, `dag` — behind one interface, so application code does not change when
the variant does.

Status: the **geth** variant (the live public PureChain network) is implemented
and tested against the real chain. `besu` and `dag` are stubs with the full
interface in place; every call raises `NotImplementedError_` naming the variant.

This is the Python half of a pair; the TypeScript library mirrors it method for
method, with the same types, error codes, and defaults.

```bash
pip install purechain-sdk
```

> Installed as `purechain-sdk`, imported as `purechain` — the distribution name
> and the import name are separate in Python. The TypeScript library is
> `@purechain/client` on npm; the two registries differ only in the name, not in
> the API.

## Usage

The API is async throughout.

```python
import asyncio
from purechain import PrivateKeySigner, TxRequest, create_client

async def main():
    async with create_client(signer=PrivateKeySigner(PRIVATE_KEY)) as client:
        tx_hash = await client.send_transaction(TxRequest(to="0xabc...", value=1))
        receipt = await client.wait_for(tx_hash)
        print(receipt.status, receipt.block_number)

asyncio.run(main())
```

Point it at your own node, or at a private deployment with its own genesis:

```python
client = create_client(
    url="http://localhost:8545",
    network={"name": "devnet", "chain_id": 424242},
)
```

## What this library does differently

PureChain is a permissioned, free-gas EVM network, and three of its properties
break assumptions that general-purpose Ethereum libraries build in. Each one is
handled here by default rather than left to the caller.

**Fees are zero, and the oracle is never consulted.** Transactions are built with
zero fees and signed locally; `eth_gasPrice` is not called. A node started
without the `--gpo.*` flags reports a non-zero price on a chain whose base fee is
pinned to zero, so trusting the oracle is how callers end up overpaying — or
getting rejected. Override with `fees` if a network ever charges:

```python
from purechain import TipFeePolicy
create_client(fees=TipFeePolicy(tip_wei=1_000_000_000))
```

**Blocks are not produced on a fixed interval.** Smart Auto Mining seals only
while transactions are pending and pauses when the network is idle, so a static
head is healthy rather than stalled. `wait_for` therefore defaults to inclusion,
not a confirmation count — waiting for depth on a chain that goes quiet right
afterwards would never resolve. Depth is opt-in and always bounded:

```python
await client.wait_for(tx_hash)                                    # inclusion
await client.wait_for(tx_hash, WaitOptions(until="final", confirmations=3))
```

A timeout against a head that never moved raises `ChainIdleError` rather than
`TimeoutError_`, so "the network is quiet" is distinguishable from "something
went wrong".

**There is no replace-by-fee.** A pending transaction cannot be bumped or
cancelled at zero fee — the pool requires a strictly higher fee, and nothing is
higher than zero. Sends are serialised per sender so concurrent calls cannot
collide on a nonce, because the usual escape hatch does not exist here.

## Capabilities are detected, not assumed

Which JSON-RPC namespaces are available is a property of the node you connected
to, not of the variant. The public endpoints run `--http.api eth,net,web3`, so
`clique_*`, `txpool_*`, `admin_*` and `debug_*` are absent even though
purechain-geth implements them.

```python
caps = await client.capabilities()
caps.zero_fee                    # True
caps.replace_by_fee              # False
caps.subscriptions               # False -- public endpoints are HTTP-only
caps.has("clique_getSigners")    # False on the public RPC
caps.validator_api               # "clique" | "qbft" | "ibft" | None
```

Anything outside the core surface is reachable through the raw escape hatch,
once you have checked for it:

```python
if caps.has("clique_getSigners"):
    signers = await client.rpc("clique_getSigners", [])
```

## Events

`eth_subscribe` is unavailable on the public endpoints, so watching polls by
default and tolerates idle gaps. Delivery is ordered and gap-free.

```python
sub = await client.watch_logs(
    token.filter("Transfer"),
    lambda log: print(token.decode_log(log).args),
)
await sub.close()
```

## Contracts

ABIs are the JSON form (a list of dicts), as produced by `solc` and consumed by
the rest of the Python ecosystem.

```python
from purechain import Contract, deploy_contract

token = Contract("0xabc...", abi, client)
balance = await token.read("balanceOf", [address])
await token.write_and_wait("transfer", [to, 100])

result = await deploy_contract(client, abi=abi, bytecode=bytecode)
print(result.address)
```

Gas is free, so an account with a zero balance can deploy and call. No balance
pre-check is performed; a balance is only needed to move value.

## Development

```bash
uv sync --all-extras           # exact versions from uv.lock
pytest                         # offline unit tests
PURECHAIN_LIVE=1 pytest        # plus read-only tests against the public network
mypy && ruff check src tests
```

`uv.lock` pins all 54 packages, so every machine and CI run resolves the same
versions. Commit it. Use `uv lock --upgrade` to move dependencies forward
deliberately, rather than letting a fresh install drift on its own.

There is a third suite that **broadcasts real transactions** to the public
network. It is behind its own flag so it never runs by accident:

```bash
PURECHAIN_LIVE_WRITE=1 pytest tests/test_live_write.py
```

It generates a throwaway key and sends zero-value transfers to itself. No
funding is needed — gas is free, which is precisely what the test proves.

## Design

These are the rules the library is built on. New code should follow them.

### Layout

```
src/purechain/
  __init__.py   public API
  wallet.py     keys, mnemonics, keystore, signature verification
  units.py      PCN <-> wei
  address.py    validate, checksum, compare
  abi.py        offline encode / decode
  metrics.py    throughput, block timing, gas utilisation (reads)
  benchmark.py  latency and throughput under load (BROADCASTS)
  core/         variant-agnostic types, errors, fee policy, capability detection
  client/       the PureChainClient interface and the create_client factory
  variants/
    evm/        shared EVM engine, signing, contracts, waiting, watching
    geth/       purechain-geth -- implemented
    besu/       stub
    dag/        stub
```

The root modules are the namespaces from rule 9. `metrics` only reads;
`benchmark` writes to the chain, which is why they are separate. They sit beside `core`,
`client` and `variants` because they are top-level concerns, not a sub-part of
any of them. The TypeScript package has the same file names in the same places.

### 1. One interface, three variants

Every variant implements the same `PureChainClient` interface. That interface
holds only what all three can genuinely do — the intersection, not the union.

This is why `wait_for` is built around a finality level, with a confirmation
count only as an opt-in extra: a DAG has no block depth to count. Anything one
variant can do beyond the interface sits behind a capability check, or on that
variant's own class.

### 2. Three layers, one direction

`core` → `client` → `variants`. Code in `core` never imports from `variants`.

Nothing in `core` assumes blocks, a block interval, or an EVM. That single
constraint is what keeps the DAG variant possible behind the same interface.

### 3. Detect, don't assume

What a node can do is a property of the node, not the variant. The same
purechain-geth build exposes `clique_*` on your own machine and not on the
public RPC.

Capabilities are read once when the client connects, then cached. Check them
before using anything outside the core surface.

### 4. Defaults match this network, not the ecosystem

Zero fees, and the gas-price oracle is never called. Wait for inclusion, not
depth. Poll for events instead of subscribing.

Each of those is unusual for an Ethereum library and correct here. Where the
network forbids something outright — replace-by-fee — the library says so with a
named error rather than failing in a confusing way.

### 5. Wrap the cryptography, own the policy

web3.py does three jobs: signing, ABI coding, transport. This library decides
fees, nonces, waiting, and retries.

web3 types never appear in the public API. That is what lets the TypeScript port
sit on ethers and still behave identically.

### 6. Always leave an escape hatch

Blocks, transactions, receipts and logs all carry `raw` — the node's response
untouched, including fields the typed surface does not name. Clients with a real
backend also expose `rpc(method, params)`, which reaches any method at all. Stubs
do not, because they have nothing to call.

A typed API you cannot step outside of is a dead end on a network that adds its
own methods.

### 7. Errors carry codes

Branch on `err.code`, never on the message text. Messages are written for humans
and will change; codes will not. The code strings are identical in both
languages.

Two classes take a trailing underscore — `NotImplementedError_`, `TimeoutError_`
— so they do not shadow Python builtins. Their `code` values match TypeScript.

### 8. Stubs are honest

An unfinished variant still exposes the whole interface, and every call fails
with an error naming the variant. You find out at the call site, not three
frames deep in an `AttributeError`.

### 9. Objects hold state, namespaces hold pure functions

A client owns a connection, so it is an object. Creating a key or parsing an
amount needs no state, so those belong in namespaces — plain modules here, since
a Python module already is a namespace:

```python
from purechain import address, units, wallet

signer = wallet.create()            # no network needed
wei = units.parse_pcn("1.5")
ok = address.is_valid(some_string)
```

There are four: `wallet`, `units`, `address`, and `abi`. Binding an ABI to a
deployed address needs a client, so that stays on the `Contract` class rather
than becoming a fifth namespace — one way to do it, not two.

### 10. The two libraries match

Same folders, same module names, same method names, same error codes. The only
intended difference is casing: `snake_case` here, `camelCase` in TypeScript.

A change to one library is a change to both.

Where a language convention genuinely differs, follow the local one and say so.
Two cases exist today: ABIs are JSON lists here and may also be
human-readable signature strings in TypeScript, and async iteration and context
managers follow Python norms.
