@paynodelabs/sdk-js 1.1.2 → 1.4.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/README.md +11 -3
- package/dist/client.d.ts +3 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +27 -10
- package/dist/constants.d.ts +3 -3
- package/dist/constants.js +4 -4
- package/dist/errors/index.d.ts +15 -14
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +32 -17
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/middleware/x402.d.ts +10 -8
- package/dist/middleware/x402.d.ts.map +1 -1
- package/dist/middleware/x402.js +24 -15
- package/dist/utils/verifier.d.ts +0 -8
- package/dist/utils/verifier.d.ts.map +1 -1
- package/dist/utils/verifier.js +32 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -41,16 +41,24 @@ The SDK includes a full merchant/agent demonstration in the `examples/` director
|
|
|
41
41
|
|
|
42
42
|
```bash
|
|
43
43
|
cp .env.example .env
|
|
44
|
-
# Edit .env with your
|
|
44
|
+
# Edit .env with your PRIVATE_KEY and RPC_URL
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
-
### 2.
|
|
47
|
+
### 2. Get Test Tokens (Required for Base Sepolia)
|
|
48
|
+
|
|
49
|
+
If you're testing on Sepolia, run the helper script to mint 1,000 mock USDC:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx ts-node examples/mint-test-tokens.ts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Run the Merchant Server (Express)
|
|
48
56
|
|
|
49
57
|
```bash
|
|
50
58
|
npx ts-node examples/express-server.ts
|
|
51
59
|
```
|
|
52
60
|
|
|
53
|
-
###
|
|
61
|
+
### 4. Run the Agent Client
|
|
54
62
|
|
|
55
63
|
In another terminal:
|
|
56
64
|
|
package/dist/client.d.ts
CHANGED
|
@@ -7,11 +7,11 @@ export declare class PayNodeAgentClient {
|
|
|
7
7
|
private rpcUrls;
|
|
8
8
|
private ERC20_ABI;
|
|
9
9
|
private ROUTER_ABI;
|
|
10
|
-
constructor(privateKey: string, rpcUrls
|
|
10
|
+
constructor(privateKey: string, rpcUrls?: string | string[]);
|
|
11
11
|
requestGate(url: string, options?: RequestOptions): Promise<Response>;
|
|
12
12
|
private handlePaymentAndRetry;
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
pay(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
|
|
14
|
+
payWithPermit(contractAddr: string, tokenAddr: string, merchantAddr: string, amount: bigint, orderId: string): Promise<string>;
|
|
15
15
|
signPermit(tokenAddr: string, spenderAddr: string, amount: bigint, deadlineSeconds?: number): Promise<{
|
|
16
16
|
deadline: number;
|
|
17
17
|
v: 27 | 28;
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,cAAe,SAAQ,WAAW;IACjD,IAAI,CAAC,EAAE,GAAG,CAAC;CACZ;AAED,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,QAAQ,CAA0B;IAC1C,OAAO,CAAC,OAAO,CAAW;IAE1B,OAAO,CAAC,SAAS,CAMf;IAEF,OAAO,CAAC,UAAU,CAGhB;gBAEU,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,MAAM,GAAG,MAAM,EAAkB;IAcpE,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAAC,QAAQ,CAAC;YA0BjE,qBAAqB;IA0E7B,GAAG,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAepH,aAAa,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwB9H,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,eAAe,GAAE,MAAa;;;;;;CAwCxG"}
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PayNodeAgentClient = void 0;
|
|
4
4
|
const ethers_1 = require("ethers");
|
|
5
5
|
const errors_1 = require("./errors");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
6
7
|
class PayNodeAgentClient {
|
|
7
8
|
wallet;
|
|
8
9
|
provider;
|
|
@@ -18,7 +19,7 @@ class PayNodeAgentClient {
|
|
|
18
19
|
"function pay(address token, address merchant, uint256 amount, bytes32 orderId) public",
|
|
19
20
|
"function payWithPermit(address payer, address token, address merchant, uint256 amount, bytes32 orderId, uint256 deadline, uint8 v, bytes32 r, bytes32 s) public"
|
|
20
21
|
];
|
|
21
|
-
constructor(privateKey, rpcUrls) {
|
|
22
|
+
constructor(privateKey, rpcUrls = constants_1.BASE_RPC_URLS) {
|
|
22
23
|
this.rpcUrls = Array.isArray(rpcUrls) ? rpcUrls : [rpcUrls];
|
|
23
24
|
const configs = this.rpcUrls.map((url, index) => ({
|
|
24
25
|
provider: new ethers_1.ethers.JsonRpcProvider(url),
|
|
@@ -49,7 +50,7 @@ class PayNodeAgentClient {
|
|
|
49
50
|
catch (error) {
|
|
50
51
|
if (error instanceof errors_1.PayNodeException)
|
|
51
52
|
throw error;
|
|
52
|
-
throw new errors_1.PayNodeException(
|
|
53
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.RpcError, undefined, error);
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
56
|
async handlePaymentAndRetry(url, options, headers) {
|
|
@@ -58,13 +59,29 @@ class PayNodeAgentClient {
|
|
|
58
59
|
const amountStr = headers.get('x-paynode-amount');
|
|
59
60
|
const tokenAddr = headers.get('x-paynode-token-address');
|
|
60
61
|
const orderIdStr = headers.get('x-paynode-order-id');
|
|
62
|
+
const chainIdStr = headers.get('x-paynode-chain-id');
|
|
63
|
+
const currency = headers.get('x-paynode-currency') || 'USDC';
|
|
61
64
|
if (!contractAddr || !merchantAddr || !amountStr || !tokenAddr || !orderIdStr) {
|
|
62
|
-
throw new errors_1.PayNodeException("Malformed 402 headers: missing metadata"
|
|
65
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, "Malformed 402 headers: missing metadata");
|
|
63
66
|
}
|
|
67
|
+
// Network safety check (v1.4)
|
|
68
|
+
if (chainIdStr) {
|
|
69
|
+
const network = await this.provider.getNetwork();
|
|
70
|
+
if (BigInt(chainIdStr) !== network.chainId) {
|
|
71
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, `Network mismatch: Current ${network.chainId}, Request ${chainIdStr}.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log(`💡 [PayNode-JS] Payment request: ${amountStr} ${currency} to ${merchantAddr}`);
|
|
64
75
|
const amount = BigInt(amountStr);
|
|
65
76
|
// v1.3 Constraint: Min payment protection
|
|
66
77
|
if (amount < 1000n) {
|
|
67
|
-
throw new errors_1.PayNodeException(
|
|
78
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow);
|
|
79
|
+
}
|
|
80
|
+
// v1.4 Constraint: Token whitelist pre-flight (Anti-FakeToken)
|
|
81
|
+
const resolvedChainId = chainIdStr ? Number(chainIdStr) : 8453;
|
|
82
|
+
const whitelist = constants_1.ACCEPTED_TOKENS[resolvedChainId];
|
|
83
|
+
if (whitelist && whitelist.length > 0 && !whitelist.some(t => t.toLowerCase() === tokenAddr.toLowerCase())) {
|
|
84
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted);
|
|
68
85
|
}
|
|
69
86
|
let txHash;
|
|
70
87
|
try {
|
|
@@ -74,21 +91,21 @@ class PayNodeAgentClient {
|
|
|
74
91
|
tokenContract.allowance(this.wallet.address, contractAddr)
|
|
75
92
|
]);
|
|
76
93
|
if (balance < amount) {
|
|
77
|
-
throw new errors_1.PayNodeException(
|
|
94
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.InsufficientFunds);
|
|
78
95
|
}
|
|
79
96
|
// Protocol v1.3: Permit-First Execution
|
|
80
97
|
if (allowance >= amount) {
|
|
81
|
-
txHash = await this.
|
|
98
|
+
txHash = await this.pay(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
|
|
82
99
|
}
|
|
83
100
|
else {
|
|
84
101
|
console.log(`⚡ [PayNode-JS] Insufficient allowance. Attempting Permit-First payment...`);
|
|
85
|
-
txHash = await this.
|
|
102
|
+
txHash = await this.payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderIdStr);
|
|
86
103
|
}
|
|
87
104
|
}
|
|
88
105
|
catch (error) {
|
|
89
106
|
if (error instanceof errors_1.PayNodeException)
|
|
90
107
|
throw error;
|
|
91
|
-
throw new errors_1.PayNodeException(
|
|
108
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed, undefined, error);
|
|
92
109
|
}
|
|
93
110
|
console.log(`✅ [PayNode-JS] Payment confirmed on-chain: ${txHash}`);
|
|
94
111
|
const retryOptions = {
|
|
@@ -101,7 +118,7 @@ class PayNodeAgentClient {
|
|
|
101
118
|
};
|
|
102
119
|
return await fetch(url, retryOptions);
|
|
103
120
|
}
|
|
104
|
-
async
|
|
121
|
+
async pay(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
|
|
105
122
|
const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
|
|
106
123
|
const orderIdBytes = ethers_1.ethers.id(orderId);
|
|
107
124
|
const feeData = await this.provider.getFeeData();
|
|
@@ -113,7 +130,7 @@ class PayNodeAgentClient {
|
|
|
113
130
|
const receipt = await tx.wait();
|
|
114
131
|
return receipt.hash;
|
|
115
132
|
}
|
|
116
|
-
async
|
|
133
|
+
async payWithPermit(contractAddr, tokenAddr, merchantAddr, amount, orderId) {
|
|
117
134
|
const sig = await this.signPermit(tokenAddr, contractAddr, amount);
|
|
118
135
|
const router = new ethers_1.ethers.Contract(contractAddr, this.ROUTER_ABI, this.wallet);
|
|
119
136
|
const orderIdBytes = ethers_1.ethers.id(orderId);
|
package/dist/constants.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** Generated by scripts/sync-config.py */
|
|
2
|
-
export declare const PAYNODE_ROUTER_ADDRESS = "
|
|
3
|
-
export declare const PAYNODE_ROUTER_ADDRESS_SANDBOX = "
|
|
2
|
+
export declare const PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
3
|
+
export declare const PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
4
4
|
export declare const BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
5
|
-
export declare const BASE_USDC_ADDRESS_SANDBOX = "
|
|
5
|
+
export declare const BASE_USDC_ADDRESS_SANDBOX = "0x109AEddD656Ed2761d1e210E179329105039c784";
|
|
6
6
|
export declare const PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
7
7
|
export declare const PROTOCOL_FEE_BPS = 100;
|
|
8
8
|
export declare const MIN_PAYMENT_AMOUNT: bigint;
|
package/dist/constants.js
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ACCEPTED_TOKENS = exports.BASE_RPC_URLS_SANDBOX = exports.BASE_RPC_URLS = exports.MIN_PAYMENT_AMOUNT = exports.PROTOCOL_FEE_BPS = exports.PROTOCOL_TREASURY = exports.BASE_USDC_ADDRESS_SANDBOX = exports.BASE_USDC_ADDRESS = exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = exports.PAYNODE_ROUTER_ADDRESS = void 0;
|
|
4
4
|
/** Generated by scripts/sync-config.py */
|
|
5
|
-
exports.PAYNODE_ROUTER_ADDRESS = "
|
|
6
|
-
exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "
|
|
5
|
+
exports.PAYNODE_ROUTER_ADDRESS = "0x4A73696ccF76E7381b044cB95127B3784369Ed63";
|
|
6
|
+
exports.PAYNODE_ROUTER_ADDRESS_SANDBOX = "0x24cD8b68aaC209217ff5a6ef1Bf55a59f2c8Ca6F";
|
|
7
7
|
exports.BASE_USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
|
|
8
|
-
exports.BASE_USDC_ADDRESS_SANDBOX = "
|
|
8
|
+
exports.BASE_USDC_ADDRESS_SANDBOX = "0x109AEddD656Ed2761d1e210E179329105039c784";
|
|
9
9
|
exports.PROTOCOL_TREASURY = "0x598bF63F5449876efafa7b36b77Deb2070621C0E";
|
|
10
10
|
exports.PROTOCOL_FEE_BPS = 100;
|
|
11
11
|
exports.MIN_PAYMENT_AMOUNT = BigInt(1000);
|
|
@@ -13,5 +13,5 @@ exports.BASE_RPC_URLS = ["https://mainnet.base.org", "https://base.meowrpc.com",
|
|
|
13
13
|
exports.BASE_RPC_URLS_SANDBOX = ["https://sepolia.base.org", "https://base-sepolia-rpc.publicnode.com"];
|
|
14
14
|
exports.ACCEPTED_TOKENS = {
|
|
15
15
|
8453: ["0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"],
|
|
16
|
-
84532: ["
|
|
16
|
+
84532: ["0x109AEddD656Ed2761d1e210E179329105039c784"]
|
|
17
17
|
};
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
|
+
/** Generated by scripts/sync-config.py */
|
|
1
2
|
export declare enum ErrorCode {
|
|
2
|
-
RpcError = "
|
|
3
|
-
InsufficientFunds = "
|
|
4
|
-
AmountTooLow = "
|
|
5
|
-
TokenNotAccepted = "
|
|
6
|
-
TransactionFailed = "
|
|
7
|
-
DuplicateTransaction = "
|
|
8
|
-
InvalidReceipt = "
|
|
9
|
-
InternalError = "
|
|
10
|
-
TransactionNotFound = "
|
|
11
|
-
WrongContract = "
|
|
12
|
-
OrderMismatch = "
|
|
13
|
-
MissingReceipt = "
|
|
3
|
+
RpcError = "rpc_error",
|
|
4
|
+
InsufficientFunds = "insufficient_funds",
|
|
5
|
+
AmountTooLow = "amount_too_low",
|
|
6
|
+
TokenNotAccepted = "token_not_accepted",
|
|
7
|
+
TransactionFailed = "transaction_failed",
|
|
8
|
+
DuplicateTransaction = "duplicate_transaction",
|
|
9
|
+
InvalidReceipt = "invalid_receipt",
|
|
10
|
+
InternalError = "internal_error",
|
|
11
|
+
TransactionNotFound = "transaction_not_found",
|
|
12
|
+
WrongContract = "wrong_contract",
|
|
13
|
+
OrderMismatch = "order_mismatch",
|
|
14
|
+
MissingReceipt = "missing_receipt"
|
|
14
15
|
}
|
|
16
|
+
export declare const ERROR_MESSAGES: Record<string, string>;
|
|
15
17
|
export declare class PayNodeException extends Error {
|
|
16
|
-
message: string;
|
|
17
18
|
code: ErrorCode;
|
|
18
19
|
details?: any | undefined;
|
|
19
|
-
constructor(
|
|
20
|
+
constructor(code: ErrorCode, message?: string, details?: any | undefined);
|
|
20
21
|
}
|
|
21
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,oBAAY,SAAS;IACnB,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/errors/index.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAC1C,oBAAY,SAAS;IACnB,QAAQ,cAAc;IACtB,iBAAiB,uBAAuB;IACxC,YAAY,mBAAmB;IAC/B,gBAAgB,uBAAuB;IACvC,iBAAiB,uBAAuB;IACxC,oBAAoB,0BAA0B;IAC9C,cAAc,oBAAoB;IAClC,aAAa,mBAAmB;IAChC,mBAAmB,0BAA0B;IAC7C,aAAa,mBAAmB;IAChC,aAAa,mBAAmB;IAChC,cAAc,oBAAoB;CACnC;AAED,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAajD,CAAC;AAEF,qBAAa,gBAAiB,SAAQ,KAAK;IACtB,IAAI,EAAE,SAAS;IAA2B,OAAO,CAAC,EAAE,GAAG;gBAAvD,IAAI,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,MAAM,EAAS,OAAO,CAAC,EAAE,GAAG,YAAA;CAM3E"}
|
package/dist/errors/index.js
CHANGED
|
@@ -1,31 +1,46 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PayNodeException = exports.ErrorCode = void 0;
|
|
3
|
+
exports.PayNodeException = exports.ERROR_MESSAGES = exports.ErrorCode = void 0;
|
|
4
|
+
/** Generated by scripts/sync-config.py */
|
|
4
5
|
var ErrorCode;
|
|
5
6
|
(function (ErrorCode) {
|
|
6
|
-
ErrorCode["RpcError"] = "
|
|
7
|
-
ErrorCode["InsufficientFunds"] = "
|
|
8
|
-
ErrorCode["AmountTooLow"] = "
|
|
9
|
-
ErrorCode["TokenNotAccepted"] = "
|
|
10
|
-
ErrorCode["TransactionFailed"] = "
|
|
11
|
-
ErrorCode["DuplicateTransaction"] = "
|
|
12
|
-
ErrorCode["InvalidReceipt"] = "
|
|
13
|
-
ErrorCode["InternalError"] = "
|
|
14
|
-
ErrorCode["TransactionNotFound"] = "
|
|
15
|
-
ErrorCode["WrongContract"] = "
|
|
16
|
-
ErrorCode["OrderMismatch"] = "
|
|
17
|
-
ErrorCode["MissingReceipt"] = "
|
|
7
|
+
ErrorCode["RpcError"] = "rpc_error";
|
|
8
|
+
ErrorCode["InsufficientFunds"] = "insufficient_funds";
|
|
9
|
+
ErrorCode["AmountTooLow"] = "amount_too_low";
|
|
10
|
+
ErrorCode["TokenNotAccepted"] = "token_not_accepted";
|
|
11
|
+
ErrorCode["TransactionFailed"] = "transaction_failed";
|
|
12
|
+
ErrorCode["DuplicateTransaction"] = "duplicate_transaction";
|
|
13
|
+
ErrorCode["InvalidReceipt"] = "invalid_receipt";
|
|
14
|
+
ErrorCode["InternalError"] = "internal_error";
|
|
15
|
+
ErrorCode["TransactionNotFound"] = "transaction_not_found";
|
|
16
|
+
ErrorCode["WrongContract"] = "wrong_contract";
|
|
17
|
+
ErrorCode["OrderMismatch"] = "order_mismatch";
|
|
18
|
+
ErrorCode["MissingReceipt"] = "missing_receipt";
|
|
18
19
|
})(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
|
|
20
|
+
exports.ERROR_MESSAGES = {
|
|
21
|
+
"rpc_error": "Failed to connect to any provided RPC nodes.",
|
|
22
|
+
"insufficient_funds": "Wallet lacks USDC or ETH for gas.",
|
|
23
|
+
"amount_too_low": "Payment amount is below the protocol minimum (1000).",
|
|
24
|
+
"token_not_accepted": "The provided token address is not in the whitelist.",
|
|
25
|
+
"transaction_failed": "On-chain transaction reverted or failed.",
|
|
26
|
+
"duplicate_transaction": "This transaction hash has already been consumed.",
|
|
27
|
+
"invalid_receipt": "The provided receipt (TxHash) is malformed or invalid.",
|
|
28
|
+
"internal_error": "An unexpected error occurred.",
|
|
29
|
+
"transaction_not_found": "Transaction not found on-chain.",
|
|
30
|
+
"wrong_contract": "Payment event was not emitted by the official PayNode contract.",
|
|
31
|
+
"order_mismatch": "OrderId in receipt does not match requested ID.",
|
|
32
|
+
"missing_receipt": "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
|
|
33
|
+
};
|
|
19
34
|
class PayNodeException extends Error {
|
|
20
|
-
message;
|
|
21
35
|
code;
|
|
22
36
|
details;
|
|
23
|
-
constructor(
|
|
24
|
-
|
|
25
|
-
|
|
37
|
+
constructor(code, message, details) {
|
|
38
|
+
const finalMessage = message || exports.ERROR_MESSAGES[code] || "An unexpected error occurred.";
|
|
39
|
+
super(finalMessage);
|
|
26
40
|
this.code = code;
|
|
27
41
|
this.details = details;
|
|
28
42
|
this.name = "PayNodeException";
|
|
43
|
+
this.message = finalMessage;
|
|
29
44
|
}
|
|
30
45
|
}
|
|
31
46
|
exports.PayNodeException = PayNodeException;
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,UAAU,CAAC;AACzB,cAAc,UAAU,CAAC;AACzB,cAAc,aAAa,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { Request, NextFunction } from 'express';
|
|
1
|
+
import { Request, Response, NextFunction } from 'express';
|
|
2
2
|
import { IdempotencyStore } from '../utils/idempotency';
|
|
3
3
|
export interface PayNodeMiddlewareOptions {
|
|
4
|
-
rpcUrls: string | string[];
|
|
5
|
-
chainId: number;
|
|
6
|
-
contractAddress: string;
|
|
7
4
|
merchantAddress: string;
|
|
8
|
-
tokenAddress: string;
|
|
9
|
-
currency: string;
|
|
10
5
|
price: string;
|
|
11
|
-
|
|
6
|
+
rpcUrls?: string | string[];
|
|
7
|
+
chainId?: number;
|
|
8
|
+
contractAddress?: string;
|
|
9
|
+
tokenAddress?: string;
|
|
10
|
+
currency?: string;
|
|
11
|
+
decimals?: number;
|
|
12
12
|
store?: IdempotencyStore;
|
|
13
13
|
generateOrderId?: (req: Request | any) => string;
|
|
14
14
|
}
|
|
15
|
-
export declare const
|
|
15
|
+
export declare const x402Gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
16
|
+
/** @deprecated Use x402Gate instead. */
|
|
17
|
+
export declare const x402_gate: (options: PayNodeMiddlewareOptions) => (req: Request | any, res: Response | any, next: NextFunction) => Promise<any>;
|
|
16
18
|
//# sourceMappingURL=x402.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"x402.d.ts","sourceRoot":"","sources":["../../src/middleware/x402.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AAQxD,MAAM,WAAW,wBAAwB;IACvC,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC5B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,KAAK,MAAM,CAAC;CAClD;AAED,eAAO,MAAM,QAAQ,GAAI,SAAS,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBAwD1E,CAAC;AAEF,wCAAwC;AACxC,eAAO,MAAM,SAAS,YAnFY,wBAAwB,MAwB1C,KAAK,OAAO,GAAG,GAAG,EAAE,KAAK,QAAQ,GAAG,GAAG,EAAE,MAAM,YAAY,iBA2D1C,CAAC"}
|
package/dist/middleware/x402.js
CHANGED
|
@@ -1,26 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.x402_gate = void 0;
|
|
3
|
+
exports.x402_gate = exports.x402Gate = void 0;
|
|
4
4
|
const errors_1 = require("../errors");
|
|
5
5
|
const verifier_1 = require("../utils/verifier");
|
|
6
6
|
const ethers_1 = require("ethers");
|
|
7
|
-
const
|
|
7
|
+
const constants_1 = require("../constants");
|
|
8
|
+
const x402Gate = (options) => {
|
|
9
|
+
const rpcUrls = options.rpcUrls || constants_1.BASE_RPC_URLS;
|
|
10
|
+
const chainId = options.chainId || 8453;
|
|
11
|
+
const contractAddress = options.contractAddress || constants_1.PAYNODE_ROUTER_ADDRESS;
|
|
12
|
+
const tokenAddress = options.tokenAddress || constants_1.BASE_USDC_ADDRESS;
|
|
13
|
+
const currency = options.currency || 'USDC';
|
|
14
|
+
const decimals = options.decimals !== undefined ? options.decimals : 6;
|
|
8
15
|
const verifier = new verifier_1.PayNodeVerifier({
|
|
9
|
-
rpcUrls
|
|
10
|
-
chainId
|
|
11
|
-
contractAddress
|
|
16
|
+
rpcUrls,
|
|
17
|
+
chainId,
|
|
18
|
+
contractAddress,
|
|
12
19
|
store: options.store
|
|
13
20
|
});
|
|
14
21
|
let rawAmount;
|
|
15
22
|
try {
|
|
16
|
-
rawAmount = (0, ethers_1.parseUnits)(options.price,
|
|
23
|
+
rawAmount = (0, ethers_1.parseUnits)(options.price, decimals);
|
|
17
24
|
}
|
|
18
25
|
catch (e) {
|
|
19
|
-
rawAmount = BigInt(Math.floor(parseFloat(options.price) * (10 **
|
|
26
|
+
rawAmount = BigInt(Math.floor(parseFloat(options.price) * (10 ** decimals)));
|
|
20
27
|
}
|
|
21
28
|
const defaultOrderIdGen = (req) => `agent_js_${Date.now()}`;
|
|
22
29
|
return async (req, res, next) => {
|
|
23
|
-
//
|
|
30
|
+
// ... rest of the logic
|
|
24
31
|
const getHeader = (name) => {
|
|
25
32
|
if (req.header && typeof req.header === 'function')
|
|
26
33
|
return req.header(name);
|
|
@@ -36,12 +43,12 @@ const x402_gate = (options) => {
|
|
|
36
43
|
if (!receiptHash) {
|
|
37
44
|
if (res.set) {
|
|
38
45
|
res.set({
|
|
39
|
-
'x-paynode-contract':
|
|
46
|
+
'x-paynode-contract': contractAddress,
|
|
40
47
|
'x-paynode-merchant': options.merchantAddress,
|
|
41
48
|
'x-paynode-amount': rawAmount.toString(),
|
|
42
|
-
'x-paynode-currency':
|
|
43
|
-
'x-paynode-token-address':
|
|
44
|
-
'x-paynode-chain-id':
|
|
49
|
+
'x-paynode-currency': currency,
|
|
50
|
+
'x-paynode-token-address': tokenAddress,
|
|
51
|
+
'x-paynode-chain-id': chainId.toString(),
|
|
45
52
|
'x-paynode-order-id': orderId
|
|
46
53
|
});
|
|
47
54
|
}
|
|
@@ -50,13 +57,13 @@ const x402_gate = (options) => {
|
|
|
50
57
|
code: errors_1.ErrorCode.MissingReceipt,
|
|
51
58
|
message: "Please pay to PayNode contract and provide 'x-paynode-receipt' header.",
|
|
52
59
|
amount: options.price,
|
|
53
|
-
currency:
|
|
60
|
+
currency: currency
|
|
54
61
|
});
|
|
55
62
|
}
|
|
56
63
|
// Phase 2: On-chain Verification
|
|
57
64
|
const result = await verifier.verifyPayment(receiptHash, {
|
|
58
65
|
merchantAddress: options.merchantAddress,
|
|
59
|
-
tokenAddress:
|
|
66
|
+
tokenAddress: tokenAddress,
|
|
60
67
|
amount: rawAmount,
|
|
61
68
|
orderId: orderId
|
|
62
69
|
});
|
|
@@ -74,4 +81,6 @@ const x402_gate = (options) => {
|
|
|
74
81
|
}
|
|
75
82
|
};
|
|
76
83
|
};
|
|
77
|
-
exports.
|
|
84
|
+
exports.x402Gate = x402Gate;
|
|
85
|
+
/** @deprecated Use x402Gate instead. */
|
|
86
|
+
exports.x402_gate = exports.x402Gate;
|
package/dist/utils/verifier.d.ts
CHANGED
|
@@ -1,13 +1,5 @@
|
|
|
1
1
|
import { PayNodeException } from '../errors';
|
|
2
2
|
import { IdempotencyStore } from './idempotency';
|
|
3
|
-
/**
|
|
4
|
-
* Default accepted token addresses across supported chains.
|
|
5
|
-
* SDK will reject any payment involving a token NOT in this whitelist,
|
|
6
|
-
* preventing fake-token attacks at the verification layer.
|
|
7
|
-
*/
|
|
8
|
-
export declare const ACCEPTED_TOKENS: Record<string, string[]>;
|
|
9
|
-
/** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
|
|
10
|
-
export declare const MIN_PAYMENT_AMOUNT = 1000n;
|
|
11
3
|
export interface PayNodeVerifierConfig {
|
|
12
4
|
rpcUrls: string | string[];
|
|
13
5
|
contractAddress: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"verifier.d.ts","sourceRoot":"","sources":["../../src/utils/verifier.ts"],"names":[],"mappings":"AACA,OAAO,EAAa,gBAAgB,EAAE,MAAM,WAAW,CAAC;AACxD,OAAO,EAAE,gBAAgB,EAA0B,MAAM,eAAe,CAAC;AAGzE,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,eAAe,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,gBAAgB,CAAC;IACzB,oGAAoG;IACpG,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAQD,qBAAa,eAAe;IAC1B,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,OAAO,CAAC,CAAS;IACzB,OAAO,CAAC,KAAK,CAAC,CAAmB;IACjC,OAAO,CAAC,cAAc,CAAC,CAAc;gBAEzB,MAAM,EAAE,qBAAqB;IAmCnC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,gBAAgB,CAAA;KAAE,CAAC;CA2FxH"}
|
package/dist/utils/verifier.js
CHANGED
|
@@ -1,25 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PayNodeVerifier =
|
|
4
|
-
const errors_1 = require("../errors");
|
|
3
|
+
exports.PayNodeVerifier = void 0;
|
|
5
4
|
const ethers_1 = require("ethers");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* preventing fake-token attacks at the verification layer.
|
|
10
|
-
*/
|
|
11
|
-
exports.ACCEPTED_TOKENS = {
|
|
12
|
-
// Base Mainnet (chainId: 8453)
|
|
13
|
-
'8453': [
|
|
14
|
-
'0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // USDC
|
|
15
|
-
],
|
|
16
|
-
// Base Sepolia (chainId: 84532)
|
|
17
|
-
'84532': [
|
|
18
|
-
'0xeAC1f2C7099CdaFfB91Aa3b8Ffd653Ef16935798', // USDC (Sandbox)
|
|
19
|
-
],
|
|
20
|
-
};
|
|
21
|
-
/** Minimum allowed payment amount to prevent dust exploits (1000 = 0.001 USDC) */
|
|
22
|
-
exports.MIN_PAYMENT_AMOUNT = 1000n;
|
|
5
|
+
const errors_1 = require("../errors");
|
|
6
|
+
const idempotency_1 = require("./idempotency");
|
|
7
|
+
const constants_1 = require("../constants");
|
|
23
8
|
const PAYNODE_ABI = [
|
|
24
9
|
"event PaymentReceived(bytes32 indexed orderId, address indexed merchant, address indexed payer, address token, uint256 amount, uint256 fee, uint256 chainId)"
|
|
25
10
|
];
|
|
@@ -32,7 +17,7 @@ class PayNodeVerifier {
|
|
|
32
17
|
acceptedTokens;
|
|
33
18
|
constructor(config) {
|
|
34
19
|
if (!config.rpcUrls || (Array.isArray(config.rpcUrls) && config.rpcUrls.length === 0)) {
|
|
35
|
-
throw new errors_1.PayNodeException(
|
|
20
|
+
throw new errors_1.PayNodeException(errors_1.ErrorCode.RpcError);
|
|
36
21
|
}
|
|
37
22
|
// Support RpcPool / FallbackProvider
|
|
38
23
|
if (Array.isArray(config.rpcUrls)) {
|
|
@@ -51,13 +36,13 @@ class PayNodeVerifier {
|
|
|
51
36
|
}
|
|
52
37
|
this.contractAddress = config.contractAddress;
|
|
53
38
|
this.chainId = config.chainId;
|
|
54
|
-
this.store = config.store;
|
|
39
|
+
this.store = config.store || new idempotency_1.MemoryIdempotencyStore();
|
|
55
40
|
let tokenList;
|
|
56
41
|
if (config.acceptedTokens !== undefined) {
|
|
57
42
|
tokenList = config.acceptedTokens;
|
|
58
43
|
}
|
|
59
44
|
else if (config.chainId) {
|
|
60
|
-
tokenList =
|
|
45
|
+
tokenList = constants_1.ACCEPTED_TOKENS[config.chainId];
|
|
61
46
|
}
|
|
62
47
|
if (tokenList && tokenList.length > 0) {
|
|
63
48
|
this.acceptedTokens = new Set(tokenList.map(t => t.toLowerCase()));
|
|
@@ -67,27 +52,20 @@ class PayNodeVerifier {
|
|
|
67
52
|
try {
|
|
68
53
|
// 0. Dust Exploit Check (Minimum Payment)
|
|
69
54
|
const expectedAmount = BigInt(expected.amount);
|
|
70
|
-
if (expectedAmount <
|
|
71
|
-
return { isValid: false, error: new errors_1.PayNodeException(
|
|
55
|
+
if (expectedAmount < constants_1.MIN_PAYMENT_AMOUNT) {
|
|
56
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.AmountTooLow) };
|
|
72
57
|
}
|
|
73
58
|
// 1. Token Whitelist Check (Anti-FakeToken)
|
|
74
59
|
if (this.acceptedTokens && !this.acceptedTokens.has(expected.tokenAddress.toLowerCase())) {
|
|
75
|
-
return { isValid: false, error: new errors_1.PayNodeException(
|
|
76
|
-
}
|
|
77
|
-
// 1. Idempotency Check
|
|
78
|
-
if (this.store) {
|
|
79
|
-
const isNew = await this.store.checkAndSet(txHash, 86400);
|
|
80
|
-
if (!isNew) {
|
|
81
|
-
return { isValid: false, error: new errors_1.PayNodeException("This transaction hash has already been consumed.", errors_1.ErrorCode.DuplicateTransaction) };
|
|
82
|
-
}
|
|
60
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TokenNotAccepted) };
|
|
83
61
|
}
|
|
84
62
|
// 2. Fetch Receipt
|
|
85
63
|
const receipt = await this.provider.getTransactionReceipt(txHash);
|
|
86
64
|
if (!receipt) {
|
|
87
|
-
return { isValid: false, error: new errors_1.PayNodeException(
|
|
65
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt) };
|
|
88
66
|
}
|
|
89
67
|
if (receipt.status !== 1) {
|
|
90
|
-
return { isValid: false, error: new errors_1.PayNodeException(
|
|
68
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.TransactionFailed) };
|
|
91
69
|
}
|
|
92
70
|
// 3. Parse Logs & Verify Contract Source
|
|
93
71
|
let paymentLog = null;
|
|
@@ -108,32 +86,45 @@ class PayNodeVerifier {
|
|
|
108
86
|
}
|
|
109
87
|
}
|
|
110
88
|
if (!paymentLog) {
|
|
111
|
-
return { isValid: false, error: new errors_1.PayNodeException(
|
|
89
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.WrongContract) };
|
|
112
90
|
}
|
|
113
91
|
const args = paymentLog.parsed.args;
|
|
114
|
-
// 4. Verify
|
|
92
|
+
// 4. Verify OrderId (bytes32 keccak256 hash comparison)
|
|
93
|
+
if (expected.orderId) {
|
|
94
|
+
if (args.orderId !== ethers_1.ethers.id(expected.orderId)) {
|
|
95
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.OrderMismatch) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// 5. Verify Merchant
|
|
115
99
|
if (args.merchant.toLowerCase() !== expected.merchantAddress.toLowerCase()) {
|
|
116
|
-
return { isValid: false, error: new errors_1.PayNodeException("Payment went to a different merchant."
|
|
100
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment went to a different merchant.") };
|
|
117
101
|
}
|
|
118
102
|
// 5. Verify Token
|
|
119
103
|
if (args.token.toLowerCase() !== expected.tokenAddress.toLowerCase()) {
|
|
120
|
-
return { isValid: false, error: new errors_1.PayNodeException("Payment used unexpected token."
|
|
104
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment used unexpected token.") };
|
|
121
105
|
}
|
|
122
106
|
// 6. Verify Amount
|
|
123
107
|
if (BigInt(args.amount) < BigInt(expected.amount)) {
|
|
124
|
-
return { isValid: false, error: new errors_1.PayNodeException("Payment amount is below required price."
|
|
108
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "Payment amount is below required price.") };
|
|
125
109
|
}
|
|
126
110
|
// 7. Verify ChainId (Cross-chain replay protection)
|
|
127
111
|
const expectedChainId = BigInt(this.chainId || (await this.provider.getNetwork()).chainId);
|
|
128
112
|
if (BigInt(args.chainId) !== expectedChainId) {
|
|
129
|
-
return { isValid: false, error: new errors_1.PayNodeException("ChainId mismatch. Invalid network."
|
|
113
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InvalidReceipt, "ChainId mismatch. Invalid network.") };
|
|
114
|
+
}
|
|
115
|
+
// 8. Idempotency Check
|
|
116
|
+
if (this.store) {
|
|
117
|
+
const isNew = await this.store.checkAndSet(txHash, 86400);
|
|
118
|
+
if (!isNew) {
|
|
119
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.DuplicateTransaction) };
|
|
120
|
+
}
|
|
130
121
|
}
|
|
131
122
|
return { isValid: true };
|
|
132
123
|
}
|
|
133
124
|
catch (e) {
|
|
134
125
|
if (e instanceof errors_1.PayNodeException)
|
|
135
126
|
return { isValid: false, error: e };
|
|
136
|
-
return { isValid: false, error: new errors_1.PayNodeException(`An unexpected error occurred: ${e.message}
|
|
127
|
+
return { isValid: false, error: new errors_1.PayNodeException(errors_1.ErrorCode.InternalError, `An unexpected error occurred: ${e.message}`) };
|
|
137
128
|
}
|
|
138
129
|
}
|
|
139
130
|
}
|