@mcpaid/sdk 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +176 -0
- package/NPM_RESTRUCTURE.md +86 -0
- package/OPERATOR-LICENSE.md +18 -0
- package/README.md +380 -0
- package/SELF-HOSTING.md +108 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts +26 -0
- package/dist/circuit-breaker/circuit-breaker.d.ts.map +1 -0
- package/dist/circuit-breaker/circuit-breaker.js +128 -0
- package/dist/circuit-breaker/circuit-breaker.js.map +1 -0
- package/dist/cli.d.ts +21 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1560 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts/ToolPayRouter.d.ts +288 -0
- package/dist/contracts/ToolPayRouter.d.ts.map +1 -0
- package/dist/contracts/ToolPayRouter.js +370 -0
- package/dist/contracts/ToolPayRouter.js.map +1 -0
- package/dist/contracts/router-registry.d.ts +19 -0
- package/dist/contracts/router-registry.d.ts.map +1 -0
- package/dist/contracts/router-registry.js +28 -0
- package/dist/contracts/router-registry.js.map +1 -0
- package/dist/fee/fee-calculator.d.ts +35 -0
- package/dist/fee/fee-calculator.d.ts.map +1 -0
- package/dist/fee/fee-calculator.js +57 -0
- package/dist/fee/fee-calculator.js.map +1 -0
- package/dist/fee/fee-ledger.d.ts +66 -0
- package/dist/fee/fee-ledger.d.ts.map +1 -0
- package/dist/fee/fee-ledger.js +181 -0
- package/dist/fee/fee-ledger.js.map +1 -0
- package/dist/gateway/config-loader.d.ts +38 -0
- package/dist/gateway/config-loader.d.ts.map +1 -0
- package/dist/gateway/config-loader.js +125 -0
- package/dist/gateway/config-loader.js.map +1 -0
- package/dist/gateway/server-registry.d.ts +25 -0
- package/dist/gateway/server-registry.d.ts.map +1 -0
- package/dist/gateway/server-registry.js +59 -0
- package/dist/gateway/server-registry.js.map +1 -0
- package/dist/gateway/toolpay-gateway.d.ts +57 -0
- package/dist/gateway/toolpay-gateway.d.ts.map +1 -0
- package/dist/gateway/toolpay-gateway.js +320 -0
- package/dist/gateway/toolpay-gateway.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/index.js.map +1 -0
- package/dist/sdk/client-agent.d.ts +47 -0
- package/dist/sdk/client-agent.d.ts.map +1 -0
- package/dist/sdk/client-agent.js +158 -0
- package/dist/sdk/client-agent.js.map +1 -0
- package/dist/sdk/server-sdk.d.ts +51 -0
- package/dist/sdk/server-sdk.d.ts.map +1 -0
- package/dist/sdk/server-sdk.js +170 -0
- package/dist/sdk/server-sdk.js.map +1 -0
- package/dist/settlement/base-contract-client.d.ts +156 -0
- package/dist/settlement/base-contract-client.d.ts.map +1 -0
- package/dist/settlement/base-contract-client.js +156 -0
- package/dist/settlement/base-contract-client.js.map +1 -0
- package/dist/settlement/nonce-store.d.ts +54 -0
- package/dist/settlement/nonce-store.d.ts.map +1 -0
- package/dist/settlement/nonce-store.js +125 -0
- package/dist/settlement/nonce-store.js.map +1 -0
- package/dist/settlement/onchain-verifier.d.ts +41 -0
- package/dist/settlement/onchain-verifier.d.ts.map +1 -0
- package/dist/settlement/onchain-verifier.js +114 -0
- package/dist/settlement/onchain-verifier.js.map +1 -0
- package/dist/settlement/payment-orchestrator.d.ts +59 -0
- package/dist/settlement/payment-orchestrator.d.ts.map +1 -0
- package/dist/settlement/payment-orchestrator.js +282 -0
- package/dist/settlement/payment-orchestrator.js.map +1 -0
- package/dist/settlement/permit-verifier.d.ts +60 -0
- package/dist/settlement/permit-verifier.d.ts.map +1 -0
- package/dist/settlement/permit-verifier.js +80 -0
- package/dist/settlement/permit-verifier.js.map +1 -0
- package/dist/settlement/session-wallet.d.ts +60 -0
- package/dist/settlement/session-wallet.d.ts.map +1 -0
- package/dist/settlement/session-wallet.js +105 -0
- package/dist/settlement/session-wallet.js.map +1 -0
- package/dist/transports/sse-transport.d.ts +27 -0
- package/dist/transports/sse-transport.d.ts.map +1 -0
- package/dist/transports/sse-transport.js +65 -0
- package/dist/transports/sse-transport.js.map +1 -0
- package/dist/transports/stdio-bridge.d.ts +29 -0
- package/dist/transports/stdio-bridge.d.ts.map +1 -0
- package/dist/transports/stdio-bridge.js +133 -0
- package/dist/transports/stdio-bridge.js.map +1 -0
- package/dist/types/circuit-breaker.d.ts +22 -0
- package/dist/types/circuit-breaker.d.ts.map +1 -0
- package/dist/types/circuit-breaker.js +5 -0
- package/dist/types/circuit-breaker.js.map +1 -0
- package/dist/types/ledger.d.ts +57 -0
- package/dist/types/ledger.d.ts.map +1 -0
- package/dist/types/ledger.js +5 -0
- package/dist/types/ledger.js.map +1 -0
- package/dist/types/mcp.d.ts +67 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +5 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/types/pricing.d.ts +68 -0
- package/dist/types/pricing.d.ts.map +1 -0
- package/dist/types/pricing.js +5 -0
- package/dist/types/pricing.js.map +1 -0
- package/dist/types/x402.d.ts +76 -0
- package/dist/types/x402.d.ts.map +1 -0
- package/dist/types/x402.js +5 -0
- package/dist/types/x402.js.map +1 -0
- package/dist/utils/units.d.ts +45 -0
- package/dist/utils/units.d.ts.map +1 -0
- package/dist/utils/units.js +141 -0
- package/dist/utils/units.js.map +1 -0
- package/package.json +72 -0
- package/src/contracts/ToolPayRouter.json +371 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolPay Nonce Store & Replay Prevention Engine
|
|
3
|
+
* Manages atomic challenge nonces, expiration TTLs, and prevents replay attacks.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from 'node:crypto';
|
|
6
|
+
export const DEFAULT_CHALLENGE_TTL_SECONDS = 120; // 2 minutes
|
|
7
|
+
export const MAX_STORED_NONCES = 10_000;
|
|
8
|
+
export class NonceStore {
|
|
9
|
+
store = new Map();
|
|
10
|
+
ttlSeconds;
|
|
11
|
+
opCount = 0;
|
|
12
|
+
constructor(ttlSeconds = DEFAULT_CHALLENGE_TTL_SECONDS) {
|
|
13
|
+
this.ttlSeconds = ttlSeconds;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Generates a unique, cryptographically random challenge nonce and stores the challenge data.
|
|
17
|
+
*/
|
|
18
|
+
createChallenge(challengeDataOmitNonce, customTtlSeconds, nowSec = Math.floor(Date.now() / 1000)) {
|
|
19
|
+
this.opCount++;
|
|
20
|
+
// Auto-prune periodically (every 50 creations) or if capacity reached
|
|
21
|
+
if (this.opCount % 50 === 0 || this.store.size >= MAX_STORED_NONCES) {
|
|
22
|
+
this.pruneExpired(nowSec);
|
|
23
|
+
// If still over capacity, evict oldest 100 entries to prevent memory exhaustion
|
|
24
|
+
if (this.store.size >= MAX_STORED_NONCES) {
|
|
25
|
+
const iter = this.store.keys();
|
|
26
|
+
for (let i = 0; i < 100; i++) {
|
|
27
|
+
const next = iter.next();
|
|
28
|
+
if (next.done)
|
|
29
|
+
break;
|
|
30
|
+
this.store.delete(next.value);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const nonce = randomUUID();
|
|
35
|
+
const ttl = customTtlSeconds ?? this.ttlSeconds;
|
|
36
|
+
const expiresAt = nowSec + ttl;
|
|
37
|
+
const challengeData = {
|
|
38
|
+
...challengeDataOmitNonce,
|
|
39
|
+
challengeNonce: nonce,
|
|
40
|
+
expiresAt,
|
|
41
|
+
};
|
|
42
|
+
this.store.set(nonce, {
|
|
43
|
+
data: challengeData,
|
|
44
|
+
status: 'PENDING',
|
|
45
|
+
createdAt: Date.now(),
|
|
46
|
+
});
|
|
47
|
+
return challengeData;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Checks if a challenge nonce is present in memory.
|
|
51
|
+
*/
|
|
52
|
+
hasChallenge(nonce) {
|
|
53
|
+
return this.store.has(nonce);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Records or restores an existing challenge into the store (e.g. from KV across isolates).
|
|
57
|
+
*/
|
|
58
|
+
recordChallenge(challenge) {
|
|
59
|
+
this.store.set(challenge.challengeNonce, {
|
|
60
|
+
data: challenge,
|
|
61
|
+
status: 'PENDING',
|
|
62
|
+
createdAt: Date.now(),
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Validates that a nonce exists, is pending, and has not expired without consuming it.
|
|
67
|
+
*/
|
|
68
|
+
peekChallenge(nonce, nowSec = Math.floor(Date.now() / 1000)) {
|
|
69
|
+
const record = this.store.get(nonce);
|
|
70
|
+
if (!record) {
|
|
71
|
+
throw new Error(`Invalid challenge nonce: "${nonce}" not found`);
|
|
72
|
+
}
|
|
73
|
+
if (record.status === 'EXPENDED') {
|
|
74
|
+
throw new Error(`Replay attack detected: challenge nonce "${nonce}" has already been used`);
|
|
75
|
+
}
|
|
76
|
+
if (record.status === 'EXPIRED' || nowSec > record.data.expiresAt) {
|
|
77
|
+
record.status = 'EXPIRED';
|
|
78
|
+
throw new Error(`Challenge nonce "${nonce}" has expired (expired at ${record.data.expiresAt}, current ${nowSec})`);
|
|
79
|
+
}
|
|
80
|
+
return record.data;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Consumes a validated nonce, transitioning it to EXPENDED.
|
|
84
|
+
*/
|
|
85
|
+
consumeChallenge(nonce) {
|
|
86
|
+
const record = this.store.get(nonce);
|
|
87
|
+
if (!record) {
|
|
88
|
+
throw new Error(`Invalid challenge nonce: "${nonce}" not found`);
|
|
89
|
+
}
|
|
90
|
+
if (record.status === 'EXPENDED') {
|
|
91
|
+
throw new Error(`Replay attack detected: challenge nonce "${nonce}" has already been used`);
|
|
92
|
+
}
|
|
93
|
+
record.status = 'EXPENDED';
|
|
94
|
+
record.expendedAt = Date.now();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Verifies that a nonce exists, is pending, and has not expired.
|
|
98
|
+
* Atomically marks it as EXPENDED.
|
|
99
|
+
*/
|
|
100
|
+
verifyAndConsume(nonce, nowSec = Math.floor(Date.now() / 1000)) {
|
|
101
|
+
const data = this.peekChallenge(nonce, nowSec);
|
|
102
|
+
this.consumeChallenge(nonce);
|
|
103
|
+
return data;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Checks the status of a nonce without consuming it.
|
|
107
|
+
*/
|
|
108
|
+
getChallenge(nonce) {
|
|
109
|
+
return this.store.get(nonce);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Garbage collects expired nonces.
|
|
113
|
+
*/
|
|
114
|
+
pruneExpired(nowSec = Math.floor(Date.now() / 1000)) {
|
|
115
|
+
let pruned = 0;
|
|
116
|
+
for (const [nonce, record] of this.store.entries()) {
|
|
117
|
+
if (nowSec > record.data.expiresAt + 300) { // keep 5 mins post-expiration for audit
|
|
118
|
+
this.store.delete(nonce);
|
|
119
|
+
pruned += 1;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return pruned;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=nonce-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nonce-store.js","sourceRoot":"","sources":["../../src/settlement/nonce-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolPay On-Chain Settlement Verifier
|
|
3
|
+
* Verifies direct on-chain crypto transfers (e.g. USDC on Base L2) against 402 challenge parameters.
|
|
4
|
+
*/
|
|
5
|
+
import { X402ChallengeData } from '../types/x402.js';
|
|
6
|
+
export interface OnChainTransactionData {
|
|
7
|
+
txHash: string;
|
|
8
|
+
from: string;
|
|
9
|
+
to: string;
|
|
10
|
+
amountMicro: bigint;
|
|
11
|
+
tokenAddress?: string;
|
|
12
|
+
chainId: number;
|
|
13
|
+
confirmed: boolean;
|
|
14
|
+
blockTimestampSec: number;
|
|
15
|
+
}
|
|
16
|
+
export interface OnChainRpcProvider {
|
|
17
|
+
getTransaction(txHash: string): Promise<OnChainTransactionData | null>;
|
|
18
|
+
}
|
|
19
|
+
export declare class OnChainVerifier {
|
|
20
|
+
private rpcProvider;
|
|
21
|
+
private claimedTxHashes;
|
|
22
|
+
private maxBlockAgeSec;
|
|
23
|
+
constructor(rpcProvider: OnChainRpcProvider, maxBlockAgeSec?: number, // max 3 minutes old
|
|
24
|
+
claimedTxHashes?: Set<string>);
|
|
25
|
+
isTxClaimed(txHash: string): boolean;
|
|
26
|
+
claimTx(txHash: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Validates an on-chain transaction against a challenge.
|
|
29
|
+
*/
|
|
30
|
+
verifyTransaction(params: {
|
|
31
|
+
txHash: string;
|
|
32
|
+
signerWallet: string;
|
|
33
|
+
challenge: X402ChallengeData;
|
|
34
|
+
nowSec?: number;
|
|
35
|
+
}): Promise<{
|
|
36
|
+
valid: boolean;
|
|
37
|
+
error?: string;
|
|
38
|
+
txData?: OnChainTransactionData;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=onchain-verifier.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onchain-verifier.d.ts","sourceRoot":"","sources":["../../src/settlement/onchain-verifier.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,OAAO,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,sBAAsB,GAAG,IAAI,CAAC,CAAC;CACxE;AAED,qBAAa,eAAe;IAC1B,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,eAAe,CAA0B;IACjD,OAAO,CAAC,cAAc,CAAS;gBAG7B,WAAW,EAAE,kBAAkB,EAC/B,cAAc,GAAE,MAAY,EAAE,oBAAoB;IAClD,eAAe,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC;IASxB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIpC,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAIpC;;OAEG;IACU,iBAAiB,CAAC,MAAM,EAAE;QACrC,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,iBAAiB,CAAC;QAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,sBAAsB,CAAA;KAAE,CAAC;CAiGjF"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolPay On-Chain Settlement Verifier
|
|
3
|
+
* Verifies direct on-chain crypto transfers (e.g. USDC on Base L2) against 402 challenge parameters.
|
|
4
|
+
*/
|
|
5
|
+
export class OnChainVerifier {
|
|
6
|
+
rpcProvider;
|
|
7
|
+
claimedTxHashes = new Set();
|
|
8
|
+
maxBlockAgeSec;
|
|
9
|
+
constructor(rpcProvider, maxBlockAgeSec = 180, // max 3 minutes old
|
|
10
|
+
claimedTxHashes) {
|
|
11
|
+
this.rpcProvider = rpcProvider;
|
|
12
|
+
this.maxBlockAgeSec = maxBlockAgeSec;
|
|
13
|
+
if (claimedTxHashes) {
|
|
14
|
+
this.claimedTxHashes = claimedTxHashes;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
isTxClaimed(txHash) {
|
|
18
|
+
return this.claimedTxHashes.has(txHash.toLowerCase());
|
|
19
|
+
}
|
|
20
|
+
claimTx(txHash) {
|
|
21
|
+
this.claimedTxHashes.add(txHash.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Validates an on-chain transaction against a challenge.
|
|
25
|
+
*/
|
|
26
|
+
async verifyTransaction(params) {
|
|
27
|
+
const now = params.nowSec || Math.floor(Date.now() / 1000);
|
|
28
|
+
const txHashNormalized = params.txHash.toLowerCase();
|
|
29
|
+
// 1. Check double-spend / txHash replay & atomically claim before async fetch
|
|
30
|
+
if (this.claimedTxHashes.has(txHashNormalized)) {
|
|
31
|
+
return {
|
|
32
|
+
valid: false,
|
|
33
|
+
error: `Transaction ${params.txHash} has already been claimed for another payment`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
this.claimedTxHashes.add(txHashNormalized);
|
|
37
|
+
try {
|
|
38
|
+
// 2. Fetch transaction from RPC
|
|
39
|
+
const tx = await this.rpcProvider.getTransaction(params.txHash);
|
|
40
|
+
if (!tx) {
|
|
41
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
42
|
+
return { valid: false, error: `Transaction ${params.txHash} not found on network` };
|
|
43
|
+
}
|
|
44
|
+
// 3. Verify confirmation
|
|
45
|
+
if (!tx.confirmed) {
|
|
46
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
47
|
+
return { valid: false, error: `Transaction ${params.txHash} is still pending confirmation` };
|
|
48
|
+
}
|
|
49
|
+
// 4. Verify chain ID
|
|
50
|
+
if (tx.chainId !== params.challenge.chainId) {
|
|
51
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
52
|
+
return {
|
|
53
|
+
valid: false,
|
|
54
|
+
error: `Transaction chain ID mismatch (expected ${params.challenge.chainId}, got ${tx.chainId})`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// 5. Verify token address if specified in challenge
|
|
58
|
+
if (params.challenge.tokenAddress && tx.tokenAddress) {
|
|
59
|
+
if (tx.tokenAddress.toLowerCase() !== params.challenge.tokenAddress.toLowerCase()) {
|
|
60
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
61
|
+
return {
|
|
62
|
+
valid: false,
|
|
63
|
+
error: `Transaction token mismatch (expected ${params.challenge.tokenAddress}, got ${tx.tokenAddress})`,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// 6. Verify recipient
|
|
68
|
+
if (tx.to.toLowerCase() !== params.challenge.recipientWallet.toLowerCase()) {
|
|
69
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
70
|
+
return {
|
|
71
|
+
valid: false,
|
|
72
|
+
error: `Transaction recipient mismatch (expected ${params.challenge.recipientWallet}, got ${tx.to})`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
// 7. Verify sender
|
|
76
|
+
if (tx.from.toLowerCase() !== params.signerWallet.toLowerCase()) {
|
|
77
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
78
|
+
return {
|
|
79
|
+
valid: false,
|
|
80
|
+
error: `Transaction sender mismatch (expected ${params.signerWallet}, got ${tx.from})`,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// 8. Verify amount
|
|
84
|
+
if (tx.amountMicro < params.challenge.amountMicro) {
|
|
85
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
86
|
+
return {
|
|
87
|
+
valid: false,
|
|
88
|
+
error: `Insufficient transaction amount (expected ${params.challenge.amountMicro} micro-units, received ${tx.amountMicro})`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// 9. Verify timestamp bounds (not in future beyond clock skew, and not too old)
|
|
92
|
+
if (tx.blockTimestampSec > now + 300) {
|
|
93
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
94
|
+
return {
|
|
95
|
+
valid: false,
|
|
96
|
+
error: `Transaction block timestamp is in the future (${tx.blockTimestampSec} > ${now})`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
if (now - tx.blockTimestampSec > this.maxBlockAgeSec) {
|
|
100
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
101
|
+
return {
|
|
102
|
+
valid: false,
|
|
103
|
+
error: `Transaction is too old (${now - tx.blockTimestampSec}s > ${this.maxBlockAgeSec}s allowed)`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
return { valid: true, txData: tx };
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
this.claimedTxHashes.delete(txHashNormalized);
|
|
110
|
+
return { valid: false, error: `RPC provider error during verification: ${err.message}` };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=onchain-verifier.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onchain-verifier.js","sourceRoot":"","sources":["../../src/settlement/onchain-verifier.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolPay Payment Orchestrator
|
|
3
|
+
* Coordinates challenges, multi-scheme verification, replay defense, and financial ledger settlement.
|
|
4
|
+
*/
|
|
5
|
+
import { NonceStore } from './nonce-store.js';
|
|
6
|
+
import { PermitVerifier } from './permit-verifier.js';
|
|
7
|
+
import { OnChainVerifier } from './onchain-verifier.js';
|
|
8
|
+
import { BaseContractClient } from './base-contract-client.js';
|
|
9
|
+
import { SessionWalletManager } from './session-wallet.js';
|
|
10
|
+
import { FeeCalculator } from '../fee/fee-calculator.js';
|
|
11
|
+
import { FeeLedger } from '../fee/fee-ledger.js';
|
|
12
|
+
import { X402ChallengeData, X402ChallengeHeaders, X402ErrorResponse, PaymentProof } from '../types/x402.js';
|
|
13
|
+
import { ServerPricingConfig, ToolPricingRule } from '../types/pricing.js';
|
|
14
|
+
import { TransactionRecord } from '../types/ledger.js';
|
|
15
|
+
export declare class PaymentOrchestrator {
|
|
16
|
+
nonceStore: NonceStore;
|
|
17
|
+
permitVerifier: PermitVerifier;
|
|
18
|
+
onChainVerifier: OnChainVerifier;
|
|
19
|
+
baseContractClient?: BaseContractClient;
|
|
20
|
+
routerContractAddress?: string;
|
|
21
|
+
sessionWalletManager: SessionWalletManager;
|
|
22
|
+
feeCalculator: FeeCalculator;
|
|
23
|
+
feeLedger: FeeLedger;
|
|
24
|
+
constructor(params: {
|
|
25
|
+
nonceStore: NonceStore;
|
|
26
|
+
permitVerifier: PermitVerifier;
|
|
27
|
+
onChainVerifier: OnChainVerifier;
|
|
28
|
+
baseContractClient?: BaseContractClient;
|
|
29
|
+
routerContractAddress?: string;
|
|
30
|
+
sessionWalletManager: SessionWalletManager;
|
|
31
|
+
feeCalculator: FeeCalculator;
|
|
32
|
+
feeLedger: FeeLedger;
|
|
33
|
+
});
|
|
34
|
+
getLedger(): FeeLedger;
|
|
35
|
+
/**
|
|
36
|
+
* Generates a payment challenge for a paid MCP tool call.
|
|
37
|
+
* Calculates gross price, platform cut, developer payout, and creates a unique nonce.
|
|
38
|
+
*/
|
|
39
|
+
generateChallenge(serverConfig: ServerPricingConfig, toolRule: ToolPricingRule, resolvedGrossMicro: bigint): {
|
|
40
|
+
challengeData: X402ChallengeData;
|
|
41
|
+
headers: X402ChallengeHeaders;
|
|
42
|
+
errorResponse: X402ErrorResponse;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* Verifies an incoming payment proof and commits the settlement to the ledger.
|
|
46
|
+
* Validates toolName, serverId, and gross price bindings before settlement.
|
|
47
|
+
* Uses verify-first, consume-last ordering so transient errors do not burn nonces.
|
|
48
|
+
*/
|
|
49
|
+
verifyAndSettle(proof: PaymentProof, nowSec?: number, expected?: {
|
|
50
|
+
serverId?: string;
|
|
51
|
+
toolName?: string;
|
|
52
|
+
minAmountMicro?: bigint;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
success: boolean;
|
|
55
|
+
record?: TransactionRecord;
|
|
56
|
+
error?: string;
|
|
57
|
+
}>;
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=payment-orchestrator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"payment-orchestrator.d.ts","sourceRoot":"","sources":["../../src/settlement/payment-orchestrator.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,iBAAiB,EACjB,YAAY,EACb,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAIvD,qBAAa,mBAAmB;IACvB,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,cAAc,CAAC;IAC/B,eAAe,EAAE,eAAe,CAAC;IACjC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oBAAoB,EAAE,oBAAoB,CAAC;IAC3C,aAAa,EAAE,aAAa,CAAC;IAC7B,SAAS,EAAE,SAAS,CAAC;gBAEhB,MAAM,EAAE;QAClB,UAAU,EAAE,UAAU,CAAC;QACvB,cAAc,EAAE,cAAc,CAAC;QAC/B,eAAe,EAAE,eAAe,CAAC;QACjC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;QACxC,qBAAqB,CAAC,EAAE,MAAM,CAAC;QAC/B,oBAAoB,EAAE,oBAAoB,CAAC;QAC3C,aAAa,EAAE,aAAa,CAAC;QAC7B,SAAS,EAAE,SAAS,CAAC;KACtB;IAWM,SAAS,IAAI,SAAS;IAI7B;;;OAGG;IACI,iBAAiB,CACtB,YAAY,EAAE,mBAAmB,EACjC,QAAQ,EAAE,eAAe,EACzB,kBAAkB,EAAE,MAAM,GACzB;QAAE,aAAa,EAAE,iBAAiB,CAAC;QAAC,OAAO,EAAE,oBAAoB,CAAC;QAAC,aAAa,EAAE,iBAAiB,CAAA;KAAE;IAyHxG;;;;OAIG;IACU,eAAe,CAC1B,KAAK,EAAE,YAAY,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE;QACT,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,cAAc,CAAC,EAAE,MAAM,CAAC;KACzB,GACA,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,iBAAiB,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CA8I7E"}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ToolPay Payment Orchestrator
|
|
3
|
+
* Coordinates challenges, multi-scheme verification, replay defense, and financial ledger settlement.
|
|
4
|
+
*/
|
|
5
|
+
import { getChainIdForNetwork } from '../utils/units.js';
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
export class PaymentOrchestrator {
|
|
8
|
+
nonceStore;
|
|
9
|
+
permitVerifier;
|
|
10
|
+
onChainVerifier;
|
|
11
|
+
baseContractClient;
|
|
12
|
+
routerContractAddress;
|
|
13
|
+
sessionWalletManager;
|
|
14
|
+
feeCalculator;
|
|
15
|
+
feeLedger;
|
|
16
|
+
constructor(params) {
|
|
17
|
+
this.nonceStore = params.nonceStore;
|
|
18
|
+
this.permitVerifier = params.permitVerifier;
|
|
19
|
+
this.onChainVerifier = params.onChainVerifier;
|
|
20
|
+
this.baseContractClient = params.baseContractClient;
|
|
21
|
+
this.routerContractAddress = params.routerContractAddress;
|
|
22
|
+
this.sessionWalletManager = params.sessionWalletManager;
|
|
23
|
+
this.feeCalculator = params.feeCalculator;
|
|
24
|
+
this.feeLedger = params.feeLedger;
|
|
25
|
+
}
|
|
26
|
+
getLedger() {
|
|
27
|
+
return this.feeLedger;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generates a payment challenge for a paid MCP tool call.
|
|
31
|
+
* Calculates gross price, platform cut, developer payout, and creates a unique nonce.
|
|
32
|
+
*/
|
|
33
|
+
generateChallenge(serverConfig, toolRule, resolvedGrossMicro) {
|
|
34
|
+
const feeConfig = serverConfig.platformFee;
|
|
35
|
+
const split = this.feeCalculator.calculateSplit(resolvedGrossMicro, serverConfig.payoutWallet, feeConfig);
|
|
36
|
+
const chainId = getChainIdForNetwork(serverConfig.network);
|
|
37
|
+
// If on Base and router address is configured, route payments through the ToolPayRouter contract
|
|
38
|
+
const recipientWallet = (serverConfig.network === 'base' || serverConfig.network === 'base-sepolia') && this.routerContractAddress
|
|
39
|
+
? this.routerContractAddress
|
|
40
|
+
: serverConfig.payoutWallet;
|
|
41
|
+
const challengeData = this.nonceStore.createChallenge({
|
|
42
|
+
serverId: serverConfig.serverId,
|
|
43
|
+
toolName: toolRule.toolName,
|
|
44
|
+
priceUsd: split.grossUsd,
|
|
45
|
+
amountMicro: split.grossMicro,
|
|
46
|
+
currency: serverConfig.currency,
|
|
47
|
+
recipientWallet,
|
|
48
|
+
verifyingContract: (serverConfig.network === 'base' || serverConfig.network === 'base-sepolia') && this.routerContractAddress
|
|
49
|
+
? this.routerContractAddress
|
|
50
|
+
: undefined,
|
|
51
|
+
developerWallet: serverConfig.payoutWallet,
|
|
52
|
+
platformTreasuryWallet: split.platformTreasuryWallet,
|
|
53
|
+
platformFeeBasisPoints: split.platformFeeBasisPoints,
|
|
54
|
+
platformCutMicro: split.platformFeeMicro,
|
|
55
|
+
developerCutMicro: split.developerPayoutMicro,
|
|
56
|
+
network: serverConfig.network,
|
|
57
|
+
chainId,
|
|
58
|
+
tokenAddress: (serverConfig.currency === 'USDC' && chainId === 8453)
|
|
59
|
+
? '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
|
|
60
|
+
: undefined,
|
|
61
|
+
});
|
|
62
|
+
const headers = {
|
|
63
|
+
'X-Payment-Required': 'true',
|
|
64
|
+
'X-Payment-Amount': challengeData.priceUsd,
|
|
65
|
+
'X-Payment-Currency': challengeData.currency,
|
|
66
|
+
'X-Payment-Recipient': challengeData.recipientWallet,
|
|
67
|
+
'X-Payment-Network': challengeData.network,
|
|
68
|
+
'X-Payment-Chain-Id': challengeData.chainId.toString(),
|
|
69
|
+
'X-Payment-Nonce': challengeData.challengeNonce,
|
|
70
|
+
'X-Payment-Expires': challengeData.expiresAt.toString(),
|
|
71
|
+
'X-Payment-Platform-Fee-Bps': challengeData.platformFeeBasisPoints.toString(),
|
|
72
|
+
};
|
|
73
|
+
const isTestnet = challengeData.network === 'base-sepolia';
|
|
74
|
+
const fundingHint = isTestnet
|
|
75
|
+
? ' Claim free testnet USDC at https://faucet.circle.com (select Base Sepolia).'
|
|
76
|
+
: ' To fund on Base Mainnet, send native USDC (0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913) via Coinbase or https://bridge.base.org.';
|
|
77
|
+
const errorResponse = {
|
|
78
|
+
status: 402,
|
|
79
|
+
headers: headers,
|
|
80
|
+
body: {
|
|
81
|
+
error: {
|
|
82
|
+
code: 402,
|
|
83
|
+
message: `Payment Required: Tool "${toolRule.toolName}" costs ${challengeData.priceUsd} ${challengeData.currency} on Base (${challengeData.network}). ` +
|
|
84
|
+
`To authorize payments for this agent, set AGENT_PRIVATE_KEY=0x... in your project .env file (or run the MCP bridge with --key <private-key>).` +
|
|
85
|
+
fundingHint +
|
|
86
|
+
` Once configured, your agent will automatically sign zero-friction micro-permits and execute paid tools seamlessly.`,
|
|
87
|
+
data: {
|
|
88
|
+
serverId: challengeData.serverId,
|
|
89
|
+
toolName: challengeData.toolName,
|
|
90
|
+
priceUsd: challengeData.priceUsd,
|
|
91
|
+
amountMicro: challengeData.amountMicro.toString(),
|
|
92
|
+
currency: challengeData.currency,
|
|
93
|
+
recipientWallet: challengeData.recipientWallet,
|
|
94
|
+
developerWallet: challengeData.developerWallet || challengeData.recipientWallet,
|
|
95
|
+
platformTreasuryWallet: challengeData.platformTreasuryWallet,
|
|
96
|
+
platformFeeBasisPoints: challengeData.platformFeeBasisPoints,
|
|
97
|
+
platformCutMicro: challengeData.platformCutMicro.toString(),
|
|
98
|
+
developerCutMicro: challengeData.developerCutMicro.toString(),
|
|
99
|
+
network: challengeData.network,
|
|
100
|
+
chainId: challengeData.chainId,
|
|
101
|
+
tokenAddress: challengeData.tokenAddress,
|
|
102
|
+
challengeNonce: challengeData.challengeNonce,
|
|
103
|
+
expiresAt: challengeData.expiresAt,
|
|
104
|
+
instructions: {
|
|
105
|
+
summary: `Tool "${toolRule.toolName}" is monetized and requires ${challengeData.priceUsd} ${challengeData.currency} on Base (${challengeData.network}).`,
|
|
106
|
+
steps: [
|
|
107
|
+
'Set AGENT_PRIVATE_KEY=0x... in your project .env file (or pass --key <private-key> to the mcpaid bridge).',
|
|
108
|
+
isTestnet
|
|
109
|
+
? 'Fund your agent wallet with free testnet USDC: https://faucet.circle.com (select Base Sepolia).'
|
|
110
|
+
: 'Fund your agent wallet with native USDC on Base (Chain ID 8453): withdraw directly from Coinbase (selecting Base network) or bridge at https://bridge.base.org.',
|
|
111
|
+
'Re-run your query; the agent bridge will automatically sign zero-gas EIP-712 permits to authorize and execute the tool.',
|
|
112
|
+
],
|
|
113
|
+
network: challengeData.network,
|
|
114
|
+
chainId: challengeData.chainId,
|
|
115
|
+
currency: challengeData.currency,
|
|
116
|
+
cost: `${challengeData.priceUsd} ${challengeData.currency}`,
|
|
117
|
+
fundingGuide: {
|
|
118
|
+
network: isTestnet ? 'Base Sepolia Testnet (84532)' : 'Base Mainnet (8453)',
|
|
119
|
+
usdcTokenAddress: isTestnet
|
|
120
|
+
? '0x036CbD53842c5426634e7929541eC2318f3dCF7e'
|
|
121
|
+
: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
|
|
122
|
+
methods: isTestnet
|
|
123
|
+
? [
|
|
124
|
+
'Claim 10 free testnet USDC at https://faucet.circle.com (select Base Sepolia).',
|
|
125
|
+
'Optional testnet gas from Superchain Faucet: https://console.optimism.io/faucet.',
|
|
126
|
+
]
|
|
127
|
+
: [
|
|
128
|
+
'Coinbase Direct: Withdraw USDC directly to your agent wallet using the "Base" network (0 gas fee).',
|
|
129
|
+
'Official Base Bridge: Bridge USDC or ETH from Ethereum Mainnet at https://bridge.base.org.',
|
|
130
|
+
'DEX Swap: Swap ETH/tokens to USDC on Aerodrome or Uniswap on Base.',
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
return { challengeData, headers, errorResponse };
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Verifies an incoming payment proof and commits the settlement to the ledger.
|
|
142
|
+
* Validates toolName, serverId, and gross price bindings before settlement.
|
|
143
|
+
* Uses verify-first, consume-last ordering so transient errors do not burn nonces.
|
|
144
|
+
*/
|
|
145
|
+
async verifyAndSettle(proof, nowSec, expected) {
|
|
146
|
+
let challenge;
|
|
147
|
+
// 1. Peek nonce from store without consuming it yet (anti-replay check)
|
|
148
|
+
try {
|
|
149
|
+
challenge = this.nonceStore.peekChallenge(proof.challengeNonce, nowSec);
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
return { success: false, error: err.message };
|
|
153
|
+
}
|
|
154
|
+
// 1b. Bind payment proof to expected context (Critical 1)
|
|
155
|
+
if (expected?.serverId && challenge.serverId !== expected.serverId) {
|
|
156
|
+
return {
|
|
157
|
+
success: false,
|
|
158
|
+
error: `Server ID mismatch: challenge issued for server "${challenge.serverId}", but request is for "${expected.serverId}"`,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (expected?.toolName && challenge.toolName !== expected.toolName) {
|
|
162
|
+
return {
|
|
163
|
+
success: false,
|
|
164
|
+
error: `Tool name mismatch: challenge issued for tool "${challenge.toolName}", but request is for "${expected.toolName}"`,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
if (expected?.minAmountMicro !== undefined && challenge.amountMicro < expected.minAmountMicro) {
|
|
168
|
+
return {
|
|
169
|
+
success: false,
|
|
170
|
+
error: `Underpayment: challenge amount ${challenge.amountMicro} is less than required tool price ${expected.minAmountMicro}`,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
// 2. Route verification by scheme
|
|
174
|
+
let settlementRef;
|
|
175
|
+
if (proof.scheme === 'eip712-permit') {
|
|
176
|
+
if (!proof.signature || proof.deadline === undefined) {
|
|
177
|
+
return { success: false, error: 'Missing EIP-712 signature or deadline in payment proof' };
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
const verifyRes = await this.permitVerifier.verifyPermit({
|
|
181
|
+
signerWallet: proof.signerWallet,
|
|
182
|
+
signature: proof.signature,
|
|
183
|
+
challenge,
|
|
184
|
+
deadlineSec: proof.deadline,
|
|
185
|
+
amountMicro: proof.maxAmountMicro ?? challenge.amountMicro,
|
|
186
|
+
nowSec,
|
|
187
|
+
});
|
|
188
|
+
if (!verifyRes.valid) {
|
|
189
|
+
return { success: false, error: verifyRes.error };
|
|
190
|
+
}
|
|
191
|
+
settlementRef = proof.signature;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
return { success: false, error: `Permit verification error: ${err.message}` };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else if (proof.scheme === 'onchain-tx') {
|
|
198
|
+
if (!proof.txHash) {
|
|
199
|
+
return { success: false, error: 'Missing txHash in onchain-tx payment proof' };
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
let verifyRes;
|
|
203
|
+
if (this.baseContractClient &&
|
|
204
|
+
this.routerContractAddress &&
|
|
205
|
+
challenge.recipientWallet.toLowerCase() === this.routerContractAddress.toLowerCase()) {
|
|
206
|
+
const contractRes = await this.baseContractClient.verifyContractPayment({
|
|
207
|
+
txHash: proof.txHash,
|
|
208
|
+
challenge,
|
|
209
|
+
signerWallet: proof.signerWallet,
|
|
210
|
+
});
|
|
211
|
+
verifyRes = { valid: contractRes.valid, error: contractRes.error };
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
verifyRes = await this.onChainVerifier.verifyTransaction({
|
|
215
|
+
txHash: proof.txHash,
|
|
216
|
+
signerWallet: proof.signerWallet,
|
|
217
|
+
challenge,
|
|
218
|
+
nowSec,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
if (!verifyRes.valid) {
|
|
222
|
+
return { success: false, error: verifyRes.error };
|
|
223
|
+
}
|
|
224
|
+
settlementRef = proof.txHash;
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
return { success: false, error: `On-chain verification error: ${err.message}` };
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else if (proof.scheme === 'session-wallet') {
|
|
231
|
+
if (!proof.sessionId) {
|
|
232
|
+
return { success: false, error: 'Missing sessionId in session-wallet payment proof' };
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
const deductRes = this.sessionWalletManager.deductPayment({
|
|
236
|
+
sessionId: proof.sessionId,
|
|
237
|
+
agentWallet: proof.signerWallet,
|
|
238
|
+
amountMicro: challenge.amountMicro,
|
|
239
|
+
});
|
|
240
|
+
if (!deductRes.success) {
|
|
241
|
+
return { success: false, error: deductRes.error };
|
|
242
|
+
}
|
|
243
|
+
settlementRef = proof.sessionId;
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
return { success: false, error: `Session deduction error: ${err.message}` };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
return { success: false, error: `Unsupported payment scheme: ${proof.scheme}` };
|
|
251
|
+
}
|
|
252
|
+
// 3. Atomically consume the challenge nonce ONLY after verification succeeds (Critical 5)
|
|
253
|
+
try {
|
|
254
|
+
this.nonceStore.consumeChallenge(proof.challengeNonce);
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
return { success: false, error: err.message };
|
|
258
|
+
}
|
|
259
|
+
// 4. Commit transaction to financial ledger with exact platform cut
|
|
260
|
+
const record = this.feeLedger.recordTransaction({
|
|
261
|
+
transactionId: `tx_${randomUUID()}`,
|
|
262
|
+
serverId: challenge.serverId,
|
|
263
|
+
toolName: challenge.toolName,
|
|
264
|
+
agentWallet: proof.signerWallet,
|
|
265
|
+
developerWallet: challenge.developerWallet || challenge.recipientWallet,
|
|
266
|
+
platformTreasuryWallet: challenge.platformTreasuryWallet,
|
|
267
|
+
scheme: proof.scheme,
|
|
268
|
+
grossAmountMicro: challenge.amountMicro,
|
|
269
|
+
platformFeeMicro: challenge.platformCutMicro,
|
|
270
|
+
developerPayoutMicro: challenge.developerCutMicro,
|
|
271
|
+
platformFeeBasisPoints: challenge.platformFeeBasisPoints,
|
|
272
|
+
settlementRef,
|
|
273
|
+
idempotencyKey: proof.idempotencyKey,
|
|
274
|
+
timestamp: (nowSec ? nowSec * 1000 : undefined),
|
|
275
|
+
});
|
|
276
|
+
return {
|
|
277
|
+
success: true,
|
|
278
|
+
record,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
//# sourceMappingURL=payment-orchestrator.js.map
|