robonomics-interface 2.0.0__tar.gz → 3.0.0__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 (78) hide show
  1. robonomics_interface-3.0.0/.gitignore +13 -0
  2. robonomics_interface-3.0.0/CHANGELOG.md +151 -0
  3. robonomics_interface-3.0.0/MIGRATION.md +394 -0
  4. robonomics_interface-3.0.0/PKG-INFO +244 -0
  5. robonomics_interface-3.0.0/README.md +213 -0
  6. robonomics_interface-3.0.0/docs/source/CHANGELOG.md +2 -0
  7. robonomics_interface-3.0.0/docs/source/MIGRATION.md +2 -0
  8. robonomics_interface-3.0.0/pyproject.toml +99 -0
  9. robonomics_interface-3.0.0/src/robonomicsinterface/__init__.py +124 -0
  10. robonomics_interface-3.0.0/src/robonomicsinterface/bip39.py +117 -0
  11. robonomics_interface-3.0.0/src/robonomicsinterface/client.py +714 -0
  12. robonomics_interface-3.0.0/src/robonomicsinterface/english.txt +2048 -0
  13. robonomics_interface-3.0.0/src/robonomicsinterface/envelope.py +119 -0
  14. robonomics_interface-3.0.0/src/robonomicsinterface/errors.py +195 -0
  15. robonomics_interface-3.0.0/src/robonomicsinterface/extrinsic.py +494 -0
  16. robonomics_interface-3.0.0/src/robonomicsinterface/keys.py +305 -0
  17. robonomics_interface-3.0.0/src/robonomicsinterface/pallets/__init__.py +22 -0
  18. robonomics_interface-3.0.0/src/robonomicsinterface/pallets/_common.py +49 -0
  19. robonomics_interface-3.0.0/src/robonomicsinterface/pallets/datalog.py +202 -0
  20. robonomics_interface-3.0.0/src/robonomicsinterface/pallets/rws.py +202 -0
  21. robonomics_interface-3.0.0/src/robonomicsinterface/pallets/system.py +153 -0
  22. robonomics_interface-3.0.0/src/robonomicsinterface/py.typed +0 -0
  23. robonomics_interface-3.0.0/src/robonomicsinterface/rpc.py +16 -0
  24. robonomics_interface-3.0.0/src/robonomicsinterface/runtime.py +439 -0
  25. robonomics_interface-3.0.0/src/robonomicsinterface/ss58.py +116 -0
  26. robonomics_interface-3.0.0/src/robonomicsinterface/storage.py +264 -0
  27. robonomics_interface-3.0.0/src/robonomicsinterface/sync.py +426 -0
  28. robonomics_interface-3.0.0/src/robonomicsinterface/transport.py +267 -0
  29. robonomics_interface-3.0.0/tests/conftest.py +25 -0
  30. robonomics_interface-3.0.0/tests/fake_node.py +77 -0
  31. robonomics_interface-3.0.0/tests/fake_server.py +191 -0
  32. robonomics_interface-3.0.0/tests/fixtures/chain_samples.json +142 -0
  33. robonomics_interface-3.0.0/tests/fixtures/compat_vectors.json +209 -0
  34. robonomics_interface-3.0.0/tests/fixtures/event_samples.json +27 -0
  35. robonomics_interface-3.0.0/tests/fixtures/extrinsic_vectors.json +39 -0
  36. robonomics_interface-3.0.0/tests/fixtures/generate_compat_vectors.py +106 -0
  37. robonomics_interface-3.0.0/tests/fixtures/metadata.hex.gz +0 -0
  38. robonomics_interface-3.0.0/tests/fixtures/metadata_spec43.hex.gz +0 -0
  39. robonomics_interface-3.0.0/tests/fixtures/substrate_vectors.json +36 -0
  40. robonomics_interface-3.0.0/tests/integration/test_devchain.py +199 -0
  41. robonomics_interface-3.0.0/tests/integration/test_mainnet.py +55 -0
  42. robonomics_interface-3.0.0/tests/test_client.py +390 -0
  43. robonomics_interface-3.0.0/tests/test_compat.py +85 -0
  44. robonomics_interface-3.0.0/tests/test_envelope.py +128 -0
  45. robonomics_interface-3.0.0/tests/test_extrinsic.py +238 -0
  46. robonomics_interface-3.0.0/tests/test_keys.py +208 -0
  47. robonomics_interface-3.0.0/tests/test_pallets.py +240 -0
  48. robonomics_interface-3.0.0/tests/test_runtime.py +164 -0
  49. robonomics_interface-3.0.0/tests/test_ss58.py +57 -0
  50. robonomics_interface-3.0.0/tests/test_stable_api.py +96 -0
  51. robonomics_interface-3.0.0/tests/test_storage.py +179 -0
  52. robonomics_interface-3.0.0/tests/test_submit.py +173 -0
  53. robonomics_interface-3.0.0/tests/test_sync.py +159 -0
  54. robonomics_interface-2.0.0/PKG-INFO +0 -92
  55. robonomics_interface-2.0.0/README.md +0 -62
  56. robonomics_interface-2.0.0/pyproject.toml +0 -58
  57. robonomics_interface-2.0.0/robonomicsinterface/__init__.py +0 -20
  58. robonomics_interface-2.0.0/robonomicsinterface/classes/__init__.py +0 -13
  59. robonomics_interface-2.0.0/robonomicsinterface/classes/account.py +0 -57
  60. robonomics_interface-2.0.0/robonomicsinterface/classes/base.py +0 -40
  61. robonomics_interface-2.0.0/robonomicsinterface/classes/chain_utils.py +0 -123
  62. robonomics_interface-2.0.0/robonomicsinterface/classes/common_functions.py +0 -77
  63. robonomics_interface-2.0.0/robonomicsinterface/classes/datalog.py +0 -102
  64. robonomics_interface-2.0.0/robonomicsinterface/classes/digital_twin.py +0 -148
  65. robonomics_interface-2.0.0/robonomicsinterface/classes/launch.py +0 -39
  66. robonomics_interface-2.0.0/robonomicsinterface/classes/liability.py +0 -252
  67. robonomics_interface-2.0.0/robonomicsinterface/classes/pubsub.py +0 -114
  68. robonomics_interface-2.0.0/robonomicsinterface/classes/reqres.py +0 -47
  69. robonomics_interface-2.0.0/robonomicsinterface/classes/rws.py +0 -176
  70. robonomics_interface-2.0.0/robonomicsinterface/classes/service_functions.py +0 -191
  71. robonomics_interface-2.0.0/robonomicsinterface/classes/subscriptions.py +0 -140
  72. robonomics_interface-2.0.0/robonomicsinterface/constants.py +0 -9
  73. robonomics_interface-2.0.0/robonomicsinterface/decorators.py +0 -49
  74. robonomics_interface-2.0.0/robonomicsinterface/exceptions.py +0 -25
  75. robonomics_interface-2.0.0/robonomicsinterface/robonomics_interface_io.py +0 -128
  76. robonomics_interface-2.0.0/robonomicsinterface/types.py +0 -13
  77. robonomics_interface-2.0.0/robonomicsinterface/utils.py +0 -98
  78. {robonomics_interface-2.0.0 → robonomics_interface-3.0.0}/LICENSE +0 -0
@@ -0,0 +1,13 @@
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/
13
+ docs/build/
@@ -0,0 +1,151 @@
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.0] — 2026-10-27
8
+
9
+ The release of the 3.0 line; the API is the one of 3.0.0rc2.
10
+
11
+ ### Fixed
12
+ - A request timeout no longer drops the connection when the node still answers a
13
+ `system_health` ping. Dropping it ended every subscription on the shared connection,
14
+ so one slow read (`query_map` on a Raspberry Pi, say) turned a pending
15
+ `author_submitAndWatchExtrinsic` into `ExtrinsicOutcomeUnknown`. A timeout on a live
16
+ node is raised as `RequestTimeout` and is not retried on another endpoint; a node that
17
+ answers neither is dropped and failed over as before.
18
+
19
+ ### Added
20
+ - The Sphinx documentation is back: API reference built from the docstrings, plus the
21
+ README, MIGRATION.md and the changelog, published to GitHub Pages from `main`.
22
+
23
+ ### Changed
24
+ - Checked against the Robonomics runtime `spec_version 52` on Polkadot (Sudo removed
25
+ from production, OpenGov, CPS v1.1): the metadata, signed extensions, and the
26
+ Datalog, RWS, System and Balances pallets are unchanged for this library. The offline
27
+ fixtures are recorded from spec 52. The development-chain tests still run on the
28
+ runtime v50 wasm, because the production runtime no longer has Sudo.
29
+
30
+ ## [3.0.0rc2] — 2026-09-27
31
+
32
+ From three days of rc1 in rrs-connector, rrs-admin and rrs-ha-integration.
33
+
34
+ ### Fixed
35
+ - The default TLS context for `wss://` endpoints is built once per client, in a worker
36
+ thread, and shared by every connection. Before, asyncio built one per connection on
37
+ the event loop, reading the CA store from disk — Home Assistant reported a blocking
38
+ call. Passing `ssl=` works as before.
39
+
40
+ ### Added
41
+ - Block hashes given as `at=` or to `client.chain.block_number()` are checked
42
+ (`0x` + 64 hex digits) before any request; a bad one raises `EncodeError`.
43
+ - A test pins the API the Report Service projects use; MIGRATION.md lists it under
44
+ "Stable API".
45
+ - Integration tests that send transactions refuse a non-loopback node unless
46
+ `ROBONOMICS_DEV_ALLOW_REMOTE=1`, and refuse production chains outright.
47
+ - A weekly read-only smoke test against mainnet in CI.
48
+
49
+ ### Changed
50
+ - Robonomics on Kusama is legacy and is shutting down. A client refuses a Kusama node
51
+ by default with "switch to Polkadot" in the reason; with `genesis_hash=None` it
52
+ connects and logs a warning. No Kusama constant is provided.
53
+
54
+ ## [3.0.0rc1] — 2026-09-24
55
+
56
+ A rewrite for the current Robonomics runtime. Not compatible with 2.x: see
57
+ [MIGRATION.md](MIGRATION.md) for the mapping and the behaviour that changed.
58
+
59
+ ### Added
60
+ - `Keypair` (ED25519) from a mnemonic (with optional password), a raw seed, a
61
+ Substrate secret URI with hard derivation (`//Alice`, `<phrase>//a//b///password`),
62
+ a public key or an address. `from_secret()` accepts only what a person should
63
+ paste: a mnemonic or a seed, never a derivation path.
64
+ - Message encryption (`encrypt_message` / `decrypt_message`) byte-compatible with
65
+ substrate-interface.
66
+ - Multi-recipient envelope: `encrypt_for_recipients`, `decrypt_package`,
67
+ `parse_decrypted`, in the format Robonomics Report Service publishes.
68
+ - SS58 helpers: `encode_address`, `decode_address`, `address_format`,
69
+ `is_valid_address` (format 32 by default).
70
+ - BIP39 English: `generate_mnemonic`, `validate_mnemonic`.
71
+ - A typed exception hierarchy rooted at `RobonomicsError`; input errors are also
72
+ `ValueError`. No exception message contains secret material.
73
+ - Runtime metadata (V14) decoded from the chain itself: `Runtime` describes one
74
+ runtime version (pallets, storage entries, constants, SCALE encode/decode);
75
+ `RuntimeCache` fetches metadata per `(genesis_hash, spec_version)`, parses it off
76
+ the event loop and reloads it after a runtime upgrade.
77
+ - Storage: every hasher (`Identity`, `Twox64Concat`, `Twox128`, `Twox256`,
78
+ `Blake2_128`, `Blake2_256`, `Blake2_128Concat`), multi-key maps and maps keyed by
79
+ a tuple; `query`, `query_map` (paged, pinned to one block, keys decoded back) and
80
+ `constant`. An absent value is `None` for an `Optional` item and the declared
81
+ default otherwise.
82
+ - New errors: `MetadataError`, `NoSuchPallet`, `NoSuchStorage`, `NoSuchConstant`,
83
+ `EncodeError`, `DecodeError`.
84
+ - `RobonomicsClient`, an asyncio client over one multiplexed WebSocket: endpoints in
85
+ priority order (e.g. a LAN node first, the public node as fallback), a node is
86
+ used only if its genesis hash matches Robonomics Polkadot and it is neither
87
+ syncing nor without peers, failback to a preferred endpoint, a timeout on every
88
+ request, automatic retry of reads on another endpoint (`retry=False` for requests
89
+ with side effects), subscriptions, and runtime-upgrade tracking through
90
+ `state_subscribeRuntimeVersion`. `query`, `query_map` and `constant` on the client.
91
+ - Transport errors, all `TransportError` (retryable): `ConnectionFailed`,
92
+ `ConnectionLost`, `RequestTimeout`, `AllEndpointsFailed` (with the reason for each
93
+ endpoint). `RpcError` for errors returned by the node.
94
+ - Extrinsics: `compose_call` checks arguments against the metadata and takes natural
95
+ values (`bytes`, `Keypair`, nested `Call`, a plain list for `BoundedVec` — no more
96
+ `[[a, b]]` for `RWS.set_devices`); signed extensions come from the metadata, in its
97
+ order, and an unknown one that carries data is refused (`UnsupportedExtension`);
98
+ mortal era by default (`Era`); `sign`, `validate` (the pool's own check via
99
+ `state_call`), `submit` (waits for the block and reads its events) and
100
+ `submit_nowait`. One submission per account at a time, so nonces never collide.
101
+ - `ExtrinsicResult` with the block, index and events of the extrinsic.
102
+ - Transaction errors, all `TransactionError`: `InvalidTransaction` (kind plus a
103
+ plain explanation — `Payment` says the account does not exist on chain),
104
+ `ExtrinsicFailed` (pallet error name and docs, e.g. `RWS.FreeWeightIsNotEnough`),
105
+ `ExtrinsicDropped`, `ExtrinsicOutcomeUnknown` (with the hash to look up).
106
+ - Pallet helpers on the client:
107
+ - `client.datalog`: `index`, `item` (slot 0 is slot 0 — in 2.x `get_item(index=0)`
108
+ meant "latest"), `latest` (also correct after the ring buffer wraps to 0, where
109
+ 2.x returned `None`), `items` (all live records in one request), `record`
110
+ (bytes or UTF-8 text, size checked, optionally through an RWS subscription),
111
+ `erase`. Records come back as `bytes` (`DatalogItem`), never as text-or-hex.
112
+ - `client.rws`: `ledger` (`Ledger` with `is_active`, `days_left`, `expires_at`),
113
+ `devices`, `is_device`, `max_devices`, `set_devices` (plain list, checked and
114
+ de-duplicated, `TooManyDevices` past the limit), `add_devices`,
115
+ `remove_devices`, `call`, `bid`, auction reads.
116
+ - `client.system` (`account` → `AccountInfo` with `exists`, `next_nonce`),
117
+ `client.balances` (`existential_deposit`, `transfer_keep_alive`,
118
+ `transfer_allow_death`), `client.chain` (block hashes and numbers, runtime
119
+ version). `XRT = 10**9`.
120
+ - `RobonomicsSync`, a blocking wrapper for scripts and cron jobs: the async client
121
+ on an event loop in a background thread, with the same methods, arguments,
122
+ errors and timeouts (a test keeps the two APIs in step). `query_map` becomes a
123
+ plain iterator. Calling it from inside a running event loop raises
124
+ `RuntimeError` rather than stalling the loop; closing it stops every thread it
125
+ started.
126
+ - Tests: offline suites on recorded mainnet data and local fake nodes; integration
127
+ tests against a development node running the mainnet runtime
128
+ (`scripts/devchain.sh`), covering transfers, datalog, sudo, RWS subscriptions and
129
+ their failures; a read-only mainnet smoke test.
130
+ - `py.typed`.
131
+
132
+ ### Changed
133
+ - Python 3.12+; built with hatchling.
134
+ - Core dependencies are `pynacl`, `scalecodec`, `websockets` and `xxhash` only;
135
+ every one installs from a wheel or as pure Python on musl/aarch64.
136
+ - ED25519 is the only key type. sr25519 returns as an optional extra in a later
137
+ release.
138
+ - Extrinsic failures raise instead of returning a hash: inclusion in a block is
139
+ checked against the block's events.
140
+ - `Keypair.sign()` signs a `str` as its UTF-8 bytes. substrate-interface decoded a
141
+ `str` starting with `0x` as hex first; pass `bytes.fromhex(...)` for that.
142
+
143
+ ### Removed
144
+ - The dependency on `substrate-interface` and, with it, `ecdsa`, the Ethereum
145
+ libraries, `smoldot-light` and the Rust bindings.
146
+ - The 2.x API: `Account`, `ServiceFunctions`, `BaseClass`, `ChainUtils`,
147
+ `CommonFunctions`, the per-pallet classes, `Subscriber`/`SubEvent`,
148
+ `TYPE_REGISTRY`, and the `robonomics_interface` CLI.
149
+ - `PubSub` and `ReqRes`: the node RPCs behind them are gone since collators moved
150
+ to `polkadot-omni-node`.
151
+ - 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.0`.
11
+ The API listed in section 8 is the same as in the release candidates.
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.