@palindromepay/sdk 1.9.8 → 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.
@@ -86,7 +86,8 @@ export declare enum SDKErrorCode {
86
86
  RPC_ERROR = "RPC_ERROR",
87
87
  EVIDENCE_ALREADY_SUBMITTED = "EVIDENCE_ALREADY_SUBMITTED",
88
88
  ESCROW_NOT_FOUND = "ESCROW_NOT_FOUND",
89
- ALREADY_ACCEPTED = "ALREADY_ACCEPTED"
89
+ ALREADY_ACCEPTED = "ALREADY_ACCEPTED",
90
+ SIGNATURE_INVALID = "SIGNATURE_INVALID"
90
91
  }
91
92
  export type EscrowWalletClient = WalletClient<Transport, Chain, Account>;
92
93
  export declare class SDKError extends Error {
@@ -235,7 +236,6 @@ export declare class PalindromePaySDK {
235
236
  /** Cache for token decimals (rarely changes, no eviction needed) */
236
237
  private tokenDecimalsCache;
237
238
  /** Cache for immutable contract values */
238
- private walletBytecodeHashCache;
239
239
  private feeReceiverCache;
240
240
  /** Cached multicall support status per chain (null = not yet detected) */
241
241
  private multicallSupported;
@@ -332,6 +332,11 @@ export declare class PalindromePaySDK {
332
332
  * Used for: deposit, confirmDelivery, requestCancel, submitArbiterDecision
333
333
  */
334
334
  signWalletAuthorization(walletClient: EscrowWalletClient, walletAddress: Address, escrowId: bigint): Promise<Hex>;
335
+ /**
336
+ * Verify a wallet authorization signature locally before submitting transaction
337
+ * This ensures the signature is cryptographically valid for the expected signer
338
+ */
339
+ private verifyWalletSignature;
335
340
  /**
336
341
  * Sign a confirm delivery message (for gasless meta-tx)
337
342
  */
@@ -350,6 +355,7 @@ export declare class PalindromePaySDK {
350
355
  isSignatureDeadlineExpired(deadline: bigint, safetySeconds?: number): boolean;
351
356
  /**
352
357
  * Predict the wallet address for a given escrow ID (before creation)
358
+ * Uses CREATE2 address computation: keccak256(0xff ++ deployer ++ salt ++ initCodeHash)
353
359
  */
354
360
  predictWalletAddress(escrowId: bigint): Promise<Address>;
355
361
  /**
@@ -489,6 +495,7 @@ export declare class PalindromePaySDK {
489
495
  escrowId: bigint;
490
496
  txHash: Hex;
491
497
  walletAddress: Address;
498
+ signatureValid: boolean;
492
499
  }>;
493
500
  /**
494
501
  * Create a new escrow and deposit funds as the buyer (single transaction)
@@ -518,6 +525,7 @@ export declare class PalindromePaySDK {
518
525
  escrowId: bigint;
519
526
  txHash: Hex;
520
527
  walletAddress: Address;
528
+ signatureValid: boolean;
521
529
  }>;
522
530
  /**
523
531
  * Deposit funds into an existing escrow as the buyer
@@ -531,13 +539,16 @@ export declare class PalindromePaySDK {
531
539
  *
532
540
  * @param walletClient - The buyer's wallet client (must have account connected)
533
541
  * @param escrowId - The escrow ID to deposit into
534
- * @returns Transaction hash
542
+ * @returns Object containing transaction hash and signature validity
535
543
  * @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
536
544
  * @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
537
545
  * @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_PAYMENT state
538
546
  * @throws {SDKError} INSUFFICIENT_BALANCE - If buyer has insufficient token balance
539
547
  */
540
- deposit(walletClient: EscrowWalletClient, escrowId: bigint): Promise<Hex>;
548
+ deposit(walletClient: EscrowWalletClient, escrowId: bigint): Promise<{
549
+ txHash: Hex;
550
+ signatureValid: boolean;
551
+ }>;
541
552
  /**
542
553
  * Accept an escrow as the seller (for buyer-created escrows)
543
554
  *
@@ -549,13 +560,16 @@ export declare class PalindromePaySDK {
549
560
  *
550
561
  * @param walletClient - The seller's wallet client (must have account connected)
551
562
  * @param escrowId - The escrow ID to accept
552
- * @returns Transaction hash
563
+ * @returns Object containing transaction hash and signature validity
553
564
  * @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
554
565
  * @throws {SDKError} NOT_SELLER - If caller is not the designated seller
555
566
  * @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
556
567
  * @throws {SDKError} ALREADY_ACCEPTED - If escrow was already accepted
557
568
  */
558
- acceptEscrow(walletClient: EscrowWalletClient, escrowId: bigint): Promise<Hex>;
569
+ acceptEscrow(walletClient: EscrowWalletClient, escrowId: bigint): Promise<{
570
+ txHash: Hex;
571
+ signatureValid: boolean;
572
+ }>;
559
573
  /**
560
574
  * Confirm delivery and release funds to the seller
561
575
  *
@@ -130,6 +130,7 @@ var SDKErrorCode;
130
130
  SDKErrorCode["EVIDENCE_ALREADY_SUBMITTED"] = "EVIDENCE_ALREADY_SUBMITTED";
131
131
  SDKErrorCode["ESCROW_NOT_FOUND"] = "ESCROW_NOT_FOUND";
132
132
  SDKErrorCode["ALREADY_ACCEPTED"] = "ALREADY_ACCEPTED";
133
+ SDKErrorCode["SIGNATURE_INVALID"] = "SIGNATURE_INVALID";
133
134
  })(SDKErrorCode || (exports.SDKErrorCode = SDKErrorCode = {}));
134
135
  class SDKError extends Error {
135
136
  constructor(message, code, details) {
@@ -199,7 +200,6 @@ class PalindromePaySDK {
199
200
  /** Cache for token decimals (rarely changes, no eviction needed) */
200
201
  this.tokenDecimalsCache = new Map();
201
202
  /** Cache for immutable contract values */
202
- this.walletBytecodeHashCache = null;
203
203
  this.feeReceiverCache = null;
204
204
  /** Cached multicall support status per chain (null = not yet detected) */
205
205
  this.multicallSupported = null;
@@ -574,6 +574,32 @@ class PalindromePaySDK {
574
574
  validateSignature(signature, "wallet authorization signature");
575
575
  return signature;
576
576
  }
577
+ /**
578
+ * Verify a wallet authorization signature locally before submitting transaction
579
+ * This ensures the signature is cryptographically valid for the expected signer
580
+ */
581
+ async verifyWalletSignature(signature, walletAddress, escrowId, expectedSigner) {
582
+ try {
583
+ return await (0, viem_1.verifyTypedData)({
584
+ address: expectedSigner,
585
+ domain: this.getWalletDomain(walletAddress),
586
+ types: this.walletAuthorizationTypes,
587
+ primaryType: "WalletAuthorization",
588
+ message: {
589
+ escrowId,
590
+ wallet: walletAddress,
591
+ participant: expectedSigner,
592
+ },
593
+ signature,
594
+ });
595
+ }
596
+ catch (error) {
597
+ this.log('warn', 'Signature verification failed', {
598
+ error: error instanceof Error ? error.message : String(error),
599
+ });
600
+ return false;
601
+ }
602
+ }
577
603
  /**
578
604
  * Sign a confirm delivery message (for gasless meta-tx)
579
605
  */
@@ -645,24 +671,28 @@ class PalindromePaySDK {
645
671
  // ==========================================================================
646
672
  /**
647
673
  * Predict the wallet address for a given escrow ID (before creation)
674
+ * Uses CREATE2 address computation: keccak256(0xff ++ deployer ++ salt ++ initCodeHash)
648
675
  */
649
676
  async predictWalletAddress(escrowId) {
650
- const salt = (0, viem_1.keccak256)((0, viem_1.pad)((0, viem_1.toBytes)(escrowId), { size: 32 }));
651
- // Get wallet bytecode hash from contract (cached - immutable value)
652
- if (!this.walletBytecodeHashCache) {
653
- this.walletBytecodeHashCache = await this.publicClient.readContract({
654
- address: this.contractAddress,
655
- abi: this.abiEscrow,
656
- functionName: "WALLET_BYTECODE_HASH",
657
- });
658
- }
659
- // Compute CREATE2 address
677
+ // Salt is keccak256 of the escrowId padded to 32 bytes
678
+ const salt = (0, viem_1.keccak256)((0, viem_1.encodeAbiParameters)([{ type: "uint256" }], [escrowId]));
679
+ // Get wallet bytecode from the ABI JSON
680
+ const walletBytecode = PalindromePayWallet_json_1.default.bytecode;
681
+ // Encode constructor arguments: (address _escrowContract, uint256 _escrowId)
660
682
  const encodedArgs = (0, viem_1.encodeAbiParameters)([{ type: "address" }, { type: "uint256" }], [this.contractAddress, escrowId]);
661
- // Note: For accurate prediction, need actual bytecode + args hash
662
- // This is a simplified version - in production, use the contract's computation
663
- const initCodeHash = (0, viem_1.keccak256)((PalindromePayWallet_json_1.default.bytecode + encodedArgs.slice(2)));
664
- const raw = (0, viem_1.keccak256)((`0xff${this.contractAddress.slice(2)}${salt.slice(2)}${initCodeHash.slice(2)}`));
665
- return (0, viem_1.getAddress)(`0x${raw.slice(26)}`);
683
+ // initCode = bytecode + encoded constructor args
684
+ const initCode = (0, viem_1.concat)([walletBytecode, encodedArgs]);
685
+ const initCodeHash = (0, viem_1.keccak256)(initCode);
686
+ // CREATE2 address: keccak256(0xff ++ deployer ++ salt ++ initCodeHash)[12:]
687
+ const data = (0, viem_1.concat)([
688
+ "0xff",
689
+ this.contractAddress,
690
+ salt,
691
+ initCodeHash,
692
+ ]);
693
+ const hash = (0, viem_1.keccak256)(data);
694
+ // Take the last 20 bytes (address is derived from bytes 12-31)
695
+ return (0, viem_1.getAddress)(`0x${hash.slice(26)}`);
666
696
  }
667
697
  // ==========================================================================
668
698
  // ESCROW DATA READING
@@ -1053,6 +1083,11 @@ class PalindromePaySDK {
1053
1083
  const predictedWallet = await this.predictWalletAddress(nextId);
1054
1084
  // Sign wallet authorization
1055
1085
  const sellerWalletSig = await this.signWalletAuthorization(walletClient, predictedWallet, nextId);
1086
+ // Pre-validate signature before submitting transaction
1087
+ const isValidSig = await this.verifyWalletSignature(sellerWalletSig, predictedWallet, nextId, walletClient.account.address);
1088
+ if (!isValidSig) {
1089
+ throw new SDKError("Seller wallet signature is invalid - cannot create escrow", SDKErrorCode.SIGNATURE_INVALID);
1090
+ }
1056
1091
  // Create escrow
1057
1092
  const hash = await this.resilientWriteContract(walletClient, {
1058
1093
  address: this.contractAddress,
@@ -1081,7 +1116,30 @@ class PalindromePaySDK {
1081
1116
  });
1082
1117
  const escrowId = events[0]?.args?.escrowId ?? nextId;
1083
1118
  const deal = await this.getEscrowByIdParsed(escrowId);
1084
- return { escrowId, txHash: hash, walletAddress: deal.wallet };
1119
+ // Verify seller signature is valid on-chain after creation
1120
+ let sellerSigValid = false;
1121
+ try {
1122
+ sellerSigValid = await this.publicClient.readContract({
1123
+ address: deal.wallet,
1124
+ abi: this.abiWallet,
1125
+ functionName: "isSignatureValid",
1126
+ args: [walletClient.account.address],
1127
+ });
1128
+ }
1129
+ catch (error) {
1130
+ this.log('warn', 'Failed to verify seller signature after escrow creation', {
1131
+ escrowId: escrowId.toString(),
1132
+ error: error instanceof Error ? error.message : String(error),
1133
+ });
1134
+ }
1135
+ if (!sellerSigValid) {
1136
+ this.log('error', 'Seller wallet signature is invalid after escrow creation', {
1137
+ escrowId: escrowId.toString(),
1138
+ seller: walletClient.account.address,
1139
+ wallet: deal.wallet,
1140
+ });
1141
+ }
1142
+ return { escrowId, txHash: hash, walletAddress: deal.wallet, signatureValid: sellerSigValid };
1085
1143
  }
1086
1144
  /**
1087
1145
  * Create a new escrow and deposit funds as the buyer (single transaction)
@@ -1144,6 +1202,11 @@ class PalindromePaySDK {
1144
1202
  const predictedWallet = await this.predictWalletAddress(nextId);
1145
1203
  // Sign wallet authorization
1146
1204
  const buyerWalletSig = await this.signWalletAuthorization(walletClient, predictedWallet, nextId);
1205
+ // Pre-validate signature before submitting transaction
1206
+ const isValidSig = await this.verifyWalletSignature(buyerWalletSig, predictedWallet, nextId, walletClient.account.address);
1207
+ if (!isValidSig) {
1208
+ throw new SDKError("Buyer wallet signature is invalid - cannot create escrow", SDKErrorCode.SIGNATURE_INVALID);
1209
+ }
1147
1210
  // Create escrow and deposit
1148
1211
  const hash = await this.resilientWriteContract(walletClient, {
1149
1212
  address: this.contractAddress,
@@ -1172,7 +1235,30 @@ class PalindromePaySDK {
1172
1235
  });
1173
1236
  const escrowId = events[0]?.args?.escrowId ?? nextId;
1174
1237
  const deal = await this.getEscrowByIdParsed(escrowId);
1175
- return { escrowId, txHash: hash, walletAddress: deal.wallet };
1238
+ // Verify buyer signature is valid on-chain after creation
1239
+ let buyerSigValid = false;
1240
+ try {
1241
+ buyerSigValid = await this.publicClient.readContract({
1242
+ address: deal.wallet,
1243
+ abi: this.abiWallet,
1244
+ functionName: "isSignatureValid",
1245
+ args: [walletClient.account.address],
1246
+ });
1247
+ }
1248
+ catch (error) {
1249
+ this.log('warn', 'Failed to verify buyer signature after escrow creation', {
1250
+ escrowId: escrowId.toString(),
1251
+ error: error instanceof Error ? error.message : String(error),
1252
+ });
1253
+ }
1254
+ if (!buyerSigValid) {
1255
+ this.log('error', 'Buyer wallet signature is invalid after escrow creation', {
1256
+ escrowId: escrowId.toString(),
1257
+ buyer: walletClient.account.address,
1258
+ wallet: deal.wallet,
1259
+ });
1260
+ }
1261
+ return { escrowId, txHash: hash, walletAddress: deal.wallet, signatureValid: buyerSigValid };
1176
1262
  }
1177
1263
  // ==========================================================================
1178
1264
  // DEPOSIT
@@ -1189,7 +1275,7 @@ class PalindromePaySDK {
1189
1275
  *
1190
1276
  * @param walletClient - The buyer's wallet client (must have account connected)
1191
1277
  * @param escrowId - The escrow ID to deposit into
1192
- * @returns Transaction hash
1278
+ * @returns Object containing transaction hash and signature validity
1193
1279
  * @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
1194
1280
  * @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
1195
1281
  * @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_PAYMENT state
@@ -1203,8 +1289,13 @@ class PalindromePaySDK {
1203
1289
  this.verifyState(deal, EscrowState.AWAITING_PAYMENT, "deposit");
1204
1290
  // Approve token spending
1205
1291
  await this.approveTokenIfNeeded(walletClient, deal.token, this.contractAddress, deal.amount);
1206
- // Sign wallet authorization
1292
+ // Sign wallet authorization using the wallet address from the escrow
1207
1293
  const buyerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
1294
+ // Pre-validate signature before submitting transaction
1295
+ const isValidSig = await this.verifyWalletSignature(buyerWalletSig, deal.wallet, escrowId, walletClient.account.address);
1296
+ if (!isValidSig) {
1297
+ throw new SDKError("Buyer wallet signature is invalid - cannot deposit", SDKErrorCode.SIGNATURE_INVALID);
1298
+ }
1208
1299
  // Deposit
1209
1300
  const hash = await this.resilientWriteContract(walletClient, {
1210
1301
  address: this.contractAddress,
@@ -1213,7 +1304,30 @@ class PalindromePaySDK {
1213
1304
  args: [escrowId, buyerWalletSig],
1214
1305
  });
1215
1306
  await this.waitForReceipt(hash);
1216
- return hash;
1307
+ // Verify buyer signature is valid on-chain after deposit
1308
+ let buyerSigValid = false;
1309
+ try {
1310
+ buyerSigValid = await this.publicClient.readContract({
1311
+ address: deal.wallet,
1312
+ abi: this.abiWallet,
1313
+ functionName: "isSignatureValid",
1314
+ args: [walletClient.account.address],
1315
+ });
1316
+ }
1317
+ catch (error) {
1318
+ this.log('warn', 'Failed to verify buyer signature after deposit', {
1319
+ escrowId: escrowId.toString(),
1320
+ error: error instanceof Error ? error.message : String(error),
1321
+ });
1322
+ }
1323
+ if (!buyerSigValid) {
1324
+ this.log('error', 'Buyer wallet signature is invalid after deposit', {
1325
+ escrowId: escrowId.toString(),
1326
+ buyer: walletClient.account.address,
1327
+ wallet: deal.wallet,
1328
+ });
1329
+ }
1330
+ return { txHash: hash, signatureValid: buyerSigValid };
1217
1331
  }
1218
1332
  // ==========================================================================
1219
1333
  // ACCEPT ESCROW (for buyer-created escrows)
@@ -1229,7 +1343,7 @@ class PalindromePaySDK {
1229
1343
  *
1230
1344
  * @param walletClient - The seller's wallet client (must have account connected)
1231
1345
  * @param escrowId - The escrow ID to accept
1232
- * @returns Transaction hash
1346
+ * @returns Object containing transaction hash and signature validity
1233
1347
  * @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
1234
1348
  * @throws {SDKError} NOT_SELLER - If caller is not the designated seller
1235
1349
  * @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
@@ -1245,8 +1359,13 @@ class PalindromePaySDK {
1245
1359
  if (deal.sellerWalletSig && deal.sellerWalletSig !== "0x") {
1246
1360
  throw new SDKError("Escrow already accepted", SDKErrorCode.ALREADY_ACCEPTED);
1247
1361
  }
1248
- // Sign wallet authorization
1362
+ // Sign wallet authorization using the wallet address from the escrow
1249
1363
  const sellerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
1364
+ // Pre-validate signature before submitting transaction
1365
+ const isValidSig = await this.verifyWalletSignature(sellerWalletSig, deal.wallet, escrowId, walletClient.account.address);
1366
+ if (!isValidSig) {
1367
+ throw new SDKError("Seller wallet signature is invalid - cannot accept escrow", SDKErrorCode.SIGNATURE_INVALID);
1368
+ }
1250
1369
  // Accept escrow
1251
1370
  const hash = await this.resilientWriteContract(walletClient, {
1252
1371
  address: this.contractAddress,
@@ -1255,7 +1374,30 @@ class PalindromePaySDK {
1255
1374
  args: [escrowId, sellerWalletSig],
1256
1375
  });
1257
1376
  await this.waitForReceipt(hash);
1258
- return hash;
1377
+ // Verify seller signature is valid on-chain after accept
1378
+ let sellerSigValid = false;
1379
+ try {
1380
+ sellerSigValid = await this.publicClient.readContract({
1381
+ address: deal.wallet,
1382
+ abi: this.abiWallet,
1383
+ functionName: "isSignatureValid",
1384
+ args: [walletClient.account.address],
1385
+ });
1386
+ }
1387
+ catch (error) {
1388
+ this.log('warn', 'Failed to verify seller signature after accept', {
1389
+ escrowId: escrowId.toString(),
1390
+ error: error instanceof Error ? error.message : String(error),
1391
+ });
1392
+ }
1393
+ if (!sellerSigValid) {
1394
+ this.log('error', 'Seller wallet signature is invalid after accept', {
1395
+ escrowId: escrowId.toString(),
1396
+ seller: walletClient.account.address,
1397
+ wallet: deal.wallet,
1398
+ });
1399
+ }
1400
+ return { txHash: hash, signatureValid: sellerSigValid };
1259
1401
  }
1260
1402
  // ==========================================================================
1261
1403
  // CONFIRM DELIVERY
@@ -1286,6 +1428,11 @@ class PalindromePaySDK {
1286
1428
  this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "confirm delivery");
1287
1429
  // Sign wallet authorization
1288
1430
  const buyerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
1431
+ // Pre-validate signature before submitting transaction
1432
+ const isValidSig = await this.verifyWalletSignature(buyerWalletSig, deal.wallet, escrowId, walletClient.account.address);
1433
+ if (!isValidSig) {
1434
+ throw new SDKError("Buyer wallet signature is invalid - cannot confirm delivery", SDKErrorCode.SIGNATURE_INVALID);
1435
+ }
1289
1436
  // Confirm delivery
1290
1437
  const hash = await this.resilientWriteContract(walletClient, {
1291
1438
  address: this.contractAddress,
@@ -1397,6 +1544,11 @@ class PalindromePaySDK {
1397
1544
  }
1398
1545
  // Sign wallet authorization
1399
1546
  const walletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
1547
+ // Pre-validate signature before submitting transaction
1548
+ const isValidSig = await this.verifyWalletSignature(walletSig, deal.wallet, escrowId, walletClient.account.address);
1549
+ if (!isValidSig) {
1550
+ throw new SDKError("Wallet signature is invalid - cannot request cancel", SDKErrorCode.SIGNATURE_INVALID);
1551
+ }
1400
1552
  // Request cancel
1401
1553
  const hash = await this.resilientWriteContract(walletClient, {
1402
1554
  address: this.contractAddress,
@@ -1634,6 +1786,11 @@ class PalindromePaySDK {
1634
1786
  this.verifyArbiter(walletClient.account.address, deal);
1635
1787
  // Sign wallet authorization
1636
1788
  const arbiterWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
1789
+ // Pre-validate signature before submitting transaction
1790
+ const isValidSig = await this.verifyWalletSignature(arbiterWalletSig, deal.wallet, escrowId, walletClient.account.address);
1791
+ if (!isValidSig) {
1792
+ throw new SDKError("Arbiter wallet signature is invalid - cannot submit decision", SDKErrorCode.SIGNATURE_INVALID);
1793
+ }
1637
1794
  // Submit decision
1638
1795
  const hash = await this.resilientWriteContract(walletClient, {
1639
1796
  address: this.contractAddress,