Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
15 changes: 15 additions & 0 deletions .changeset/assemble-from-encoded-parts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"@evolution-sdk/evolution": minor
---

A transaction, body, witness set or auxiliary data can be assembled from parts that are already encoded.

`Transaction.fromEncoded`, `TransactionBody.fromEncoded`, `TransactionWitnessSet.fromEncoded` and `AuxiliaryData.fromEncoded` take the bytes of each part and frame them, rather than taking values and encoding them. A caller that holds a body it must not re-encode — because its hash is already committed to, or because a signer displayed exactly those bytes — can now assemble around it.

`Redeemers.keepDataBytes`, `Redeemers.spliceMap` and `Redeemers.toScriptDataHashFromEncoded` are the same idea for the script-data hash: the hash is taken over the bytes as they arrived, not over a re-encode that happens to look equivalent.

`Transaction.cborHeaderSize` reports the header width at an offset, which is what makes a part addressable: a caller slicing one element out of an array needs to know where the element starts, and a re-encode cannot promise the same bytes.

`Transaction.setIntendedCodecOptions` / `getIntendedCodecOptions` record which options a transaction is meant to be encoded under, so a builder's choice reaches the hash computations taken during the build instead of each one defaulting independently.

`TransactionBody` also gains the `WithFormat` pair (`fromCBORBytesWithFormat`, `fromCBORHexWithFormat`, `toCBORBytesWithFormat`, `toCBORHexWithFormat`) that the other codecs in this package already have, plus `toCBORBytes` taking `CodecOptions`.
9 changes: 9 additions & 0 deletions .changeset/asset-unit-two-spellings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@evolution-sdk/evolution": patch
---

An asset unit is spelled two ways, and both now mean the same asset everywhere.

The canonical spelling is the concatenation `policyId(56) + assetName`, which `getUnits` emits. `fromUnit` has always also accepted `policyId.assetName`, and a dot cannot occur in hex, so the form is unambiguous.

The two disagreed: `fromUnit("<policy>.<name>", 42n)` built the asset, and `getByUnit` with that **same string** answered `0n`, because it sliced the dotted text at 56. Zero is a plausible answer — it reads as "this asset is not here" — so the disagreement was silent, and silent is how it reaches a balance. One `splitUnit` now serves `fromUnit`, `fromRecord` and `getByUnit`.
13 changes: 13 additions & 0 deletions .changeset/backup-keys-encrypted-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@evolution-sdk/backup-keys": minor
---

New package: a password-encrypted, authenticated envelope for moving a wallet secret between applications, with a JSON-file, `web+cardano:` link and QR binding.

⚠️ **Not a ratified CIP.** It implements a draft submitted for a number; until one is assigned, nothing in the package claims a CIP it does not hold.

`EncryptedExport` is Argon2id over the password and XChaCha20-Poly1305 over the payload, in a self-describing envelope. What it is careful about:

- **One KDF profile, pinned at the import boundary.** The Argon2id cost travels inside the envelope, so `parse` / `parseLink` require the single v1 profile exactly: an absurd memory cost is a denial of service rather than a backup, and a *weaker* one would lower the format's floor to whatever an exporter felt like paying. `decrypt` takes an already-typed envelope and enforces the ceiling rather than the equality, so a cheaper profile a caller hands it deliberately is still honoured.
- **A password is bytes.** It is hashed as the UTF-8 it was given — no trim, no case-fold, no Unicode normalisation, because whitespace is content and a trimmed password derives a different key. An empty password is refused outright.
- **The derivation is async**, because 256 MiB of Argon2id blocks a single-threaded host for seconds.
13 changes: 13 additions & 0 deletions .changeset/bip39-passphrase-bytes-and-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@evolution-sdk/evolution": minor
---

A BIP-39 passphrase is bytes, CIP-1854 and the DRep role are expressible, and a public key can be derived along a path.

`Bip32PrivateKey.fromBip39Entropy` took a `string` only, so a caller holding raw passphrase bytes had to decode them as UTF-8 first. Bytes that are not valid UTF-8 do not survive that: they decode to U+FFFD, and the key derived from the mangled passphrase is a different key — a different wallet, with nothing anywhere to say so. It now takes `Uint8Array | string`, and the two agree whenever the text is the bytes.

`CardanoPath.indices` hardcoded purpose 1852 and typed `role` as `0 | 2`, so neither CIP-1854 (shared wallets) nor role 3 (DRep) could be expressed. `PURPOSE_CIP1852` and `PURPOSE_CIP1854` are exported and the role is open.

`Bip32PublicKey.derivePath` is the path form of the single-step `deriveChild` it already had. Deriving addresses from a co-signer's account xpub — no seed, no signing ceremony — needs exactly that.

`AddressTag` reads an address's parts straight from its bytes: `fromHeader`, `fromBytes`, `networkId`, `paymentCredential` and `stakeCredential`, each answering `undefined` rather than guessing when the header says the address has no such part.
11 changes: 11 additions & 0 deletions .changeset/canonical-jcs-and-cip116.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/canonical": minor
---

New package: RFC 8785 JSON canonicalization (`Jcs`) and the CIP-116 JSON shape (`Cip116`).

Both exist because something takes a **digest** over their output. A byte that differs from whoever computed the original digest is a rejected payment or a failed signature check, so "looks right" is not a standard either can be held to.

`Jcs` is a leaf — no imports, nothing Cardano-specific: keys sorted by UTF-16 code unit, shortest escapes, ECMAScript number formatting. Two things it is explicit about: **DEL (0x7f) is not escaped**, because RFC 8785 defers to RFC 8259 which requires escaping only below 0x20, and escaping it would produce a digest nobody else computes; and it is **not** JSON-LD / RDF-Dataset canonicalization (URDNA2015), which is what governance metadata uses and which needs a full JSON-LD to N-Quads stack.

Values with no canonical form — `undefined`, `NaN`, infinities, `bigint`, functions, symbols — are refused rather than coerced. Guessing one produces a digest that is wrong rather than one that is missing.
11 changes: 11 additions & 0 deletions .changeset/cbor-builder-and-byte-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/evolution": minor
---

`CBORBuilder` offers the append-as-you-go shape over `CBOR`, and `CBOR` gains the byte helpers a splicing caller needs.

Some callers describe a document as they walk it rather than constructing the whole value first. `CBORBuilder.Serializer` gives them that shape while **the bytes are still produced by `CBOR`'s encoder** — writes accumulate a value and its `CBORFormat`, and `finalize()` encodes through `toCBORBytesWithFormat`. It is deliberately not a second encoder: two encoders in one library that must agree byte for byte is a drift hazard. The declared length drives the structure, so an unfinished container is a `finalize()` error rather than malformed bytes a node rejects later. `Deserializer` is the read side, lenient as a CBOR reader should be: non-minimal heads and indefinite containers are accepted, and whole-item skips delegate to `CBOR.decodeItemWithOffsetAndFormat`.

Nothing here streams, and the module says so: `finalize()` needs the whole document and `Deserializer` reads a buffer already in memory.

`CBOR` also exports what a caller splicing pre-encoded children needs: `encodeMapAsDefinite`, `DefiniteArray`, `Raw`, `compareByteKeys` and `sortByteKeyedMap`. The two comparison helpers are the canonical byte-key order in one place, rather than re-derived per call site.
14 changes: 14 additions & 0 deletions .changeset/cddl-conformance-and-reachable-codecs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@evolution-sdk/evolution": minor
---

CDDL conformance for four types, and the codec pairs their siblings already had.

- `pool_params.pool_owners` is `set<addr_keyhash>`, which the chain and CML both write `#6.258`. `PoolParams.PoolOwnersCDDL` writes the tag, and `poolOwnersArray` reads either form.
- `UnitInterval` gains the four-member codec pair (`fromCBORBytes`, `fromCBORHex`, `toCBORBytes`, `toCBORHex`) that every sibling type has, and CML agrees on its bytes.
- `VotingProcedures` exports codecs for `Voter` on its own (`voterToCBORBytes` / `voterToCBORHex` / `voterFromCBORBytes` / `voterFromCBORHex`): a governance UI reads a voter out of a body one at a time, not only as part of the whole map.
- `CostModel` is exported from the package entry point, so cost models can be built without a subpath import.

`MinAda` is new: `MinAda.forOutput(output, coinsPerUtxoByte)` computes the minimum lovelace an output must carry, including the 160-byte UTxO entry overhead the ledger adds, and iterates because raising the coin can lengthen its own encoding.

`NonnegativeInterval.fromDecimal` parses a protocol parameter written as a decimal (`"0.0577"`) into the exact rational the ledger uses, instead of a float; `ceilDiv` sits beside the other exact-rational helpers. `Time` gains the slot/POSIX conversions a TTL needs, and `Label`, `Metadata`, `TransactionMetadatum` and `Certificate` get the conformance fixes their CDDL demands.
13 changes: 13 additions & 0 deletions .changeset/cip113-programmable-tokens.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@evolution-sdk/cip113": minor
---

New package: CIP-113 programmable tokens — the consumer transfer path, the registry codecs, the deployment records and the issuer derivation.

`Cip113` is what a holder needs: the registry and params codecs, the withdraw-zero redeemers, the smart-wallet address, and `buildTransferTx`, which assembles the unsigned transfer. `Query` fetches the chain state a transfer needs, `Classify` answers what a UTxO is, and `Budgets` carries measured ex-units for the framework scripts.

**Everything is parameterised by a descriptor rather than branching on a version id.** That is not future-proofing: the on-chain shapes moved four times inside one audit window, and a codec that branches on an id has to be edited for each move while one that reads a descriptor does not.

`Deployment` says *where* an instance lives. Script hashes are deploy-time data, never constants — an instance deployed from a different compilation has different hashes, and a constant would send a transfer to a script that does not exist. Where a chain has more than one record, resolution is the consumer's to make explicit: guessing points every holder's smart-wallet address at the wrong protocol.

The ex-units in `Budgets` are re-derivable: `harness/budgets.ts` rebuilds a deployment from the reference blueprint, prices the framework scripts on a local Plutus VM through `@evolution-sdk/aiken-uplc`, and **fails if the recorded presets no longer reproduce** — so an upstream cost change surfaces as a failing check rather than as a rejected transaction. Without a blueprint clone it skips loudly rather than passing quietly.
11 changes: 11 additions & 0 deletions .changeset/cip139-query-layer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/cip139": minor
---

New package: the CIP-139 Universal Query Layer — the read surface that turns an own-data wallet into a full-data one.

CIP-30 serves the user's own addresses and UTxOs, which leaves a dApp with two sources of truth, its own query layer and the wallet's. When they disagree the wallet may refuse to sign a transaction spending a UTxO it has not seen. CIP-139 lets the wallet answer for arbitrary chain data instead.

Method names are the operation names from `cip-0144.md` verbatim (`cip139_getUtxosByAddress`), so a JSON-RPC transport can use them unmodified rather than maintaining a mapping that drifts from the spec.

It is built over the provider port and `@evolution-sdk/review`'s decode seam, so the same query layer works against any backend that implements the port — and a read the backend cannot answer surfaces as an absence rather than as an empty result presented as fact.
11 changes: 11 additions & 0 deletions .changeset/cip21-multiasset-canonical-order.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/evolution": minor
---

`multiasset` and `mint` maps are written in CIP-21 canonical key order.

CIP-21 (Category: Wallets, Status: Active) requires canonical CBOR map-key order in the transaction body: shorter keys first, then lexicographic. It matters because a hardware wallet hashes the body in chunks and the client has to reconstruct those exact bytes. An insertion-ordered multiasset is accepted by a node and cannot be reliably signed by a device; for a mint it is also a transaction-id divergence, because the mint field lives in the body.

The sort is applied **by the encoder that owns the map** — `MultiAsset.FromCDDL` and `Mint.FromCDDL` — and deliberately not through a global codec option, because the same option would also reorder Plutus map keys, which must keep the order they were given. This is narrower than the default-encode-path sorting in the closed #584: nothing outside these two maps changes order.

⚠️ CML 6.2.0 is not an oracle here — it writes multiasset keys in insertion order, so it reproduces whatever order it was handed. The expected bytes in `Cip21CanonicalOrder.test.ts` come from cardano-serialization-lib 15.0.3, which sorts.
11 changes: 11 additions & 0 deletions .changeset/cip30-read-surface.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/cip30": minor
---

New package: the CIP-30 wallet read surface over a provider and an account, returning the exact wire encodings — plus CIP-95 governance keys, CIP-103, CIP-104 and CIP-142.

The scope is deliberately the **read** surface. It does not carry `signTx` / `signTxs` / `signData`: a signing method reachable straight off a connector is a footgun for anyone embedding it, and this package owns byte-exactness, not authorization. A wallet answers signing requests with a sentinel its host catches and then runs its own review and key-custody ceremony. The signing *encodings* live in `ReferenceSigning`, separate and labelled unsafe, so they can be tested without being reachable by accident.

`getAccountPublicKey` is consent-gated. It reveals every address a wallet has used and its whole history, and CIP-30 treats it as a transparent read, so nothing else in a wallet's flow would prompt for it. The method *requires* a `ConsentTag`, which makes it type-level impossible to serve without the embedding app's approval implementation.

The returned shapes are the wire's, not a convenience: `getChangeAddress` answers raw address bytes as hex rather than bech32, and `getUtxos` / `getBalance` answer `TransactionUnspentOutput` and `Value` hex, because a dApp compares those strings against what it built.
11 changes: 11 additions & 0 deletions .changeset/cips-companions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/cips": minor
---

New package: thirteen small CIP companions, built on the core types rather than beside them.

`Cip4` (wallet checksum), `Cip14` (the `asset1…` fingerprint), `Uri` (the `web+cardano:` scheme, CIP-13 plus 134 and 162), `Cip27` (royalty metadata), `Cip16` (key serialisation layouts and their CIP-5 prefixes), `Cip67` (the asset-name label registry), `Cip68` (datum metadata), `Cip88` (token policy registration, signed by the policy key), `GovMeta` (the off-chain governance document an anchor points at, CIP-100/108/119/136), `Cip102` (royalty datum), `Cip105` (Conway DRep / CC-cold / CC-hot key chains), `Cip129` (governance identifiers) and `Cip1855` (forging policy keys). The three CIPs still **proposed** say so in the module and in the README.

Each is driven by an oracle outside this library, not by its own output: `Cip4` against the `@emurgo/cip4-js` reference implementation, `Cip27` and `Cip88` against cardano-multiplatform-lib 6.2 for the metadata bytes and the policy-key signature, and `GovMeta` against coreutils `b2sum -l 256` for the anchor hash. `Cip4` also documents its one divergence from the reference: this strips a `0x` prefix and the reference does not, so the same key yields two different plates.

The CIP-13 grammar for the URI amount and delegation fields is `*digit [ "." *digit ]`, which `""` and `"."` satisfy. `web+cardano:addr1…?amount=` parsed to `{ amount: "" }` and `?amount=.` to `{ amount: "." }`; a consumer reading either gets `NaN`, and an empty amount is not the same fact as no amount at all. Both are a `UriError` now, and the digit forms the grammar allows (`10`, `0.5`, `1.`, `.5`) still parse.
13 changes: 13 additions & 0 deletions .changeset/coin-selection-random-improve.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@evolution-sdk/evolution": minor
---

`randomImproveSelection` is implemented. It was declared and never defined.

`CoinSelection` exported `randomImproveSelection` and `optimalSelection` as `export declare const`, so both were `undefined` at runtime, and `phases/Selection.ts` refused the matching algorithm names with "not yet implemented". A caller who set `coinSelectionAlgorithm: "random-improve"` got an error, and one who imported `randomImproveSelection` directly got `undefined`.

Random-improve is now a faithful port of CSL's `cip2_random_improve_by`: phase 1 selects at random until the target is covered, phase 2 swaps a chosen input for a remaining one when that moves the per-input contribution closer to twice the target without exceeding three times it. It is asset-aware — a target naming native assets covers each asset before the coin, then tops up for fees — and `largestFirstSelection` gains the same multi-asset path through the shared core.

Randomness defaults to `crypto.getRandomValues` and **throws rather than falling back** to a predictable PRNG when WebCrypto is absent, because an observer being unable to predict which UTxOs a wallet combines is the entire point of the strategy. `randomImproveSelectionWith({ seed })` exists for tests and examples; the deterministic strategies never construct an RNG at all, so largest-first keeps working where no WebCrypto exists.

`optimalSelection` and the `"optimal"` member of `CoinSelectionAlgorithm` are removed. Neither was implemented, and a declared export that is `undefined` at runtime fails later and less clearly than a name that does not resolve.
14 changes: 14 additions & 0 deletions .changeset/cose-header-map-and-sign-data.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@evolution-sdk/evolution": minor
---

The COSE header map is keyed structurally, and CIP-8 sign-data gets the surface a wallet and a device need.

`HeaderMap` holds a `Map` keyed by the Label **object**, and every `labelFromInt(n)` / `labelFromText(s)` call builds a fresh one — so both accessors answered by identity, which no caller can satisfy: `getHeader` never found a label that was present, and `setHeader` appended rather than replaced, leaving a COSE header map with a duplicate key whose two values both encode and whose reader takes whichever it reaches first.

On top of that:

- `SignData.signData` and `assembleSignData` produce a CIP-8 `COSESign1` — assembling separately from signing is what a hardware wallet needs, because the device returns a signature over bytes the caller must then frame.
- `SignData.verifyMessage` verifies one, and `SignData.addressSignerCredential` reports which credential a protected `address` header claims, or `undefined` when it claims none. A verifier that checks the signature alone accepts a perfectly valid signature over an address the key has nothing to do with; CIP-8's first check is what makes that false, and it needs the claimed credential to run.
- `COSEKey.publicKey` reads the verification key out of a `COSE_Key`, which is what a verifier is handed in the CIP-30 flow.
- The `kid` (label 4) carries the signing public key through the protected headers. It is optional in CIP-8 and CIP-30 wallets differ, so a verifier holding only the signed message needs it present when the signer chose to include it, and must not depend on it when they did not.
11 changes: 11 additions & 0 deletions .changeset/dijkstra-prefork-fields.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
"@evolution-sdk/dijkstra": minor
---

New package: typed reads of the Dijkstra (pv12) pre-fork `transaction_body` keys, plus the `script_require_guard` native-script variant.

⚠️ **Pre-fork.** It is grounded against the draft ledger CDDL only — no library ships Dijkstra, and the draft has already moved once: field 27 was added, and 26 was re-keyed from a credential to a reward account and gained a third, bare-`coin` arm. Every shape needs re-verifying before it is relied on against a live chain, and the package says so in its README and module docs.

What it reads: `guards` (14, which replaces `required_signers` and is a key-hash set **or** a credential oset), `sub_transactions` (23, as raw sub-transaction CBOR), `required_top_level_guards` (24), `direct_deposits` (25), `account_balance_intervals` (26) and `starting_account_balance_intervals` (27).

The byte layer already survives these: an unrecognised body key is preserved verbatim, so a Dijkstra transaction passes through a Conway-era codec untouched. What was missing is a *reading* of them, which is what a review surface and an analyzer need in order to say what such a transaction does rather than showing a reviewer an opaque key.
Loading