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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions packages/mesh-contract/src/marketplace/offchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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)
Expand All @@ -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,
Expand Down
43 changes: 25 additions & 18 deletions packages/mesh-core-csl/src/offline-providers/offline-evaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,17 @@ export class OfflineEvaluator implements IEvaluator {
additionalUtxos: UTxO[],
additionalTxs: string[],
): Promise<Omit<Action, "data">[]> {
// 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<string>();

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}`);
}
Expand All @@ -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(
Expand All @@ -167,7 +174,7 @@ export class OfflineEvaluator implements IEvaluator {
}
return evaluateTransaction(
tx,
additionalUtxos,
resolvedUtxos,
additionalTxs,
this.costModels,
this.slotConfig,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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,
Expand Down