@envarai/envarpay 0.2.0-alpha.13 → 0.2.0-alpha.15

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.
@@ -14,7 +14,7 @@ export interface ExpiredUnusedProof {
14
14
  authorizationUsed: false;
15
15
  }
16
16
  /** Checks the canonical chain receipt and exact official-token Transfer. */
17
- export declare function evmReceiptVerifier(rpcUrls: Readonly<Record<string, string>>, confirmations?: number): (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
17
+ export declare function evmReceiptVerifier(rpcUrls: Readonly<Record<string, string>>, confirmations?: number, confirmationWaitMs?: number): (receipt: SettleResponse, payload: PaymentPayload, requirements: PaymentRequirements) => Promise<boolean>;
18
18
  /** Locate an original EIP-3009 settlement even when facilitator lost its tx response. */
19
19
  export declare function evmSettlementRecovery(rpcUrls: Readonly<Record<string, string>>): {
20
20
  expiredUnusedProof: (payload: PaymentPayload, requirements: PaymentRequirements) => Promise<ExpiredUnusedProof | undefined>;
@@ -1,4 +1,4 @@
1
- import { createPublicClient, http, decodeEventLog, parseAbi } from 'viem';
1
+ import { createPublicClient, http, decodeEventLog, parseAbi, TransactionReceiptNotFoundError } from 'viem';
2
2
  import { CommerceError } from './types.js';
3
3
  import { validateUrl } from './config.js';
4
4
  const abi = parseAbi(['event Transfer(address indexed from, address indexed to, uint256 value)']);
@@ -8,10 +8,23 @@ const officialUsdc = {
8
8
  'eip155:84532': '0x036cbd53842c5426634e7929541ec2318f3dcf7e',
9
9
  'eip155:8453': '0x833589fcd6edb6e08f4c7c32d4f71b54bda02913',
10
10
  };
11
+ function transientRpcRead(error) {
12
+ for (let cause = error, depth = 0; cause && typeof cause === 'object' && depth < 8; depth++) {
13
+ const value = cause;
14
+ if (value.name === 'TimeoutError' || [408, 429, 500, 502, 503, 504].includes(value.status ?? 0) || [-32005, -32011].includes(value.code ?? 0))
15
+ return true;
16
+ if (value.name === 'HttpRequestError' && value.status === undefined)
17
+ return true;
18
+ cause = value.cause;
19
+ }
20
+ return false;
21
+ }
11
22
  /** Checks the canonical chain receipt and exact official-token Transfer. */
12
- export function evmReceiptVerifier(rpcUrls, confirmations = 2) {
23
+ export function evmReceiptVerifier(rpcUrls, confirmations = 2, confirmationWaitMs = 12000) {
13
24
  if (!Number.isInteger(confirmations) || confirmations < 1)
14
25
  throw new CommerceError('confirmation_policy', 'Positive confirmation depth required');
26
+ if (!Number.isInteger(confirmationWaitMs) || confirmationWaitMs < 0 || confirmationWaitMs > 30000)
27
+ throw new CommerceError('confirmation_policy', 'Confirmation wait must be between 0 and 30000 milliseconds');
15
28
  return async (receipt, payload, requirements) => {
16
29
  const rpc = rpcUrls[requirements.network];
17
30
  if (!rpc)
@@ -21,17 +34,53 @@ export function evmReceiptVerifier(rpcUrls, confirmations = 2) {
21
34
  if (!Number.isSafeInteger(chainId) || !/^0x[0-9a-fA-F]{64}$/.test(receipt.transaction) || receipt.network !== requirements.network)
22
35
  return false;
23
36
  const client = createPublicClient({ transport: http(rpc, { timeout: 15000, retryCount: 0 }) });
24
- if (await client.getChainId() !== chainId)
25
- return false;
26
- const mined = await client.getTransactionReceipt({ hash: receipt.transaction });
27
- if (mined.status !== 'success')
28
- return false;
29
- const block = await client.getBlock({ blockNumber: mined.blockNumber });
30
- if (block.hash !== mined.blockHash)
31
- return false;
32
- const latest = await client.getBlockNumber();
33
- if (latest - mined.blockNumber + 1n < BigInt(confirmations))
37
+ const deadline = Date.now() + confirmationWaitMs;
38
+ const wait = async () => {
39
+ const remaining = deadline - Date.now();
40
+ if (remaining <= 0)
41
+ return false;
42
+ await new Promise(resolve => setTimeout(resolve, Math.min(1000, remaining)));
43
+ return Date.now() < deadline;
44
+ };
45
+ // All retries read the original transaction on the configured RPC. A rate
46
+ // limit at any verification stage must not be mistaken for chain evidence.
47
+ const read = async (operation) => {
48
+ for (;;) {
49
+ try {
50
+ return await operation();
51
+ }
52
+ catch (error) {
53
+ if (!transientRpcRead(error) || !await wait())
54
+ throw error;
55
+ }
56
+ }
57
+ };
58
+ if (await read(() => client.getChainId()) !== chainId)
34
59
  return false;
60
+ let mined;
61
+ for (;;) {
62
+ try {
63
+ mined = await read(() => client.getTransactionReceipt({ hash: receipt.transaction }));
64
+ }
65
+ catch (error) {
66
+ if (error instanceof TransactionReceiptNotFoundError) {
67
+ if (await wait())
68
+ continue;
69
+ return false;
70
+ }
71
+ throw error;
72
+ }
73
+ if (mined.status !== 'success' || mined.transactionHash.toLowerCase() !== receipt.transaction.toLowerCase())
74
+ return false;
75
+ const block = await read(() => client.getBlock({ blockNumber: mined.blockNumber }));
76
+ if (block.hash !== mined.blockHash)
77
+ return false;
78
+ const latest = await read(() => client.getBlockNumber({ cacheTime: 0 }));
79
+ if (latest - mined.blockNumber + 1n >= BigInt(confirmations))
80
+ break;
81
+ if (!await wait())
82
+ return false;
83
+ }
35
84
  const auth = payload.payload.authorization;
36
85
  if (!auth?.from || !auth.nonce || auth.to?.toLowerCase() !== requirements.payTo.toLowerCase() || auth.value !== requirements.amount)
37
86
  return false;
@@ -131,8 +180,9 @@ export function evmSettlementRecovery(rpcUrls) {
131
180
  // RPC lookup; never reinterpret incomplete search as proof of non-payment.
132
181
  if (latest - BigInt(fromBlock) > 100000n)
133
182
  throw new CommerceError('archive_reconciliation_required', 'Original authorization requires an archive log lookup');
134
- for (let start = BigInt(fromBlock); start <= latest; start += 2000n) {
135
- const end = start + 1999n < latest ? start + 1999n : latest;
183
+ // The default public Base RPCs limit eth_getLogs to 200-block ranges.
184
+ for (let start = BigInt(fromBlock); start <= latest; start += 200n) {
185
+ const end = start + 199n < latest ? start + 199n : latest;
136
186
  const logs = await client.getLogs({ address: requirements.asset, event: authorizationAbi[0], args: { authorizer: auth.from, nonce: auth.nonce }, fromBlock: start, toBlock: end, strict: true });
137
187
  const settled = logs.filter(log => !log.removed && log.transactionHash);
138
188
  if (settled.length > 1)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@envarai/envarpay",
3
- "version": "0.2.0-alpha.13",
3
+ "version": "0.2.0-alpha.15",
4
4
  "description": "Independent A2A service commerce, budgeted buyers and x402/MPP payments",
5
5
  "type": "module",
6
6
  "license": "MIT",