robonomics-interface 2.0.0__tar.gz → 3.0.0rc2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. robonomics_interface-3.0.0rc2/.gitignore +12 -0
  2. robonomics_interface-3.0.0rc2/CHANGELOG.md +128 -0
  3. robonomics_interface-3.0.0rc2/MIGRATION.md +394 -0
  4. robonomics_interface-3.0.0rc2/PKG-INFO +235 -0
  5. robonomics_interface-3.0.0rc2/README.md +204 -0
  6. robonomics_interface-3.0.0rc2/pyproject.toml +94 -0
  7. robonomics_interface-3.0.0rc2/src/robonomicsinterface/__init__.py +124 -0
  8. robonomics_interface-3.0.0rc2/src/robonomicsinterface/bip39.py +117 -0
  9. robonomics_interface-3.0.0rc2/src/robonomicsinterface/client.py +699 -0
  10. robonomics_interface-3.0.0rc2/src/robonomicsinterface/english.txt +2048 -0
  11. robonomics_interface-3.0.0rc2/src/robonomicsinterface/envelope.py +119 -0
  12. robonomics_interface-3.0.0rc2/src/robonomicsinterface/errors.py +195 -0
  13. robonomics_interface-3.0.0rc2/src/robonomicsinterface/extrinsic.py +494 -0
  14. robonomics_interface-3.0.0rc2/src/robonomicsinterface/keys.py +305 -0
  15. robonomics_interface-3.0.0rc2/src/robonomicsinterface/pallets/__init__.py +22 -0
  16. robonomics_interface-3.0.0rc2/src/robonomicsinterface/pallets/_common.py +49 -0
  17. robonomics_interface-3.0.0rc2/src/robonomicsinterface/pallets/datalog.py +202 -0
  18. robonomics_interface-3.0.0rc2/src/robonomicsinterface/pallets/rws.py +202 -0
  19. robonomics_interface-3.0.0rc2/src/robonomicsinterface/pallets/system.py +153 -0
  20. robonomics_interface-3.0.0rc2/src/robonomicsinterface/py.typed +0 -0
  21. robonomics_interface-3.0.0rc2/src/robonomicsinterface/rpc.py +16 -0
  22. robonomics_interface-3.0.0rc2/src/robonomicsinterface/runtime.py +439 -0
  23. robonomics_interface-3.0.0rc2/src/robonomicsinterface/ss58.py +116 -0
  24. robonomics_interface-3.0.0rc2/src/robonomicsinterface/storage.py +264 -0
  25. robonomics_interface-3.0.0rc2/src/robonomicsinterface/sync.py +426 -0
  26. robonomics_interface-3.0.0rc2/src/robonomicsinterface/transport.py +267 -0
  27. robonomics_interface-3.0.0rc2/tests/conftest.py +25 -0
  28. robonomics_interface-3.0.0rc2/tests/fake_node.py +77 -0
  29. robonomics_interface-3.0.0rc2/tests/fake_server.py +191 -0
  30. robonomics_interface-3.0.0rc2/tests/fixtures/chain_samples.json +142 -0
  31. robonomics_interface-3.0.0rc2/tests/fixtures/compat_vectors.json +209 -0
  32. robonomics_interface-3.0.0rc2/tests/fixtures/event_samples.json +27 -0
  33. robonomics_interface-3.0.0rc2/tests/fixtures/extrinsic_vectors.json +39 -0
  34. robonomics_interface-3.0.0rc2/tests/fixtures/generate_compat_vectors.py +106 -0
  35. robonomics_interface-3.0.0rc2/tests/fixtures/metadata.hex.gz +0 -0
  36. robonomics_interface-3.0.0rc2/tests/fixtures/metadata_spec43.hex.gz +0 -0
  37. robonomics_interface-3.0.0rc2/tests/fixtures/substrate_vectors.json +36 -0
  38. robonomics_interface-3.0.0rc2/tests/integration/test_devchain.py +199 -0
  39. robonomics_interface-3.0.0rc2/tests/integration/test_mainnet.py +55 -0
  40. robonomics_interface-3.0.0rc2/tests/test_client.py +364 -0
  41. robonomics_interface-3.0.0rc2/tests/test_compat.py +85 -0
  42. robonomics_interface-3.0.0rc2/tests/test_envelope.py +128 -0
  43. robonomics_interface-3.0.0rc2/tests/test_extrinsic.py +238 -0
  44. robonomics_interface-3.0.0rc2/tests/test_keys.py +208 -0
  45. robonomics_interface-3.0.0rc2/tests/test_pallets.py +240 -0
  46. robonomics_interface-3.0.0rc2/tests/test_runtime.py +164 -0
  47. robonomics_interface-3.0.0rc2/tests/test_ss58.py +57 -0
  48. robonomics_interface-3.0.0rc2/tests/test_stable_api.py +96 -0
  49. robonomics_interface-3.0.0rc2/tests/test_storage.py +179 -0
  50. robonomics_interface-3.0.0rc2/tests/test_submit.py +173 -0
  51. robonomics_interface-3.0.0rc2/tests/test_sync.py +159 -0
  52. robonomics_interface-2.0.0/PKG-INFO +0 -92
  53. robonomics_interface-2.0.0/README.md +0 -62
  54. robonomics_interface-2.0.0/pyproject.toml +0 -58
  55. robonomics_interface-2.0.0/robonomicsinterface/__init__.py +0 -20
  56. robonomics_interface-2.0.0/robonomicsinterface/classes/__init__.py +0 -13
  57. robonomics_interface-2.0.0/robonomicsinterface/classes/account.py +0 -57
  58. robonomics_interface-2.0.0/robonomicsinterface/classes/base.py +0 -40
  59. robonomics_interface-2.0.0/robonomicsinterface/classes/chain_utils.py +0 -123
  60. robonomics_interface-2.0.0/robonomicsinterface/classes/common_functions.py +0 -77
  61. robonomics_interface-2.0.0/robonomicsinterface/classes/datalog.py +0 -102
  62. robonomics_interface-2.0.0/robonomicsinterface/classes/digital_twin.py +0 -148
  63. robonomics_interface-2.0.0/robonomicsinterface/classes/launch.py +0 -39
  64. robonomics_interface-2.0.0/robonomicsinterface/classes/liability.py +0 -252
  65. robonomics_interface-2.0.0/robonomicsinterface/classes/pubsub.py +0 -114
  66. robonomics_interface-2.0.0/robonomicsinterface/classes/reqres.py +0 -47
  67. robonomics_interface-2.0.0/robonomicsinterface/classes/rws.py +0 -176
  68. robonomics_interface-2.0.0/robonomicsinterface/classes/service_functions.py +0 -191
  69. robonomics_interface-2.0.0/robonomicsinterface/classes/subscriptions.py +0 -140
  70. robonomics_interface-2.0.0/robonomicsinterface/constants.py +0 -9
  71. robonomics_interface-2.0.0/robonomicsinterface/decorators.py +0 -49
  72. robonomics_interface-2.0.0/robonomicsinterface/exceptions.py +0 -25
  73. robonomics_interface-2.0.0/robonomicsinterface/robonomics_interface_io.py +0 -128
  74. robonomics_interface-2.0.0/robonomicsinterface/types.py +0 -13
  75. robonomics_interface-2.0.0/robonomicsinterface/utils.py +0 -98
  76. {robonomics_interface-2.0.0 → robonomics_interface-3.0.0rc2}/LICENSE +0 -0
@@ -0,0 +1,12 @@
1
+ dist/
2
+ build/
3
+ .venv/
4
+ .idea/
5
+ .vscode/
6
+ __pycache__/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .mypy_cache/
10
+ .ruff_cache/
11
+ .coverage
12
+ .devchain/
@@ -0,0 +1,128 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [3.0.0rc2] — 2026-09-27
8
+
9
+ From three days of rc1 in rrs-connector, rrs-admin and rrs-ha-integration.
10
+
11
+ ### Fixed
12
+ - The default TLS context for `wss://` endpoints is built once per client, in a worker
13
+ thread, and shared by every connection. Before, asyncio built one per connection on
14
+ the event loop, reading the CA store from disk — Home Assistant reported a blocking
15
+ call. Passing `ssl=` works as before.
16
+
17
+ ### Added
18
+ - Block hashes given as `at=` or to `client.chain.block_number()` are checked
19
+ (`0x` + 64 hex digits) before any request; a bad one raises `EncodeError`.
20
+ - A test pins the API the Report Service projects use; MIGRATION.md lists it under
21
+ "Stable API".
22
+ - Integration tests that send transactions refuse a non-loopback node unless
23
+ `ROBONOMICS_DEV_ALLOW_REMOTE=1`, and refuse production chains outright.
24
+ - A weekly read-only smoke test against mainnet in CI.
25
+
26
+ ### Changed
27
+ - Robonomics on Kusama is legacy and is shutting down. A client refuses a Kusama node
28
+ by default with "switch to Polkadot" in the reason; with `genesis_hash=None` it
29
+ connects and logs a warning. No Kusama constant is provided.
30
+
31
+ ## [3.0.0rc1] — 2026-09-24
32
+
33
+ A rewrite for the current Robonomics runtime. Not compatible with 2.x: see
34
+ [MIGRATION.md](MIGRATION.md) for the mapping and the behaviour that changed.
35
+
36
+ ### Added
37
+ - `Keypair` (ED25519) from a mnemonic (with optional password), a raw seed, a
38
+ Substrate secret URI with hard derivation (`//Alice`, `<phrase>//a//b///password`),
39
+ a public key or an address. `from_secret()` accepts only what a person should
40
+ paste: a mnemonic or a seed, never a derivation path.
41
+ - Message encryption (`encrypt_message` / `decrypt_message`) byte-compatible with
42
+ substrate-interface.
43
+ - Multi-recipient envelope: `encrypt_for_recipients`, `decrypt_package`,
44
+ `parse_decrypted`, in the format Robonomics Report Service publishes.
45
+ - SS58 helpers: `encode_address`, `decode_address`, `address_format`,
46
+ `is_valid_address` (format 32 by default).
47
+ - BIP39 English: `generate_mnemonic`, `validate_mnemonic`.
48
+ - A typed exception hierarchy rooted at `RobonomicsError`; input errors are also
49
+ `ValueError`. No exception message contains secret material.
50
+ - Runtime metadata (V14) decoded from the chain itself: `Runtime` describes one
51
+ runtime version (pallets, storage entries, constants, SCALE encode/decode);
52
+ `RuntimeCache` fetches metadata per `(genesis_hash, spec_version)`, parses it off
53
+ the event loop and reloads it after a runtime upgrade.
54
+ - Storage: every hasher (`Identity`, `Twox64Concat`, `Twox128`, `Twox256`,
55
+ `Blake2_128`, `Blake2_256`, `Blake2_128Concat`), multi-key maps and maps keyed by
56
+ a tuple; `query`, `query_map` (paged, pinned to one block, keys decoded back) and
57
+ `constant`. An absent value is `None` for an `Optional` item and the declared
58
+ default otherwise.
59
+ - New errors: `MetadataError`, `NoSuchPallet`, `NoSuchStorage`, `NoSuchConstant`,
60
+ `EncodeError`, `DecodeError`.
61
+ - `RobonomicsClient`, an asyncio client over one multiplexed WebSocket: endpoints in
62
+ priority order (e.g. a LAN node first, the public node as fallback), a node is
63
+ used only if its genesis hash matches Robonomics Polkadot and it is neither
64
+ syncing nor without peers, failback to a preferred endpoint, a timeout on every
65
+ request, automatic retry of reads on another endpoint (`retry=False` for requests
66
+ with side effects), subscriptions, and runtime-upgrade tracking through
67
+ `state_subscribeRuntimeVersion`. `query`, `query_map` and `constant` on the client.
68
+ - Transport errors, all `TransportError` (retryable): `ConnectionFailed`,
69
+ `ConnectionLost`, `RequestTimeout`, `AllEndpointsFailed` (with the reason for each
70
+ endpoint). `RpcError` for errors returned by the node.
71
+ - Extrinsics: `compose_call` checks arguments against the metadata and takes natural
72
+ values (`bytes`, `Keypair`, nested `Call`, a plain list for `BoundedVec` — no more
73
+ `[[a, b]]` for `RWS.set_devices`); signed extensions come from the metadata, in its
74
+ order, and an unknown one that carries data is refused (`UnsupportedExtension`);
75
+ mortal era by default (`Era`); `sign`, `validate` (the pool's own check via
76
+ `state_call`), `submit` (waits for the block and reads its events) and
77
+ `submit_nowait`. One submission per account at a time, so nonces never collide.
78
+ - `ExtrinsicResult` with the block, index and events of the extrinsic.
79
+ - Transaction errors, all `TransactionError`: `InvalidTransaction` (kind plus a
80
+ plain explanation — `Payment` says the account does not exist on chain),
81
+ `ExtrinsicFailed` (pallet error name and docs, e.g. `RWS.FreeWeightIsNotEnough`),
82
+ `ExtrinsicDropped`, `ExtrinsicOutcomeUnknown` (with the hash to look up).
83
+ - Pallet helpers on the client:
84
+ - `client.datalog`: `index`, `item` (slot 0 is slot 0 — in 2.x `get_item(index=0)`
85
+ meant "latest"), `latest` (also correct after the ring buffer wraps to 0, where
86
+ 2.x returned `None`), `items` (all live records in one request), `record`
87
+ (bytes or UTF-8 text, size checked, optionally through an RWS subscription),
88
+ `erase`. Records come back as `bytes` (`DatalogItem`), never as text-or-hex.
89
+ - `client.rws`: `ledger` (`Ledger` with `is_active`, `days_left`, `expires_at`),
90
+ `devices`, `is_device`, `max_devices`, `set_devices` (plain list, checked and
91
+ de-duplicated, `TooManyDevices` past the limit), `add_devices`,
92
+ `remove_devices`, `call`, `bid`, auction reads.
93
+ - `client.system` (`account` → `AccountInfo` with `exists`, `next_nonce`),
94
+ `client.balances` (`existential_deposit`, `transfer_keep_alive`,
95
+ `transfer_allow_death`), `client.chain` (block hashes and numbers, runtime
96
+ version). `XRT = 10**9`.
97
+ - `RobonomicsSync`, a blocking wrapper for scripts and cron jobs: the async client
98
+ on an event loop in a background thread, with the same methods, arguments,
99
+ errors and timeouts (a test keeps the two APIs in step). `query_map` becomes a
100
+ plain iterator. Calling it from inside a running event loop raises
101
+ `RuntimeError` rather than stalling the loop; closing it stops every thread it
102
+ started.
103
+ - Tests: offline suites on recorded mainnet data and local fake nodes; integration
104
+ tests against a development node running the mainnet runtime
105
+ (`scripts/devchain.sh`), covering transfers, datalog, sudo, RWS subscriptions and
106
+ their failures; a read-only mainnet smoke test.
107
+ - `py.typed`.
108
+
109
+ ### Changed
110
+ - Python 3.12+; built with hatchling.
111
+ - Core dependencies are `pynacl`, `scalecodec`, `websockets` and `xxhash` only;
112
+ every one installs from a wheel or as pure Python on musl/aarch64.
113
+ - ED25519 is the only key type. sr25519 returns as an optional extra in a later
114
+ release.
115
+ - Extrinsic failures raise instead of returning a hash: inclusion in a block is
116
+ checked against the block's events.
117
+ - `Keypair.sign()` signs a `str` as its UTF-8 bytes. substrate-interface decoded a
118
+ `str` starting with `0x` as hex first; pass `bytes.fromhex(...)` for that.
119
+
120
+ ### Removed
121
+ - The dependency on `substrate-interface` and, with it, `ecdsa`, the Ethereum
122
+ libraries, `smoldot-light` and the Rust bindings.
123
+ - The 2.x API: `Account`, `ServiceFunctions`, `BaseClass`, `ChainUtils`,
124
+ `CommonFunctions`, the per-pallet classes, `Subscriber`/`SubEvent`,
125
+ `TYPE_REGISTRY`, and the `robonomics_interface` CLI.
126
+ - `PubSub` and `ReqRes`: the node RPCs behind them are gone since collators moved
127
+ to `polkadot-omni-node`.
128
+ - IPFS helpers (`web_3_auth`, `ipfs_32_bytes_to_qm_hash`, `ipfs_qm_hash_to_32_bytes`).
@@ -0,0 +1,394 @@
1
+ # Migrating to robonomics-interface 3.0
2
+
3
+ 3.0 is a rewrite. It keeps the wire formats — the same address from the same
4
+ mnemonic, the same ciphertext, the same extrinsic bytes — and changes the API.
5
+ This guide is written for moving the Robonomics Report Service projects
6
+ (`rrs-connector`, `rrs-admin`, `rrs-ha-integration`) off `robonomics-interface`
7
+ 2.x, `substrate-interface` and the copied `chain/` packages, but applies to any
8
+ 2.x user.
9
+
10
+ Pin the exact version everywhere: `robonomics-interface==3.0.0rc2`, then `==3.0.0`.
11
+ The API listed in section 8 stays as it is between release candidates and 3.0.0.
12
+
13
+ ## 1. What changed, and why
14
+
15
+ | | 2.x | 3.0 |
16
+ | --- | --- | --- |
17
+ | Dependencies | `substrate-interface` (Rust bindings, `ecdsa`, Ethereum libs, `smoldot-light`) | `pynacl`, `scalecodec`, `websockets`, `xxhash`; installs on musl/aarch64 with no compiler |
18
+ | Keys | sr25519 by default, ed25519 optional | ED25519 only (sr25519 returns as an extra in 3.1) |
19
+ | Connection | hidden, per object, synchronous, no request timeout | explicit `RobonomicsClient` (asyncio) or `RobonomicsSync`; timeout on every request; endpoints in priority order with health checks and failover |
20
+ | Types | hand-written type registry | the chain's own metadata |
21
+ | Writes | returns a hash; failure inside the block goes unnoticed | reads the block's events; a failed call raises `ExtrinsicFailed` naming the pallet error |
22
+ | Errors | whatever the transport library raised | one hierarchy under `RobonomicsError`; network failures are `TransportError` (retryable), everything else is not |
23
+
24
+ ## 2. Dependencies
25
+
26
+ Remove `substrate-interface`, `websocket-client` and any copy of the `chain/`
27
+ package. Add `robonomics-interface==3.0.0rc1`. It brings `pynacl`, `scalecodec`,
28
+ `websockets` and `xxhash`; do not list them separately unless the project uses
29
+ them directly.
30
+
31
+ Home Assistant: the manifest `requirements` becomes `["robonomics-interface==3.0.0rc1", ...]`
32
+ (plus the project's own, e.g. `pinatapy-vourhey`). HA already constrains
33
+ `websockets>=15`, which 3.0 accepts. No `aiohttp` session is needed: the library
34
+ has its own WebSocket client.
35
+
36
+ ## 3. The client
37
+
38
+ ### Async (Home Assistant, any asyncio code)
39
+
40
+ ```python
41
+ from robonomicsinterface import RobonomicsClient
42
+
43
+ client = RobonomicsClient(endpoints) # str or list, in order of preference
44
+ await client.connect() # or: async with RobonomicsClient(...) as client:
45
+ ...
46
+ await client.close()
47
+ ```
48
+
49
+ Create one client and keep it: it holds one multiplexed connection and the
50
+ parsed runtime metadata (parsing takes seconds on a Raspberry Pi; the library
51
+ does it off the event loop). In an integration, create it per config entry and
52
+ close it on unload (`entry.async_on_unload(client.close)`).
53
+
54
+ In Home Assistant, pass HA's shared TLS context:
55
+ `RobonomicsClient(endpoints, ssl=homeassistant.util.ssl.client_context())`. It honours
56
+ HA's CA settings and is built once for all integrations. Without `ssl=`, the library
57
+ builds its own default context once per client, off the event loop (since rc2; rc1
58
+ built one per connection on the loop, which HA reported as a blocking call).
59
+
60
+ ### Blocking (scripts, cron jobs, CLIs)
61
+
62
+ ```python
63
+ from robonomicsinterface import RobonomicsSync
64
+
65
+ with RobonomicsSync(endpoints) as client:
66
+ client.datalog.latest(address)
67
+ ```
68
+
69
+ Same methods and arguments without `await`; `query_map` is a plain iterator.
70
+ It must not be called from a thread running an event loop — it raises
71
+ `RuntimeError` there, because blocking would stall that loop.
72
+
73
+ ### Options that replace 2.x workarounds
74
+
75
+ | Need | 3.0 |
76
+ | --- | --- |
77
+ | Per-request timeout (was: `interface.websocket.settimeout(...)`) | `RobonomicsClient(..., timeout=30.0, connect_timeout=10.0)` |
78
+ | Several nodes, move on when one fails (was: hand-written `_reconnect` / `change_current_wss`) | pass them all: `RobonomicsClient([local, public])`; reads are retried on the next endpoint (`retries=2`), the client returns to a preferred endpoint on its own (`failback_interval=300`) |
79
+ | Know which node is in use | `client.endpoint` |
80
+ | Kusama | **Legacy: Robonomics on Kusama is shutting down — switch to Polkadot.** The default genesis check refuses a Kusama node and says so; with `genesis_hash=None` the client connects and logs a warning. No Kusama constant is provided. |
81
+ | A node on the local network | put it first: `["ws://192.168.1.10:9944", DEFAULT_ENDPOINT]`; a node that is syncing or has no peers is skipped |
82
+
83
+ ## 4. Errors
84
+
85
+ ```
86
+ RobonomicsError
87
+ ├── TransportError .retryable = True: the node was not reached or stopped answering
88
+ │ ├── ConnectionFailed (.endpoint, .reason)
89
+ │ ├── ConnectionLost
90
+ │ ├── RequestTimeout (also a TimeoutError)
91
+ │ └── AllEndpointsFailed (.failures: one ConnectionFailed per endpoint)
92
+ ├── RpcError the node answered with an error (.code, .message, .data)
93
+ ├── TransactionError an extrinsic did not do what was asked
94
+ │ ├── InvalidTransaction refused before sending (.kind, .explanation)
95
+ │ ├── ExtrinsicFailed in a block, call failed (.pallet, .error, .docs, .result)
96
+ │ ├── ExtrinsicDropped dropped by the node
97
+ │ └── ExtrinsicOutcomeUnknown sent, outcome unknown (.extrinsic_hash) — do not resend blindly
98
+ ├── MetadataError / NoSuchPallet / NoSuchStorage / NoSuchCall / NoSuchConstant / UnsupportedExtension
99
+ ├── EncodeError / DecodeError (ValueError) e.g. TooManyDevices is an EncodeError
100
+ ├── InvalidAddress / InvalidKey / InvalidMnemonic (ValueError)
101
+ ├── NoSecretKey, DecryptionError
102
+ └── EnvelopeError: RecipientError, PackageError (ValueError), PayloadError
103
+ ```
104
+
105
+ Replace `TRANSIENT_ERRORS = (OSError, TimeoutError, WebSocketException)` and
106
+ `RETRYABLE_ERRORS = (RpcError, ChainError, TimeoutError, OSError)` with
107
+ `TransportError`. The client already retries reads across endpoints, so an
108
+ outer retry loop only needs to cover a whole run, if at all. `RpcError` is not a
109
+ network failure and repeating the request will not help.
110
+
111
+ ## 5. API mapping
112
+
113
+ ### Keys and addresses
114
+
115
+ | 2.x / substrate-interface / `chain/` | 3.0 |
116
+ | --- | --- |
117
+ | `Keypair.create_from_mnemonic(m, crypto_type=KeypairType.ED25519, ss58_format=32)` | `Keypair.from_mnemonic(m)` |
118
+ | `chain.Keypair.create_from_secret(s)` (mnemonic or `0x` seed) | `Keypair.from_secret(s)` |
119
+ | `Account(seed, crypto_type=KeypairType.ED25519)` | `Keypair.from_secret(seed)` |
120
+ | `account.get_address()`, `keypair.ss58_address` | `keypair.address` |
121
+ | `account.keypair` | the `Keypair` itself |
122
+ | `Keypair(ss58_address=a, crypto_type=KeypairType.ED25519)`, `chain.Keypair.create_from_address(a)` | `Keypair.from_address(a)` (public only) |
123
+ | `chain.Keypair.create_from_public_key(pk)` | `Keypair.from_public_key(pk)` |
124
+ | `Keypair.generate_mnemonic()` | `generate_mnemonic()` or `Keypair.generate_mnemonic()` |
125
+ | `is_valid_ss58_address(a)` | `is_valid_address(a, ss58_format=None)` — see pitfall 4 |
126
+ | `chain.ss58_decode(a)` / `SS58Error` | `decode_address(a)` / `InvalidAddress` (a `ValueError`) |
127
+ | `chain.ss58_encode(pk, 32)` | `encode_address(pk)` |
128
+ | `"//Alice"` for tests | `Keypair.from_uri("//Alice")` (ED25519, as polkadot-sdk derives it) |
129
+
130
+ ### Encryption
131
+
132
+ | Before | 3.0 |
133
+ | --- | --- |
134
+ | `keypair.encrypt_message(msg, recipient_pk)` / `decrypt_message(box, sender_pk)` | same names, same bytes; failures raise `DecryptionError` |
135
+ | `chain.envelope.encrypt_for_recipients(data, sender, addresses, meta)` | `encrypt_for_recipients(...)`, same arguments and output |
136
+ | `chain.envelope.decrypt_package(package, recipient, sender_address)`, connector's `multi_envelope_decrypt_data(...)` | `decrypt_package(package, recipient_keypair, sender_address)` |
137
+ | `parse_decrypted(text)` | `parse_decrypted(text)` |
138
+ | `chain.envelope` errors | `RecipientError`, `PackageError`, `PayloadError` (all `EnvelopeError`) |
139
+
140
+ ### Reading the chain
141
+
142
+ | Before | 3.0 (async; `RobonomicsSync` is the same without `await`) |
143
+ | --- | --- |
144
+ | `Datalog(Account(remote_ws=url)).get_index(a)` → `{"start", "end"}` | `await client.datalog.index(a)` → `DatalogIndex(start, end, window_size)` with `.count`, `.positions` (oldest first), `.latest_position` |
145
+ | `interface.get_constant("Datalog", "WindowSize").value` | `await client.datalog.window_size()` or `await client.constant("Datalog", "WindowSize")` |
146
+ | `chainstate_query("Datalog", "DatalogItem", [a, i])` → `(ts, content)` | `await client.datalog.item(a, i)` → `DatalogItem(index, timestamp_ms, data)` or `None` |
147
+ | walking `ring_buffer_indices(...)` and calling `get_item` per slot | `await client.datalog.items(a)` — every live record, oldest first, in one request; `await client.datalog.latest(a)` |
148
+ | `Datalog.get_item(a)` (latest) | `await client.datalog.latest(a)` |
149
+ | `substrate.query("RWS", "Devices", [pool]).value or []` | `await client.rws.devices(pool)` → `list[str]` |
150
+ | `substrate.query("RWS", "Ledger", [pool]).value` | `await client.rws.ledger(pool)` → `Ledger` or `None`: `.kind` (`"Daily"`/`"Lifetime"`), `.days`, `.tps`, `.issued_at`, `.expires_at`, `.is_active()`, `.days_left()`, `.free_weight` |
151
+ | `substrate.query("System", "Account", [a]).value` | `await client.system.account(a)` → `AccountInfo`: `.free`, `.reserved`, `.frozen`, `.nonce`, `.exists` |
152
+ | anything else: `substrate.query(pallet, item, [keys])` | `await client.query(pallet, item, *keys)`; maps: `client.query_map(pallet, item)`; constants: `client.constant(pallet, name)` |
153
+ | `RWS.MaxDevicesAmount` | `await client.rws.max_devices()` |
154
+
155
+ ### Writing
156
+
157
+ | Before | 3.0 |
158
+ | --- | --- |
159
+ | `chain.RobonomicsClient.record_datalog(kp, data, owner)` | `await client.datalog.record(kp, data, subscription_owner=owner)` |
160
+ | `Datalog(Account(seed), rws_sub_owner=owner).record(data)` | same as above |
161
+ | `compose_call("RWS", "set_devices", {"devices": [devices]})` + sign + `submit_extrinsic(wait_for_inclusion=True)` + `receipt.is_success` | `await client.rws.set_devices(owner_kp, devices)` — a **flat** list |
162
+ | read devices, edit, `set_devices` | `await client.rws.add_devices(owner_kp, *new)` / `remove_devices(owner_kp, *old)` (`None` when nothing changes) |
163
+ | `CommonFunctions.transfer_tokens(to, amount)` | `await client.balances.transfer_keep_alive(kp, to, amount)` |
164
+ | any other call | `call = await client.compose_call(pallet, function, args)`; `await client.submit(call, kp)` |
165
+
166
+ Every write returns an `ExtrinsicResult` (`.block_hash`, `.block_number`,
167
+ `.index`, `.events`, `.find(pallet, name)`) or raises. Options for all writes:
168
+ `wait_for="in_block"|"finalized"`, `tip`, `era_period` (64; `None` for immortal),
169
+ `validate` (ask the runtime first; on by default), `timeout` (120 s).
170
+
171
+ ## 6. Pitfalls — behaviour that changed silently in meaning
172
+
173
+ 1. **`item(a, 0)` is slot 0.** In 2.x `get_item(index=0)` meant "latest". The
174
+ connector worked around it through `_service_functions`; that code goes.
175
+ 2. **Only live slots are returned.** The ring buffer keeps at most
176
+ `WindowSize - 1` (127) records; the slot at `end` may still hold an
177
+ overwritten record in storage. `item()` returns `None` for it, where a raw
178
+ storage read returned the stale record.
179
+ 3. **Records are `bytes`.** scalecodec returned a record as text when it was
180
+ printable and as `0x…` hex otherwise, so `str(content)` could silently turn a
181
+ CID into hex. Use `item.text` (UTF-8 or `None`) or `item.data`. When writing,
182
+ a `str` is sent as UTF-8 — `"0x12"` stays the four characters it is.
183
+ 4. **`is_valid_address(a)` checks for Robonomics (format 32) by default.**
184
+ `is_valid_ss58_address(a)` accepted any network. Pass `ss58_format=None` to
185
+ keep the old behaviour, or keep the default to reject `5…` addresses.
186
+ `Keypair.from_address` and `decode_address` accept any format unless given one.
187
+ 5. **`set_devices` takes a flat list and replaces the whole list.** The
188
+ `[[a, b]]` wrapping and the decode-back guard in `rrs-admin` go. Addresses of
189
+ any SS58 format are accepted, normalised to format 32 and de-duplicated; more
190
+ than `MaxDevicesAmount` (32) raises `TooManyDevices` before anything is sent.
191
+ `add_devices`/`remove_devices` read, edit and write — two concurrent edits of
192
+ one owner can lose one.
193
+ 6. **A failed call raises.** Inclusion in a block is not success. Catch
194
+ `ExtrinsicFailed` and look at `.pallet`/`.error`:
195
+ - `RWS.NotLinkedDevice` — the signer is not in the owner's device list;
196
+ - `RWS.FreeWeightIsNotEnough` — no allowance left: an **expired daily
197
+ subscription stays on chain** and fails this way (not with
198
+ `NoSubscription`), and so does a fresh one before it has accrued;
199
+ - `RWS.NoSubscription` — the owner has none.
200
+ 7. **`InvalidTransaction` with `.kind == "Payment"`** means the signer does not
201
+ exist on chain (balance below the existential deposit, 0.000001 XRT), even for
202
+ free RWS calls. `client.system.exists(a)` tells beforehand. It is true when the
203
+ account has providers or sufficients — not the `free or nonce` test `rrs-admin`
204
+ used, which misses reserved-only accounts.
205
+ 8. **`ExtrinsicOutcomeUnknown` is not a failure.** The extrinsic was sent and
206
+ may still land; look it up by `.extrinsic_hash` before sending again.
207
+ 9. **Mortal era by default.** Extrinsics are valid for ~64 blocks, so a stuck
208
+ one cannot land an hour later, nor be replayed if the account is ever reaped.
209
+ 10. **`Keypair.from_secret` refuses derivation paths** (`//Alice`); use
210
+ `Keypair.from_uri` in tests. `Keypair.sign(str)` signs UTF-8; the 2.x stack
211
+ decoded a `str` starting with `0x` as hex first.
212
+ 11. **Keypairs keep secrets to themselves:** `repr()` shows the address, errors
213
+ never include the input, pickling raises `TypeError`.
214
+ 12. **One key type.** Accounts must be ED25519 — which report encryption
215
+ already required. A mnemonic that was used with sr25519 gives a different
216
+ address here.
217
+
218
+ ## 7. Per project
219
+
220
+ ### rrs-connector
221
+
222
+ - `pyproject.toml`: drop `substrate-interface` and `websocket-client`; replace
223
+ `robonomics-interface>=2.0.0` with `==3.0.0rc1`.
224
+ - `robonomics/datalog_reader.py`: rebuild on `RobonomicsSync`, keeping
225
+ `DatalogRecord`, `DatalogIndexRange`, `DatalogScan` and the cursor logic
226
+ (`list_new_records`, `list_last_records`) as they are:
227
+
228
+ ```python
229
+ class DatalogReader:
230
+ def __init__(self, wss_endpoints, request_timeout_seconds, max_attempts=1, backoff_seconds=0):
231
+ if not wss_endpoints:
232
+ raise ValueError("At least one WSS endpoint is required")
233
+ self.client = RobonomicsSync(
234
+ list(wss_endpoints),
235
+ timeout=request_timeout_seconds,
236
+ retries=max(max_attempts - 1, 0),
237
+ ).connect()
238
+
239
+ def get_window_size(self) -> int:
240
+ return self.client.datalog.window_size()
241
+
242
+ def get_index_range(self, sender_address):
243
+ index = self.client.datalog.index(sender_address)
244
+ return DatalogIndexRange(index.start, index.end)
245
+
246
+ def get_item(self, sender_address, datalog_index):
247
+ item = self.client.datalog.item(sender_address, datalog_index)
248
+ if item is None or item.text is None:
249
+ return None
250
+ return DatalogRecord(sender_address, item.index, item.timestamp_ms, payload=item.text)
251
+ ```
252
+
253
+ `_interface`, `_reconnect`, the socket timeout and the direct
254
+ `chainstate_query` all go. `ring_buffer_indices` can stay (it matches
255
+ `DatalogIndex.positions`) or be replaced; `client.datalog.items(a)` reads all
256
+ records in one request, which is cheaper than one `get_item` per slot. Add a
257
+ `close()` and call it at the end of a run.
258
+ - `robonomics/retry.py`: `TRANSIENT_ERRORS = (TransportError,)`; with the
259
+ client retrying across endpoints, `before_retry=_reconnect` is no longer needed.
260
+ - `keygen.py`: `mnemonic = generate_mnemonic()`, `Keypair.from_mnemonic(...)`,
261
+ `.address`. The read-back check stays.
262
+ - `config.py`: `is_valid_address(address, ss58_format=None)` to keep accepting
263
+ any format — or the default, to require Robonomics addresses (pitfall 4).
264
+ - `proton_pass.py`: `load_integrator_account` returns
265
+ `Keypair.from_secret(seed)` and compares `.address`.
266
+ - `pipeline.py`, `reports/recipients.py`: the `Account` type becomes `Keypair`
267
+ (`AccountLoader = Callable[[str], Keypair]`).
268
+ - `reports/decryptor.py`: `multi_envelope_decrypt_data` becomes a thin wrapper
269
+ over `decrypt_package(package, recipient_keypair, sender_address)` that maps
270
+ `PackageError`/`PayloadError` to `ReportDecryptionError`; `decrypt_msg` and
271
+ the `substrateinterface` imports go. `parse_decrypted` can come from the library.
272
+ - Tests: `tests/test_decryptor.py`, `test_keygen.py`, `test_retry.py`,
273
+ `conftest.py` import `substrateinterface`; switch them to the library. Keep
274
+ any fixture ciphertext produced by the old stack: it is the compatibility check.
275
+
276
+ ### rrs-admin
277
+
278
+ - `pyproject.toml`: drop `substrate-interface`; add `robonomics-interface==3.0.0rc1`.
279
+ - Delete `src/rrs_admin/chain/` (the third copy of the integration's key code);
280
+ import from `robonomicsinterface`: `Keypair.create_from_mnemonic` →
281
+ `Keypair.from_mnemonic`, `create_from_secret` → `from_secret`, `.ss58_address`
282
+ → `.address`, `ss58_decode` → `decode_address`, `SS58Error` → `InvalidAddress`,
283
+ `validate_mnemonic` unchanged, `MnemonicError` → `InvalidMnemonic`.
284
+ Files: `pools.py`, `sites.py`.
285
+ - `rws.py`:
286
+
287
+ ```python
288
+ def read_subscription(url, pool) -> Subscription:
289
+ with RobonomicsSync(url) as client:
290
+ devices = client.rws.devices(pool)
291
+ ledger = client.rws.ledger(pool)
292
+ account = client.system.account(pool)
293
+ ... # Subscription(kind=ledger.kind, issued=ledger.issued_at,
294
+ # expires=ledger.expires_at, free_weight=ledger.free_weight,
295
+ # balance=account.free / XRT)
296
+
297
+ def account_exists(url, address) -> bool:
298
+ with RobonomicsSync(url) as client:
299
+ return client.system.exists(address)
300
+
301
+ def set_devices(url, pool_seed, devices) -> str:
302
+ REDACT.add(pool_seed)
303
+ pool = Keypair.from_mnemonic(pool_seed)
304
+ planned = list(dict.fromkeys(encode_address(decode_address(d)) for d in devices))
305
+ with RobonomicsSync(url) as client:
306
+ try:
307
+ result = client.rws.set_devices(pool, planned)
308
+ except TransactionError as e:
309
+ raise ChainError(REDACT(f"set_devices failed: {e}")) from e
310
+ if client.rws.devices(pool) != planned:
311
+ raise ChainError("the chain holds a different device list than planned")
312
+ return result.block_hash
313
+ ```
314
+
315
+ `_addresses_in` and the `[devices]` wrapping go (pitfall 5). Reading the list
316
+ back after inclusion is a stronger check than decoding the call before signing.
317
+ `Subscription.kind` changes from `"Daily 30 days"` formatting done by hand to
318
+ `ledger.kind`/`ledger.days`.
319
+
320
+ ### rrs-ha-integration
321
+
322
+ - `manifest.json` `requirements`: `["robonomics-interface==3.0.0rc1", "pinatapy-vourhey==0.1.9"]`.
323
+ - Delete `custom_components/robonomics_report_service/chain/` and its tests
324
+ (`tests/test_chain.py`, `tests/test_chain_client.py`): the library carries
325
+ them, byte-for-byte vectors included. Keep `tests/fixtures/substrate_vectors.json`
326
+ in a test that runs the library against it, as the project's own contract.
327
+ - `robonomics.py`:
328
+ - one `RobonomicsClient(POLKADOT_ENDPOINTS, ssl=client_context())` (from
329
+ `homeassistant.util.ssl`) per config entry, created in `async_setup_entry`,
330
+ closed on unload; remove
331
+ `_clients`, `current_wss`, `change_current_wss` and the endpoint loop in
332
+ `_send_datalog`;
333
+ - publishing: `await client.datalog.record(self.sender_keypair, data, subscription_owner=self._owner_address or self.sender_address)`;
334
+ - errors: `ExtrinsicFailed` and `InvalidTransaction` → the integration's
335
+ `RobonomicsError` with the library's message (it already explains
336
+ `Payment`, `NotLinkedDevice` and `FreeWeightIsNotEnough`); `TransportError` →
337
+ retry later; `ExtrinsicOutcomeUnknown` → do not unpin the files from Pinata
338
+ yet, the record may have landed.
339
+ - Kusama is legacy and shutting down: drop `NETWORK_KUSAMA` from `const.py` and
340
+ the network choice from the config flow, and move existing Kusama entries to
341
+ Polkadot — or, until the network stops, keep the option labelled "legacy — switch
342
+ to Polkadot" and create its client with `genesis_hash=None`.
343
+ - `config_flow.py`: `Keypair.from_secret`, `generate_mnemonic()`,
344
+ `decode_address` (raises `InvalidAddress`, a `ValueError`, so the existing
345
+ `except ValueError` still works).
346
+ - `utils/encrypt_tools.py`, `utils/file_handler.py`: import `Keypair`,
347
+ `encrypt_for_recipients`, `decrypt_package`, `parse_decrypted` and the
348
+ envelope errors from `robonomicsinterface`; `encrypt_message`/`decrypt_message`
349
+ wrappers become `keypair.encrypt_message(...)` + hex.
350
+ - Nothing may block the event loop: use `RobonomicsClient` only, never
351
+ `RobonomicsSync` (which refuses to run there anyway).
352
+
353
+ ## 8. Stable API
354
+
355
+ The Report Service projects build on the names below. They do not change between
356
+ release candidates and 3.0.0; if one ever has to, the change is written here, and
357
+ `tests/test_stable_api.py` — which pins this list — changes in the same commit.
358
+
359
+ - `RobonomicsClient(endpoints, genesis_hash=, ssl=, timeout=, retries=)`, `connect()`,
360
+ `close()`, `async with`; `RobonomicsSync`, the same without `await`.
361
+ - `client.datalog.items`, `.record(..., subscription_owner=)`, `.window_size`;
362
+ `client.rws.devices`, `.ledger`, `.set_devices`; `client.system.account`, `.exists`.
363
+ - `Keypair.from_secret`, `.from_mnemonic`, `.address`, `.public_key`, `.verify`,
364
+ `.encrypt_message`, `.decrypt_message`; `generate_mnemonic`, `is_valid_address`,
365
+ `decode_address`, `encode_address`, `address_format`.
366
+ - `encrypt_for_recipients`, `decrypt_package`, `parse_decrypted`.
367
+ - `DatalogItem(index, timestamp_ms, data, text)`, `Ledger(kind, days, issued_at,
368
+ expires_at, free_weight)`, `AccountInfo(free, exists)`, `XRT`, `ROBONOMICS_GENESIS_HASH`.
369
+ - Errors: `TransportError` (`ConnectionLost`, `RequestTimeout`, `ConnectionFailed`,
370
+ `AllEndpointsFailed`), `RpcError(method, code, message)`, `TransactionError`,
371
+ `ExtrinsicFailed(pallet, error)`, `ExtrinsicOutcomeUnknown(extrinsic_hash)`,
372
+ `ExtrinsicDropped`, `InvalidTransaction(kind, explanation)`, `RecipientError`,
373
+ `EnvelopeError`.
374
+
375
+ Changes since rc1: none to this list. rc2 adds the TLS fix, block hash checks and
376
+ the Kusama legacy handling (see CHANGELOG).
377
+
378
+ ## 9. Checklist
379
+
380
+ - [ ] No `substrateinterface`, `websocket`, `robonomicsinterface.Account` or
381
+ `chain.` import left (`grep -rn "substrateinterface\|from websocket\|Account\b\|\.chain" src`).
382
+ - [ ] The same mnemonic gives the same address as before (the project's
383
+ fixtures, or `substrate_vectors.json`).
384
+ - [ ] A report encrypted by the old stack still decrypts, and a new one opens
385
+ with the connector.
386
+ - [ ] Datalog slot 0 reads as slot 0; a wrapped buffer reads 127 records, oldest first.
387
+ - [ ] `set_devices` with a flat list, read back from the chain.
388
+ - [ ] A missing account raises `InvalidTransaction` with `kind == "Payment"`; a
389
+ device outside the list raises `ExtrinsicFailed` `RWS.NotLinkedDevice`.
390
+ - [ ] Home Assistant: the integration installs in `python:3.13-alpine` on aarch64
391
+ and HA logs no "Detected blocking call".
392
+ - [ ] Integration tests against a development node, if the project has any:
393
+ `scripts/devchain.sh` in this repository starts one with funded ED25519
394
+ accounts.