Skip to content
Closed
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
22 changes: 10 additions & 12 deletions packages/mesh-contract/src/marketplace/offchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
import { applyParamsToScript } from "@meshsdk/core-cst";

import { MeshTxInitiator, MeshTxInitiatorInput } from "../common";
import { computeMarketplacePurchasePayouts } from "./purchase-fee-math";
import blueprintV1 from "./aiken-workspace-v1/plutus.json";
import blueprintV2 from "./aiken-workspace-v2/plutus.json";

Expand Down Expand Up @@ -178,27 +179,24 @@ 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;
}
const { ownerToReceiveLovelace, sellerToReceiveLovelace } =
computeMarketplacePurchasePayouts(
inputDatum.fields[1].int,
this.feePercentageBasisPoint,
inputLovelace,
);

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);

if (sellerToReceiveLovelace > 0) {
if (sellerToReceiveLovelace > 0n) {
const sellerAddress = serializeAddressObj(
inputDatum.fields[0],
this.networkId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Zero-dep regression harness for marketplace purchaseAsset BigInt fee math.
* Run: node --test packages/mesh-contract/src/marketplace/purchase-fee-math.node.test.mjs
* Full helper assertions: packages/mesh-contract/src/marketplace/purchase-fee-math.test.ts (jest)
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));

/** Prior buggy formula from purchaseAsset. */
function buggyLegacyMix(priceInt, feePercentageBasisPoint, inputLovelace) {
let ownerToReceiveLovelace =
(priceInt * feePercentageBasisPoint) / 10000;
if (feePercentageBasisPoint > 0 && ownerToReceiveLovelace < 1000000) {
ownerToReceiveLovelace = 1000000;
}
const sellerToReceiveLovelace = priceInt + Number(inputLovelace);
return { ownerToReceiveLovelace, sellerToReceiveLovelace };
}

describe("purchaseAsset fee math BigInt regression", () => {
it("legacy Number-mix throws TypeError when price is BigInt", () => {
assert.throws(
() => buggyLegacyMix(100_000_000n, 250, "2000000"),
(err) =>
err instanceof TypeError &&
/Cannot mix BigInt and other types/.test(err.message),
);
});

it("offchain.ts purchaseAsset uses computeMarketplacePurchasePayouts", () => {
const src = readFileSync(join(__dirname, "offchain.ts"), "utf8");
assert.match(src, /computeMarketplacePurchasePayouts/);
assert.doesNotMatch(
src,
/\(inputDatum\.fields\[1\]\.int as number\) \* this\.feePercentageBasisPoint/,
);
assert.doesNotMatch(
src,
/\(inputDatum\.fields\[1\]\.int as number\) \+ Number\(inputLovelace\)/,
);
});

it("purchase-fee-math.ts uses BigInt arithmetic", () => {
const src = readFileSync(join(__dirname, "purchase-fee-math.ts"), "utf8");
assert.match(src, /BigInt\(priceInt\)/);
assert.match(src, /10000n/);
assert.doesNotMatch(src, /as number\) \* feePercentageBasisPoint/);
});
});
34 changes: 34 additions & 0 deletions packages/mesh-contract/src/marketplace/purchase-fee-math.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, it } from "@jest/globals";
import { computeMarketplacePurchasePayouts } from "./purchase-fee-math";

describe("computeMarketplacePurchasePayouts", () => {
it("does not throw TypeError when datum price int is BigInt", () => {
expect(() =>
computeMarketplacePurchasePayouts(100_000_000n, 250, "2000000"),
).not.toThrow();
});

it("computes owner fee (2.5%) and seller payout with BigInt price", () => {
// 100 ADA price, 2.5% fee => 2.5 ADA (above 1 ADA minimum)
const { ownerToReceiveLovelace, sellerToReceiveLovelace } =
computeMarketplacePurchasePayouts(100_000_000n, 250, "2000000");
expect(ownerToReceiveLovelace).toBe(2_500_000n);
expect(sellerToReceiveLovelace).toBe(102_000_000n);
});

it("enforces minimum 1 ADA owner fee when basis points > 0", () => {
const { ownerToReceiveLovelace } = computeMarketplacePurchasePayouts(
1_000n,
250,
"0",
);
expect(ownerToReceiveLovelace).toBe(1_000_000n);
});

it("still works when price int is a Number", () => {
const { ownerToReceiveLovelace, sellerToReceiveLovelace } =
computeMarketplacePurchasePayouts(100_000_000, 250, "2000000");
expect(ownerToReceiveLovelace).toBe(2_500_000n);
expect(sellerToReceiveLovelace).toBe(102_000_000n);
});
});
24 changes: 24 additions & 0 deletions packages/mesh-contract/src/marketplace/purchase-fee-math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* BigInt-safe fee / payout math for MeshMarketplaceContract.purchaseAsset.
* On-chain datum ints may deserialize as bigint; mixing with Number throws
* TypeError: Cannot mix BigInt and other types.
*/
export function computeMarketplacePurchasePayouts(
priceInt: number | bigint,
feePercentageBasisPoint: number,
inputLovelace: string | number | bigint,
): {
ownerToReceiveLovelace: bigint;
sellerToReceiveLovelace: bigint;
} {
const price = BigInt(priceInt);
const feeBp = BigInt(feePercentageBasisPoint);
// Match prior Math.ceil(float) semantics for positive amounts.
let ownerToReceiveLovelace =
feeBp === 0n ? 0n : (price * feeBp + 9999n) / 10000n;
if (feePercentageBasisPoint > 0 && ownerToReceiveLovelace < 1000000n) {
ownerToReceiveLovelace = 1000000n;
}
const sellerToReceiveLovelace = price + BigInt(inputLovelace);
return { ownerToReceiveLovelace, sellerToReceiveLovelace };
}