Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
848e842
merging upstream main
Kammerlo Jul 21, 2026
a891fbd
chore: removing masumi-usdm endpoint
Kammerlo Jul 21, 2026
2b4c973
chore: splitting client and server defined settings
Kammerlo Jul 21, 2026
3705875
doc: doc upgrades
Kammerlo Jul 23, 2026
b3b6417
docs(cardano): focus Masumi V2 payment flow
schaier-io Aug 3, 2026
2f68f0b
style(python): format README examples
schaier-io Aug 3, 2026
6bc8d27
docs(cardano): fix settlement verification
schaier-io Aug 3, 2026
cb6af05
Merge pull request #21 from masumi-network/codex/masumi-v2-x402-v2
Kammerlo Aug 3, 2026
bad7337
feat: updates according to the new spec
Kammerlo Aug 4, 2026
9021378
Merge remote-tracking branch 'fork/main' into chore/merge-upstream
Kammerlo Aug 4, 2026
4f00315
Merge branch 'main' into chore/merge-upstream
Kammerlo Aug 4, 2026
ce8ca97
fix: fixing e2e tests
Kammerlo Aug 4, 2026
cc35e7c
fix: fixing pipeline
Kammerlo Aug 4, 2026
0593cbb
fix(cardano): harden Masumi payment validation
schaier-io Aug 4, 2026
4d24d5e
fix(cardano): harden payment verification
schaier-io Aug 4, 2026
497aa76
fix(cardano): harden replay and submission
schaier-io Aug 4, 2026
9e7c24f
fix(cardano): close replay and settlement gaps
schaier-io Aug 4, 2026
ac452d1
fix(cardano): address CodeRabbit findings
schaier-io Aug 4, 2026
0751ef5
fix(cardano): validate payments before submission
schaier-io Aug 4, 2026
6937c89
fix(cardano): close Masumi interop gaps in the escrow lock
schaier-io Aug 4, 2026
b243f96
fix(cardano): bound the funds a Masumi 402 can immobilise
schaier-io Aug 4, 2026
8c7ef74
fix(cardano): address CodeRabbit review findings
schaier-io Aug 4, 2026
186d1c2
fix(cardano): make the deadline horizon buyer policy, not a verifier …
schaier-io Aug 4, 2026
3061920
Merge pull request #23 from masumi-network/codex/fix-masumi-cardano-i…
Kammerlo Aug 5, 2026
0fa4463
docs: updated docs to latest changes
Kammerlo Aug 5, 2026
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
14 changes: 13 additions & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,26 @@ You need **three separate Stellar accounts** for e2e tests (client, server, faci

#### Cardano Preprod

The Cardano e2e endpoint is paid in **lovelace** (native tADA), so you only need test ADA — no stablecoin trustline or sourcing.
The Cardano e2e endpoints are paid in **lovelace** (native tADA) by default, so you only need test ADA — no stablecoin trustline or sourcing.

1. Create a preprod wallet to obtain a **24-word mnemonic** and an `addr_test1...` address — either with a CIP-30 wallet (Eternl / Lace in **preprod** mode) or programmatically via `PrivateKey.generateMnemonic()` from `@evolution-sdk/evolution`. You only need a **client** mnemonic (`CLIENT_CARDANO_MNEMONIC`); the server just needs an address (`SERVER_CARDANO_ADDRESS`) — the client's `addr_test1...` works. `FACILITATOR_CARDANO_MNEMONIC` is **optional**: the facilitator only broadcasts the client's signed transaction, so it runs provider-only when no mnemonic is set.
2. Fund the **client** wallet with test ADA from the [Cardano testnets faucet](https://docs.cardano.org/cardano-testnets/tools/faucet/) (select **Preprod**); ~10 tADA is plenty. Only the client needs funds — it builds, signs, and pays the fee; the facilitator only broadcasts the transaction.
3. Get a free **Blockfrost** preprod project id at [blockfrost.io](https://blockfrost.io/) and set `BLOCKFROST_PROJECT_ID` (and `BLOCKFROST_PREPROD_URL`).

> **Note:** Run Cardano standalone via the chain-agnostic apps and the `cardano` family filter: `pnpm test --clients=fetch --servers=express --facilitators=typescript --families=cardano`. Without `--families=cardano` the harness expands to every protocol family those apps support and requires all of their credentials (EVM, SVM, etc.).

###### Native-token (tUSDM) run

The asset is a config axis, not a separate endpoint: the same `default` / `masumi` / `script` methods run against a native token when `SERVER_CARDANO_ASSET` is set to a `policyId.assetNameHex` unit. This is the only place the token paths are exercised end to end (structural lovelace on the escrow output, exact token matching), so run it whenever the Masumi or signer funding logic changes:

```bash
SERVER_CARDANO_ASSET=e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9.0014df10745553444d \
SERVER_CARDANO_AMOUNT=1000000 \
pnpm test --clients=fetch --servers=express --facilitators=typescript --families=cardano
```

The client wallet must hold **tUSDM** on preprod in addition to a few tADA — a native-token lock still carries structural lovelace (post-result min-UTXO plus collateral), which the client signer funds itself.

##### TON testnet funding for TVM e2e and examples

- **Testnet TON**: use [@testgiver_ton_bot](https://t.me/testgiver_ton_bot) to fund the facilitator and payer wallets with TON for relay fees. The facilitator wallet must hold **at least 1.1 TON** before running tests.
Expand Down
2 changes: 1 addition & 1 deletion e2e/clients/axios/test.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "axios",
"type": "client",
"language": "typescript",
"protocolFamilies": ["evm", "svm", "avm", "aptos", "hedera", "keeta", "near", "stellar", "tvm", "ccd", "cardano", "xrpl"],
"protocolFamilies": ["evm", "svm", "avm", "aptos", "hedera", "keeta", "near", "stellar", "tvm", "ccd", "xrpl", "cardano"],
"x402Versions": [1, 2],
"schemes": ["exact", "upto", "batch-settlement"],
"evm": {
Expand Down
2 changes: 1 addition & 1 deletion e2e/clients/fetch/test.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "fetch",
"type": "client",
"language": "typescript",
"protocolFamilies": ["evm", "svm", "avm", "aptos", "hedera", "keeta", "near", "stellar", "tvm", "ccd", "cardano", "xrpl"],
"protocolFamilies": ["evm", "svm", "avm", "aptos", "hedera", "keeta", "near", "stellar", "tvm", "ccd", "xrpl", "cardano"],
"x402Versions": [1, 2],
"schemes": ["exact", "upto", "batch-settlement"],
"evm": {
Expand Down
26 changes: 6 additions & 20 deletions e2e/facilitators/typescript/test.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,21 @@
"tvm",
"near",
"ccd",
"cardano",
"xrpl"
],
"x402Versions": [
1,
2
],
"schemes": [
"exact",
"upto",
"batch-settlement"
"xrpl",
"cardano"
],
"x402Versions": [1, 2],
"schemes": ["exact", "upto", "batch-settlement"],
"extensions": [
"bazaar",
"eip2612GasSponsoring",
"erc20ApprovalGasSponsoring"
],
"evm": {
"assetTransferMethods": [
"eip3009",
"permit2"
]
"assetTransferMethods": ["eip3009", "permit2"]
},
"environment": {
"required": [
"PORT",
"EVM_PRIVATE_KEY",
"SVM_PRIVATE_KEY"
],
"required": ["PORT", "EVM_PRIVATE_KEY", "SVM_PRIVATE_KEY"],
"optional": [
"AVM_PRIVATE_KEY",
"APTOS_PRIVATE_KEY",
Expand Down
12 changes: 12 additions & 0 deletions e2e/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

182 changes: 117 additions & 65 deletions e2e/servers/express/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { KEETA_TESTNET_CAIP2 } from "@x402/keeta";
import { ExactKeetaScheme } from "@x402/keeta/exact/server";
import { ExactStellarScheme } from "@x402/stellar/exact/server";
import { ExactCardanoScheme } from "@x402/cardano/exact/server";
import { masumiContractAddress, getDefaultUsdmAsset } from "@x402/cardano";
import { issueMasumiRequirements, toMasumiSellerSigner } from "@x402/cardano";
import { ExactTvmScheme } from "@x402/tvm/exact/server";
import { ExactNearScheme } from "@x402/near/exact/server";
import type { XrplAssetTransferMethod } from "@x402/xrpl";
Expand Down Expand Up @@ -59,6 +59,13 @@ const HEDERA_PAYEE_ADDRESS = process.env.HEDERA_PAYEE_ADDRESS as string | undefi
const KEETA_PAYEE_ADDRESS = process.env.KEETA_PAYEE_ADDRESS as string | undefined;
const STELLAR_PAYEE_ADDRESS = process.env.STELLAR_PAYEE_ADDRESS as string | undefined;
const CARDANO_PAYEE_ADDRESS = process.env.CARDANO_PAYEE_ADDRESS as string | undefined;
// Asset the Cardano endpoints charge in. Defaults to lovelace (tADA) so the e2e
// is faucet-fundable; set CARDANO_ASSET to a `policyId.assetNameHex` unit (e.g.
// getDefaultUsdmAsset(CARDANO_NETWORK)) to run the same methods against a native
// token. CARDANO_AMOUNT is in the asset's smallest unit and must clear the
// masumi escrow's min-UTXO when paying lovelace.
const CARDANO_ASSET = process.env.CARDANO_ASSET || "lovelace";
const CARDANO_AMOUNT = process.env.CARDANO_AMOUNT || "5000000";
// Script (assetTransferMethod=script) fixture: a minimal always-succeeds Plutus
// V3 validator and its enterprise (preprod/preview) script address. The
// facilitator reconstructs this address from the script in `extra` and verifies
Expand All @@ -81,16 +88,65 @@ const CARDANO_DISCOVERY = declareDiscoveryExtension({
},
},
});
// Masumi lock deadlines for the demo, future-relative so the anchored tx TTL
// (invalidHereafter = pay_by_time) stays valid across a run. A real integration
// takes these from the Masumi purchase instead of a fixed offset at startup.
const CARDANO_MASUMI_BASE_MS = Date.now();
const CARDANO_MASUMI_TIMES = {
payByTime: (CARDANO_MASUMI_BASE_MS + 30 * 60_000).toString(),
submitResultTime: (CARDANO_MASUMI_BASE_MS + 60 * 60_000).toString(),
unlockTime: (CARDANO_MASUMI_BASE_MS + 90 * 60_000).toString(),
externalDisputeUnlockTime: (CARDANO_MASUMI_BASE_MS + 120 * 60_000).toString(),
// How long a Cardano payment stays valid. The Masumi client anchors the tx TTL
// to pay_by_time, and the facilitator refuses a TTL further ahead than
// maxTimeoutSeconds, so pay_by_time cannot exceed issuance + this value.
const CARDANO_MAX_TIMEOUT_SECONDS = 600;
// Canonical block inclusion is the bar for the e2e: the spec default of 1 would
// make every Cardano scenario wait an extra ~20s block for no added signal.
const CARDANO_CONFIRMATION_POLICY = { l1Confirmations: 0 };
// The seller signs the Masumi terms with its selling wallet. The escrow pays
// this address, so a real deployment MUST set CARDANO_SELLER_MNEMONIC; the
// well-known test phrase below only keeps the e2e endpoint self-contained. The
// key needs no funds — it only authorizes the terms.
const CARDANO_TEST_SELLER_MNEMONIC =
"test test test test test test test test test test test junk";
const CARDANO_SELLER = CARDANO_PAYEE_ADDRESS
? toMasumiSellerSigner({
mnemonic: process.env.CARDANO_SELLER_MNEMONIC || CARDANO_TEST_SELLER_MNEMONIC,
network: CARDANO_NETWORK,
})
: undefined;
/**
* Issues one Masumi offer. A spec-conformant Masumi 402 carries a request
* commitment and a seller signature over termsDigest, so it must be issued
* rather than hand-written, and each issuance draws a fresh sellerNonce.
*
* The lock deadlines clear the spec's minimum intervals: pay_by + 5min <=
* submit_result, submit_result + 15min <= unlock, unlock + 15min <= dispute.
*
* @returns The issued requirements: payTo (the escrow address), the price, and
* the `extra` block carrying the commitment, terms and authorization.
*/
const issueCardanoMasumiOffer = async () => {
const payByMs = Date.now() + CARDANO_MAX_TIMEOUT_SECONDS * 1000;
return issueMasumiRequirements({
network: CARDANO_NETWORK,
asset: CARDANO_ASSET,
amount: CARDANO_AMOUNT,
maxTimeoutSeconds: CARDANO_MAX_TIMEOUT_SECONDS,
sellerAddress: CARDANO_SELLER!.sellerAddress,
signTerms: CARDANO_SELLER!.signTerms,
commitment: [
{
name: "parameters",
canonicalization: "jcs",
mediaType: "application/json",
content: { endpoint: "/exact/cardano/masumi" },
},
],
payByTime: payByMs.toString(),
submitResultTime: (payByMs + 5 * 60_000).toString(),
unlockTime: (payByMs + 20 * 60_000).toString(),
externalDisputeUnlockTime: (payByMs + 35 * 60_000).toString(),
settlementPolicy: "l1",
confirmationPolicy: CARDANO_CONFIRMATION_POLICY,
});
};
// The offer currently on the wire. termsDigest binds to exactly one settled
// transaction, so an offer is one-shot: a second payer under the same terms is
// a replay the facilitator refuses. Re-issued per unpaid request below.
let CARDANO_MASUMI_REQUIREMENTS = CARDANO_SELLER ? await issueCardanoMasumiOffer() : undefined;
const TVM_PAYEE_ADDRESS = process.env.TVM_PAYEE_ADDRESS as string | undefined;
const NEAR_NETWORK = (process.env.NEAR_NETWORK || "near:testnet") as `${string}:${string}`;
const NEAR_PAYEE_ADDRESS = process.env.NEAR_PAYEE_ADDRESS as string | undefined;
Expand Down Expand Up @@ -140,10 +196,18 @@ if (!facilitatorUrl) {
const app = express();

// Create facilitator clients (mock facilitator as fallback for startup validation)
const facilitatorClients = [new HTTPFacilitatorClient({ url: facilitatorUrl })];
// Cardano settles on ~20-second blocks, so its `settle()` cannot finish inside
// the 30s facilitator-client default. Raising the ceiling costs nothing on the
// fast chains — they still return as soon as they are done.
const FACILITATOR_TIMEOUT_MS = 180_000;
const facilitatorClients = [
new HTTPFacilitatorClient({ url: facilitatorUrl, timeoutMs: FACILITATOR_TIMEOUT_MS }),
];
const mockFacilitatorUrl = process.env.MOCK_FACILITATOR_URL;
if (mockFacilitatorUrl) {
facilitatorClients.push(new HTTPFacilitatorClient({ url: mockFacilitatorUrl }));
facilitatorClients.push(
new HTTPFacilitatorClient({ url: mockFacilitatorUrl, timeoutMs: FACILITATOR_TIMEOUT_MS }),
);
}

// Create x402 resource server
Expand Down Expand Up @@ -305,6 +369,19 @@ app.use("/exact/cardano", (req, res, next) => {
next();
});

/**
* Re-issues the Masumi offer ahead of each unpaid request, so two clients
* hitting this endpoint never share a termsDigest. A request carrying
* PAYMENT-SIGNATURE is the paid retry of an offer the client was already
* quoted and must be matched against that same offer, so it is left alone.
*/
app.use("/exact/cardano/masumi", async (req, _res, next) => {
if (CARDANO_SELLER && !req.headers["payment-signature"]) {
CARDANO_MASUMI_REQUIREMENTS = await issueCardanoMasumiOffer();
}
next();
});

/**
* Pre-middleware guard for optional TVM endpoint
* Returns 501 Not Implemented if TVM is not configured
Expand Down Expand Up @@ -785,69 +862,45 @@ app.use(
: {}),
...(CARDANO_PAYEE_ADDRESS
? {
// One endpoint per Cardano assetTransferMethod; all pay lovelace
// (tADA) so the e2e is faucet-fundable (preprod USDM is not).
// One endpoint per Cardano assetTransferMethod. The asset is a config
// axis (CARDANO_ASSET), not a separate endpoint, so the same three
// methods run unchanged against tADA or a native token.
"GET /exact/cardano/default": {
accepts: {
payTo: CARDANO_PAYEE_ADDRESS!,
scheme: "exact",
price: { amount: "2000000", asset: "lovelace" },
network: CARDANO_NETWORK,
extra: { assetTransferMethod: "default" },
},
extensions: { ...CARDANO_DISCOVERY },
},
"GET /exact/cardano/masumi": {
accepts: {
// Locks ADA into the vested_pay escrow (Web3CardanoV2): payTo is
// the escrow script address, seller is the payee.
payTo: masumiContractAddress(CARDANO_NETWORK),
scheme: "exact",
price: { amount: "5000000", asset: "lovelace" },
price: { amount: CARDANO_AMOUNT, asset: CARDANO_ASSET },
network: CARDANO_NETWORK,
extra: {
assetTransferMethod: "masumi",
paymentType: "Web3CardanoV2",
// Escrow address for this deployment (must equal payTo).
contractAddress: masumiContractAddress(CARDANO_NETWORK),
sellerAddress: CARDANO_PAYEE_ADDRESS!,
agentIdentifier: "deadbeefdeadbeefdeadbeefdeadbeef",
inputHash: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
collateralReturnLovelace: "0",
// Purchase-bound identifiers; real values come from the Masumi
// purchase. Fixed placeholders keep the e2e lock deterministic.
referenceKey: "aa".repeat(32),
referenceSignature: "bb".repeat(64),
sellerNonce: "cc".repeat(32),
identifierFromPurchaser: "dd".repeat(32),
...CARDANO_MASUMI_TIMES,
assetTransferMethod: "default",
confirmationPolicy: CARDANO_CONFIRMATION_POLICY,
},
},
extensions: { ...CARDANO_DISCOVERY },
},
"GET /exact/cardano/masumi-usdm": {
"GET /exact/cardano/masumi": {
accepts: {
// Masumi lock paying a native token (tUSDM) instead of ADA. The
// escrow output also carries structural lovelace (min-UTXO +
// collateral), which the client signer funds automatically. The
// buyer wallet must hold tUSDM (preprod) plus ~4 ADA for the lock.
payTo: masumiContractAddress(CARDANO_NETWORK),
// Locks into the vested_pay escrow (Web3CardanoV2). payTo is the
// escrow script address the verifier re-derives from the
// deployment parameters — it is never hand-supplied. For a
// native-token lock the escrow output also carries structural
// lovelace (min-UTXO + collateral) the client computes itself.
payTo: CARDANO_MASUMI_REQUIREMENTS!.payTo,
scheme: "exact",
price: { amount: "1000000", asset: getDefaultUsdmAsset(CARDANO_NETWORK) },
price: {
amount: CARDANO_MASUMI_REQUIREMENTS!.amount,
asset: CARDANO_MASUMI_REQUIREMENTS!.asset,
},
network: CARDANO_NETWORK,
extra: {
assetTransferMethod: "masumi",
paymentType: "Web3CardanoV2",
contractAddress: masumiContractAddress(CARDANO_NETWORK),
sellerAddress: CARDANO_PAYEE_ADDRESS!,
agentIdentifier: "deadbeefdeadbeefdeadbeefdeadbeef",
inputHash: "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
collateralReturnLovelace: "0",
referenceKey: "aa".repeat(32),
referenceSignature: "bb".repeat(64),
sellerNonce: "cc".repeat(32),
identifierFromPurchaser: "dd".repeat(32),
...CARDANO_MASUMI_TIMES,
maxTimeoutSeconds: CARDANO_MAX_TIMEOUT_SECONDS,
// Carries the request commitment, the seller-signed terms, the
// CIP-8 authorization over termsDigest and the compatibility
// identifier. A getter, because the middleware reads it per
// request and the pre-middleware above rotates the offer between
// 402s; the paid retry sees the same block it was quoted, since
// regenerating it would change termsDigest.
get extra() {
return CARDANO_MASUMI_REQUIREMENTS!.extra;
},
},
extensions: { ...CARDANO_DISCOVERY },
Expand All @@ -858,10 +911,11 @@ app.use(
// the script descriptor below and verifies it matches.
payTo: CARDANO_SCRIPT_ADDRESS,
scheme: "exact",
price: { amount: "2000000", asset: "lovelace" },
price: { amount: CARDANO_AMOUNT, asset: CARDANO_ASSET },
network: CARDANO_NETWORK,
extra: {
assetTransferMethod: "script",
confirmationPolicy: CARDANO_CONFIRMATION_POLICY,
script: { type: "plutusV3", code: CARDANO_SCRIPT_CODE },
// Optional inline datum (CBOR hex) attached to the payTo output.
// A real contract declares whatever datum it needs; the client
Expand Down Expand Up @@ -1132,7 +1186,6 @@ if (CARDANO_PAYEE_ADDRESS) {
[
"/exact/cardano/default",
"/exact/cardano/masumi",
"/exact/cardano/masumi-usdm",
"/exact/cardano/script",
],
(req, res) => {
Expand Down Expand Up @@ -1253,7 +1306,6 @@ app.listen(parseInt(PORT), () => {
║ • GET /exact/stellar (Stellar) ║
║ • GET /exact/cardano/default (Cardano) ║
║ • GET /exact/cardano/masumi (Cardano) ║
║ • GET /exact/cardano/masumi-usdm (Cardano) ║
║ • GET /exact/cardano/script (Cardano) ║
║ • GET /exact/tvm (TVM) ║
║ • GET /health (no payment required) ║
Expand Down
Loading
Loading