From 28b57e33fdfed4650425dae592550ffb0cfb952a Mon Sep 17 00:00:00 2001 From: Kyle Cox Date: Wed, 14 Oct 2026 14:55:30 +0000 Subject: [PATCH] fix(provider): accept null for Blockfrost's nullable protocol parameters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Blockfrost OpenAPI spec marks 15 fields of /epochs/latest/parameters nullable, but BlockfrostProtocolParameters used Schema.optional, which rejects null — one null failed the whole getProtocolParameters decode. The 15 fields now use Schema.optionalWith(..., { nullable: true }); the decoded null reaches transformProtocolParameters, whose existing ?? 0n / || 0 defaults apply. Part 1 of #569; the native-script rebuild (part 2) and stale-field removal (part 3) are left out deliberately. New test: all 15 fields null decode and fall back to the defaults. Blockfrost file 15/15; full suite 1679 passed / 75 skipped / 0 failed. --- .changeset/blockfrost-nullable-params.md | 5 +++ .../src/sdk/provider/internal/Blockfrost.ts | 39 ++++++++++-------- .../test/provider/Blockfrost.test.ts | 40 +++++++++++++++++++ 3 files changed, 67 insertions(+), 17 deletions(-) create mode 100644 .changeset/blockfrost-nullable-params.md diff --git a/.changeset/blockfrost-nullable-params.md b/.changeset/blockfrost-nullable-params.md new file mode 100644 index 00000000..1a535b79 --- /dev/null +++ b/.changeset/blockfrost-nullable-params.md @@ -0,0 +1,5 @@ +--- +"@evolution-sdk/evolution": patch +--- + +Blockfrost protocol parameters now accept `null` for the 15 fields the Blockfrost OpenAPI spec marks nullable (`max_val_size`, `cost_models`, `cost_models_raw`, `price_mem`, `price_step`, `max_tx_ex_mem`, `max_tx_ex_steps`, `max_block_ex_mem`, `max_block_ex_steps`, `collateral_percent`, `max_collateral_inputs`, `coins_per_utxo_size`, `min_fee_ref_script_cost_per_byte`, `drep_deposit`, `gov_action_deposit`). Previously one `null` failed the whole `getProtocolParameters` decode; the decoded `null` now reaches the transform, whose existing defaults apply. diff --git a/packages/evolution/src/sdk/provider/internal/Blockfrost.ts b/packages/evolution/src/sdk/provider/internal/Blockfrost.ts index aca84b5f..190c4c3e 100644 --- a/packages/evolution/src/sdk/provider/internal/Blockfrost.ts +++ b/packages/evolution/src/sdk/provider/internal/Blockfrost.ts @@ -27,28 +27,33 @@ export const BlockfrostProtocolParameters = Schema.Struct({ key_deposit: Schema.BigInt, min_utxo: Schema.optional(Schema.BigInt), max_tx_size: Schema.BigInt, - max_val_size: Schema.optional(Schema.BigInt), + // The Blockfrost OpenAPI spec marks the fields below nullable: a `null` must + // decode (to null) instead of failing the whole response; the transform's + // `?? 0n` / `|| 0` defaults then apply. + max_val_size: Schema.optionalWith(Schema.BigInt, { nullable: true }), utxo_cost_per_word: Schema.optional(Schema.BigInt), - cost_models: Schema.optional( - Schema.Record({ key: Schema.String, value: Schema.Record({ key: Schema.String, value: Schema.BigInt }) }) + cost_models: Schema.optionalWith( + Schema.Record({ key: Schema.String, value: Schema.Record({ key: Schema.String, value: Schema.BigInt }) }), + { nullable: true } ), - cost_models_raw: Schema.optional( - Schema.Record({ key: Schema.String, value: Schema.Array(Schema.BigInt) }) + cost_models_raw: Schema.optionalWith( + Schema.Record({ key: Schema.String, value: Schema.Array(Schema.BigInt) }), + { nullable: true } ), - price_mem: Schema.optional(Schema.NumberFromString), - price_step: Schema.optional(Schema.NumberFromString), - max_tx_ex_mem: Schema.optional(Schema.BigInt), - max_tx_ex_steps: Schema.optional(Schema.BigInt), - max_block_ex_mem: Schema.optional(Schema.BigInt), - max_block_ex_steps: Schema.optional(Schema.BigInt), + price_mem: Schema.optionalWith(Schema.NumberFromString, { nullable: true }), + price_step: Schema.optionalWith(Schema.NumberFromString, { nullable: true }), + max_tx_ex_mem: Schema.optionalWith(Schema.BigInt, { nullable: true }), + max_tx_ex_steps: Schema.optionalWith(Schema.BigInt, { nullable: true }), + max_block_ex_mem: Schema.optionalWith(Schema.BigInt, { nullable: true }), + max_block_ex_steps: Schema.optionalWith(Schema.BigInt, { nullable: true }), max_block_size: Schema.BigInt, - collateral_percent: Schema.optional(Schema.BigInt), - max_collateral_inputs: Schema.optional(Schema.BigInt), - coins_per_utxo_size: Schema.optional(Schema.BigInt), - min_fee_ref_script_cost_per_byte: Schema.optional(Schema.NumberFromString), + collateral_percent: Schema.optionalWith(Schema.BigInt, { nullable: true }), + max_collateral_inputs: Schema.optionalWith(Schema.BigInt, { nullable: true }), + coins_per_utxo_size: Schema.optionalWith(Schema.BigInt, { nullable: true }), + min_fee_ref_script_cost_per_byte: Schema.optionalWith(Schema.NumberFromString, { nullable: true }), // Conway era governance parameters - drep_deposit: Schema.optional(Schema.BigInt), - gov_action_deposit: Schema.optional(Schema.BigInt) + drep_deposit: Schema.optionalWith(Schema.BigInt, { nullable: true }), + gov_action_deposit: Schema.optionalWith(Schema.BigInt, { nullable: true }) }) export type BlockfrostProtocolParameters = Schema.Schema.Type diff --git a/packages/evolution/test/provider/Blockfrost.test.ts b/packages/evolution/test/provider/Blockfrost.test.ts index 0ea0ae96..93aece8d 100644 --- a/packages/evolution/test/provider/Blockfrost.test.ts +++ b/packages/evolution/test/provider/Blockfrost.test.ts @@ -171,6 +171,46 @@ describe("Blockfrost getProtocolParameters", () => { }) }) + it("accepts null for every nullable protocol parameter and falls back to the defaults", async () => { + // The OpenAPI spec marks these 15 fields nullable; one null must not fail the decode + const payload = JSON.parse(PROTOCOL_PARAMETERS) as Record + for (const field of [ + "max_val_size", + "cost_models", + "cost_models_raw", + "price_mem", + "price_step", + "max_tx_ex_mem", + "max_tx_ex_steps", + "max_block_ex_mem", + "max_block_ex_steps", + "collateral_percent", + "max_collateral_inputs", + "coins_per_utxo_size", + "min_fee_ref_script_cost_per_byte", + "drep_deposit", + "gov_action_deposit" + ]) { + payload[field] = null + } + stubRoutes({ "/epochs/latest/parameters": JSON.stringify(payload) }) + + const params = await Effect.runPromise(BlockfrostEffect.getProtocolParameters(BASE, PROJECT_ID)) + + expect(params.maxValSize).toBe(0) + expect(params.priceMem).toBe(0) + expect(params.priceStep).toBe(0) + expect(params.maxTxExMem).toBe(0n) + expect(params.maxTxExSteps).toBe(0n) + expect(params.coinsPerUtxoByte).toBe(0n) + expect(params.collateralPercentage).toBe(0) + expect(params.maxCollateralInputs).toBe(0) + expect(params.minFeeRefScriptCostPerByte).toBe(0) + expect(params.drepDeposit).toBe(0n) + expect(params.govActionDeposit).toBe(0n) + expect(params.costModels).toEqual({ PlutusV1: {}, PlutusV2: {}, PlutusV3: {} }) + }) + it("falls back to the named cost models as numbers", async () => { stubRoutes({ "/epochs/latest/parameters": PROTOCOL_PARAMETERS.replace(/"cost_models_raw": \{[^}]*\},/, "") })