diff --git a/packages/mesh-contract/src/marketplace/offchain.ts b/packages/mesh-contract/src/marketplace/offchain.ts index f7c5c4b39..a5324508b 100644 --- a/packages/mesh-contract/src/marketplace/offchain.ts +++ b/packages/mesh-contract/src/marketplace/offchain.ts @@ -57,6 +57,11 @@ export class MeshMarketplaceContract extends MeshTxInitiator { feePercentageBasisPoint: number, ) { super(inputs); + if (feePercentageBasisPoint < 0 || feePercentageBasisPoint > 10000) { + throw new Error( + `feePercentageBasisPoint must be between 0 and 10000, got ${feePercentageBasisPoint}`, + ); + } this.ownerAddress = ownerAddress; this.feePercentageBasisPoint = feePercentageBasisPoint; @@ -154,9 +159,14 @@ export class MeshMarketplaceContract extends MeshTxInitiator { marketplaceUtxo.output.plutusData!, ); - const inputLovelace = marketplaceUtxo.output.amount.find( + const lovelaceEntry = marketplaceUtxo.output.amount.find( (a) => a.unit === "lovelace", - )!.quantity; + ); + if (!lovelaceEntry) { + throw new Error( + "purchaseAsset: marketplace UTxO is missing a lovelace entry", + ); + } const tx = this.mesh .spendingPlutusScript(this.languageVersion) @@ -178,27 +188,36 @@ export class MeshMarketplaceContract extends MeshTxInitiator { ) .selectUtxosFrom(utxos); - let ownerToReceiveLovelace = - ((inputDatum.fields[1].int as number) * this.feePercentageBasisPoint) / - 10000; - if (this.feePercentageBasisPoint > 0 && ownerToReceiveLovelace < 1000000) { - ownerToReceiveLovelace = 1000000; + // Keep all lovelace arithmetic in BigInt to avoid precision loss for + // prices above Number.MAX_SAFE_INTEGER (~9M ADA). + const priceBig = BigInt(inputDatum.fields[1].int); + const feeBig = BigInt(Math.round(this.feePercentageBasisPoint)); + + // Ceiling division: (a * b + d - 1) / d mirrors Math.ceil without floats. + let ownerToReceiveLovelace = (priceBig * feeBig + 9999n) / 10000n; + // Apply 1-ADA minimum only when the listing has a non-zero price; a free + // listing (price = 0) should not incur a mandatory fee. + if ( + this.feePercentageBasisPoint > 0 && + priceBig > 0n && + ownerToReceiveLovelace < 1000000n + ) { + ownerToReceiveLovelace = 1000000n; } - if (ownerToReceiveLovelace > 0) { + if (ownerToReceiveLovelace > 0n) { const ownerToReceive = [ { unit: "lovelace", - quantity: Math.ceil(ownerToReceiveLovelace).toString(), + quantity: ownerToReceiveLovelace.toString(), }, ]; tx.txOut(this.ownerAddress, ownerToReceive); } - const sellerToReceiveLovelace = - (inputDatum.fields[1].int as number) + Number(inputLovelace); + const sellerToReceiveLovelace = priceBig + BigInt(lovelaceEntry.quantity); - if (sellerToReceiveLovelace > 0) { + if (sellerToReceiveLovelace > 0n) { const sellerAddress = serializeAddressObj( inputDatum.fields[0], this.networkId, diff --git a/packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts b/packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts index 628b86036..5fc25e55b 100644 --- a/packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts +++ b/packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts @@ -122,14 +122,17 @@ export class OfflineEvaluator implements IEvaluator { additionalUtxos: UTxO[], additionalTxs: string[], ): Promise[]> { - // Track which utxos is resolved + // Work on a copy so the caller's `additionalUtxos` array is never mutated. + const resolvedUtxos: UTxO[] = [...additionalUtxos]; + + // Track which utxos are resolved const foundUtxos = new Set(); - for (const utxo of additionalUtxos) { + for (const utxo of resolvedUtxos) { foundUtxos.add(`${utxo.input.txHash}:${utxo.input.outputIndex}`); } - for (const tx of additionalTxs) { - const outputs = getTransactionOutputs(tx); + for (const additionalTx of additionalTxs) { + const outputs = getTransactionOutputs(additionalTx); for (const output of outputs) { foundUtxos.add(`${output.input.txHash}:${output.input.outputIndex}`); } @@ -138,20 +141,24 @@ export class OfflineEvaluator implements IEvaluator { (input) => !foundUtxos.has(`${input.txHash}:${input.outputIndex}`), ); const txHashesSet = new Set(inputsToResolve.map((input) => input.txHash)); - for (const txHash of txHashesSet) { - const utxos = await this.fetcher.fetchUTxOs(txHash); + + // Resolve each distinct parent transaction's UTxOs concurrently. + const fetchedUtxoSets = await Promise.all( + [...txHashesSet].map((txHash) => this.fetcher.fetchUTxOs(txHash)), + ); + for (const utxos of fetchedUtxoSets) { for (const utxo of utxos) { - if (utxo) - if ( - inputsToResolve.find( - (input) => - input.txHash === txHash && - input.outputIndex === utxo.input.outputIndex, - ) - ) { - additionalUtxos.push(utxo); - foundUtxos.add(`${utxo.input.txHash}:${utxo.input.outputIndex}`); - } + if ( + utxo && + inputsToResolve.some( + (input) => + input.txHash === utxo.input.txHash && + input.outputIndex === utxo.input.outputIndex, + ) + ) { + resolvedUtxos.push(utxo); + foundUtxos.add(`${utxo.input.txHash}:${utxo.input.outputIndex}`); + } } } const missing = inputsToResolve.filter( @@ -167,7 +174,7 @@ export class OfflineEvaluator implements IEvaluator { } return evaluateTransaction( tx, - additionalUtxos, + resolvedUtxos, additionalTxs, this.costModels, this.slotConfig, diff --git a/packages/mesh-core-csl/test/offline-providers/evaluator.test.ts b/packages/mesh-core-csl/test/offline-providers/evaluator.test.ts index 3411e290d..b71390daa 100644 --- a/packages/mesh-core-csl/test/offline-providers/evaluator.test.ts +++ b/packages/mesh-core-csl/test/offline-providers/evaluator.test.ts @@ -1,4 +1,4 @@ -import { SLOT_CONFIG_NETWORK } from "@meshsdk/common"; +import { SLOT_CONFIG_NETWORK, UTxO } from "@meshsdk/common"; import { OfflineEvaluator } from "@meshsdk/core-csl"; import { OfflineFetcher } from "@meshsdk/provider"; @@ -119,7 +119,12 @@ describe("Offline Evaluator", () => { }; fetcher.addUTxOs([utxo_1, utxo_2, utxo_3, utxo_4, utxo_5, utxo_6, utxo_7]); - const res = await evaluator.evaluateTx(txHex, [], []); + // All inputs are resolved from the fetcher (across several parent txs, in + // parallel). The caller's array must not be mutated in the process — the + // previous implementation pushed the resolved UTxOs into it. + const additionalUtxos: UTxO[] = []; + const res = await evaluator.evaluateTx(txHex, additionalUtxos, []); + expect(additionalUtxos).toHaveLength(0); expect(res).toStrictEqual([ { index: 0,