@palindromepay/sdk 2.1.8 → 2.1.9

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.
@@ -269,6 +269,14 @@ export declare class PalindromePaySDK {
269
269
  * Execute an async operation with retry logic.
270
270
  */
271
271
  private withRetry;
272
+ /**
273
+ * Execute a contract read with retry on transient RPC failures.
274
+ *
275
+ * Idempotent reads are safe to retry, so they honour the same
276
+ * enableRetry/maxRetries/retryDelay config as writes. On a persistent
277
+ * failure the original error is propagated unchanged (no API change).
278
+ */
279
+ private read;
272
280
  /**
273
281
  * Extract error message from unknown error type.
274
282
  */
@@ -360,13 +368,35 @@ export declare class PalindromePaySDK {
360
368
  */
361
369
  predictWalletAddress(escrowId: bigint): Promise<Address>;
362
370
  /**
363
- * Get raw escrow data from contract
371
+ * Build the LRU cache key for an escrow's raw on-chain data.
372
+ */
373
+ private escrowCacheKey;
374
+ /**
375
+ * Drop all cached data for an escrow. Called after any write that may
376
+ * change the escrow's on-chain state so subsequent reads from this SDK
377
+ * instance never observe stale data.
378
+ */
379
+ private invalidateEscrow;
380
+ /**
381
+ * Get raw escrow data from contract.
382
+ *
383
+ * Results are cached in the LRU cache for `cacheTTL` ms to de-duplicate
384
+ * rapid repeated reads (e.g. a UI rendering several components for the same
385
+ * escrow). Writes that mutate the escrow invalidate this entry, and callers
386
+ * that require strictly fresh data (all write pre-flight checks) pass
387
+ * `forceRefresh = true`.
388
+ *
389
+ * @param escrowId - The escrow ID
390
+ * @param forceRefresh - Bypass the cache and read fresh from chain
364
391
  */
365
- getEscrowById(escrowId: bigint): Promise<RawEscrowData>;
392
+ getEscrowById(escrowId: bigint, forceRefresh?: boolean): Promise<RawEscrowData>;
366
393
  /**
367
- * Get parsed escrow data
394
+ * Get parsed escrow data.
395
+ *
396
+ * @param escrowId - The escrow ID
397
+ * @param forceRefresh - Bypass the cache and read fresh from chain
368
398
  */
369
- getEscrowByIdParsed(escrowId: bigint): Promise<EscrowData>;
399
+ getEscrowByIdParsed(escrowId: bigint, forceRefresh?: boolean): Promise<EscrowData>;
370
400
  /**
371
401
  * Get next escrow ID
372
402
  */
@@ -337,21 +337,24 @@ class PalindromePaySDK {
337
337
  * Wait for transaction receipt with timeout and retry logic.
338
338
  */
339
339
  async waitForReceipt(hash) {
340
- const receipt = await this.withRetry(async () => {
341
- try {
342
- return await this.publicClient.waitForTransactionReceipt({
343
- hash,
344
- timeout: this.receiptTimeout,
345
- });
346
- }
347
- catch (error) {
348
- const errorMessage = isViemError(error) ? error.message : String(error);
349
- if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
350
- throw new SDKError(`Transaction receipt timeout after ${this.receiptTimeout}ms`, SDKErrorCode.TRANSACTION_FAILED, { hash });
351
- }
352
- throw error;
340
+ // Retry the raw receipt wait so transient timeouts get another attempt.
341
+ // The timeout is only converted to a (non-retryable) SDKError after all
342
+ // attempts are exhausted — throwing the SDKError inside withRetry would
343
+ // short-circuit the retry loop (see withRetry's errorName === "SDKError" guard).
344
+ let receipt;
345
+ try {
346
+ receipt = await this.withRetry(() => this.publicClient.waitForTransactionReceipt({
347
+ hash,
348
+ timeout: this.receiptTimeout,
349
+ }), "waitForTransactionReceipt");
350
+ }
351
+ catch (error) {
352
+ const errorMessage = isViemError(error) ? error.message : String(error);
353
+ if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
354
+ throw new SDKError(`Transaction receipt timeout after ${this.receiptTimeout}ms`, SDKErrorCode.TRANSACTION_FAILED, { hash });
353
355
  }
354
- }, "waitForTransactionReceipt");
356
+ throw error;
357
+ }
355
358
  // Check receipt status (post-Byzantium: status 'reverted' means on-chain failure)
356
359
  if (receipt.status === "reverted") {
357
360
  throw new SDKError(`Transaction reverted on-chain (hash: ${hash})`, SDKErrorCode.TRANSACTION_FAILED, { hash, blockNumber: receipt.blockNumber });
@@ -388,6 +391,16 @@ class PalindromePaySDK {
388
391
  }
389
392
  throw lastError ?? new SDKError(`${operationName} failed after ${this.maxRetries} attempts`, SDKErrorCode.RPC_ERROR);
390
393
  }
394
+ /**
395
+ * Execute a contract read with retry on transient RPC failures.
396
+ *
397
+ * Idempotent reads are safe to retry, so they honour the same
398
+ * enableRetry/maxRetries/retryDelay config as writes. On a persistent
399
+ * failure the original error is propagated unchanged (no API change).
400
+ */
401
+ async read(params) {
402
+ return this.withRetry(async () => (await (0, actions_1.readContract)(this.publicClient, params)), `read:${params.functionName}`);
403
+ }
391
404
  /**
392
405
  * Extract error message from unknown error type.
393
406
  */
@@ -690,34 +703,67 @@ class PalindromePaySDK {
690
703
  * Calls the contract's computeWalletAddress function - single source of truth
691
704
  */
692
705
  async predictWalletAddress(escrowId) {
693
- const predicted = await (0, actions_1.readContract)(this.publicClient, {
706
+ return this.read({
694
707
  address: this.contractAddress,
695
708
  abi: PalindromePay_json_1.default.abi,
696
709
  functionName: "computeWalletAddress",
697
710
  args: [escrowId],
698
711
  });
699
- return predicted;
700
712
  }
701
713
  // ==========================================================================
702
714
  // ESCROW DATA READING
703
715
  // ==========================================================================
704
716
  /**
705
- * Get raw escrow data from contract
717
+ * Build the LRU cache key for an escrow's raw on-chain data.
706
718
  */
707
- async getEscrowById(escrowId) {
708
- const raw = await (0, actions_1.readContract)(this.publicClient, {
719
+ escrowCacheKey(escrowId) {
720
+ return `escrow-${escrowId}`;
721
+ }
722
+ /**
723
+ * Drop all cached data for an escrow. Called after any write that may
724
+ * change the escrow's on-chain state so subsequent reads from this SDK
725
+ * instance never observe stale data.
726
+ */
727
+ invalidateEscrow(escrowId) {
728
+ this.escrowCache.delete(this.escrowCacheKey(escrowId));
729
+ this.escrowCache.delete(`status-${escrowId}`);
730
+ }
731
+ /**
732
+ * Get raw escrow data from contract.
733
+ *
734
+ * Results are cached in the LRU cache for `cacheTTL` ms to de-duplicate
735
+ * rapid repeated reads (e.g. a UI rendering several components for the same
736
+ * escrow). Writes that mutate the escrow invalidate this entry, and callers
737
+ * that require strictly fresh data (all write pre-flight checks) pass
738
+ * `forceRefresh = true`.
739
+ *
740
+ * @param escrowId - The escrow ID
741
+ * @param forceRefresh - Bypass the cache and read fresh from chain
742
+ */
743
+ async getEscrowById(escrowId, forceRefresh = false) {
744
+ const cacheKey = this.escrowCacheKey(escrowId);
745
+ if (!forceRefresh) {
746
+ const cached = this.getCacheValue(cacheKey);
747
+ if (cached)
748
+ return cached;
749
+ }
750
+ const raw = await this.read({
709
751
  address: this.contractAddress,
710
752
  abi: this.abiEscrow,
711
753
  functionName: "getEscrow",
712
754
  args: [escrowId],
713
755
  });
756
+ this.setCacheValue(cacheKey, raw);
714
757
  return raw;
715
758
  }
716
759
  /**
717
- * Get parsed escrow data
760
+ * Get parsed escrow data.
761
+ *
762
+ * @param escrowId - The escrow ID
763
+ * @param forceRefresh - Bypass the cache and read fresh from chain
718
764
  */
719
- async getEscrowByIdParsed(escrowId) {
720
- const raw = await this.getEscrowById(escrowId);
765
+ async getEscrowByIdParsed(escrowId, forceRefresh = false) {
766
+ const raw = await this.getEscrowById(escrowId, forceRefresh);
721
767
  return {
722
768
  token: raw.token,
723
769
  buyer: raw.buyer,
@@ -741,7 +787,7 @@ class PalindromePaySDK {
741
787
  * Get next escrow ID
742
788
  */
743
789
  async getNextEscrowId() {
744
- return (0, actions_1.readContract)(this.publicClient, {
790
+ return this.read({
745
791
  address: this.contractAddress,
746
792
  abi: this.abiEscrow,
747
793
  functionName: "nextEscrowId",
@@ -761,7 +807,7 @@ class PalindromePaySDK {
761
807
  * @returns The bitmap as a bigint
762
808
  */
763
809
  async getNonceBitmap(escrowId, signer, wordIndex = 0n) {
764
- return this.publicClient.readContract({
810
+ return this.read({
765
811
  address: this.contractAddress,
766
812
  abi: this.abiEscrow,
767
813
  functionName: "getNonceBitmap",
@@ -961,7 +1007,7 @@ class PalindromePaySDK {
961
1007
  * Get dispute submission status
962
1008
  */
963
1009
  async getDisputeSubmissionStatus(escrowId) {
964
- const status = await this.publicClient.readContract({
1010
+ const status = await this.read({
965
1011
  address: this.contractAddress,
966
1012
  abi: this.abiEscrow,
967
1013
  functionName: "disputeStatus",
@@ -979,7 +1025,7 @@ class PalindromePaySDK {
979
1025
  if (this.tokenDecimalsCache.has(tokenAddress)) {
980
1026
  return this.tokenDecimalsCache.get(tokenAddress);
981
1027
  }
982
- const decimals = await this.publicClient.readContract({
1028
+ const decimals = await this.read({
983
1029
  address: tokenAddress,
984
1030
  abi: this.abiERC20,
985
1031
  functionName: "decimals",
@@ -988,7 +1034,7 @@ class PalindromePaySDK {
988
1034
  return decimals;
989
1035
  }
990
1036
  async getTokenBalance(account, tokenAddress) {
991
- return this.publicClient.readContract({
1037
+ return this.read({
992
1038
  address: tokenAddress,
993
1039
  abi: this.abiERC20,
994
1040
  functionName: "balanceOf",
@@ -996,7 +1042,7 @@ class PalindromePaySDK {
996
1042
  });
997
1043
  }
998
1044
  async getTokenAllowance(owner, spender, tokenAddress) {
999
- return this.publicClient.readContract({
1045
+ return this.read({
1000
1046
  address: tokenAddress,
1001
1047
  abi: this.abiERC20,
1002
1048
  functionName: "allowance",
@@ -1301,7 +1347,7 @@ class PalindromePaySDK {
1301
1347
  */
1302
1348
  async deposit(walletClient, escrowId) {
1303
1349
  assertWalletClient(walletClient);
1304
- const deal = await this.getEscrowByIdParsed(escrowId);
1350
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1305
1351
  // Verify caller and state using helpers
1306
1352
  this.verifyBuyer(walletClient.account.address, deal);
1307
1353
  this.verifyState(deal, EscrowState.AWAITING_PAYMENT, "deposit");
@@ -1322,6 +1368,7 @@ class PalindromePaySDK {
1322
1368
  args: [escrowId, buyerWalletSig],
1323
1369
  });
1324
1370
  await this.waitForReceipt(hash);
1371
+ this.invalidateEscrow(escrowId);
1325
1372
  // Verify buyer signature is valid on-chain after deposit
1326
1373
  let buyerSigValid = false;
1327
1374
  try {
@@ -1369,7 +1416,7 @@ class PalindromePaySDK {
1369
1416
  */
1370
1417
  async acceptEscrow(walletClient, escrowId) {
1371
1418
  assertWalletClient(walletClient);
1372
- const deal = await this.getEscrowByIdParsed(escrowId);
1419
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1373
1420
  // Verify caller and state using helpers
1374
1421
  this.verifySeller(walletClient.account.address, deal);
1375
1422
  this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "accept");
@@ -1392,6 +1439,7 @@ class PalindromePaySDK {
1392
1439
  args: [escrowId, sellerWalletSig],
1393
1440
  });
1394
1441
  await this.waitForReceipt(hash);
1442
+ this.invalidateEscrow(escrowId);
1395
1443
  // Verify seller signature is valid on-chain after accept
1396
1444
  let sellerSigValid = false;
1397
1445
  try {
@@ -1440,7 +1488,7 @@ class PalindromePaySDK {
1440
1488
  */
1441
1489
  async confirmDelivery(walletClient, escrowId) {
1442
1490
  assertWalletClient(walletClient);
1443
- const deal = await this.getEscrowByIdParsed(escrowId);
1491
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1444
1492
  // Verify caller and state using helpers
1445
1493
  this.verifyBuyer(walletClient.account.address, deal);
1446
1494
  this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "confirm delivery");
@@ -1459,6 +1507,7 @@ class PalindromePaySDK {
1459
1507
  args: [escrowId, buyerWalletSig],
1460
1508
  });
1461
1509
  await this.waitForReceipt(hash);
1510
+ this.invalidateEscrow(escrowId);
1462
1511
  return hash;
1463
1512
  }
1464
1513
  /**
@@ -1492,6 +1541,7 @@ class PalindromePaySDK {
1492
1541
  args: [escrowId, coordSignature, deadline, nonce, buyerWalletSig],
1493
1542
  });
1494
1543
  await this.waitForReceipt(hash);
1544
+ this.invalidateEscrow(escrowId);
1495
1545
  return hash;
1496
1546
  }
1497
1547
  /**
@@ -1549,7 +1599,7 @@ class PalindromePaySDK {
1549
1599
  */
1550
1600
  async requestCancel(walletClient, escrowId) {
1551
1601
  assertWalletClient(walletClient);
1552
- const deal = await this.getEscrowByIdParsed(escrowId);
1602
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1553
1603
  // Verify caller is buyer or seller
1554
1604
  const isBuyer = addressEquals(walletClient.account.address, deal.buyer);
1555
1605
  const isSeller = addressEquals(walletClient.account.address, deal.seller);
@@ -1575,6 +1625,7 @@ class PalindromePaySDK {
1575
1625
  args: [escrowId, walletSig],
1576
1626
  });
1577
1627
  await this.waitForReceipt(hash);
1628
+ this.invalidateEscrow(escrowId);
1578
1629
  return hash;
1579
1630
  }
1580
1631
  /**
@@ -1598,7 +1649,7 @@ class PalindromePaySDK {
1598
1649
  */
1599
1650
  async cancelByTimeout(walletClient, escrowId) {
1600
1651
  assertWalletClient(walletClient);
1601
- const deal = await this.getEscrowByIdParsed(escrowId);
1652
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1602
1653
  // Verify caller using helper
1603
1654
  this.verifyBuyer(walletClient.account.address, deal);
1604
1655
  // Cancel by timeout
@@ -1609,6 +1660,7 @@ class PalindromePaySDK {
1609
1660
  args: [escrowId],
1610
1661
  });
1611
1662
  await this.waitForReceipt(hash);
1663
+ this.invalidateEscrow(escrowId);
1612
1664
  return hash;
1613
1665
  }
1614
1666
  /**
@@ -1638,7 +1690,7 @@ class PalindromePaySDK {
1638
1690
  */
1639
1691
  async autoRelease(walletClient, escrowId) {
1640
1692
  assertWalletClient(walletClient);
1641
- const deal = await this.getEscrowByIdParsed(escrowId);
1693
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1642
1694
  // Verify caller and state using helpers
1643
1695
  this.verifySeller(walletClient.account.address, deal);
1644
1696
  this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "auto-release");
@@ -1666,6 +1718,7 @@ class PalindromePaySDK {
1666
1718
  args: [escrowId],
1667
1719
  });
1668
1720
  await this.waitForReceipt(hash);
1721
+ this.invalidateEscrow(escrowId);
1669
1722
  return hash;
1670
1723
  }
1671
1724
  // ==========================================================================
@@ -1701,6 +1754,7 @@ class PalindromePaySDK {
1701
1754
  args: [escrowId],
1702
1755
  });
1703
1756
  await this.waitForReceipt(hash);
1757
+ this.invalidateEscrow(escrowId);
1704
1758
  return hash;
1705
1759
  }
1706
1760
  /**
@@ -1733,6 +1787,7 @@ class PalindromePaySDK {
1733
1787
  args: [escrowId, signature, deadline, nonce],
1734
1788
  });
1735
1789
  await this.waitForReceipt(hash);
1790
+ this.invalidateEscrow(escrowId);
1736
1791
  return hash;
1737
1792
  }
1738
1793
  /**
@@ -1770,6 +1825,7 @@ class PalindromePaySDK {
1770
1825
  args: [escrowId, role, ipfsHash],
1771
1826
  });
1772
1827
  await this.waitForReceipt(hash);
1828
+ this.invalidateEscrow(escrowId);
1773
1829
  return hash;
1774
1830
  }
1775
1831
  /**
@@ -1799,7 +1855,7 @@ class PalindromePaySDK {
1799
1855
  */
1800
1856
  async submitArbiterDecision(walletClient, escrowId, resolution, ipfsHash) {
1801
1857
  assertWalletClient(walletClient);
1802
- const deal = await this.getEscrowByIdParsed(escrowId);
1858
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1803
1859
  // Verify caller using helper
1804
1860
  this.verifyArbiter(walletClient.account.address, deal);
1805
1861
  // Sign wallet authorization
@@ -1817,6 +1873,7 @@ class PalindromePaySDK {
1817
1873
  args: [escrowId, resolution, ipfsHash, arbiterWalletSig],
1818
1874
  });
1819
1875
  await this.waitForReceipt(hash);
1876
+ this.invalidateEscrow(escrowId);
1820
1877
  return hash;
1821
1878
  }
1822
1879
  // ==========================================================================
@@ -1844,7 +1901,7 @@ class PalindromePaySDK {
1844
1901
  */
1845
1902
  async withdraw(walletClient, escrowId) {
1846
1903
  assertWalletClient(walletClient);
1847
- const deal = await this.getEscrowByIdParsed(escrowId);
1904
+ const deal = await this.getEscrowByIdParsed(escrowId, true);
1848
1905
  // Verify final state
1849
1906
  if (![EscrowState.COMPLETE, EscrowState.REFUNDED, EscrowState.CANCELED].includes(deal.state)) {
1850
1907
  throw new SDKError(`Cannot withdraw in state: ${this.STATE_NAMES[deal.state]}`, SDKErrorCode.INVALID_STATE);
@@ -1857,6 +1914,7 @@ class PalindromePaySDK {
1857
1914
  args: [],
1858
1915
  });
1859
1916
  await this.waitForReceipt(hash);
1917
+ this.invalidateEscrow(escrowId);
1860
1918
  return hash;
1861
1919
  }
1862
1920
  /**
@@ -1864,7 +1922,7 @@ class PalindromePaySDK {
1864
1922
  */
1865
1923
  async getWalletSignatureCount(escrowId) {
1866
1924
  const deal = await this.getEscrowByIdParsed(escrowId);
1867
- const count = await this.publicClient.readContract({
1925
+ const count = await this.read({
1868
1926
  address: deal.wallet,
1869
1927
  abi: this.abiWallet,
1870
1928
  functionName: "getValidSignatureCount",
@@ -1876,7 +1934,7 @@ class PalindromePaySDK {
1876
1934
  */
1877
1935
  async getWalletBalance(escrowId) {
1878
1936
  const deal = await this.getEscrowByIdParsed(escrowId);
1879
- return this.publicClient.readContract({
1937
+ return this.read({
1880
1938
  address: deal.wallet,
1881
1939
  abi: this.abiWallet,
1882
1940
  functionName: "getBalance",
@@ -2839,7 +2897,7 @@ class PalindromePaySDK {
2839
2897
  if (!forceRefresh && this.feeReceiverCache) {
2840
2898
  return this.feeReceiverCache;
2841
2899
  }
2842
- this.feeReceiverCache = await this.publicClient.readContract({
2900
+ this.feeReceiverCache = await this.read({
2843
2901
  address: this.contractAddress,
2844
2902
  abi: this.abiEscrow,
2845
2903
  functionName: "FEE_RECEIVER",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@palindromepay/sdk",
3
- "version": "2.1.8",
3
+ "version": "2.1.9",
4
4
  "description": "TypeScript SDK for PalindromeCryptoEscrow - Secure blockchain escrow with buyer/seller protection",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",