Repository navigation
chore(release): 0.2.0 - #45
isaacdoidge wants to merge 1 commit into
Conversation
- pyproject.toml: version 0.2.0; uv.lock updated to match (only the supra-sdk package entry changes). - CHANGELOG.md: new, in the Keep a Changelog 1.1.0 format, with the entries for 0.2.0, 0.1.1 and 0.1.0. The 0.2.0 entry lists the changes since v0.1.1 as they stand at this commit; entries marked Breaking say what to change in code written for 0.1.1. - README.md: the first sentence of the Compatibility section names the SDK version, 0.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| sent the request. Addresses and table handles must be account addresses (an `AccountAddress` or a hex string) and are | ||
| sent in canonical form; transaction and block hashes must be 64 hex digits with an optional `0x`; heights and epochs | ||
| must be non-negative integers or strings of decimal digits; other values, such as resource types, module names and | ||
| event types, must be non-empty strings. Each value is sent as one percent-encoded path segment, so a value containing |
There was a problem hiding this comment.
This guarantee doesn't hold for . and ... encode_path_segment leaves dots unencoded (quote(value, safe=":,")), and httpx removes dot segments when it builds the request URL, so the value escapes its segment instead of raising:
await client.account_module("0x1", "..") # GET https://host/prefix/rpc/v3/accounts/0x1
await client.events_by_type("..") # GET https://host/prefix/rpc/v3account_module then silently returns the account endpoint's body. I'd fix this in encode_path_segment before tagging, by raising ValueError for . and .., which is what this entry promises for invalid values. Narrowing the sentence would leave the bug in place.
| access token through `ApiClientConfig.access_token`. `ApiClient.base_url` always ends in `/`. | ||
| - **Breaking:** both `MultiSignature` classes (`ed25519` and `asymmetric_crypto_wrapper`) sort their signatures by key | ||
| index, the order in which the node pairs them with bitmap bits, so the order the caller supplies them in does not | ||
| matter and `.signatures` is sorted. The bytes of a MultiKey signature built from signatures out of index order change. |
There was a problem hiding this comment.
The ed25519 MultiSignature changed the same way. 0.1.1 serialized ed25519.MultiSignature.signatures in the order the caller passed them, and 0.2.0 sorts them by index. So a MultiEd25519 signature built from out-of-order signatures also has different bytes, and its transaction a different hash. Suggest: "The bytes of a MultiEd25519 or MultiKey signature built from signatures out of index order change."
| error the input led to, such as `IndexError`). | ||
| - **Breaking:** `str()` of an `ApiError` keeps the form `{message: …, status_code: …}` and holds `ApiError.message`, | ||
| the node's message when the JSON error body has one, where 0.1.1 held the whole response body; the body is | ||
| `ApiError.body`. A request that times out raises `ClientTimeoutError` instead of the `httpx` timeout class; |
There was a problem hiding this comment.
except httpx.TimeoutException still works. Code that catches one of the specific classes (httpx.ReadTimeout, httpx.ConnectTimeout, httpx.WriteTimeout, httpx.PoolTimeout) no longer catches anything, because ClientTimeoutError subclasses httpx.TimeoutException directly. Breaking entries are meant to say what to change, so it's worth adding: catch ClientTimeoutError (or httpx.TimeoutException) and read .phase to tell the phases apart.
| so f-strings, `print` and log output of a private key do not contain the key. `hex()` exports the key and is | ||
| unchanged. Code that used `str(private_key)` or an f-string to export the key calls `private_key.hex()`. | ||
| - `Account.store` writes the file with mode 0600, readable and writable by its owner only; in 0.1.1 the mode followed | ||
| the umask. It writes a temporary file in the same directory and renames it over the path, so an existing file is |
There was a problem hiding this comment.
Renaming a temp file over path also changes what happens with links. If path is a symlink (for example into a secrets mount), 0.1.1's open(path, "w") wrote through to the target. os.replace now replaces the symlink itself with a regular file and leaves the target unchanged. A hard link is split the same way. Also, the directory now has to be writable for mkstemp, where before only the file did. Worth a sentence here, since it changes where the key ends up.
| - **Breaking:** `faucet()` raises `UnexpectedResponseError` when a 200 body is neither `"TryLater"` nor a JSON object | ||
| whose `Accepted` is a non-empty string, including a body that is not JSON. For such bodies 0.1.1 returned `None`, | ||
| `""` or the value as it was with `wait_for_faucet=False`, raised `FaucetRequestNotAcceptedError` for a missing or | ||
| empty `Accepted` with `wait_for_faucet=True`, and raised `json.JSONDecodeError` for a body that is not JSON. Code |
There was a problem hiding this comment.
This list misses one 0.1.1 outcome for these bodies. A 200 JSON body that isn't an object (for example "Busy", [], null or a number) raised AttributeError from res_data.get("Accepted"). That's the same failure the TryLater fix on line 239 describes. Code that caught AttributeError for these bodies needs the same pointer to UnexpectedResponseError.
| `AutomationRegistrationParams.build(...)`, which returns version 1 without a task type and version 2 with one. | ||
| `SupraClient.register_automation_task` takes `automation_task_type` and `automation_priority`; without a task type it | ||
| registers version 1 parameters, as 0.1.1 did. A priority is allowed only with `AutomationTaskType.SYSTEM`: with any | ||
| other task type or none, `build`, the `AutomationRegistrationParamsV2` constructor and `register_automation_task` |
There was a problem hiding this comment.
The AutomationRegistrationParamsV2 constructor doesn't raise ValueError for "none". With task_type=None and a priority, _check_task_type_and_priority raises TypeError("task_type must be an AutomationTaskType"). Only build and register_automation_task raise ValueError for a missing task type; the constructor raises ValueError only for USER. Either reword this, or make the constructor raise ValueError for None too.
| MultiKey `asymmetric_crypto_wrapper.MultiSignature` decode, so a `SignedTransaction` from a MultiEd25519 or MultiKey | ||
| account round-trips through BCS. A MultiKey supports Ed25519 keys only; another key type raises `ValueError` when | ||
| decoded. `MultiEd25519Authenticator.verify` verifies the signature; it raised `NotImplementedError`. The multi-key, | ||
| multi-signature and authenticator classes have `__eq__` and `__hash__`. |
There was a problem hiding this comment.
This reads as covering every authenticator class, but only MultiEd25519Authenticator and MultiKeyAuthenticator gained both methods. Authenticator, AccountAuthenticator, Ed25519Authenticator, SingleSenderAuthenticator, MultiAgentAuthenticator and FeePayerAuthenticator define __eq__ without __hash__, so they're unhashable. SingleKeyAuthenticator has no __eq__ at all, so a single-key SignedTransaction decoded from its own bytes compares unequal to the original, while a multi-key one compares equal. Suggest naming the two classes.
|
|
||
| ## [Unreleased] | ||
|
|
||
| ## [0.2.0] - 2026-11-04 |
There was a problem hiding this comment.
This date is already a day behind, and per the description the publish day isn't known yet. The tag goes on the merge commit, so the date here at merge time is what ships in the tagged tree and gets copied into the GitHub release notes. It needs to be set in the last push before merging.
| `TransactionWaitTimeoutReachedError` with `last_status=None`. 0.1.1 raised `ApiError` with status 404 at once. Code | ||
| that caught that `ApiError` catches `TransactionWaitTimeoutReachedError` and checks `last_status is None`. Other 404 | ||
| responses, such as one for a wrong base URL, still raise at once, as `ApiNotFoundError`. The first poll is sent | ||
| without a sleep before it, no poll starts after the deadline, each poll and sleep is bounded by the time remaining, |
There was a problem hiding this comment.
This isn't quite true when transaction_wait_time_in_seconds is 0 or negative. _wait_until_final always makes the first poll, and with remaining <= 0 it calls asyncio.wait_for(fetch(...), timeout=None). That poll starts after the deadline and is bounded only by the HTTP timeout, plus retries if a RetryPolicy is set. Either qualify the sentence ("after the first poll") or bound the first poll too.
| units), and uses the gas used times 1.5, rounded up, at least 1,500 units and at most what the balance pays for. | ||
| When the simulation reports the sequence number as ahead of the account's (a transaction sent behind uncommitted | ||
| ones), the account's sequence number is read and the simulation is repeated with it; the transaction keeps its own | ||
| number. A sender given only as an address gets 20,000 units, capped by the balance. When the estimate cannot be |
There was a problem hiding this comment.
The 20,000-unit fallback also applies when the sender is an Account but fee_payer is only an address (create_raw_transaction(account, payload, fee_payer=address)). to_signed stays None there, so nothing is simulated. That also makes line 152 ("makes a balance read and a simulation request") wrong for both address cases, which only read the balance.
| timeout. | ||
| - **`ApiClientConfig.timeout`** and **`ApiClientConfig.retry`**, keyword-only, also on `SupraClientConfig`. `timeout` | ||
| (default 60) limits each phase of a request, in seconds: connecting, waiting for a pooled connection, writing and | ||
| reading. A value that is not a finite number above 0 raises `ValueError`. `retry` takes a |
There was a problem hiding this comment.
ApiClientConfig(timeout=0) doesn't raise. The check runs in ApiClient.__init__, so the ValueError comes when the ApiClient or SupraClient is built. Worth saying so, because anyone validating config up front will expect the dataclass itself to reject the value.
| - **`TransactionConfig.gas_unit_price`** defaults to `None`, which uses the node's `min_configured_gas_price` from | ||
| `GET /rpc/v3/transactions/estimate_gas_price` instead of 100. | ||
|
|
||
| ## [0.1.0] - 2025-09-19 |
There was a problem hiding this comment.
The two older dates come from different sources. PyPI has 0.1.0 uploaded on 2025-10-27 and 0.1.1 on 2026-02-10. The 0.1.1 entry uses the PyPI date, but 0.1.0 uses the tag and GitHub release date (2025-09-19). Since 0.2.0 will use the publish day, 0.1.0 should probably be 2025-10-27.
| ## Compatibility | ||
|
|
||
| The SDK is tested against Supra node `supra_node_v11.7.2` (Mainnet and Testnet) and `supra_node_v12.0.0.rc7` (Devnet). | ||
| 0.2.0 is tested against Supra node `supra_node_v11.7.2` (Mainnet and Testnet) and `supra_node_v12.0.0.rc7` (Devnet). |
There was a problem hiding this comment.
With the version in this sentence, master's README will say "0.2.0 is tested against…" for everything merged after the release. RELEASING.md's steps (bump pyproject.toml, add the CHANGELOG entry) don't include updating it, so it will go stale. The 0.2.0 CHANGELOG entry already records the tested node versions for this release, so I'd keep the README version-neutral, or else add a README step to RELEASING.md.
| 0.2.0 is tested against Supra node `supra_node_v11.7.2` (Mainnet and Testnet) and `supra_node_v12.0.0.rc7` (Devnet). | |
| The SDK is tested against Supra node `supra_node_v11.7.2` (Mainnet and Testnet) and `supra_node_v12.0.0.rc7` (Devnet). |
| - **Breaking:** `behave` and `types-six` are no longer runtime dependencies. The wheel requires `httpx[http2]`, | ||
| `PyNaCl` and `typing-extensions`; a project that used `behave` or `types-six` through `supra-sdk` declares them | ||
| itself. The `PyNaCl` lower bound is 1.6.2 (was 1.5.0). | ||
| - `block_by_height` takes `with_finalized_transactions`, the node's parameter name, as its third parameter. The keyword |
There was a problem hiding this comment.
The file says it follows Keep a Changelog 1.1.0, which has a Deprecated section for this. This release deprecates two things: the with_finalized_transaction keyword here, and passing a value to the primitive tag constructors (line 166). Both are listed only under Changed. A ### Deprecated section between Changed and Removed would make them easy to find before they're removed. Also, the sections currently run Fixed then Removed; the spec's order is Added, Changed, Deprecated, Removed, Fixed, Security.
What
Prepares the 0.2.0 release, as
RELEASING.mddescribes:pyproject.toml: version 0.2.0;uv.lockchanges only thesupra-sdkentry's version.CHANGELOG.md(new, Keep a Changelog 1.1.0): the 0.2.0 entry lists the changes sincev0.1.1(fix: redact private keys in str() and store account files owner-only #18-docs: add a Compatibility section to the README #44) as theystand on
master, with entries for 0.1.1 and 0.1.0. Entries marked Breaking: say what to change in code writtenfor 0.1.1; there is no separate migration guide.
Breaking changes in 0.2.0
faucetraiseTransactionFailedErrorunless the transaction succeeds;wait_for_transaction=Falsekeeps the old behaviourlast_status=Noneinstead of raising a 404max_gas_amountandexpiration_ttldefault toNone(estimated max gas; expiry 5 s inside the node's maximum)Noneat the end of a feed (was"").valueremoved; writeU64Tag()str(ApiError)holds the node's message;ClientTimeoutError; pool wait limited by the timeoutview()andfaucet()raiseUnexpectedResponseErrorfor a malformed 200 body?or#refusedValueErrorinstead ofassertbehaveandtypes-sixno longer runtime dependenciesasymmetric_crypto_wrapper.index_to_bitmap_valueremovedstr(PrivateKey)returns a redacted formaccount_history's page type (#43) and the user-task priority refusal (#33) are not listed as breaking: neither APIexists in 0.1.1.
Merging
Merging this is followed by the
v0.2.0tag, which runs the publish workflow. Merge once the PyPI trusted publisherand the
pypienvironment are configured, and update the date of the 0.2.0 entry to the publish day first.Checks
make fmt-check,make lint,make typecheck,make test-unit(397 tests, OK, 26 node-backed skipped),make test-spec(248 scenarios),make build: pass. The built wheel's metadata:Version: 0.2.0,License-Expression: Apache-2.0,Requires-Disthttpx[http2],pynacl>=1.6.2,typing-extensions;py.typedincluded.
🤖 Generated with Claude Code