From 2af1592e8e55841811096290435c61950a5535df Mon Sep 17 00:00:00 2001 From: brokri-lgtm <> Date: Wed, 4 Feb 2026 16:42:23 +0100 Subject: [PATCH 1/2] fixing lrp datum serialisation re canonicality --- src/contracts/lrp/helpers.ts | 16 +++++---- src/contracts/lrp/types.ts | 68 +++++++++++++++++++++++++++--------- 2 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/contracts/lrp/helpers.ts b/src/contracts/lrp/helpers.ts index 603c7de..82778ee 100644 --- a/src/contracts/lrp/helpers.ts +++ b/src/contracts/lrp/helpers.ts @@ -93,7 +93,8 @@ export function buildRedemptionsTx( lovelacesForRedemption, ); - const lrpDatum = parseLrpDatumOrThrow(getInlineDatumOrThrow(lrpUtxo)); + const lrpRawInlineDatum = getInlineDatumOrThrow(lrpUtxo); + const lrpDatum = parseLrpDatumOrThrow(lrpRawInlineDatum); const resultVal = addAssets( lrpUtxo.assets, @@ -137,11 +138,14 @@ export function buildRedemptionsTx( lrpUtxo.address, { kind: 'inline', - value: serialiseLrpDatum({ - ...lrpDatum, - lovelacesToSpend: - lrpDatum.lovelacesToSpend - lovelacesForRedemption, - }), + value: serialiseLrpDatum( + { + ...lrpDatum, + lovelacesToSpend: + lrpDatum.lovelacesToSpend - lovelacesForRedemption, + }, + { _tag: 'adaptiveReplace', spentLrpDatum: lrpRawInlineDatum }, + ), }, resultVal, ); diff --git a/src/contracts/lrp/types.ts b/src/contracts/lrp/types.ts index b62ab4d..7f3b79f 100644 --- a/src/contracts/lrp/types.ts +++ b/src/contracts/lrp/types.ts @@ -5,6 +5,7 @@ import { OnChainDecimalSchema, } from '../../types/on-chain-decimal'; import { option as O, function as F } from 'fp-ts'; +import { match, P } from 'ts-pattern'; export const LRPParamsSchema = Data.Object({ versionRecordToken: AssetClassSchema, @@ -63,27 +64,60 @@ export function parseLrpDatumOrThrow(datum: Datum): LRPDatum { ); } -export function serialiseLrpDatum(datum: LRPDatum): Datum { +type LRPSerialisationOptions = + // No replacing, just use non-canonical format. + | { _tag: 'noReplace' } + // Adaptive replace, when the spentDatum is canonical, do the replace, + // otherwise use the non canonical. + | { _tag: 'adaptiveReplace'; spentLrpDatum: string }; + +export function serialiseLrpDatum( + datum: LRPDatum, + // Overall, we don't want to do the replacing. We just use non-canonical format. + serialisationOptions: LRPSerialisationOptions = { _tag: 'noReplace' }, +): Datum { const d = Data.to(datum, LRPDatum); - // If the lrp was created using a canonical on-chain decimal, we need to serialise it canonically. - // This is due to some issue related to how Aiken compares objects. - // See "Wrong continuing output" trace, specifically the spread of the previous datum ie. expecting the serialisation to be the same as it was created with - // We however do not want to do this for any lrps that are being build canonical. - const ocdSerialisedCanonical = Data.to( - datum.maxPrice, - OnChainDecimal, - { canonical: true }, - ); - const ocdSerialisedNonCanonical = Data.to( - datum.maxPrice, - OnChainDecimal, - { canonical: false }, - ); + return match(serialisationOptions) + .returnType() + .with({ _tag: 'noReplace' }, () => d) + .with( + { _tag: 'adaptiveReplace', spentLrpDatum: P.select() }, + (spentLrpDatum) => { + const isSpentDatumCanonical = spentLrpDatum.includes( + Data.to( + parseLrpDatumOrThrow(spentLrpDatum).maxPrice, + OnChainDecimal, + { + canonical: true, + }, + ), + ); + + // When spent datum was canonical, replace. + if (isSpentDatumCanonical) { + // If the lrp was created using a canonical on-chain decimal, we need to serialise it canonically. + // This is due to some issue related to how Aiken compares objects. + // See "Wrong continuing output" trace, specifically the spread of the previous datum ie. expecting the serialisation to be the same as it was created with + // We however do not want to do this for any lrps that are being build canonical. + const ocdSerialisedCanonical = Data.to( + datum.maxPrice, + OnChainDecimal, + { canonical: true }, + ); + const ocdSerialisedNonCanonical = Data.to( + datum.maxPrice, + OnChainDecimal, + { canonical: false }, + ); - return d.replace(ocdSerialisedNonCanonical, ocdSerialisedCanonical); + return d.replace(ocdSerialisedNonCanonical, ocdSerialisedCanonical); + } - // return d; + return d; + }, + ) + .exhaustive(); } export function serialiseLrpRedeemer(redeemer: LRPRedeemer): Redeemer { From f4be4569b6cbd1186af1831204a23dc41fae2c57 Mon Sep 17 00:00:00 2001 From: brokri-lgtm <> Date: Wed, 4 Feb 2026 16:47:16 +0100 Subject: [PATCH 2/2] lint + format fixes --- src/contracts/stability-pool/types-new.ts | 1 - src/contracts/staking/types-new.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/contracts/stability-pool/types-new.ts b/src/contracts/stability-pool/types-new.ts index b8a6d0c..dd84cc0 100644 --- a/src/contracts/stability-pool/types-new.ts +++ b/src/contracts/stability-pool/types-new.ts @@ -2,7 +2,6 @@ import { Core as EvoCore } from '@evolution-sdk/evolution'; import { match, P } from 'ts-pattern'; import { AddressSchema } from '@3rd-eye-labs/cardano-offchain-common'; import { DEFAULT_SCHEMA_OPTIONS } from '../../types/evolution-schema-options'; -import { Data } from '@lucid-evolution/lucid'; export const SPIntegerSchema = EvoCore.TSchema.Struct({ value: EvoCore.TSchema.Integer, diff --git a/src/contracts/staking/types-new.ts b/src/contracts/staking/types-new.ts index 9bd75df..1d55f28 100644 --- a/src/contracts/staking/types-new.ts +++ b/src/contracts/staking/types-new.ts @@ -2,7 +2,6 @@ import { Core as EvoCore } from '@evolution-sdk/evolution'; import { option as O, function as F } from 'fp-ts'; import { match, P } from 'ts-pattern'; import { DEFAULT_SCHEMA_OPTIONS } from '../../types/evolution-schema-options'; -import { Data } from '@lucid-evolution/lucid'; const StakingPosLockedAmtSchema = EvoCore.TSchema.Map( EvoCore.TSchema.Integer,