@palindromepay/sdk 1.9.4
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/LICENCE +21 -0
- package/README.md +344 -0
- package/dist/PalindromeEscrowSDK.d.ts +1217 -0
- package/dist/PalindromeEscrowSDK.js +2710 -0
- package/dist/contract/PalindromeCryptoEscrow.json +1180 -0
- package/dist/contract/PalindromeEscrowWallet.json +306 -0
- package/dist/contract/USDT.json +310 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/subgraph/queries.d.ts +21 -0
- package/dist/subgraph/queries.js +466 -0
- package/dist/types/escrow.d.ts +48 -0
- package/dist/types/escrow.js +2 -0
- package/package.json +68 -0
|
@@ -0,0 +1,2710 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2025 Palindrome Finance
|
|
3
|
+
// Licensed under the MIT License. See LICENSE file for details.
|
|
4
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
|
+
};
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.PalindromeEscrowSDK = exports.SDKError = exports.SDKErrorCode = exports.Role = exports.DisputeResolution = exports.EscrowState = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* PALINDROME CRYPTO ESCROW SDK
|
|
11
|
+
*
|
|
12
|
+
* Corrected and optimized SDK matching the actual smart contract interfaces.
|
|
13
|
+
*
|
|
14
|
+
* Key contract functions:
|
|
15
|
+
* - createEscrow(token, buyer, amount, maturityDays, arbiter, title, ipfsHash, sellerWalletSig)
|
|
16
|
+
* - createEscrowAndDeposit(token, seller, amount, maturityDays, arbiter, title, ipfsHash, buyerWalletSig)
|
|
17
|
+
* - deposit(escrowId, buyerWalletSig)
|
|
18
|
+
* - acceptEscrow(escrowId, sellerWalletSig)
|
|
19
|
+
* - confirmDelivery(escrowId, buyerWalletSig)
|
|
20
|
+
* - confirmDeliverySigned(escrowId, coordSignature, deadline, nonce, buyerWalletSig)
|
|
21
|
+
* - requestCancel(escrowId, walletSig)
|
|
22
|
+
* - cancelByTimeout(escrowId)
|
|
23
|
+
* - autoRelease(escrowId)
|
|
24
|
+
* - startDispute(escrowId)
|
|
25
|
+
* - startDisputeSigned(escrowId, signature, deadline, nonce)
|
|
26
|
+
* - submitDisputeMessage(escrowId, role, ipfsHash)
|
|
27
|
+
* - submitArbiterDecision(escrowId, resolution, ipfsHash, arbiterWalletSig)
|
|
28
|
+
* - Wallet: withdraw()
|
|
29
|
+
*/
|
|
30
|
+
const viem_1 = require("viem");
|
|
31
|
+
const actions_1 = require("viem/actions");
|
|
32
|
+
const PalindromeCryptoEscrow_json_1 = __importDefault(require("./contract/PalindromeCryptoEscrow.json"));
|
|
33
|
+
const PalindromeEscrowWallet_json_1 = __importDefault(require("./contract/PalindromeEscrowWallet.json"));
|
|
34
|
+
const USDT_json_1 = __importDefault(require("./contract/USDT.json"));
|
|
35
|
+
const client_1 = require("@apollo/client");
|
|
36
|
+
/** Type guard to check if error is a Viem error */
|
|
37
|
+
function isViemError(error) {
|
|
38
|
+
return (typeof error === 'object' &&
|
|
39
|
+
error !== null &&
|
|
40
|
+
'message' in error &&
|
|
41
|
+
typeof error.message === 'string');
|
|
42
|
+
}
|
|
43
|
+
/** Type guard to check if error is a contract error */
|
|
44
|
+
function isContractError(error) {
|
|
45
|
+
return isViemError(error) && 'contractAddress' in error;
|
|
46
|
+
}
|
|
47
|
+
/** Type guard to check if error has a short message */
|
|
48
|
+
function hasShortMessage(error) {
|
|
49
|
+
return (isViemError(error) &&
|
|
50
|
+
'shortMessage' in error &&
|
|
51
|
+
typeof error.shortMessage === 'string');
|
|
52
|
+
}
|
|
53
|
+
/** Default console logger */
|
|
54
|
+
const defaultLogger = {
|
|
55
|
+
debug: (msg, ctx) => ctx !== undefined ? console.debug(msg, ctx) : console.debug(msg),
|
|
56
|
+
info: (msg, ctx) => ctx !== undefined ? console.info(msg, ctx) : console.info(msg),
|
|
57
|
+
warn: (msg, ctx) => ctx !== undefined ? console.warn(msg, ctx) : console.warn(msg),
|
|
58
|
+
error: (msg, ctx) => ctx !== undefined ? console.error(msg, ctx) : console.error(msg),
|
|
59
|
+
};
|
|
60
|
+
/** No-op logger (disables all logging) */
|
|
61
|
+
const noOpLogger = {
|
|
62
|
+
debug: () => { },
|
|
63
|
+
info: () => { },
|
|
64
|
+
warn: () => { },
|
|
65
|
+
error: () => { },
|
|
66
|
+
};
|
|
67
|
+
// ==========================================================================
|
|
68
|
+
// IMPORTS CONTINUED
|
|
69
|
+
// ==========================================================================
|
|
70
|
+
const queries_1 = require("./subgraph/queries");
|
|
71
|
+
const chains_1 = require("viem/chains");
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// ENUMS & TYPES
|
|
74
|
+
// ============================================================================
|
|
75
|
+
var EscrowState;
|
|
76
|
+
(function (EscrowState) {
|
|
77
|
+
EscrowState[EscrowState["AWAITING_PAYMENT"] = 0] = "AWAITING_PAYMENT";
|
|
78
|
+
EscrowState[EscrowState["AWAITING_DELIVERY"] = 1] = "AWAITING_DELIVERY";
|
|
79
|
+
EscrowState[EscrowState["DISPUTED"] = 2] = "DISPUTED";
|
|
80
|
+
EscrowState[EscrowState["COMPLETE"] = 3] = "COMPLETE";
|
|
81
|
+
EscrowState[EscrowState["REFUNDED"] = 4] = "REFUNDED";
|
|
82
|
+
EscrowState[EscrowState["CANCELED"] = 5] = "CANCELED";
|
|
83
|
+
})(EscrowState || (exports.EscrowState = EscrowState = {}));
|
|
84
|
+
var DisputeResolution;
|
|
85
|
+
(function (DisputeResolution) {
|
|
86
|
+
DisputeResolution[DisputeResolution["Complete"] = 3] = "Complete";
|
|
87
|
+
DisputeResolution[DisputeResolution["Refunded"] = 4] = "Refunded";
|
|
88
|
+
})(DisputeResolution || (exports.DisputeResolution = DisputeResolution = {}));
|
|
89
|
+
var Role;
|
|
90
|
+
(function (Role) {
|
|
91
|
+
Role[Role["None"] = 0] = "None";
|
|
92
|
+
Role[Role["Buyer"] = 1] = "Buyer";
|
|
93
|
+
Role[Role["Seller"] = 2] = "Seller";
|
|
94
|
+
Role[Role["Arbiter"] = 3] = "Arbiter";
|
|
95
|
+
})(Role || (exports.Role = Role = {}));
|
|
96
|
+
// ==========================================================================
|
|
97
|
+
// CONSTANTS
|
|
98
|
+
// ==========================================================================
|
|
99
|
+
/** Maximum length for string fields (title, IPFS hash) */
|
|
100
|
+
const MAX_STRING_LENGTH = 500;
|
|
101
|
+
/** User rejection error code from wallet */
|
|
102
|
+
const USER_REJECTION_CODE = 4001;
|
|
103
|
+
/** Seconds per day for maturity calculations */
|
|
104
|
+
const SECONDS_PER_DAY = 86400n;
|
|
105
|
+
/** Nonce bitmap word size in bits */
|
|
106
|
+
const NONCE_BITMAP_SIZE = 256;
|
|
107
|
+
/** Default cache TTL in milliseconds */
|
|
108
|
+
const DEFAULT_CACHE_TTL = 5000;
|
|
109
|
+
/** Default transaction receipt timeout in milliseconds */
|
|
110
|
+
const DEFAULT_RECEIPT_TIMEOUT = 60000;
|
|
111
|
+
/** Default polling interval in milliseconds */
|
|
112
|
+
const DEFAULT_POLLING_INTERVAL = 5000;
|
|
113
|
+
// ==========================================================================
|
|
114
|
+
// ERROR CODES
|
|
115
|
+
// ==========================================================================
|
|
116
|
+
var SDKErrorCode;
|
|
117
|
+
(function (SDKErrorCode) {
|
|
118
|
+
SDKErrorCode["WALLET_NOT_CONNECTED"] = "WALLET_NOT_CONNECTED";
|
|
119
|
+
SDKErrorCode["WALLET_ACCOUNT_MISSING"] = "WALLET_ACCOUNT_MISSING";
|
|
120
|
+
SDKErrorCode["NOT_BUYER"] = "NOT_BUYER";
|
|
121
|
+
SDKErrorCode["NOT_SELLER"] = "NOT_SELLER";
|
|
122
|
+
SDKErrorCode["NOT_ARBITER"] = "NOT_ARBITER";
|
|
123
|
+
SDKErrorCode["INVALID_STATE"] = "INVALID_STATE";
|
|
124
|
+
SDKErrorCode["INSUFFICIENT_BALANCE"] = "INSUFFICIENT_BALANCE";
|
|
125
|
+
SDKErrorCode["ALLOWANCE_FAILED"] = "ALLOWANCE_FAILED";
|
|
126
|
+
SDKErrorCode["SIGNATURE_EXPIRED"] = "SIGNATURE_EXPIRED";
|
|
127
|
+
SDKErrorCode["TRANSACTION_FAILED"] = "TRANSACTION_FAILED";
|
|
128
|
+
SDKErrorCode["INVALID_ROLE"] = "INVALID_ROLE";
|
|
129
|
+
SDKErrorCode["INVALID_TOKEN"] = "INVALID_TOKEN";
|
|
130
|
+
SDKErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
131
|
+
SDKErrorCode["RPC_ERROR"] = "RPC_ERROR";
|
|
132
|
+
SDKErrorCode["EVIDENCE_ALREADY_SUBMITTED"] = "EVIDENCE_ALREADY_SUBMITTED";
|
|
133
|
+
SDKErrorCode["ESCROW_NOT_FOUND"] = "ESCROW_NOT_FOUND";
|
|
134
|
+
SDKErrorCode["ALREADY_ACCEPTED"] = "ALREADY_ACCEPTED";
|
|
135
|
+
})(SDKErrorCode || (exports.SDKErrorCode = SDKErrorCode = {}));
|
|
136
|
+
class SDKError extends Error {
|
|
137
|
+
constructor(message, code, details) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.code = code;
|
|
140
|
+
this.details = details;
|
|
141
|
+
this.name = "SDKError";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
exports.SDKError = SDKError;
|
|
145
|
+
// ============================================================================
|
|
146
|
+
// UTILITY FUNCTIONS
|
|
147
|
+
// ============================================================================
|
|
148
|
+
function assertWalletClient(client) {
|
|
149
|
+
if (!client) {
|
|
150
|
+
throw new SDKError("Wallet client is required", SDKErrorCode.WALLET_NOT_CONNECTED);
|
|
151
|
+
}
|
|
152
|
+
if (!client.account) {
|
|
153
|
+
throw new SDKError("Wallet account is required", SDKErrorCode.WALLET_ACCOUNT_MISSING);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Validate and normalize an Ethereum address using EIP-55 checksum.
|
|
158
|
+
* Throws SDKError if invalid.
|
|
159
|
+
*/
|
|
160
|
+
function validateAddress(address, fieldName = "address") {
|
|
161
|
+
try {
|
|
162
|
+
return (0, viem_1.getAddress)(address);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
throw new SDKError(`Invalid ${fieldName}: ${address}`, SDKErrorCode.VALIDATION_ERROR);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Check if address is the zero address
|
|
170
|
+
*/
|
|
171
|
+
function isZeroAddress(address) {
|
|
172
|
+
return address === viem_1.zeroAddress;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Validate signature format (65 bytes = 130 hex chars + 0x prefix)
|
|
176
|
+
*/
|
|
177
|
+
function validateSignature(signature, context = "signature") {
|
|
178
|
+
if (!/^0x[0-9a-fA-F]{130}$/.test(signature)) {
|
|
179
|
+
throw new SDKError(`Invalid ${context} format: expected 65-byte hex signature`, SDKErrorCode.VALIDATION_ERROR);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Compare two addresses for equality (case-insensitive, normalized).
|
|
184
|
+
* More efficient than repeated toLowerCase() calls.
|
|
185
|
+
*/
|
|
186
|
+
function addressEquals(a, b) {
|
|
187
|
+
try {
|
|
188
|
+
return (0, viem_1.getAddress)(a) === (0, viem_1.getAddress)(b);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ============================================================================
|
|
195
|
+
// MAIN SDK CLASS
|
|
196
|
+
// ============================================================================
|
|
197
|
+
class PalindromeEscrowSDK {
|
|
198
|
+
constructor(config) {
|
|
199
|
+
/** LRU cache for escrow data with automatic eviction */
|
|
200
|
+
this.escrowCache = new Map();
|
|
201
|
+
/** Cache for token decimals (rarely changes, no eviction needed) */
|
|
202
|
+
this.tokenDecimalsCache = new Map();
|
|
203
|
+
/** Cache for immutable contract values */
|
|
204
|
+
this.walletBytecodeHashCache = null;
|
|
205
|
+
this.feeReceiverCache = null;
|
|
206
|
+
/** Cached multicall support status per chain (null = not yet detected) */
|
|
207
|
+
this.multicallSupported = null;
|
|
208
|
+
this.STATE_NAMES = [
|
|
209
|
+
"AWAITING_PAYMENT",
|
|
210
|
+
"AWAITING_DELIVERY",
|
|
211
|
+
"DISPUTED",
|
|
212
|
+
"COMPLETE",
|
|
213
|
+
"REFUNDED",
|
|
214
|
+
"CANCELED",
|
|
215
|
+
];
|
|
216
|
+
this.walletAuthorizationTypes = {
|
|
217
|
+
WalletAuthorization: [
|
|
218
|
+
{ name: "escrowId", type: "uint256" },
|
|
219
|
+
{ name: "wallet", type: "address" },
|
|
220
|
+
{ name: "participant", type: "address" },
|
|
221
|
+
],
|
|
222
|
+
};
|
|
223
|
+
this.confirmDeliveryTypes = {
|
|
224
|
+
ConfirmDelivery: [
|
|
225
|
+
{ name: "escrowId", type: "uint256" },
|
|
226
|
+
{ name: "buyer", type: "address" },
|
|
227
|
+
{ name: "seller", type: "address" },
|
|
228
|
+
{ name: "arbiter", type: "address" },
|
|
229
|
+
{ name: "token", type: "address" },
|
|
230
|
+
{ name: "amount", type: "uint256" },
|
|
231
|
+
{ name: "depositTime", type: "uint256" },
|
|
232
|
+
{ name: "deadline", type: "uint256" },
|
|
233
|
+
{ name: "nonce", type: "uint256" },
|
|
234
|
+
],
|
|
235
|
+
};
|
|
236
|
+
this.startDisputeTypes = {
|
|
237
|
+
StartDispute: [
|
|
238
|
+
{ name: "escrowId", type: "uint256" },
|
|
239
|
+
{ name: "buyer", type: "address" },
|
|
240
|
+
{ name: "seller", type: "address" },
|
|
241
|
+
{ name: "arbiter", type: "address" },
|
|
242
|
+
{ name: "token", type: "address" },
|
|
243
|
+
{ name: "amount", type: "uint256" },
|
|
244
|
+
{ name: "depositTime", type: "uint256" },
|
|
245
|
+
{ name: "deadline", type: "uint256" },
|
|
246
|
+
{ name: "nonce", type: "uint256" },
|
|
247
|
+
],
|
|
248
|
+
};
|
|
249
|
+
/** Cached fee basis points (lazily computed from contract) */
|
|
250
|
+
this.cachedFeeBps = null;
|
|
251
|
+
if (!config.contractAddress) {
|
|
252
|
+
throw new SDKError("contractAddress is required", SDKErrorCode.VALIDATION_ERROR);
|
|
253
|
+
}
|
|
254
|
+
this.contractAddress = config.contractAddress;
|
|
255
|
+
this.abiEscrow = PalindromeCryptoEscrow_json_1.default.abi;
|
|
256
|
+
this.abiWallet = PalindromeEscrowWallet_json_1.default.abi;
|
|
257
|
+
this.abiERC20 = USDT_json_1.default.abi;
|
|
258
|
+
this.publicClient = config.publicClient;
|
|
259
|
+
this.walletClient = config.walletClient;
|
|
260
|
+
this.chain = config.chain ?? chains_1.hardhat;
|
|
261
|
+
this.cacheTTL = config.cacheTTL ?? DEFAULT_CACHE_TTL;
|
|
262
|
+
this.maxCacheSize = config.maxCacheSize ?? 1000;
|
|
263
|
+
this.enableRetry = config.enableRetry ?? true;
|
|
264
|
+
this.maxRetries = config.maxRetries ?? 3;
|
|
265
|
+
this.retryDelay = config.retryDelay ?? 1000;
|
|
266
|
+
this.gasBuffer = config.gasBuffer ?? 20;
|
|
267
|
+
this.receiptTimeout = config.receiptTimeout ?? DEFAULT_RECEIPT_TIMEOUT;
|
|
268
|
+
this.skipSimulation = config.skipSimulation ?? false;
|
|
269
|
+
this.defaultGasLimit = config.defaultGasLimit ?? 500000n;
|
|
270
|
+
this.logLevel = config.logLevel ?? 'info';
|
|
271
|
+
this.logger = config.logger ?? (this.logLevel === 'none' ? noOpLogger : defaultLogger);
|
|
272
|
+
this.apollo = config.apolloClient ?? new client_1.ApolloClient({
|
|
273
|
+
link: new client_1.HttpLink({
|
|
274
|
+
uri: config.subgraphUrl ?? "https://api.studio.thegraph.com/query/121986/palindrome-finance-subgraph/version/latest",
|
|
275
|
+
}),
|
|
276
|
+
cache: new client_1.InMemoryCache(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
// ==========================================================================
|
|
280
|
+
// PRIVATE HELPERS
|
|
281
|
+
// ==========================================================================
|
|
282
|
+
/**
|
|
283
|
+
* Internal logging helper that respects log level configuration
|
|
284
|
+
*/
|
|
285
|
+
log(level, message, context) {
|
|
286
|
+
const levels = ['debug', 'info', 'warn', 'error', 'none'];
|
|
287
|
+
const currentLevelIndex = levels.indexOf(this.logLevel);
|
|
288
|
+
const messageLevelIndex = levels.indexOf(level);
|
|
289
|
+
if (messageLevelIndex >= currentLevelIndex && messageLevelIndex < 4) {
|
|
290
|
+
this.logger[level](message, context);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Execute a contract write with resilient simulation handling.
|
|
295
|
+
*
|
|
296
|
+
* This method handles unreliable RPC simulation on certain chains (e.g., Base Sepolia)
|
|
297
|
+
* by falling back to direct transaction sending when simulation fails.
|
|
298
|
+
*
|
|
299
|
+
* @param walletClient - The wallet client to use
|
|
300
|
+
* @param params - Contract call parameters
|
|
301
|
+
* @param params.address - Contract address
|
|
302
|
+
* @param params.abi - Contract ABI
|
|
303
|
+
* @param params.functionName - Function to call
|
|
304
|
+
* @param params.args - Function arguments
|
|
305
|
+
* @returns Transaction hash
|
|
306
|
+
*/
|
|
307
|
+
async resilientWriteContract(walletClient, params) {
|
|
308
|
+
const { address, abi, functionName, args } = params;
|
|
309
|
+
// Path 1: Skip simulation entirely if configured
|
|
310
|
+
if (this.skipSimulation) {
|
|
311
|
+
this.log('debug', `Skipping simulation for ${functionName}, sending directly`);
|
|
312
|
+
return this.sendTransactionDirect(walletClient, params);
|
|
313
|
+
}
|
|
314
|
+
// Path 2: Try normal write with simulation
|
|
315
|
+
try {
|
|
316
|
+
return await walletClient.writeContract({
|
|
317
|
+
address,
|
|
318
|
+
abi,
|
|
319
|
+
functionName,
|
|
320
|
+
args,
|
|
321
|
+
account: walletClient.account,
|
|
322
|
+
chain: this.chain,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
// Path 3: Conditional fallback based on error type
|
|
327
|
+
if (!this.isSimulationErrorType(error)) {
|
|
328
|
+
throw error; // Not a simulation error, propagate
|
|
329
|
+
}
|
|
330
|
+
// Simulation failed - send directly as fallback
|
|
331
|
+
this.log('warn', `Simulation failed for ${functionName}, bypassing to send directly`, {
|
|
332
|
+
error: isViemError(error) ? error.message?.slice(0, 100) : String(error),
|
|
333
|
+
functionName,
|
|
334
|
+
});
|
|
335
|
+
return this.sendTransactionDirect(walletClient, params);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Wait for transaction receipt with timeout and retry logic.
|
|
340
|
+
*/
|
|
341
|
+
async waitForReceipt(hash) {
|
|
342
|
+
return this.withRetry(async () => {
|
|
343
|
+
try {
|
|
344
|
+
return await this.publicClient.waitForTransactionReceipt({
|
|
345
|
+
hash,
|
|
346
|
+
timeout: this.receiptTimeout,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
catch (error) {
|
|
350
|
+
const errorMessage = isViemError(error) ? error.message : String(error);
|
|
351
|
+
if (errorMessage.includes("timed out") || errorMessage.includes("timeout")) {
|
|
352
|
+
throw new SDKError(`Transaction receipt timeout after ${this.receiptTimeout}ms`, SDKErrorCode.TRANSACTION_FAILED, { hash });
|
|
353
|
+
}
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}, "waitForTransactionReceipt");
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Execute an async operation with retry logic.
|
|
360
|
+
*/
|
|
361
|
+
async withRetry(operation, operationName = "operation") {
|
|
362
|
+
let lastError;
|
|
363
|
+
for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
|
|
364
|
+
try {
|
|
365
|
+
return await operation();
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
lastError = error;
|
|
369
|
+
// Don't retry on validation errors or user rejections
|
|
370
|
+
const errorCode = error?.code;
|
|
371
|
+
const errorName = error?.name;
|
|
372
|
+
if (errorCode === SDKErrorCode.VALIDATION_ERROR ||
|
|
373
|
+
errorCode === USER_REJECTION_CODE || // User rejected
|
|
374
|
+
errorName === "SDKError") {
|
|
375
|
+
throw error;
|
|
376
|
+
}
|
|
377
|
+
// Only retry if retries are enabled and we have attempts left
|
|
378
|
+
if (!this.enableRetry || attempt >= this.maxRetries) {
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
// Wait before retrying with exponential backoff
|
|
382
|
+
const delay = this.retryDelay * Math.pow(2, attempt - 1);
|
|
383
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
throw lastError ?? new SDKError(`${operationName} failed after ${this.maxRetries} attempts`, SDKErrorCode.RPC_ERROR);
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Extract error message from unknown error type.
|
|
390
|
+
*/
|
|
391
|
+
extractErrorMessage(error) {
|
|
392
|
+
if (error instanceof Error) {
|
|
393
|
+
return error.message;
|
|
394
|
+
}
|
|
395
|
+
if (isViemError(error)) {
|
|
396
|
+
return error.shortMessage || error.message;
|
|
397
|
+
}
|
|
398
|
+
if (typeof error === 'string') {
|
|
399
|
+
return error;
|
|
400
|
+
}
|
|
401
|
+
return String(error);
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Validate common escrow creation parameters.
|
|
405
|
+
*/
|
|
406
|
+
validateCreateEscrowParams(params) {
|
|
407
|
+
const { tokenAddress, buyerAddress, sellerAddress, amount, maturityDays, title } = params;
|
|
408
|
+
if (!tokenAddress || tokenAddress === viem_1.zeroAddress) {
|
|
409
|
+
throw new SDKError("Invalid token address", SDKErrorCode.VALIDATION_ERROR);
|
|
410
|
+
}
|
|
411
|
+
if (!buyerAddress || buyerAddress === viem_1.zeroAddress) {
|
|
412
|
+
throw new SDKError("Invalid buyer address", SDKErrorCode.VALIDATION_ERROR);
|
|
413
|
+
}
|
|
414
|
+
if (!sellerAddress || sellerAddress === viem_1.zeroAddress) {
|
|
415
|
+
throw new SDKError("Invalid seller address", SDKErrorCode.VALIDATION_ERROR);
|
|
416
|
+
}
|
|
417
|
+
// Note: arbiterAddress can be zeroAddress (no arbiter)
|
|
418
|
+
if (amount <= 0n) {
|
|
419
|
+
throw new SDKError("Amount must be greater than 0", SDKErrorCode.VALIDATION_ERROR);
|
|
420
|
+
}
|
|
421
|
+
if (maturityDays < 0n) {
|
|
422
|
+
throw new SDKError("Maturity days cannot be negative", SDKErrorCode.VALIDATION_ERROR);
|
|
423
|
+
}
|
|
424
|
+
if (!title || title.trim().length === 0) {
|
|
425
|
+
throw new SDKError("Title cannot be empty", SDKErrorCode.VALIDATION_ERROR);
|
|
426
|
+
}
|
|
427
|
+
if (title.length > MAX_STRING_LENGTH) {
|
|
428
|
+
throw new SDKError(`Title must be ${MAX_STRING_LENGTH} characters or less`, SDKErrorCode.VALIDATION_ERROR);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Verify caller is the buyer, throw if not.
|
|
433
|
+
*/
|
|
434
|
+
verifyBuyer(caller, escrow) {
|
|
435
|
+
if (caller.toLowerCase() !== escrow.buyer.toLowerCase()) {
|
|
436
|
+
throw new SDKError("Only buyer can perform this action", SDKErrorCode.NOT_BUYER);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Verify caller is the seller, throw if not.
|
|
441
|
+
*/
|
|
442
|
+
verifySeller(caller, escrow) {
|
|
443
|
+
if (caller.toLowerCase() !== escrow.seller.toLowerCase()) {
|
|
444
|
+
throw new SDKError("Only seller can perform this action", SDKErrorCode.NOT_SELLER);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Verify caller is the arbiter, throw if not.
|
|
449
|
+
*/
|
|
450
|
+
verifyArbiter(caller, escrow) {
|
|
451
|
+
if (caller.toLowerCase() !== escrow.arbiter.toLowerCase()) {
|
|
452
|
+
throw new SDKError("Only arbiter can perform this action", SDKErrorCode.NOT_ARBITER);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Verify escrow is in expected state, throw if not.
|
|
457
|
+
*/
|
|
458
|
+
verifyState(escrow, expectedState, actionName) {
|
|
459
|
+
if (escrow.state !== expectedState) {
|
|
460
|
+
throw new SDKError(`Cannot ${actionName}: escrow is in state ${escrow.state}, expected ${expectedState}`, SDKErrorCode.INVALID_STATE);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Send transaction directly without simulation.
|
|
465
|
+
* Encodes function data manually and sends with fixed gas limit.
|
|
466
|
+
*
|
|
467
|
+
* @param walletClient - The wallet client to send from
|
|
468
|
+
* @param params - Contract write parameters
|
|
469
|
+
* @returns Transaction hash
|
|
470
|
+
*/
|
|
471
|
+
async sendTransactionDirect(walletClient, params) {
|
|
472
|
+
const { address, abi, functionName, args } = params;
|
|
473
|
+
const data = (0, viem_1.encodeFunctionData)({ abi, functionName, args });
|
|
474
|
+
return walletClient.sendTransaction({
|
|
475
|
+
to: address,
|
|
476
|
+
data,
|
|
477
|
+
account: walletClient.account,
|
|
478
|
+
chain: this.chain,
|
|
479
|
+
gas: this.defaultGasLimit,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Detect if error is a simulation failure (not user rejection or validation error).
|
|
484
|
+
*
|
|
485
|
+
* @param error - The error to check
|
|
486
|
+
* @returns True if error is from simulation failure
|
|
487
|
+
*/
|
|
488
|
+
isSimulationErrorType(error) {
|
|
489
|
+
if (!isViemError(error)) {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
// User rejection is NOT a simulation error
|
|
493
|
+
if (error.code === USER_REJECTION_CODE) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
// Check for simulation-specific error patterns
|
|
497
|
+
return !!(error.message?.includes("simulation") ||
|
|
498
|
+
error.message?.includes("eth_call") ||
|
|
499
|
+
error.message?.includes("execution reverted") ||
|
|
500
|
+
error.cause?.message?.includes("simulation"));
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Set a value in the LRU cache with automatic eviction.
|
|
504
|
+
*/
|
|
505
|
+
setCacheValue(key, data) {
|
|
506
|
+
// If at capacity, remove oldest entry (first in Map)
|
|
507
|
+
if (this.escrowCache.size >= this.maxCacheSize) {
|
|
508
|
+
const oldestKey = this.escrowCache.keys().next().value;
|
|
509
|
+
if (oldestKey) {
|
|
510
|
+
this.escrowCache.delete(oldestKey);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
// Delete and re-add to move to end (most recently used)
|
|
514
|
+
this.escrowCache.delete(key);
|
|
515
|
+
this.escrowCache.set(key, { data, timestamp: Date.now() });
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Get a value from the LRU cache, refreshing its position.
|
|
519
|
+
*/
|
|
520
|
+
getCacheValue(key) {
|
|
521
|
+
const cached = this.escrowCache.get(key);
|
|
522
|
+
if (!cached)
|
|
523
|
+
return undefined;
|
|
524
|
+
// Check TTL
|
|
525
|
+
if (Date.now() - cached.timestamp > this.cacheTTL) {
|
|
526
|
+
this.escrowCache.delete(key);
|
|
527
|
+
return undefined;
|
|
528
|
+
}
|
|
529
|
+
// Move to end (most recently used)
|
|
530
|
+
this.escrowCache.delete(key);
|
|
531
|
+
this.escrowCache.set(key, cached);
|
|
532
|
+
return cached.data;
|
|
533
|
+
}
|
|
534
|
+
// ==========================================================================
|
|
535
|
+
// WALLET SIGNATURE HELPERS (EIP-712)
|
|
536
|
+
// ==========================================================================
|
|
537
|
+
/**
|
|
538
|
+
* Get the EIP-712 domain for wallet authorization signatures
|
|
539
|
+
*/
|
|
540
|
+
getWalletDomain(walletAddress) {
|
|
541
|
+
return {
|
|
542
|
+
name: "PalindromeEscrowWallet",
|
|
543
|
+
version: "1",
|
|
544
|
+
chainId: this.chain.id,
|
|
545
|
+
verifyingContract: walletAddress,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Get the EIP-712 domain for escrow contract signatures
|
|
550
|
+
*/
|
|
551
|
+
getEscrowDomain() {
|
|
552
|
+
return {
|
|
553
|
+
name: "PalindromeCryptoEscrow",
|
|
554
|
+
version: "1",
|
|
555
|
+
chainId: this.chain.id,
|
|
556
|
+
verifyingContract: this.contractAddress,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Sign a wallet authorization for a participant
|
|
561
|
+
* Used for: deposit, confirmDelivery, requestCancel, submitArbiterDecision
|
|
562
|
+
*/
|
|
563
|
+
async signWalletAuthorization(walletClient, walletAddress, escrowId) {
|
|
564
|
+
assertWalletClient(walletClient);
|
|
565
|
+
const signature = await walletClient.signTypedData({
|
|
566
|
+
account: walletClient.account,
|
|
567
|
+
domain: this.getWalletDomain(walletAddress),
|
|
568
|
+
types: this.walletAuthorizationTypes,
|
|
569
|
+
primaryType: "WalletAuthorization",
|
|
570
|
+
message: {
|
|
571
|
+
escrowId,
|
|
572
|
+
wallet: walletAddress,
|
|
573
|
+
participant: walletClient.account.address,
|
|
574
|
+
},
|
|
575
|
+
});
|
|
576
|
+
validateSignature(signature, "wallet authorization signature");
|
|
577
|
+
return signature;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Sign a confirm delivery message (for gasless meta-tx)
|
|
581
|
+
*/
|
|
582
|
+
async signConfirmDelivery(walletClient, escrowId, deadline, nonce) {
|
|
583
|
+
assertWalletClient(walletClient);
|
|
584
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
585
|
+
const signature = await walletClient.signTypedData({
|
|
586
|
+
account: walletClient.account,
|
|
587
|
+
domain: this.getEscrowDomain(),
|
|
588
|
+
types: this.confirmDeliveryTypes,
|
|
589
|
+
primaryType: "ConfirmDelivery",
|
|
590
|
+
message: {
|
|
591
|
+
escrowId,
|
|
592
|
+
buyer: deal.buyer,
|
|
593
|
+
seller: deal.seller,
|
|
594
|
+
arbiter: deal.arbiter,
|
|
595
|
+
token: deal.token,
|
|
596
|
+
amount: deal.amount,
|
|
597
|
+
depositTime: deal.depositTime,
|
|
598
|
+
deadline,
|
|
599
|
+
nonce,
|
|
600
|
+
},
|
|
601
|
+
});
|
|
602
|
+
validateSignature(signature, "confirm delivery signature");
|
|
603
|
+
return signature;
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Sign a start dispute message (for gasless meta-tx)
|
|
607
|
+
*/
|
|
608
|
+
async signStartDispute(walletClient, escrowId, deadline, nonce) {
|
|
609
|
+
assertWalletClient(walletClient);
|
|
610
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
611
|
+
const signature = await walletClient.signTypedData({
|
|
612
|
+
account: walletClient.account,
|
|
613
|
+
domain: this.getEscrowDomain(),
|
|
614
|
+
types: this.startDisputeTypes,
|
|
615
|
+
primaryType: "StartDispute",
|
|
616
|
+
message: {
|
|
617
|
+
escrowId,
|
|
618
|
+
buyer: deal.buyer,
|
|
619
|
+
seller: deal.seller,
|
|
620
|
+
arbiter: deal.arbiter,
|
|
621
|
+
token: deal.token,
|
|
622
|
+
amount: deal.amount,
|
|
623
|
+
depositTime: deal.depositTime,
|
|
624
|
+
deadline,
|
|
625
|
+
nonce,
|
|
626
|
+
},
|
|
627
|
+
});
|
|
628
|
+
validateSignature(signature, "start dispute signature");
|
|
629
|
+
return signature;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Create a signature deadline (timestamp + minutes)
|
|
633
|
+
*/
|
|
634
|
+
async createSignatureDeadline(minutesFromNow = 10) {
|
|
635
|
+
const block = await this.publicClient.getBlock();
|
|
636
|
+
return BigInt(Number(block.timestamp) + minutesFromNow * 60);
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Check if signature deadline has expired
|
|
640
|
+
*/
|
|
641
|
+
isSignatureDeadlineExpired(deadline, safetySeconds = 5) {
|
|
642
|
+
const now = Math.floor(Date.now() / 1000) + safetySeconds;
|
|
643
|
+
return BigInt(now) > deadline;
|
|
644
|
+
}
|
|
645
|
+
// ==========================================================================
|
|
646
|
+
// ADDRESS PREDICTION (CREATE2)
|
|
647
|
+
// ==========================================================================
|
|
648
|
+
/**
|
|
649
|
+
* Predict the wallet address for a given escrow ID (before creation)
|
|
650
|
+
*/
|
|
651
|
+
async predictWalletAddress(escrowId) {
|
|
652
|
+
const salt = (0, viem_1.keccak256)((0, viem_1.pad)((0, viem_1.toBytes)(escrowId), { size: 32 }));
|
|
653
|
+
// Get wallet bytecode hash from contract (cached - immutable value)
|
|
654
|
+
if (!this.walletBytecodeHashCache) {
|
|
655
|
+
this.walletBytecodeHashCache = await this.publicClient.readContract({
|
|
656
|
+
address: this.contractAddress,
|
|
657
|
+
abi: this.abiEscrow,
|
|
658
|
+
functionName: "WALLET_BYTECODE_HASH",
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
// Compute CREATE2 address
|
|
662
|
+
const encodedArgs = (0, viem_1.encodeAbiParameters)([{ type: "address" }, { type: "uint256" }], [this.contractAddress, escrowId]);
|
|
663
|
+
// Note: For accurate prediction, need actual bytecode + args hash
|
|
664
|
+
// This is a simplified version - in production, use the contract's computation
|
|
665
|
+
const initCodeHash = (0, viem_1.keccak256)((PalindromeEscrowWallet_json_1.default.bytecode + encodedArgs.slice(2)));
|
|
666
|
+
const raw = (0, viem_1.keccak256)((`0xff${this.contractAddress.slice(2)}${salt.slice(2)}${initCodeHash.slice(2)}`));
|
|
667
|
+
return (0, viem_1.getAddress)(`0x${raw.slice(26)}`);
|
|
668
|
+
}
|
|
669
|
+
// ==========================================================================
|
|
670
|
+
// ESCROW DATA READING
|
|
671
|
+
// ==========================================================================
|
|
672
|
+
/**
|
|
673
|
+
* Get raw escrow data from contract
|
|
674
|
+
*/
|
|
675
|
+
async getEscrowById(escrowId) {
|
|
676
|
+
const raw = await (0, actions_1.readContract)(this.publicClient, {
|
|
677
|
+
address: this.contractAddress,
|
|
678
|
+
abi: this.abiEscrow,
|
|
679
|
+
functionName: "getEscrow",
|
|
680
|
+
args: [escrowId],
|
|
681
|
+
});
|
|
682
|
+
return raw;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Get parsed escrow data
|
|
686
|
+
*/
|
|
687
|
+
async getEscrowByIdParsed(escrowId) {
|
|
688
|
+
const raw = await this.getEscrowById(escrowId);
|
|
689
|
+
return {
|
|
690
|
+
token: raw.token,
|
|
691
|
+
buyer: raw.buyer,
|
|
692
|
+
seller: raw.seller,
|
|
693
|
+
arbiter: raw.arbiter,
|
|
694
|
+
wallet: raw.wallet,
|
|
695
|
+
amount: raw.amount,
|
|
696
|
+
depositTime: raw.depositTime,
|
|
697
|
+
maturityTime: raw.maturityTime,
|
|
698
|
+
disputeStartTime: raw.disputeStartTime,
|
|
699
|
+
state: Number(raw.state),
|
|
700
|
+
buyerCancelRequested: raw.buyerCancelRequested,
|
|
701
|
+
sellerCancelRequested: raw.sellerCancelRequested,
|
|
702
|
+
tokenDecimals: Number(raw.tokenDecimals),
|
|
703
|
+
sellerWalletSig: raw.sellerWalletSig,
|
|
704
|
+
buyerWalletSig: raw.buyerWalletSig,
|
|
705
|
+
arbiterWalletSig: raw.arbiterWalletSig,
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Get next escrow ID
|
|
710
|
+
*/
|
|
711
|
+
async getNextEscrowId() {
|
|
712
|
+
return (0, actions_1.readContract)(this.publicClient, {
|
|
713
|
+
address: this.contractAddress,
|
|
714
|
+
abi: this.abiEscrow,
|
|
715
|
+
functionName: "nextEscrowId",
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
// ==========================================================================
|
|
719
|
+
// NONCE MANAGEMENT (Contract-Based)
|
|
720
|
+
// ==========================================================================
|
|
721
|
+
/**
|
|
722
|
+
* Get the nonce bitmap from the contract.
|
|
723
|
+
*
|
|
724
|
+
* Each bitmap word contains NONCE_BITMAP_SIZE nonce states. A set bit means the nonce is used.
|
|
725
|
+
*
|
|
726
|
+
* @param escrowId - The escrow ID
|
|
727
|
+
* @param signer - The signer's address
|
|
728
|
+
* @param wordIndex - The word index (nonce / NONCE_BITMAP_SIZE)
|
|
729
|
+
* @returns The bitmap as a bigint
|
|
730
|
+
*/
|
|
731
|
+
async getNonceBitmap(escrowId, signer, wordIndex = 0n) {
|
|
732
|
+
return this.publicClient.readContract({
|
|
733
|
+
address: this.contractAddress,
|
|
734
|
+
abi: this.abiEscrow,
|
|
735
|
+
functionName: "getNonceBitmap",
|
|
736
|
+
args: [escrowId, signer, wordIndex],
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Check if a specific nonce has been used.
|
|
741
|
+
*
|
|
742
|
+
* @param escrowId - The escrow ID
|
|
743
|
+
* @param signer - The signer's address
|
|
744
|
+
* @param nonce - The nonce to check
|
|
745
|
+
* @returns True if the nonce has been used
|
|
746
|
+
*/
|
|
747
|
+
async isNonceUsed(escrowId, signer, nonce) {
|
|
748
|
+
const wordIndex = nonce / 256n;
|
|
749
|
+
const bitIndex = nonce % 256n;
|
|
750
|
+
const bitmap = await this.getNonceBitmap(escrowId, signer, wordIndex);
|
|
751
|
+
return (bitmap & (1n << bitIndex)) !== 0n;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Get the next available nonce for a signer.
|
|
755
|
+
*
|
|
756
|
+
* Queries the contract's nonce bitmap and finds the first unused nonce.
|
|
757
|
+
* This is the recommended way to get a nonce for signed transactions.
|
|
758
|
+
*
|
|
759
|
+
* @param escrowId - The escrow ID
|
|
760
|
+
* @param signer - The signer's address
|
|
761
|
+
* @returns The next available nonce
|
|
762
|
+
* @throws SDKError if nonce space is exhausted (> 25,600 nonces used)
|
|
763
|
+
*/
|
|
764
|
+
async getUserNonce(escrowId, signer) {
|
|
765
|
+
// Start with word 0 (nonces 0-255)
|
|
766
|
+
let wordIndex = 0n;
|
|
767
|
+
while (wordIndex < PalindromeEscrowSDK.MAX_NONCE_WORDS) {
|
|
768
|
+
const bitmap = await this.getNonceBitmap(escrowId, signer, wordIndex);
|
|
769
|
+
// If bitmap is all 1s (all NONCE_BITMAP_SIZE nonces used), check next word
|
|
770
|
+
if (bitmap === (1n << 256n) - 1n) {
|
|
771
|
+
wordIndex++;
|
|
772
|
+
continue;
|
|
773
|
+
}
|
|
774
|
+
// Find first zero bit (unused nonce)
|
|
775
|
+
for (let i = 0n; i < 256n; i++) {
|
|
776
|
+
if ((bitmap & (1n << i)) === 0n) {
|
|
777
|
+
return wordIndex * 256n + i;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
// Shouldn't reach here, but just in case
|
|
781
|
+
wordIndex++;
|
|
782
|
+
}
|
|
783
|
+
throw new SDKError("Nonce space exhausted: too many nonces used for this escrow/signer", SDKErrorCode.VALIDATION_ERROR);
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Calculate estimated number of bitmap words needed for nonce count.
|
|
787
|
+
* Uses conservative estimate with buffer to minimize round trips.
|
|
788
|
+
*
|
|
789
|
+
* @param count - Number of nonces needed
|
|
790
|
+
* @returns Estimated number of bitmap words to fetch
|
|
791
|
+
*/
|
|
792
|
+
getEstimatedWordCount(count) {
|
|
793
|
+
return Math.min(Math.ceil(count / 128) + 1, // Conservative estimate with buffer
|
|
794
|
+
Number(PalindromeEscrowSDK.MAX_NONCE_WORDS));
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Detect if chain supports Multicall3 and cache result.
|
|
798
|
+
* Performs a test multicall on first invocation and caches the result.
|
|
799
|
+
*
|
|
800
|
+
* @param escrowId - Escrow ID for test call
|
|
801
|
+
* @param signer - Signer address for test call
|
|
802
|
+
*/
|
|
803
|
+
async detectMulticallSupport(escrowId, signer) {
|
|
804
|
+
if (this.multicallSupported !== null) {
|
|
805
|
+
return; // Already detected
|
|
806
|
+
}
|
|
807
|
+
this.log('debug', 'Detecting multicall support...');
|
|
808
|
+
try {
|
|
809
|
+
await (0, actions_1.multicall)(this.publicClient, {
|
|
810
|
+
contracts: [{
|
|
811
|
+
address: this.contractAddress,
|
|
812
|
+
abi: this.abiEscrow,
|
|
813
|
+
functionName: "getNonceBitmap",
|
|
814
|
+
args: [escrowId, signer, 0n],
|
|
815
|
+
}],
|
|
816
|
+
});
|
|
817
|
+
this.multicallSupported = true;
|
|
818
|
+
this.log('info', 'Multicall3 supported on this chain');
|
|
819
|
+
}
|
|
820
|
+
catch (error) {
|
|
821
|
+
const message = this.extractErrorMessage(error);
|
|
822
|
+
if (message.includes("multicall") || message.includes("Chain")) {
|
|
823
|
+
this.multicallSupported = false;
|
|
824
|
+
this.log('info', 'Multicall3 not supported, using sequential calls');
|
|
825
|
+
}
|
|
826
|
+
else {
|
|
827
|
+
throw error; // Different error, re-throw
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
/**
|
|
832
|
+
* Fetch nonce bitmaps using either multicall or sequential calls.
|
|
833
|
+
* Automatically uses multicall if supported, otherwise falls back to sequential.
|
|
834
|
+
*
|
|
835
|
+
* @param escrowId - The escrow ID
|
|
836
|
+
* @param signer - The signer's address
|
|
837
|
+
* @param wordCount - Number of bitmap words to fetch
|
|
838
|
+
* @returns Array of bitmap results with status
|
|
839
|
+
*/
|
|
840
|
+
async fetchNonceBitmaps(escrowId, signer, wordCount) {
|
|
841
|
+
if (this.multicallSupported) {
|
|
842
|
+
// Use multicall for efficiency
|
|
843
|
+
const results = await (0, actions_1.multicall)(this.publicClient, {
|
|
844
|
+
contracts: Array.from({ length: wordCount }, (_, i) => ({
|
|
845
|
+
address: this.contractAddress,
|
|
846
|
+
abi: this.abiEscrow,
|
|
847
|
+
functionName: "getNonceBitmap",
|
|
848
|
+
args: [escrowId, signer, BigInt(i)],
|
|
849
|
+
})),
|
|
850
|
+
});
|
|
851
|
+
return results;
|
|
852
|
+
}
|
|
853
|
+
else {
|
|
854
|
+
// Sequential fallback for chains without multicall
|
|
855
|
+
return Promise.all(Array.from({ length: wordCount }, async (_, i) => {
|
|
856
|
+
try {
|
|
857
|
+
const result = await this.getNonceBitmap(escrowId, signer, BigInt(i));
|
|
858
|
+
return { status: "success", result };
|
|
859
|
+
}
|
|
860
|
+
catch {
|
|
861
|
+
return { status: "failure" };
|
|
862
|
+
}
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Scan bitmap words for available (unused) nonces.
|
|
868
|
+
* Performs bit-level scanning with early exit when count is reached.
|
|
869
|
+
*
|
|
870
|
+
* @param bitmapResults - Array of bitmap words fetched from contract
|
|
871
|
+
* @param count - Maximum number of nonces to find
|
|
872
|
+
* @returns Array of available nonce values
|
|
873
|
+
*/
|
|
874
|
+
scanBitmapsForNonces(bitmapResults, count) {
|
|
875
|
+
const nonces = [];
|
|
876
|
+
for (let wordIdx = 0; wordIdx < bitmapResults.length && nonces.length < count; wordIdx++) {
|
|
877
|
+
const wordResult = bitmapResults[wordIdx];
|
|
878
|
+
if (wordResult.status !== "success" || wordResult.result === undefined) {
|
|
879
|
+
continue; // Skip failed fetches
|
|
880
|
+
}
|
|
881
|
+
const bitmap = wordResult.result;
|
|
882
|
+
const baseNonce = BigInt(wordIdx) * BigInt(NONCE_BITMAP_SIZE);
|
|
883
|
+
// Scan each bit in the word (0 = available, 1 = used)
|
|
884
|
+
for (let bitPos = 0n; bitPos < BigInt(NONCE_BITMAP_SIZE) && nonces.length < count; bitPos++) {
|
|
885
|
+
if ((bitmap & (1n << bitPos)) === 0n) {
|
|
886
|
+
nonces.push(baseNonce + bitPos);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
return nonces;
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Get multiple available nonces at once (for batch operations).
|
|
894
|
+
*
|
|
895
|
+
* @param escrowId - The escrow ID
|
|
896
|
+
* @param signer - The signer's address
|
|
897
|
+
* @param count - Number of nonces to retrieve (max NONCE_BITMAP_SIZE)
|
|
898
|
+
* @returns Array of available nonces
|
|
899
|
+
* @throws SDKError if count exceeds limit or nonce space is exhausted
|
|
900
|
+
*/
|
|
901
|
+
async getMultipleNonces(escrowId, signer, count) {
|
|
902
|
+
// 1. Input validation
|
|
903
|
+
if (count > NONCE_BITMAP_SIZE) {
|
|
904
|
+
throw new SDKError(`Cannot request more than ${NONCE_BITMAP_SIZE} nonces at once`, SDKErrorCode.VALIDATION_ERROR);
|
|
905
|
+
}
|
|
906
|
+
if (count <= 0) {
|
|
907
|
+
return [];
|
|
908
|
+
}
|
|
909
|
+
// 2. Detect multicall support (cached after first call)
|
|
910
|
+
await this.detectMulticallSupport(escrowId, signer);
|
|
911
|
+
// 3. Fetch bitmap words
|
|
912
|
+
const estimatedWords = this.getEstimatedWordCount(count);
|
|
913
|
+
const bitmapResults = await this.fetchNonceBitmaps(escrowId, signer, estimatedWords);
|
|
914
|
+
// 4. Scan bitmaps for available nonces
|
|
915
|
+
const nonces = this.scanBitmapsForNonces(bitmapResults, count);
|
|
916
|
+
// 5. Verify we found enough nonces
|
|
917
|
+
if (nonces.length < count) {
|
|
918
|
+
throw new SDKError(`Could only find ${nonces.length} available nonces out of ${count} requested`, SDKErrorCode.VALIDATION_ERROR);
|
|
919
|
+
}
|
|
920
|
+
return nonces;
|
|
921
|
+
}
|
|
922
|
+
/**
|
|
923
|
+
* @deprecated No longer needed - nonces are tracked by the contract
|
|
924
|
+
*/
|
|
925
|
+
resetNonceTracker() {
|
|
926
|
+
// No-op: Contract tracks nonces now
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
929
|
+
* Get dispute submission status
|
|
930
|
+
*/
|
|
931
|
+
async getDisputeSubmissionStatus(escrowId) {
|
|
932
|
+
const status = await this.publicClient.readContract({
|
|
933
|
+
address: this.contractAddress,
|
|
934
|
+
abi: this.abiEscrow,
|
|
935
|
+
functionName: "disputeStatus",
|
|
936
|
+
args: [escrowId],
|
|
937
|
+
});
|
|
938
|
+
const buyer = (status & 1n) !== 0n;
|
|
939
|
+
const seller = (status & 2n) !== 0n;
|
|
940
|
+
const arbiter = (status & 4n) !== 0n;
|
|
941
|
+
return { buyer, seller, arbiter, allSubmitted: buyer && seller && arbiter };
|
|
942
|
+
}
|
|
943
|
+
// ==========================================================================
|
|
944
|
+
// TOKEN UTILITIES
|
|
945
|
+
// ==========================================================================
|
|
946
|
+
async getTokenDecimals(tokenAddress) {
|
|
947
|
+
if (this.tokenDecimalsCache.has(tokenAddress)) {
|
|
948
|
+
return this.tokenDecimalsCache.get(tokenAddress);
|
|
949
|
+
}
|
|
950
|
+
const decimals = await this.publicClient.readContract({
|
|
951
|
+
address: tokenAddress,
|
|
952
|
+
abi: this.abiERC20,
|
|
953
|
+
functionName: "decimals",
|
|
954
|
+
});
|
|
955
|
+
this.tokenDecimalsCache.set(tokenAddress, decimals);
|
|
956
|
+
return decimals;
|
|
957
|
+
}
|
|
958
|
+
async getTokenBalance(account, tokenAddress) {
|
|
959
|
+
return this.publicClient.readContract({
|
|
960
|
+
address: tokenAddress,
|
|
961
|
+
abi: this.abiERC20,
|
|
962
|
+
functionName: "balanceOf",
|
|
963
|
+
args: [account],
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
async getTokenAllowance(owner, spender, tokenAddress) {
|
|
967
|
+
return this.publicClient.readContract({
|
|
968
|
+
address: tokenAddress,
|
|
969
|
+
abi: this.abiERC20,
|
|
970
|
+
functionName: "allowance",
|
|
971
|
+
args: [owner, spender],
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
formatTokenAmount(amount, decimals) {
|
|
975
|
+
const divisor = 10n ** BigInt(decimals);
|
|
976
|
+
const integerPart = amount / divisor;
|
|
977
|
+
const fractionalPart = (amount % divisor).toString().padStart(decimals, "0");
|
|
978
|
+
return `${integerPart}.${fractionalPart}`;
|
|
979
|
+
}
|
|
980
|
+
/**
|
|
981
|
+
* Approve token spending if needed
|
|
982
|
+
*/
|
|
983
|
+
async approveTokenIfNeeded(walletClient, token, spender, amount) {
|
|
984
|
+
assertWalletClient(walletClient);
|
|
985
|
+
const currentAllowance = await this.getTokenAllowance(walletClient.account.address, spender, token);
|
|
986
|
+
if (currentAllowance >= amount)
|
|
987
|
+
return null;
|
|
988
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
989
|
+
address: token,
|
|
990
|
+
abi: this.abiERC20,
|
|
991
|
+
functionName: "approve",
|
|
992
|
+
args: [spender, amount],
|
|
993
|
+
});
|
|
994
|
+
await this.waitForReceipt(hash);
|
|
995
|
+
return hash;
|
|
996
|
+
}
|
|
997
|
+
// ==========================================================================
|
|
998
|
+
// ESCROW CREATION
|
|
999
|
+
// ==========================================================================
|
|
1000
|
+
/**
|
|
1001
|
+
* Create a new escrow as the seller
|
|
1002
|
+
*
|
|
1003
|
+
* This function creates a new escrow where the caller (seller) is offering goods/services
|
|
1004
|
+
* to a buyer. The escrow starts in AWAITING_PAYMENT state until the buyer deposits funds.
|
|
1005
|
+
*
|
|
1006
|
+
* The seller's wallet authorization signature is automatically generated and attached,
|
|
1007
|
+
* which will be used later for 2-of-3 multisig withdrawals from the escrow wallet.
|
|
1008
|
+
*
|
|
1009
|
+
* @param walletClient - The seller's wallet client (must have account connected)
|
|
1010
|
+
* @param params - Escrow creation parameters
|
|
1011
|
+
* @param params.token - ERC20 token address for payment
|
|
1012
|
+
* @param params.buyer - Buyer's wallet address
|
|
1013
|
+
* @param params.amount - Payment amount in token's smallest unit (e.g., wei for 18 decimals)
|
|
1014
|
+
* @param params.maturityTimeDays - Optional days until maturity (default: 1, min: 1, max: 3650)
|
|
1015
|
+
* @param params.arbiter - Optional arbiter address for dispute resolution
|
|
1016
|
+
* @param params.title - Escrow title/description (1-500 characters, supports encrypted hashes)
|
|
1017
|
+
* @param params.ipfsHash - Optional IPFS hash for additional details
|
|
1018
|
+
* @returns Object containing escrowId, transaction hash, and wallet address
|
|
1019
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1020
|
+
* @throws {SDKError} VALIDATION_ERROR - If parameters are invalid
|
|
1021
|
+
* @throws {SDKError} TRANSACTION_FAILED - If the transaction fails
|
|
1022
|
+
*/
|
|
1023
|
+
async createEscrow(walletClient, params) {
|
|
1024
|
+
assertWalletClient(walletClient);
|
|
1025
|
+
// Validate and normalize addresses
|
|
1026
|
+
const token = validateAddress(params.token, "token");
|
|
1027
|
+
const buyer = validateAddress(params.buyer, "buyer");
|
|
1028
|
+
const arbiter = params.arbiter
|
|
1029
|
+
? validateAddress(params.arbiter, "arbiter")
|
|
1030
|
+
: viem_1.zeroAddress;
|
|
1031
|
+
const sellerAddress = walletClient.account.address;
|
|
1032
|
+
const maturityDays = params.maturityTimeDays ?? 1n;
|
|
1033
|
+
// Validate using helper
|
|
1034
|
+
this.validateCreateEscrowParams({
|
|
1035
|
+
tokenAddress: token,
|
|
1036
|
+
buyerAddress: buyer,
|
|
1037
|
+
sellerAddress,
|
|
1038
|
+
arbiterAddress: arbiter,
|
|
1039
|
+
amount: params.amount,
|
|
1040
|
+
maturityDays,
|
|
1041
|
+
title: params.title,
|
|
1042
|
+
});
|
|
1043
|
+
// Validate arbiter is not buyer or seller
|
|
1044
|
+
if (!isZeroAddress(arbiter)) {
|
|
1045
|
+
if ((0, viem_1.getAddress)(arbiter) === (0, viem_1.getAddress)(buyer)) {
|
|
1046
|
+
throw new SDKError("Arbiter cannot be the buyer", SDKErrorCode.VALIDATION_ERROR);
|
|
1047
|
+
}
|
|
1048
|
+
if ((0, viem_1.getAddress)(arbiter) === (0, viem_1.getAddress)(sellerAddress)) {
|
|
1049
|
+
throw new SDKError("Arbiter cannot be the seller", SDKErrorCode.VALIDATION_ERROR);
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const ipfsHash = params.ipfsHash ?? "";
|
|
1053
|
+
// Predict next escrow ID and wallet address
|
|
1054
|
+
const nextId = await this.getNextEscrowId();
|
|
1055
|
+
const predictedWallet = await this.predictWalletAddress(nextId);
|
|
1056
|
+
// Sign wallet authorization
|
|
1057
|
+
const sellerWalletSig = await this.signWalletAuthorization(walletClient, predictedWallet, nextId);
|
|
1058
|
+
// Create escrow
|
|
1059
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1060
|
+
address: this.contractAddress,
|
|
1061
|
+
abi: this.abiEscrow,
|
|
1062
|
+
functionName: "createEscrow",
|
|
1063
|
+
args: [
|
|
1064
|
+
token,
|
|
1065
|
+
buyer,
|
|
1066
|
+
params.amount,
|
|
1067
|
+
maturityDays,
|
|
1068
|
+
arbiter,
|
|
1069
|
+
params.title,
|
|
1070
|
+
ipfsHash,
|
|
1071
|
+
sellerWalletSig,
|
|
1072
|
+
],
|
|
1073
|
+
});
|
|
1074
|
+
const receipt = await this.waitForReceipt(hash);
|
|
1075
|
+
if (receipt.status !== "success") {
|
|
1076
|
+
throw new SDKError("Transaction failed", SDKErrorCode.TRANSACTION_FAILED, { txHash: hash });
|
|
1077
|
+
}
|
|
1078
|
+
// Parse event to get escrow ID
|
|
1079
|
+
const events = (0, viem_1.parseEventLogs)({
|
|
1080
|
+
abi: this.abiEscrow,
|
|
1081
|
+
eventName: "EscrowCreated",
|
|
1082
|
+
logs: receipt.logs,
|
|
1083
|
+
});
|
|
1084
|
+
const escrowId = events[0]?.args?.escrowId ?? nextId;
|
|
1085
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1086
|
+
return { escrowId, txHash: hash, walletAddress: deal.wallet };
|
|
1087
|
+
}
|
|
1088
|
+
/**
|
|
1089
|
+
* Create a new escrow and deposit funds as the buyer (single transaction)
|
|
1090
|
+
*
|
|
1091
|
+
* This function creates a new escrow and immediately deposits the payment in one transaction.
|
|
1092
|
+
* The escrow starts in AWAITING_DELIVERY state. The seller must call `acceptEscrow` to
|
|
1093
|
+
* provide their wallet signature before funds can be released.
|
|
1094
|
+
*
|
|
1095
|
+
* Token approval is automatically handled if needed.
|
|
1096
|
+
*
|
|
1097
|
+
* @param walletClient - The buyer's wallet client (must have account connected)
|
|
1098
|
+
* @param params - Escrow creation parameters
|
|
1099
|
+
* @param params.token - ERC20 token address for payment
|
|
1100
|
+
* @param params.seller - Seller's wallet address
|
|
1101
|
+
* @param params.amount - Payment amount in token's smallest unit (e.g., wei for 18 decimals)
|
|
1102
|
+
* @param params.maturityTimeDays - Optional days until maturity (default: 1, min: 1, max: 3650)
|
|
1103
|
+
* @param params.arbiter - Optional arbiter address for dispute resolution
|
|
1104
|
+
* @param params.title - Escrow title/description (1-500 characters, supports encrypted hashes)
|
|
1105
|
+
* @param params.ipfsHash - Optional IPFS hash for additional details
|
|
1106
|
+
* @returns Object containing escrowId, transaction hash, and wallet address
|
|
1107
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1108
|
+
* @throws {SDKError} VALIDATION_ERROR - If parameters are invalid
|
|
1109
|
+
* @throws {SDKError} INSUFFICIENT_BALANCE - If buyer has insufficient token balance
|
|
1110
|
+
* @throws {SDKError} TRANSACTION_FAILED - If the transaction fails
|
|
1111
|
+
*/
|
|
1112
|
+
async createEscrowAndDeposit(walletClient, params) {
|
|
1113
|
+
assertWalletClient(walletClient);
|
|
1114
|
+
// Validate and normalize addresses
|
|
1115
|
+
const token = validateAddress(params.token, "token");
|
|
1116
|
+
const seller = validateAddress(params.seller, "seller");
|
|
1117
|
+
const arbiter = params.arbiter
|
|
1118
|
+
? validateAddress(params.arbiter, "arbiter")
|
|
1119
|
+
: viem_1.zeroAddress;
|
|
1120
|
+
const buyerAddress = walletClient.account.address;
|
|
1121
|
+
const maturityDays = params.maturityTimeDays ?? 1n;
|
|
1122
|
+
// Validate using helper
|
|
1123
|
+
this.validateCreateEscrowParams({
|
|
1124
|
+
tokenAddress: token,
|
|
1125
|
+
buyerAddress,
|
|
1126
|
+
sellerAddress: seller,
|
|
1127
|
+
arbiterAddress: arbiter,
|
|
1128
|
+
amount: params.amount,
|
|
1129
|
+
maturityDays,
|
|
1130
|
+
title: params.title,
|
|
1131
|
+
});
|
|
1132
|
+
// Validate arbiter is not buyer or seller
|
|
1133
|
+
if (!isZeroAddress(arbiter)) {
|
|
1134
|
+
if ((0, viem_1.getAddress)(arbiter) === (0, viem_1.getAddress)(seller)) {
|
|
1135
|
+
throw new SDKError("Arbiter cannot be the seller", SDKErrorCode.VALIDATION_ERROR);
|
|
1136
|
+
}
|
|
1137
|
+
if ((0, viem_1.getAddress)(arbiter) === (0, viem_1.getAddress)(buyerAddress)) {
|
|
1138
|
+
throw new SDKError("Arbiter cannot be the buyer", SDKErrorCode.VALIDATION_ERROR);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
const ipfsHash = params.ipfsHash ?? "";
|
|
1142
|
+
// Approve token spending
|
|
1143
|
+
await this.approveTokenIfNeeded(walletClient, token, this.contractAddress, params.amount);
|
|
1144
|
+
// Predict next escrow ID and wallet address
|
|
1145
|
+
const nextId = await this.getNextEscrowId();
|
|
1146
|
+
const predictedWallet = await this.predictWalletAddress(nextId);
|
|
1147
|
+
// Sign wallet authorization
|
|
1148
|
+
const buyerWalletSig = await this.signWalletAuthorization(walletClient, predictedWallet, nextId);
|
|
1149
|
+
// Create escrow and deposit
|
|
1150
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1151
|
+
address: this.contractAddress,
|
|
1152
|
+
abi: this.abiEscrow,
|
|
1153
|
+
functionName: "createEscrowAndDeposit",
|
|
1154
|
+
args: [
|
|
1155
|
+
token,
|
|
1156
|
+
seller,
|
|
1157
|
+
params.amount,
|
|
1158
|
+
maturityDays,
|
|
1159
|
+
arbiter,
|
|
1160
|
+
params.title,
|
|
1161
|
+
ipfsHash,
|
|
1162
|
+
buyerWalletSig,
|
|
1163
|
+
],
|
|
1164
|
+
});
|
|
1165
|
+
const receipt = await this.waitForReceipt(hash);
|
|
1166
|
+
if (receipt.status !== "success") {
|
|
1167
|
+
throw new SDKError("Transaction failed", SDKErrorCode.TRANSACTION_FAILED, { txHash: hash });
|
|
1168
|
+
}
|
|
1169
|
+
// Parse event to get escrow ID
|
|
1170
|
+
const events = (0, viem_1.parseEventLogs)({
|
|
1171
|
+
abi: this.abiEscrow,
|
|
1172
|
+
eventName: "EscrowCreated",
|
|
1173
|
+
logs: receipt.logs,
|
|
1174
|
+
});
|
|
1175
|
+
const escrowId = events[0]?.args?.escrowId ?? nextId;
|
|
1176
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1177
|
+
return { escrowId, txHash: hash, walletAddress: deal.wallet };
|
|
1178
|
+
}
|
|
1179
|
+
// ==========================================================================
|
|
1180
|
+
// DEPOSIT
|
|
1181
|
+
// ==========================================================================
|
|
1182
|
+
/**
|
|
1183
|
+
* Deposit funds into an existing escrow as the buyer
|
|
1184
|
+
*
|
|
1185
|
+
* This function is used when the seller created the escrow via `createEscrow`.
|
|
1186
|
+
* The buyer deposits the required payment amount, transitioning the escrow from
|
|
1187
|
+
* AWAITING_PAYMENT to AWAITING_DELIVERY state.
|
|
1188
|
+
*
|
|
1189
|
+
* Token approval is automatically handled if needed.
|
|
1190
|
+
* The buyer's wallet authorization signature is automatically generated.
|
|
1191
|
+
*
|
|
1192
|
+
* @param walletClient - The buyer's wallet client (must have account connected)
|
|
1193
|
+
* @param escrowId - The escrow ID to deposit into
|
|
1194
|
+
* @returns Transaction hash
|
|
1195
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1196
|
+
* @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
|
|
1197
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_PAYMENT state
|
|
1198
|
+
* @throws {SDKError} INSUFFICIENT_BALANCE - If buyer has insufficient token balance
|
|
1199
|
+
*/
|
|
1200
|
+
async deposit(walletClient, escrowId) {
|
|
1201
|
+
assertWalletClient(walletClient);
|
|
1202
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1203
|
+
// Verify caller and state using helpers
|
|
1204
|
+
this.verifyBuyer(walletClient.account.address, deal);
|
|
1205
|
+
this.verifyState(deal, EscrowState.AWAITING_PAYMENT, "deposit");
|
|
1206
|
+
// Approve token spending
|
|
1207
|
+
await this.approveTokenIfNeeded(walletClient, deal.token, this.contractAddress, deal.amount);
|
|
1208
|
+
// Sign wallet authorization
|
|
1209
|
+
const buyerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
|
|
1210
|
+
// Deposit
|
|
1211
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1212
|
+
address: this.contractAddress,
|
|
1213
|
+
abi: this.abiEscrow,
|
|
1214
|
+
functionName: "deposit",
|
|
1215
|
+
args: [escrowId, buyerWalletSig],
|
|
1216
|
+
});
|
|
1217
|
+
await this.waitForReceipt(hash);
|
|
1218
|
+
return hash;
|
|
1219
|
+
}
|
|
1220
|
+
// ==========================================================================
|
|
1221
|
+
// ACCEPT ESCROW (for buyer-created escrows)
|
|
1222
|
+
// ==========================================================================
|
|
1223
|
+
/**
|
|
1224
|
+
* Accept an escrow as the seller (for buyer-created escrows)
|
|
1225
|
+
*
|
|
1226
|
+
* This function is required when the buyer created the escrow via `createEscrowAndDeposit`.
|
|
1227
|
+
* The seller must accept to provide their wallet authorization signature, which is
|
|
1228
|
+
* required for the 2-of-3 multisig withdrawal mechanism.
|
|
1229
|
+
*
|
|
1230
|
+
* Without accepting, the seller cannot receive funds even if the buyer confirms delivery.
|
|
1231
|
+
*
|
|
1232
|
+
* @param walletClient - The seller's wallet client (must have account connected)
|
|
1233
|
+
* @param escrowId - The escrow ID to accept
|
|
1234
|
+
* @returns Transaction hash
|
|
1235
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1236
|
+
* @throws {SDKError} NOT_SELLER - If caller is not the designated seller
|
|
1237
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
|
|
1238
|
+
* @throws {SDKError} ALREADY_ACCEPTED - If escrow was already accepted
|
|
1239
|
+
*/
|
|
1240
|
+
async acceptEscrow(walletClient, escrowId) {
|
|
1241
|
+
assertWalletClient(walletClient);
|
|
1242
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1243
|
+
// Verify caller and state using helpers
|
|
1244
|
+
this.verifySeller(walletClient.account.address, deal);
|
|
1245
|
+
this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "accept");
|
|
1246
|
+
// Check if already accepted (seller sig already exists)
|
|
1247
|
+
if (deal.sellerWalletSig && deal.sellerWalletSig !== "0x") {
|
|
1248
|
+
throw new SDKError("Escrow already accepted", SDKErrorCode.ALREADY_ACCEPTED);
|
|
1249
|
+
}
|
|
1250
|
+
// Sign wallet authorization
|
|
1251
|
+
const sellerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
|
|
1252
|
+
// Accept escrow
|
|
1253
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1254
|
+
address: this.contractAddress,
|
|
1255
|
+
abi: this.abiEscrow,
|
|
1256
|
+
functionName: "acceptEscrow",
|
|
1257
|
+
args: [escrowId, sellerWalletSig],
|
|
1258
|
+
});
|
|
1259
|
+
await this.waitForReceipt(hash);
|
|
1260
|
+
return hash;
|
|
1261
|
+
}
|
|
1262
|
+
// ==========================================================================
|
|
1263
|
+
// CONFIRM DELIVERY
|
|
1264
|
+
// ==========================================================================
|
|
1265
|
+
/**
|
|
1266
|
+
* Confirm delivery and release funds to the seller
|
|
1267
|
+
*
|
|
1268
|
+
* This function is called by the buyer after receiving the goods/services.
|
|
1269
|
+
* It transitions the escrow to COMPLETE state and authorizes payment release to the seller.
|
|
1270
|
+
*
|
|
1271
|
+
* After confirmation, anyone can call `withdraw` on the escrow wallet to execute
|
|
1272
|
+
* the actual token transfer (requires 2-of-3 signatures: buyer + seller).
|
|
1273
|
+
*
|
|
1274
|
+
* A 1% fee is deducted from the payment amount.
|
|
1275
|
+
*
|
|
1276
|
+
* @param walletClient - The buyer's wallet client (must have account connected)
|
|
1277
|
+
* @param escrowId - The escrow ID to confirm
|
|
1278
|
+
* @returns Transaction hash
|
|
1279
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1280
|
+
* @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
|
|
1281
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
|
|
1282
|
+
*/
|
|
1283
|
+
async confirmDelivery(walletClient, escrowId) {
|
|
1284
|
+
assertWalletClient(walletClient);
|
|
1285
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1286
|
+
// Verify caller and state using helpers
|
|
1287
|
+
this.verifyBuyer(walletClient.account.address, deal);
|
|
1288
|
+
this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "confirm delivery");
|
|
1289
|
+
// Sign wallet authorization
|
|
1290
|
+
const buyerWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
|
|
1291
|
+
// Confirm delivery
|
|
1292
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1293
|
+
address: this.contractAddress,
|
|
1294
|
+
abi: this.abiEscrow,
|
|
1295
|
+
functionName: "confirmDelivery",
|
|
1296
|
+
args: [escrowId, buyerWalletSig],
|
|
1297
|
+
});
|
|
1298
|
+
await this.waitForReceipt(hash);
|
|
1299
|
+
return hash;
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Confirm delivery via meta-transaction (gasless for buyer)
|
|
1303
|
+
*
|
|
1304
|
+
* This function allows a relayer to submit the confirm delivery transaction on behalf
|
|
1305
|
+
* of the buyer. The buyer signs the confirmation off-chain, and the relayer pays the gas.
|
|
1306
|
+
*
|
|
1307
|
+
* Use `prepareConfirmDeliverySigned` to generate the required signatures.
|
|
1308
|
+
*
|
|
1309
|
+
* @param walletClient - The relayer's wallet client (pays gas)
|
|
1310
|
+
* @param escrowId - The escrow ID to confirm
|
|
1311
|
+
* @param coordSignature - Buyer's EIP-712 signature for the confirmation
|
|
1312
|
+
* @param deadline - Signature expiration timestamp (must be within 24 hours)
|
|
1313
|
+
* @param nonce - Buyer's nonce for replay protection
|
|
1314
|
+
* @param buyerWalletSig - Buyer's wallet authorization signature
|
|
1315
|
+
* @returns Transaction hash
|
|
1316
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1317
|
+
* @throws {SDKError} SIGNATURE_EXPIRED - If deadline has passed
|
|
1318
|
+
*/
|
|
1319
|
+
async confirmDeliverySigned(walletClient, escrowId, coordSignature, deadline, nonce, buyerWalletSig) {
|
|
1320
|
+
assertWalletClient(walletClient);
|
|
1321
|
+
if (this.isSignatureDeadlineExpired(deadline)) {
|
|
1322
|
+
throw new SDKError("Signature deadline expired", SDKErrorCode.SIGNATURE_EXPIRED);
|
|
1323
|
+
}
|
|
1324
|
+
// Anyone can submit (typically relayer)
|
|
1325
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1326
|
+
address: this.contractAddress,
|
|
1327
|
+
abi: this.abiEscrow,
|
|
1328
|
+
functionName: "confirmDeliverySigned",
|
|
1329
|
+
args: [escrowId, coordSignature, deadline, nonce, buyerWalletSig],
|
|
1330
|
+
});
|
|
1331
|
+
await this.waitForReceipt(hash);
|
|
1332
|
+
return hash;
|
|
1333
|
+
}
|
|
1334
|
+
/**
|
|
1335
|
+
* Prepare signatures for gasless confirm delivery
|
|
1336
|
+
*
|
|
1337
|
+
* This helper function generates all the signatures needed for a gasless confirm delivery.
|
|
1338
|
+
* The buyer signs off-chain, and the resulting data can be sent to a relayer who will
|
|
1339
|
+
* submit the transaction and pay the gas.
|
|
1340
|
+
*
|
|
1341
|
+
* The deadline is set to 60 minutes from the current block timestamp.
|
|
1342
|
+
*
|
|
1343
|
+
* @param buyerWalletClient - The buyer's wallet client (must have account connected)
|
|
1344
|
+
* @param escrowId - The escrow ID to confirm
|
|
1345
|
+
* @returns Object containing coordSignature, buyerWalletSig, deadline, and nonce
|
|
1346
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1347
|
+
* @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
|
|
1348
|
+
*/
|
|
1349
|
+
async prepareConfirmDeliverySigned(buyerWalletClient, escrowId) {
|
|
1350
|
+
assertWalletClient(buyerWalletClient);
|
|
1351
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1352
|
+
// Verify caller using helper
|
|
1353
|
+
this.verifyBuyer(buyerWalletClient.account.address, deal);
|
|
1354
|
+
// Parallelize deadline and nonce fetching
|
|
1355
|
+
const [deadline, nonce] = await Promise.all([
|
|
1356
|
+
this.createSignatureDeadline(60), // 60 minutes
|
|
1357
|
+
this.getUserNonce(escrowId, buyerWalletClient.account.address),
|
|
1358
|
+
]);
|
|
1359
|
+
// Sign coordinator message and wallet authorization in parallel
|
|
1360
|
+
const [coordSignature, buyerWalletSig] = await Promise.all([
|
|
1361
|
+
this.signConfirmDelivery(buyerWalletClient, escrowId, deadline, nonce),
|
|
1362
|
+
this.signWalletAuthorization(buyerWalletClient, deal.wallet, escrowId),
|
|
1363
|
+
]);
|
|
1364
|
+
return { coordSignature, buyerWalletSig, deadline, nonce };
|
|
1365
|
+
}
|
|
1366
|
+
// ==========================================================================
|
|
1367
|
+
// CANCEL FLOWS
|
|
1368
|
+
// ==========================================================================
|
|
1369
|
+
/**
|
|
1370
|
+
* Request cancellation of an escrow
|
|
1371
|
+
*
|
|
1372
|
+
* This function allows either the buyer or seller to request cancellation.
|
|
1373
|
+
* Both parties must call this function for a mutual cancellation to occur.
|
|
1374
|
+
*
|
|
1375
|
+
* - If only one party requests: The request is recorded, awaiting the other party
|
|
1376
|
+
* - If both parties request: The escrow is automatically canceled and funds returned to buyer
|
|
1377
|
+
*
|
|
1378
|
+
* Use `getCancelRequestStatus` to check if the other party has already requested.
|
|
1379
|
+
*
|
|
1380
|
+
* @param walletClient - The buyer's or seller's wallet client
|
|
1381
|
+
* @param escrowId - The escrow ID to cancel
|
|
1382
|
+
* @returns Transaction hash
|
|
1383
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1384
|
+
* @throws {SDKError} INVALID_ROLE - If caller is not buyer or seller
|
|
1385
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
|
|
1386
|
+
*/
|
|
1387
|
+
async requestCancel(walletClient, escrowId) {
|
|
1388
|
+
assertWalletClient(walletClient);
|
|
1389
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1390
|
+
// Verify caller is buyer or seller
|
|
1391
|
+
const isBuyer = addressEquals(walletClient.account.address, deal.buyer);
|
|
1392
|
+
const isSeller = addressEquals(walletClient.account.address, deal.seller);
|
|
1393
|
+
if (!isBuyer && !isSeller) {
|
|
1394
|
+
throw new SDKError("Only buyer or seller can request cancel", SDKErrorCode.INVALID_ROLE);
|
|
1395
|
+
}
|
|
1396
|
+
// Verify state
|
|
1397
|
+
if (deal.state !== EscrowState.AWAITING_DELIVERY) {
|
|
1398
|
+
throw new SDKError(`Invalid state: ${this.STATE_NAMES[deal.state]}. Expected: AWAITING_DELIVERY`, SDKErrorCode.INVALID_STATE);
|
|
1399
|
+
}
|
|
1400
|
+
// Sign wallet authorization
|
|
1401
|
+
const walletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
|
|
1402
|
+
// Request cancel
|
|
1403
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1404
|
+
address: this.contractAddress,
|
|
1405
|
+
abi: this.abiEscrow,
|
|
1406
|
+
functionName: "requestCancel",
|
|
1407
|
+
args: [escrowId, walletSig],
|
|
1408
|
+
});
|
|
1409
|
+
await this.waitForReceipt(hash);
|
|
1410
|
+
return hash;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Cancel escrow by timeout (unilateral cancellation by buyer)
|
|
1414
|
+
*
|
|
1415
|
+
* This function allows the buyer to cancel the escrow unilaterally if:
|
|
1416
|
+
* - The buyer has already requested cancellation via `requestCancel`
|
|
1417
|
+
* - The maturity time has passed
|
|
1418
|
+
* - The seller has not agreed to mutual cancellation
|
|
1419
|
+
* - No dispute is active
|
|
1420
|
+
* - An arbiter is assigned to the escrow
|
|
1421
|
+
*
|
|
1422
|
+
* Funds are returned to the buyer without any fee deduction.
|
|
1423
|
+
*
|
|
1424
|
+
* @param walletClient - The buyer's wallet client (must have account connected)
|
|
1425
|
+
* @param escrowId - The escrow ID to cancel
|
|
1426
|
+
* @returns Transaction hash
|
|
1427
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1428
|
+
* @throws {SDKError} NOT_BUYER - If caller is not the designated buyer
|
|
1429
|
+
* @throws {SDKError} INVALID_STATE - If conditions for timeout cancel are not met
|
|
1430
|
+
*/
|
|
1431
|
+
async cancelByTimeout(walletClient, escrowId) {
|
|
1432
|
+
assertWalletClient(walletClient);
|
|
1433
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1434
|
+
// Verify caller using helper
|
|
1435
|
+
this.verifyBuyer(walletClient.account.address, deal);
|
|
1436
|
+
// Cancel by timeout
|
|
1437
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1438
|
+
address: this.contractAddress,
|
|
1439
|
+
abi: this.abiEscrow,
|
|
1440
|
+
functionName: "cancelByTimeout",
|
|
1441
|
+
args: [escrowId],
|
|
1442
|
+
});
|
|
1443
|
+
await this.waitForReceipt(hash);
|
|
1444
|
+
return hash;
|
|
1445
|
+
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Auto-release funds to seller after maturity time
|
|
1448
|
+
*
|
|
1449
|
+
* This function allows the seller to claim funds unilaterally after the maturity time
|
|
1450
|
+
* has passed. This protects sellers when buyers become unresponsive after receiving
|
|
1451
|
+
* goods/services.
|
|
1452
|
+
*
|
|
1453
|
+
* Requirements:
|
|
1454
|
+
* - Escrow must be in AWAITING_DELIVERY state
|
|
1455
|
+
* - No dispute has been started
|
|
1456
|
+
* - Buyer has not requested cancellation
|
|
1457
|
+
* - maturityTime has passed
|
|
1458
|
+
* - Seller has already provided wallet signature (via createEscrow or acceptEscrow)
|
|
1459
|
+
*
|
|
1460
|
+
* A 1% fee is deducted from the payment amount.
|
|
1461
|
+
*
|
|
1462
|
+
* @param walletClient - The seller's wallet client (must have account connected)
|
|
1463
|
+
* @param escrowId - The escrow ID to auto-release
|
|
1464
|
+
* @returns Transaction hash
|
|
1465
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1466
|
+
* @throws {SDKError} NOT_SELLER - If caller is not the designated seller
|
|
1467
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
|
|
1468
|
+
* @throws {SDKError} INVALID_STATE - If dispute is active or buyer requested cancel
|
|
1469
|
+
* @throws {SDKError} VALIDATION_ERROR - If seller wallet signature is missing
|
|
1470
|
+
*/
|
|
1471
|
+
async autoRelease(walletClient, escrowId) {
|
|
1472
|
+
assertWalletClient(walletClient);
|
|
1473
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1474
|
+
// Verify caller and state using helpers
|
|
1475
|
+
this.verifySeller(walletClient.account.address, deal);
|
|
1476
|
+
this.verifyState(deal, EscrowState.AWAITING_DELIVERY, "auto-release");
|
|
1477
|
+
// Verify no dispute is active
|
|
1478
|
+
if (deal.disputeStartTime > 0n) {
|
|
1479
|
+
throw new SDKError("Cannot auto-release: dispute is active", SDKErrorCode.INVALID_STATE);
|
|
1480
|
+
}
|
|
1481
|
+
// Verify buyer hasn't requested cancellation
|
|
1482
|
+
if (deal.buyerCancelRequested) {
|
|
1483
|
+
throw new SDKError("Cannot auto-release: buyer has requested cancellation", SDKErrorCode.INVALID_STATE);
|
|
1484
|
+
}
|
|
1485
|
+
// Verify deposit exists
|
|
1486
|
+
if (deal.depositTime === 0n) {
|
|
1487
|
+
throw new SDKError("Cannot auto-release: no deposit made", SDKErrorCode.INVALID_STATE);
|
|
1488
|
+
}
|
|
1489
|
+
// Verify seller signature exists
|
|
1490
|
+
if (!deal.sellerWalletSig || deal.sellerWalletSig === "0x" || deal.sellerWalletSig.length !== 132) {
|
|
1491
|
+
throw new SDKError("Cannot auto-release: seller wallet signature missing. Call acceptEscrow first if buyer created the escrow.", SDKErrorCode.VALIDATION_ERROR);
|
|
1492
|
+
}
|
|
1493
|
+
// Auto-release
|
|
1494
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1495
|
+
address: this.contractAddress,
|
|
1496
|
+
abi: this.abiEscrow,
|
|
1497
|
+
functionName: "autoRelease",
|
|
1498
|
+
args: [escrowId],
|
|
1499
|
+
});
|
|
1500
|
+
await this.waitForReceipt(hash);
|
|
1501
|
+
return hash;
|
|
1502
|
+
}
|
|
1503
|
+
// ==========================================================================
|
|
1504
|
+
// DISPUTE FLOWS
|
|
1505
|
+
// ==========================================================================
|
|
1506
|
+
/**
|
|
1507
|
+
* Start a dispute for an escrow
|
|
1508
|
+
*
|
|
1509
|
+
* This function initiates a dispute when there's a disagreement between buyer and seller.
|
|
1510
|
+
* Once started, the escrow enters DISPUTED state and requires arbiter resolution.
|
|
1511
|
+
*
|
|
1512
|
+
* Either the buyer or seller can start a dispute. An arbiter must be assigned to the
|
|
1513
|
+
* escrow for disputes to be possible.
|
|
1514
|
+
*
|
|
1515
|
+
* After starting a dispute:
|
|
1516
|
+
* 1. Both parties should submit evidence via `submitDisputeMessage`
|
|
1517
|
+
* 2. The arbiter reviews evidence and makes a decision via `submitArbiterDecision`
|
|
1518
|
+
* 3. The arbiter can rule in favor of buyer (REFUNDED) or seller (COMPLETE)
|
|
1519
|
+
*
|
|
1520
|
+
* @param walletClient - The buyer's or seller's wallet client
|
|
1521
|
+
* @param escrowId - The escrow ID to dispute
|
|
1522
|
+
* @returns Transaction hash
|
|
1523
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1524
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in AWAITING_DELIVERY state
|
|
1525
|
+
*/
|
|
1526
|
+
async startDispute(walletClient, escrowId) {
|
|
1527
|
+
assertWalletClient(walletClient);
|
|
1528
|
+
// Start dispute
|
|
1529
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1530
|
+
address: this.contractAddress,
|
|
1531
|
+
abi: this.abiEscrow,
|
|
1532
|
+
functionName: "startDispute",
|
|
1533
|
+
args: [escrowId],
|
|
1534
|
+
});
|
|
1535
|
+
await this.waitForReceipt(hash);
|
|
1536
|
+
return hash;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Start a dispute via meta-transaction (gasless for buyer/seller)
|
|
1540
|
+
*
|
|
1541
|
+
* This function allows a relayer to submit the start dispute transaction on behalf
|
|
1542
|
+
* of the buyer or seller. The initiator signs off-chain, and the relayer pays the gas.
|
|
1543
|
+
*
|
|
1544
|
+
* Use `signStartDispute` to generate the required signature.
|
|
1545
|
+
*
|
|
1546
|
+
* @param walletClient - The relayer's wallet client (pays gas)
|
|
1547
|
+
* @param escrowId - The escrow ID to dispute
|
|
1548
|
+
* @param signature - Buyer's or seller's EIP-712 signature
|
|
1549
|
+
* @param deadline - Signature expiration timestamp (must be within 24 hours)
|
|
1550
|
+
* @param nonce - Signer's nonce for replay protection
|
|
1551
|
+
* @returns Transaction hash
|
|
1552
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1553
|
+
* @throws {SDKError} SIGNATURE_EXPIRED - If deadline has passed
|
|
1554
|
+
*/
|
|
1555
|
+
async startDisputeSigned(walletClient, escrowId, signature, deadline, nonce) {
|
|
1556
|
+
assertWalletClient(walletClient);
|
|
1557
|
+
if (this.isSignatureDeadlineExpired(deadline)) {
|
|
1558
|
+
throw new SDKError("Signature deadline expired", SDKErrorCode.SIGNATURE_EXPIRED);
|
|
1559
|
+
}
|
|
1560
|
+
// Anyone can submit (typically relayer)
|
|
1561
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1562
|
+
address: this.contractAddress,
|
|
1563
|
+
abi: this.abiEscrow,
|
|
1564
|
+
functionName: "startDisputeSigned",
|
|
1565
|
+
args: [escrowId, signature, deadline, nonce],
|
|
1566
|
+
});
|
|
1567
|
+
await this.waitForReceipt(hash);
|
|
1568
|
+
return hash;
|
|
1569
|
+
}
|
|
1570
|
+
/**
|
|
1571
|
+
* Submit evidence for a dispute
|
|
1572
|
+
*
|
|
1573
|
+
* This function allows the buyer or seller to submit evidence supporting their case.
|
|
1574
|
+
* Each party can only submit evidence once. Evidence is stored on IPFS and the hash
|
|
1575
|
+
* is recorded on-chain.
|
|
1576
|
+
*
|
|
1577
|
+
* The arbiter will review submitted evidence before making a decision. Both parties
|
|
1578
|
+
* should submit evidence for a fair resolution. After 30 days, the arbiter can make
|
|
1579
|
+
* a decision even without complete evidence.
|
|
1580
|
+
*
|
|
1581
|
+
* @param walletClient - The buyer's or seller's wallet client
|
|
1582
|
+
* @param escrowId - The escrow ID
|
|
1583
|
+
* @param role - The caller's role (Role.Buyer or Role.Seller)
|
|
1584
|
+
* @param ipfsHash - IPFS hash containing the evidence (max 500 characters)
|
|
1585
|
+
* @returns Transaction hash
|
|
1586
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1587
|
+
* @throws {SDKError} EVIDENCE_ALREADY_SUBMITTED - If caller already submitted evidence
|
|
1588
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in DISPUTED state
|
|
1589
|
+
*/
|
|
1590
|
+
async submitDisputeMessage(walletClient, escrowId, role, ipfsHash) {
|
|
1591
|
+
assertWalletClient(walletClient);
|
|
1592
|
+
// Check if already submitted
|
|
1593
|
+
const hasSubmitted = await this.hasSubmittedEvidence(escrowId, role);
|
|
1594
|
+
if (hasSubmitted) {
|
|
1595
|
+
throw new SDKError(`${role === Role.Buyer ? "Buyer" : "Seller"} has already submitted evidence`, SDKErrorCode.EVIDENCE_ALREADY_SUBMITTED);
|
|
1596
|
+
}
|
|
1597
|
+
// Submit dispute message
|
|
1598
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1599
|
+
address: this.contractAddress,
|
|
1600
|
+
abi: this.abiEscrow,
|
|
1601
|
+
functionName: "submitDisputeMessage",
|
|
1602
|
+
args: [escrowId, role, ipfsHash],
|
|
1603
|
+
});
|
|
1604
|
+
await this.waitForReceipt(hash);
|
|
1605
|
+
return hash;
|
|
1606
|
+
}
|
|
1607
|
+
/**
|
|
1608
|
+
* Submit arbiter's decision to resolve a dispute
|
|
1609
|
+
*
|
|
1610
|
+
* This function is called by the designated arbiter to resolve a dispute.
|
|
1611
|
+
* The arbiter reviews evidence submitted by both parties and makes a final decision.
|
|
1612
|
+
*
|
|
1613
|
+
* The arbiter can rule:
|
|
1614
|
+
* - DisputeResolution.Complete (3): Funds go to seller (with 1% fee)
|
|
1615
|
+
* - DisputeResolution.Refunded (4): Funds go to buyer (no fee)
|
|
1616
|
+
*
|
|
1617
|
+
* Requirements:
|
|
1618
|
+
* - Both parties must have submitted evidence, OR
|
|
1619
|
+
* - 30 days + 1 hour timeout has passed since dispute started
|
|
1620
|
+
*
|
|
1621
|
+
* The arbiter's wallet signature is automatically generated for the multisig.
|
|
1622
|
+
*
|
|
1623
|
+
* @param walletClient - The arbiter's wallet client (must have account connected)
|
|
1624
|
+
* @param escrowId - The escrow ID to resolve
|
|
1625
|
+
* @param resolution - DisputeResolution.Complete or DisputeResolution.Refunded
|
|
1626
|
+
* @param ipfsHash - IPFS hash containing the decision explanation
|
|
1627
|
+
* @returns Transaction hash
|
|
1628
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1629
|
+
* @throws {SDKError} NOT_ARBITER - If caller is not the designated arbiter
|
|
1630
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in DISPUTED state
|
|
1631
|
+
*/
|
|
1632
|
+
async submitArbiterDecision(walletClient, escrowId, resolution, ipfsHash) {
|
|
1633
|
+
assertWalletClient(walletClient);
|
|
1634
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1635
|
+
// Verify caller using helper
|
|
1636
|
+
this.verifyArbiter(walletClient.account.address, deal);
|
|
1637
|
+
// Sign wallet authorization
|
|
1638
|
+
const arbiterWalletSig = await this.signWalletAuthorization(walletClient, deal.wallet, escrowId);
|
|
1639
|
+
// Submit decision
|
|
1640
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1641
|
+
address: this.contractAddress,
|
|
1642
|
+
abi: this.abiEscrow,
|
|
1643
|
+
functionName: "submitArbiterDecision",
|
|
1644
|
+
args: [escrowId, resolution, ipfsHash, arbiterWalletSig],
|
|
1645
|
+
});
|
|
1646
|
+
await this.waitForReceipt(hash);
|
|
1647
|
+
return hash;
|
|
1648
|
+
}
|
|
1649
|
+
// ==========================================================================
|
|
1650
|
+
// WALLET WITHDRAWAL
|
|
1651
|
+
// ==========================================================================
|
|
1652
|
+
/**
|
|
1653
|
+
* Withdraw funds from the escrow wallet
|
|
1654
|
+
*
|
|
1655
|
+
* This function executes the actual token transfer from the escrow wallet to the
|
|
1656
|
+
* designated recipient. The wallet contract uses a 2-of-3 multisig mechanism:
|
|
1657
|
+
*
|
|
1658
|
+
* - COMPLETE state: Requires buyer + seller signatures → funds go to seller
|
|
1659
|
+
* - REFUNDED state: Requires buyer + arbiter signatures → funds go to buyer
|
|
1660
|
+
* - CANCELED state: Requires buyer + seller signatures → funds go to buyer
|
|
1661
|
+
*
|
|
1662
|
+
* Anyone can call this function (typically the recipient), as the signatures
|
|
1663
|
+
* were already collected during the escrow lifecycle. The wallet contract
|
|
1664
|
+
* automatically reads and verifies signatures from the escrow contract.
|
|
1665
|
+
*
|
|
1666
|
+
* @param walletClient - Any wallet client (typically the recipient)
|
|
1667
|
+
* @param escrowId - The escrow ID to withdraw from
|
|
1668
|
+
* @returns Transaction hash
|
|
1669
|
+
* @throws {SDKError} WALLET_NOT_CONNECTED - If wallet client is not connected
|
|
1670
|
+
* @throws {SDKError} INVALID_STATE - If escrow is not in a final state (COMPLETE/REFUNDED/CANCELED)
|
|
1671
|
+
*/
|
|
1672
|
+
async withdraw(walletClient, escrowId) {
|
|
1673
|
+
assertWalletClient(walletClient);
|
|
1674
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1675
|
+
// Verify final state
|
|
1676
|
+
if (![EscrowState.COMPLETE, EscrowState.REFUNDED, EscrowState.CANCELED].includes(deal.state)) {
|
|
1677
|
+
throw new SDKError(`Cannot withdraw in state: ${this.STATE_NAMES[deal.state]}`, SDKErrorCode.INVALID_STATE);
|
|
1678
|
+
}
|
|
1679
|
+
// Withdraw from wallet
|
|
1680
|
+
const hash = await this.resilientWriteContract(walletClient, {
|
|
1681
|
+
address: deal.wallet,
|
|
1682
|
+
abi: this.abiWallet,
|
|
1683
|
+
functionName: "withdraw",
|
|
1684
|
+
args: [],
|
|
1685
|
+
});
|
|
1686
|
+
await this.waitForReceipt(hash);
|
|
1687
|
+
return hash;
|
|
1688
|
+
}
|
|
1689
|
+
/**
|
|
1690
|
+
* Get valid signature count for wallet
|
|
1691
|
+
*/
|
|
1692
|
+
async getWalletSignatureCount(escrowId) {
|
|
1693
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1694
|
+
const count = await this.publicClient.readContract({
|
|
1695
|
+
address: deal.wallet,
|
|
1696
|
+
abi: this.abiWallet,
|
|
1697
|
+
functionName: "getValidSignatureCount",
|
|
1698
|
+
});
|
|
1699
|
+
return Number(count);
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Get wallet balance
|
|
1703
|
+
*/
|
|
1704
|
+
async getWalletBalance(escrowId) {
|
|
1705
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1706
|
+
return this.publicClient.readContract({
|
|
1707
|
+
address: deal.wallet,
|
|
1708
|
+
abi: this.abiWallet,
|
|
1709
|
+
functionName: "getBalance",
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
// ==========================================================================
|
|
1713
|
+
// SUBGRAPH QUERIES
|
|
1714
|
+
// ==========================================================================
|
|
1715
|
+
async getEscrows() {
|
|
1716
|
+
const { data } = await this.apollo.query({
|
|
1717
|
+
query: queries_1.ALL_ESCROWS_QUERY,
|
|
1718
|
+
fetchPolicy: "network-only",
|
|
1719
|
+
});
|
|
1720
|
+
return data?.escrows ?? [];
|
|
1721
|
+
}
|
|
1722
|
+
async getEscrowsByBuyer(buyer) {
|
|
1723
|
+
const { data } = await this.apollo.query({
|
|
1724
|
+
query: queries_1.ESCROWS_BY_BUYER_QUERY,
|
|
1725
|
+
variables: { buyer: buyer.toLowerCase() },
|
|
1726
|
+
fetchPolicy: "network-only",
|
|
1727
|
+
});
|
|
1728
|
+
return data?.escrows ?? [];
|
|
1729
|
+
}
|
|
1730
|
+
async getEscrowsBySeller(seller) {
|
|
1731
|
+
const { data } = await this.apollo.query({
|
|
1732
|
+
query: queries_1.ESCROWS_BY_SELLER_QUERY,
|
|
1733
|
+
variables: { seller: seller.toLowerCase() },
|
|
1734
|
+
fetchPolicy: "network-only",
|
|
1735
|
+
});
|
|
1736
|
+
return data?.escrows ?? [];
|
|
1737
|
+
}
|
|
1738
|
+
async getEscrowDetail(id) {
|
|
1739
|
+
const { data } = await this.apollo.query({
|
|
1740
|
+
query: queries_1.ESCROW_DETAIL_QUERY,
|
|
1741
|
+
variables: { id },
|
|
1742
|
+
fetchPolicy: "network-only",
|
|
1743
|
+
});
|
|
1744
|
+
return data?.escrow;
|
|
1745
|
+
}
|
|
1746
|
+
async getDisputeMessages(escrowId) {
|
|
1747
|
+
const { data } = await this.apollo.query({
|
|
1748
|
+
query: queries_1.DISPUTE_MESSAGES_BY_ESCROW_QUERY,
|
|
1749
|
+
variables: { escrowId },
|
|
1750
|
+
fetchPolicy: "network-only",
|
|
1751
|
+
});
|
|
1752
|
+
return data?.escrow?.disputeMessages ?? [];
|
|
1753
|
+
}
|
|
1754
|
+
// ==========================================================================
|
|
1755
|
+
// UTILITY METHODS
|
|
1756
|
+
// ==========================================================================
|
|
1757
|
+
/**
|
|
1758
|
+
* Get a human-readable status label with color and description for an escrow state.
|
|
1759
|
+
* This is useful for displaying escrow status in UIs.
|
|
1760
|
+
*
|
|
1761
|
+
* @param state - The escrow state enum value
|
|
1762
|
+
* @returns An object containing:
|
|
1763
|
+
* - label: Human-readable status name
|
|
1764
|
+
* - color: Suggested UI color (orange, blue, red, green, gray)
|
|
1765
|
+
* - description: Detailed explanation of what this state means
|
|
1766
|
+
*
|
|
1767
|
+
* @example
|
|
1768
|
+
* ```typescript
|
|
1769
|
+
* const sdk = new PalindromeEscrowSDK(...);
|
|
1770
|
+
* const escrow = await sdk.getEscrowByIdParsed(1n);
|
|
1771
|
+
* const status = sdk.getStatusLabel(escrow.state);
|
|
1772
|
+
* console.log(status.label); // "Awaiting Payment"
|
|
1773
|
+
* console.log(status.color); // "orange"
|
|
1774
|
+
* console.log(status.description); // "Buyer needs to deposit funds"
|
|
1775
|
+
* ```
|
|
1776
|
+
*/
|
|
1777
|
+
getStatusLabel(state) {
|
|
1778
|
+
const labels = {
|
|
1779
|
+
[EscrowState.AWAITING_PAYMENT]: {
|
|
1780
|
+
label: "Awaiting Payment",
|
|
1781
|
+
color: "orange",
|
|
1782
|
+
description: "Buyer needs to deposit funds",
|
|
1783
|
+
},
|
|
1784
|
+
[EscrowState.AWAITING_DELIVERY]: {
|
|
1785
|
+
label: "Awaiting Delivery",
|
|
1786
|
+
color: "blue",
|
|
1787
|
+
description: "Seller should deliver product/service",
|
|
1788
|
+
},
|
|
1789
|
+
[EscrowState.DISPUTED]: {
|
|
1790
|
+
label: "Disputed",
|
|
1791
|
+
color: "red",
|
|
1792
|
+
description: "Dispute in progress - arbiter will resolve",
|
|
1793
|
+
},
|
|
1794
|
+
[EscrowState.COMPLETE]: {
|
|
1795
|
+
label: "Complete",
|
|
1796
|
+
color: "green",
|
|
1797
|
+
description: "Transaction completed successfully",
|
|
1798
|
+
},
|
|
1799
|
+
[EscrowState.REFUNDED]: {
|
|
1800
|
+
label: "Refunded",
|
|
1801
|
+
color: "gray",
|
|
1802
|
+
description: "Funds returned to buyer",
|
|
1803
|
+
},
|
|
1804
|
+
[EscrowState.CANCELED]: {
|
|
1805
|
+
label: "Canceled",
|
|
1806
|
+
color: "gray",
|
|
1807
|
+
description: "Escrow was canceled",
|
|
1808
|
+
},
|
|
1809
|
+
};
|
|
1810
|
+
return labels[state];
|
|
1811
|
+
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Get user role for an escrow
|
|
1814
|
+
*/
|
|
1815
|
+
getUserRole(userAddress, escrow) {
|
|
1816
|
+
if (addressEquals(userAddress, escrow.buyer))
|
|
1817
|
+
return Role.Buyer;
|
|
1818
|
+
if (addressEquals(userAddress, escrow.seller))
|
|
1819
|
+
return Role.Seller;
|
|
1820
|
+
if (addressEquals(userAddress, escrow.arbiter))
|
|
1821
|
+
return Role.Arbiter;
|
|
1822
|
+
return Role.None;
|
|
1823
|
+
}
|
|
1824
|
+
// ==========================================================================
|
|
1825
|
+
// SIMULATION & GAS ESTIMATION
|
|
1826
|
+
// ==========================================================================
|
|
1827
|
+
/**
|
|
1828
|
+
* Simulate a transaction before executing
|
|
1829
|
+
* Returns success status, gas estimate, and revert reason if failed
|
|
1830
|
+
*/
|
|
1831
|
+
async simulateTransaction(walletClient, functionName, args, contractAddress) {
|
|
1832
|
+
assertWalletClient(walletClient);
|
|
1833
|
+
const target = contractAddress ?? this.contractAddress;
|
|
1834
|
+
try {
|
|
1835
|
+
// Simulate the call
|
|
1836
|
+
const { result } = await this.publicClient.simulateContract({
|
|
1837
|
+
address: target,
|
|
1838
|
+
abi: this.abiEscrow,
|
|
1839
|
+
functionName,
|
|
1840
|
+
args,
|
|
1841
|
+
account: walletClient.account,
|
|
1842
|
+
});
|
|
1843
|
+
// Estimate gas
|
|
1844
|
+
const gasEstimate = await this.publicClient.estimateContractGas({
|
|
1845
|
+
address: target,
|
|
1846
|
+
abi: this.abiEscrow,
|
|
1847
|
+
functionName,
|
|
1848
|
+
args,
|
|
1849
|
+
account: walletClient.account,
|
|
1850
|
+
});
|
|
1851
|
+
return {
|
|
1852
|
+
success: true,
|
|
1853
|
+
gasEstimate,
|
|
1854
|
+
result,
|
|
1855
|
+
};
|
|
1856
|
+
}
|
|
1857
|
+
catch (error) {
|
|
1858
|
+
// Extract revert reason
|
|
1859
|
+
let revertReason = "Unknown error";
|
|
1860
|
+
if (isViemError(error)) {
|
|
1861
|
+
if (error.cause?.reason) {
|
|
1862
|
+
revertReason = error.cause.reason;
|
|
1863
|
+
}
|
|
1864
|
+
else if (hasShortMessage(error)) {
|
|
1865
|
+
revertReason = error.shortMessage;
|
|
1866
|
+
}
|
|
1867
|
+
else if (error.message) {
|
|
1868
|
+
// Try to parse revert reason from error message
|
|
1869
|
+
const match = error.message.match(/reverted with reason string '([^']+)'/);
|
|
1870
|
+
if (match) {
|
|
1871
|
+
revertReason = match[1];
|
|
1872
|
+
}
|
|
1873
|
+
else {
|
|
1874
|
+
revertReason = error.message.slice(0, 200);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
return {
|
|
1879
|
+
success: false,
|
|
1880
|
+
revertReason,
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
/**
|
|
1885
|
+
* Simulate deposit before executing
|
|
1886
|
+
*/
|
|
1887
|
+
async simulateDeposit(walletClient, escrowId) {
|
|
1888
|
+
assertWalletClient(walletClient);
|
|
1889
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1890
|
+
// Check approval first
|
|
1891
|
+
const allowance = await this.getTokenAllowance(walletClient.account.address, this.contractAddress, deal.token);
|
|
1892
|
+
const needsApproval = allowance < deal.amount;
|
|
1893
|
+
// Simulate the deposit (without actual wallet sig for simulation)
|
|
1894
|
+
const result = await this.simulateTransaction(walletClient, "deposit", [escrowId, "0x"]);
|
|
1895
|
+
return {
|
|
1896
|
+
...result,
|
|
1897
|
+
needsApproval,
|
|
1898
|
+
approvalAmount: needsApproval ? deal.amount - allowance : 0n,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
/**
|
|
1902
|
+
* Simulate confirm delivery before executing
|
|
1903
|
+
*/
|
|
1904
|
+
async simulateConfirmDelivery(walletClient, escrowId) {
|
|
1905
|
+
return this.simulateTransaction(walletClient, "confirmDelivery", [escrowId, "0x"]);
|
|
1906
|
+
}
|
|
1907
|
+
/**
|
|
1908
|
+
* Simulate withdraw before executing
|
|
1909
|
+
*/
|
|
1910
|
+
async simulateWithdraw(walletClient, escrowId) {
|
|
1911
|
+
assertWalletClient(walletClient);
|
|
1912
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
1913
|
+
const signatureCount = await this.getWalletSignatureCount(escrowId);
|
|
1914
|
+
try {
|
|
1915
|
+
const gasEstimate = await this.publicClient.estimateContractGas({
|
|
1916
|
+
address: deal.wallet,
|
|
1917
|
+
abi: this.abiWallet,
|
|
1918
|
+
functionName: "withdraw",
|
|
1919
|
+
args: [],
|
|
1920
|
+
account: walletClient.account,
|
|
1921
|
+
});
|
|
1922
|
+
return {
|
|
1923
|
+
success: true,
|
|
1924
|
+
gasEstimate,
|
|
1925
|
+
signatureCount,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
catch (error) {
|
|
1929
|
+
let revertReason = "Unknown error";
|
|
1930
|
+
if (error?.shortMessage) {
|
|
1931
|
+
revertReason = error.shortMessage;
|
|
1932
|
+
}
|
|
1933
|
+
else if (error?.message) {
|
|
1934
|
+
revertReason = error.message.slice(0, 200);
|
|
1935
|
+
}
|
|
1936
|
+
return {
|
|
1937
|
+
success: false,
|
|
1938
|
+
revertReason,
|
|
1939
|
+
signatureCount,
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
/**
|
|
1944
|
+
* Estimate gas for a transaction with buffer
|
|
1945
|
+
*/
|
|
1946
|
+
async estimateGasWithBuffer(walletClient, functionName, args, contractAddress) {
|
|
1947
|
+
assertWalletClient(walletClient);
|
|
1948
|
+
const target = contractAddress ?? this.contractAddress;
|
|
1949
|
+
const gasEstimate = await this.publicClient.estimateContractGas({
|
|
1950
|
+
address: target,
|
|
1951
|
+
abi: this.abiEscrow,
|
|
1952
|
+
functionName,
|
|
1953
|
+
args,
|
|
1954
|
+
account: walletClient.account,
|
|
1955
|
+
});
|
|
1956
|
+
// Add buffer (default 20%)
|
|
1957
|
+
return gasEstimate + (gasEstimate * BigInt(this.gasBuffer)) / 100n;
|
|
1958
|
+
}
|
|
1959
|
+
// ==========================================================================
|
|
1960
|
+
// HEALTH CHECK
|
|
1961
|
+
// ==========================================================================
|
|
1962
|
+
/**
|
|
1963
|
+
* Health check
|
|
1964
|
+
*/
|
|
1965
|
+
async healthCheck() {
|
|
1966
|
+
const errors = [];
|
|
1967
|
+
let rpcConnected = false;
|
|
1968
|
+
let contractDeployed = false;
|
|
1969
|
+
let subgraphConnected = false;
|
|
1970
|
+
try {
|
|
1971
|
+
await this.publicClient.getBlockNumber();
|
|
1972
|
+
rpcConnected = true;
|
|
1973
|
+
}
|
|
1974
|
+
catch (e) {
|
|
1975
|
+
const message = this.extractErrorMessage(e);
|
|
1976
|
+
errors.push(`RPC error: ${message}`);
|
|
1977
|
+
}
|
|
1978
|
+
try {
|
|
1979
|
+
await this.getNextEscrowId();
|
|
1980
|
+
contractDeployed = true;
|
|
1981
|
+
}
|
|
1982
|
+
catch (e) {
|
|
1983
|
+
const message = this.extractErrorMessage(e);
|
|
1984
|
+
errors.push(`Contract error: ${message}`);
|
|
1985
|
+
}
|
|
1986
|
+
try {
|
|
1987
|
+
await this.getEscrows();
|
|
1988
|
+
subgraphConnected = true;
|
|
1989
|
+
}
|
|
1990
|
+
catch (e) {
|
|
1991
|
+
const message = this.extractErrorMessage(e);
|
|
1992
|
+
errors.push(`Subgraph error: ${message}`);
|
|
1993
|
+
}
|
|
1994
|
+
return { rpcConnected, contractDeployed, subgraphConnected, errors };
|
|
1995
|
+
}
|
|
1996
|
+
// ==========================================================================
|
|
1997
|
+
// CACHE MANAGEMENT
|
|
1998
|
+
// ==========================================================================
|
|
1999
|
+
/**
|
|
2000
|
+
* Get escrow status with optional caching
|
|
2001
|
+
*/
|
|
2002
|
+
async getEscrowStatus(escrowId, forceRefresh = false) {
|
|
2003
|
+
const cacheKey = `status-${escrowId}`;
|
|
2004
|
+
if (!forceRefresh) {
|
|
2005
|
+
const cached = this.getCacheValue(cacheKey);
|
|
2006
|
+
if (cached)
|
|
2007
|
+
return cached;
|
|
2008
|
+
}
|
|
2009
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
2010
|
+
const statusInfo = this.getStatusLabel(deal.state);
|
|
2011
|
+
const result = {
|
|
2012
|
+
state: deal.state,
|
|
2013
|
+
stateName: this.STATE_NAMES[deal.state],
|
|
2014
|
+
...statusInfo,
|
|
2015
|
+
};
|
|
2016
|
+
this.setCacheValue(cacheKey, result);
|
|
2017
|
+
return result;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Get cache statistics
|
|
2021
|
+
*/
|
|
2022
|
+
getCacheStats() {
|
|
2023
|
+
return {
|
|
2024
|
+
escrowCacheSize: this.escrowCache.size,
|
|
2025
|
+
tokenDecimalsCacheSize: this.tokenDecimalsCache.size,
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Clear all caches
|
|
2030
|
+
*/
|
|
2031
|
+
clearAllCaches() {
|
|
2032
|
+
this.escrowCache.clear();
|
|
2033
|
+
this.tokenDecimalsCache.clear();
|
|
2034
|
+
this.feeReceiverCache = null;
|
|
2035
|
+
this.cachedFeeBps = null;
|
|
2036
|
+
this.multicallSupported = null;
|
|
2037
|
+
// Note: walletBytecodeHashCache is immutable and never needs clearing
|
|
2038
|
+
}
|
|
2039
|
+
/**
|
|
2040
|
+
* Clear escrow cache only
|
|
2041
|
+
*/
|
|
2042
|
+
clearEscrowCache() {
|
|
2043
|
+
this.escrowCache.clear();
|
|
2044
|
+
}
|
|
2045
|
+
/**
|
|
2046
|
+
* Clear multicall cache (useful when switching chains)
|
|
2047
|
+
*/
|
|
2048
|
+
clearMulticallCache() {
|
|
2049
|
+
this.multicallSupported = null;
|
|
2050
|
+
}
|
|
2051
|
+
// ==========================================================================
|
|
2052
|
+
// EVENT WATCHING
|
|
2053
|
+
// ==========================================================================
|
|
2054
|
+
/**
|
|
2055
|
+
* Watch for escrow events related to a user
|
|
2056
|
+
*/
|
|
2057
|
+
watchUserEscrows(userAddress, callback, options) {
|
|
2058
|
+
const unwatch = this.publicClient.watchContractEvent({
|
|
2059
|
+
address: this.contractAddress,
|
|
2060
|
+
abi: this.abiEscrow,
|
|
2061
|
+
eventName: "EscrowCreated",
|
|
2062
|
+
fromBlock: options?.fromBlock,
|
|
2063
|
+
onLogs: (logs) => {
|
|
2064
|
+
for (const log of logs) {
|
|
2065
|
+
const parsedLog = log;
|
|
2066
|
+
if (!parsedLog.args) {
|
|
2067
|
+
continue; // Skip logs without args
|
|
2068
|
+
}
|
|
2069
|
+
const args = parsedLog.args;
|
|
2070
|
+
if (addressEquals(args.buyer, userAddress) ||
|
|
2071
|
+
addressEquals(args.seller, userAddress)) {
|
|
2072
|
+
callback(args.escrowId, args);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
},
|
|
2076
|
+
});
|
|
2077
|
+
return { dispose: unwatch };
|
|
2078
|
+
}
|
|
2079
|
+
/**
|
|
2080
|
+
* Watch for state changes on a specific escrow
|
|
2081
|
+
*/
|
|
2082
|
+
watchEscrowStateChanges(escrowId, callback, options) {
|
|
2083
|
+
let lastState = null;
|
|
2084
|
+
const interval = options?.pollingInterval ?? DEFAULT_POLLING_INTERVAL;
|
|
2085
|
+
const poll = async () => {
|
|
2086
|
+
try {
|
|
2087
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
2088
|
+
if (lastState !== null && deal.state !== lastState) {
|
|
2089
|
+
callback(deal.state, lastState);
|
|
2090
|
+
}
|
|
2091
|
+
lastState = deal.state;
|
|
2092
|
+
}
|
|
2093
|
+
catch (e) {
|
|
2094
|
+
const message = this.extractErrorMessage(e);
|
|
2095
|
+
this.log('error', 'Error polling escrow state', {
|
|
2096
|
+
escrowId: escrowId.toString(),
|
|
2097
|
+
error: message
|
|
2098
|
+
});
|
|
2099
|
+
}
|
|
2100
|
+
};
|
|
2101
|
+
// Initial poll
|
|
2102
|
+
poll();
|
|
2103
|
+
// Set up interval
|
|
2104
|
+
const intervalId = setInterval(poll, interval);
|
|
2105
|
+
return {
|
|
2106
|
+
dispose: () => clearInterval(intervalId),
|
|
2107
|
+
};
|
|
2108
|
+
}
|
|
2109
|
+
// ==========================================================================
|
|
2110
|
+
// ADDITIONAL HELPERS
|
|
2111
|
+
// ==========================================================================
|
|
2112
|
+
/**
|
|
2113
|
+
* Check if user has submitted evidence for a dispute
|
|
2114
|
+
*/
|
|
2115
|
+
async hasSubmittedEvidence(escrowId, role) {
|
|
2116
|
+
const status = await this.getDisputeSubmissionStatus(escrowId);
|
|
2117
|
+
switch (role) {
|
|
2118
|
+
case Role.Buyer:
|
|
2119
|
+
return status.buyer;
|
|
2120
|
+
case Role.Seller:
|
|
2121
|
+
return status.seller;
|
|
2122
|
+
case Role.Arbiter:
|
|
2123
|
+
return status.arbiter;
|
|
2124
|
+
default:
|
|
2125
|
+
return false;
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Get cancellation request status for an escrow
|
|
2130
|
+
*
|
|
2131
|
+
* This helper function checks whether the buyer and/or seller have requested
|
|
2132
|
+
* cancellation. Use this to determine if mutual cancellation is pending or complete.
|
|
2133
|
+
*
|
|
2134
|
+
* Cancellation flow:
|
|
2135
|
+
* - Either party calls `requestCancel` to initiate
|
|
2136
|
+
* - If only one party requested, the other must also call `requestCancel` for mutual cancel
|
|
2137
|
+
* - When both request, the escrow is automatically canceled and funds return to buyer
|
|
2138
|
+
* - Alternatively, buyer can use `cancelByTimeout` after maturity time (if arbiter is set)
|
|
2139
|
+
*
|
|
2140
|
+
* @param escrowId - The escrow ID to check
|
|
2141
|
+
* @returns Object with buyer/seller cancel request status and whether mutual cancel is complete
|
|
2142
|
+
*/
|
|
2143
|
+
async getCancelRequestStatus(escrowId) {
|
|
2144
|
+
const deal = await this.getEscrowByIdParsed(escrowId);
|
|
2145
|
+
return {
|
|
2146
|
+
buyerRequested: deal.buyerCancelRequested,
|
|
2147
|
+
sellerRequested: deal.sellerCancelRequested,
|
|
2148
|
+
mutualCancelComplete: deal.buyerCancelRequested && deal.sellerCancelRequested,
|
|
2149
|
+
};
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Get user balances for multiple tokens (batched for performance).
|
|
2153
|
+
* Uses Promise.all to fetch all balances and decimals in parallel.
|
|
2154
|
+
*/
|
|
2155
|
+
async getUserBalances(userAddress, tokens) {
|
|
2156
|
+
if (tokens.length === 0) {
|
|
2157
|
+
return new Map();
|
|
2158
|
+
}
|
|
2159
|
+
// Batch fetch all balances and decimals in parallel
|
|
2160
|
+
const [balances, decimalsArray] = await Promise.all([
|
|
2161
|
+
Promise.all(tokens.map((token) => this.getTokenBalance(userAddress, token))),
|
|
2162
|
+
Promise.all(tokens.map((token) => this.getTokenDecimals(token))),
|
|
2163
|
+
]);
|
|
2164
|
+
const result = new Map();
|
|
2165
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2166
|
+
const token = tokens[i];
|
|
2167
|
+
const balance = balances[i];
|
|
2168
|
+
const decimals = decimalsArray[i];
|
|
2169
|
+
const formatted = this.formatTokenAmount(balance, decimals);
|
|
2170
|
+
result.set(token, { balance, decimals, formatted });
|
|
2171
|
+
}
|
|
2172
|
+
return result;
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* Get maturity info for an escrow
|
|
2176
|
+
*/
|
|
2177
|
+
getMaturityInfo(depositTime, maturityDays) {
|
|
2178
|
+
const hasDeadline = maturityDays > 0n;
|
|
2179
|
+
const maturityTimestamp = depositTime + maturityDays * 86400n;
|
|
2180
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
2181
|
+
const isPassed = now >= maturityTimestamp;
|
|
2182
|
+
const remainingSeconds = isPassed ? 0 : Number(maturityTimestamp - now);
|
|
2183
|
+
return {
|
|
2184
|
+
hasDeadline,
|
|
2185
|
+
maturityDays: Number(maturityDays),
|
|
2186
|
+
maturityTimestamp,
|
|
2187
|
+
isPassed,
|
|
2188
|
+
remainingSeconds,
|
|
2189
|
+
};
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Get all escrows for a user (as buyer or seller)
|
|
2193
|
+
*/
|
|
2194
|
+
async getUserEscrows(userAddress) {
|
|
2195
|
+
const [buyerEscrows, sellerEscrows] = await Promise.all([
|
|
2196
|
+
this.getEscrowsByBuyer(userAddress),
|
|
2197
|
+
this.getEscrowsBySeller(userAddress),
|
|
2198
|
+
]);
|
|
2199
|
+
// Merge and deduplicate by ID
|
|
2200
|
+
const escrowMap = new Map();
|
|
2201
|
+
for (const escrow of [...buyerEscrows, ...sellerEscrows]) {
|
|
2202
|
+
escrowMap.set(escrow.id, escrow);
|
|
2203
|
+
}
|
|
2204
|
+
return Array.from(escrowMap.values());
|
|
2205
|
+
}
|
|
2206
|
+
// ============================================================================
|
|
2207
|
+
// STATE & ROLE VALIDATION HELPERS
|
|
2208
|
+
// ============================================================================
|
|
2209
|
+
/**
|
|
2210
|
+
* Check if a user can deposit to an escrow.
|
|
2211
|
+
* A user can deposit if:
|
|
2212
|
+
* - The escrow exists and is in AWAITING_PAYMENT state
|
|
2213
|
+
* - The user is either the buyer or seller
|
|
2214
|
+
*
|
|
2215
|
+
* @param userAddress - The address of the user to check
|
|
2216
|
+
* @param escrowId - The escrow ID to check
|
|
2217
|
+
* @returns True if the user can deposit, false otherwise
|
|
2218
|
+
*/
|
|
2219
|
+
async canUserDeposit(userAddress, escrowId) {
|
|
2220
|
+
try {
|
|
2221
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2222
|
+
// Must be in AWAITING_PAYMENT state
|
|
2223
|
+
if (escrow.state !== EscrowState.AWAITING_PAYMENT) {
|
|
2224
|
+
return false;
|
|
2225
|
+
}
|
|
2226
|
+
// Must be buyer or seller
|
|
2227
|
+
return addressEquals(userAddress, escrow.buyer) ||
|
|
2228
|
+
addressEquals(userAddress, escrow.seller);
|
|
2229
|
+
}
|
|
2230
|
+
catch {
|
|
2231
|
+
return false;
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
/**
|
|
2235
|
+
* Check if a user can accept an escrow (seller accepting after buyer deposit).
|
|
2236
|
+
* A user can accept if:
|
|
2237
|
+
* - The escrow is in AWAITING_DELIVERY state
|
|
2238
|
+
* - The user is the seller
|
|
2239
|
+
*
|
|
2240
|
+
* @param userAddress - The address of the user to check
|
|
2241
|
+
* @param escrowId - The escrow ID to check
|
|
2242
|
+
* @returns True if the user can accept, false otherwise
|
|
2243
|
+
*/
|
|
2244
|
+
async canUserAcceptEscrow(userAddress, escrowId) {
|
|
2245
|
+
try {
|
|
2246
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2247
|
+
// Must be in AWAITING_DELIVERY state
|
|
2248
|
+
if (escrow.state !== EscrowState.AWAITING_DELIVERY) {
|
|
2249
|
+
return false;
|
|
2250
|
+
}
|
|
2251
|
+
// Must be seller
|
|
2252
|
+
return addressEquals(userAddress, escrow.seller);
|
|
2253
|
+
}
|
|
2254
|
+
catch {
|
|
2255
|
+
return false;
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
/**
|
|
2259
|
+
* Check if a user can confirm delivery (buyer confirming receipt).
|
|
2260
|
+
* A user can confirm delivery if:
|
|
2261
|
+
* - The escrow is in AWAITING_DELIVERY or DISPUTED state
|
|
2262
|
+
* - The user is the buyer
|
|
2263
|
+
*
|
|
2264
|
+
* @param userAddress - The address of the user to check
|
|
2265
|
+
* @param escrowId - The escrow ID to check
|
|
2266
|
+
* @returns True if the user can confirm delivery, false otherwise
|
|
2267
|
+
*/
|
|
2268
|
+
async canUserConfirmDelivery(userAddress, escrowId) {
|
|
2269
|
+
try {
|
|
2270
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2271
|
+
// Must be in AWAITING_DELIVERY or DISPUTED state
|
|
2272
|
+
if (escrow.state !== EscrowState.AWAITING_DELIVERY &&
|
|
2273
|
+
escrow.state !== EscrowState.DISPUTED) {
|
|
2274
|
+
return false;
|
|
2275
|
+
}
|
|
2276
|
+
// Must be buyer
|
|
2277
|
+
return addressEquals(userAddress, escrow.buyer);
|
|
2278
|
+
}
|
|
2279
|
+
catch {
|
|
2280
|
+
return false;
|
|
2281
|
+
}
|
|
2282
|
+
}
|
|
2283
|
+
/**
|
|
2284
|
+
* Check if a user can start a dispute.
|
|
2285
|
+
* A user can start a dispute if:
|
|
2286
|
+
* - The escrow is in AWAITING_DELIVERY state
|
|
2287
|
+
* - The escrow has an arbiter set
|
|
2288
|
+
* - The user is either the buyer or seller
|
|
2289
|
+
*
|
|
2290
|
+
* @param userAddress - The address of the user to check
|
|
2291
|
+
* @param escrowId - The escrow ID to check
|
|
2292
|
+
* @returns True if the user can start a dispute, false otherwise
|
|
2293
|
+
*/
|
|
2294
|
+
async canUserStartDispute(userAddress, escrowId) {
|
|
2295
|
+
try {
|
|
2296
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2297
|
+
// Must be in AWAITING_DELIVERY state
|
|
2298
|
+
if (escrow.state !== EscrowState.AWAITING_DELIVERY) {
|
|
2299
|
+
return false;
|
|
2300
|
+
}
|
|
2301
|
+
// Must have arbiter
|
|
2302
|
+
if (!this.hasArbiter(escrow)) {
|
|
2303
|
+
return false;
|
|
2304
|
+
}
|
|
2305
|
+
// Must be buyer or seller
|
|
2306
|
+
return addressEquals(userAddress, escrow.buyer) ||
|
|
2307
|
+
addressEquals(userAddress, escrow.seller);
|
|
2308
|
+
}
|
|
2309
|
+
catch {
|
|
2310
|
+
return false;
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Check if an escrow can be withdrawn (auto-release check).
|
|
2315
|
+
* An escrow can be withdrawn if:
|
|
2316
|
+
* - It's in AWAITING_DELIVERY state
|
|
2317
|
+
* - The maturity time has passed (if deadline is set)
|
|
2318
|
+
*
|
|
2319
|
+
* @param escrowId - The escrow ID to check
|
|
2320
|
+
* @returns True if the escrow can be withdrawn, false otherwise
|
|
2321
|
+
*/
|
|
2322
|
+
async canUserWithdraw(escrowId) {
|
|
2323
|
+
try {
|
|
2324
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2325
|
+
// Must be in AWAITING_DELIVERY state
|
|
2326
|
+
if (escrow.state !== EscrowState.AWAITING_DELIVERY) {
|
|
2327
|
+
return false;
|
|
2328
|
+
}
|
|
2329
|
+
// Check if maturity time has passed (if deadline is set)
|
|
2330
|
+
if (escrow.maturityTime > 0n) {
|
|
2331
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
2332
|
+
return now >= escrow.maturityTime;
|
|
2333
|
+
}
|
|
2334
|
+
// No deadline set, cannot auto-withdraw
|
|
2335
|
+
return false;
|
|
2336
|
+
}
|
|
2337
|
+
catch {
|
|
2338
|
+
return false;
|
|
2339
|
+
}
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* Check if a seller can perform auto-release.
|
|
2343
|
+
* A seller can auto-release if:
|
|
2344
|
+
* - The escrow is in AWAITING_DELIVERY state
|
|
2345
|
+
* - The maturity time has passed (if deadline is set)
|
|
2346
|
+
* - The user is the seller
|
|
2347
|
+
*
|
|
2348
|
+
* @param userAddress - The address of the user to check
|
|
2349
|
+
* @param escrowId - The escrow ID to check
|
|
2350
|
+
* @returns True if the seller can auto-release, false otherwise
|
|
2351
|
+
*/
|
|
2352
|
+
async canSellerAutoRelease(userAddress, escrowId) {
|
|
2353
|
+
try {
|
|
2354
|
+
const escrow = await this.getEscrowByIdParsed(escrowId);
|
|
2355
|
+
// Must be seller
|
|
2356
|
+
if (!addressEquals(userAddress, escrow.seller)) {
|
|
2357
|
+
return false;
|
|
2358
|
+
}
|
|
2359
|
+
// Must be in AWAITING_DELIVERY state
|
|
2360
|
+
if (escrow.state !== EscrowState.AWAITING_DELIVERY) {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
// Check if maturity time has passed (if deadline is set)
|
|
2364
|
+
if (escrow.maturityTime > 0n) {
|
|
2365
|
+
const now = BigInt(Math.floor(Date.now() / 1000));
|
|
2366
|
+
return now >= escrow.maturityTime;
|
|
2367
|
+
}
|
|
2368
|
+
// No deadline set, cannot auto-release
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
catch {
|
|
2372
|
+
return false;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
/**
|
|
2376
|
+
* Check if an address is the buyer in an escrow.
|
|
2377
|
+
*
|
|
2378
|
+
* @param userAddress - The address to check
|
|
2379
|
+
* @param escrow - The escrow data
|
|
2380
|
+
* @returns True if the address is the buyer
|
|
2381
|
+
*/
|
|
2382
|
+
isBuyer(userAddress, escrow) {
|
|
2383
|
+
return addressEquals(userAddress, escrow.buyer);
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Check if an address is the seller in an escrow.
|
|
2387
|
+
*
|
|
2388
|
+
* @param userAddress - The address to check
|
|
2389
|
+
* @param escrow - The escrow data
|
|
2390
|
+
* @returns True if the address is the seller
|
|
2391
|
+
*/
|
|
2392
|
+
isSeller(userAddress, escrow) {
|
|
2393
|
+
return addressEquals(userAddress, escrow.seller);
|
|
2394
|
+
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Check if an address is the arbiter in an escrow.
|
|
2397
|
+
*
|
|
2398
|
+
* @param userAddress - The address to check
|
|
2399
|
+
* @param escrow - The escrow data
|
|
2400
|
+
* @returns True if the address is the arbiter
|
|
2401
|
+
*/
|
|
2402
|
+
isArbiter(userAddress, escrow) {
|
|
2403
|
+
return addressEquals(userAddress, escrow.arbiter);
|
|
2404
|
+
}
|
|
2405
|
+
/**
|
|
2406
|
+
* Check if an escrow has an arbiter set.
|
|
2407
|
+
*
|
|
2408
|
+
* @param escrow - The escrow data
|
|
2409
|
+
* @returns True if the escrow has an arbiter (non-zero address)
|
|
2410
|
+
*/
|
|
2411
|
+
hasArbiter(escrow) {
|
|
2412
|
+
return escrow.arbiter !== viem_1.zeroAddress;
|
|
2413
|
+
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Compare two addresses for equality (case-insensitive, normalized).
|
|
2416
|
+
* This is a public utility method that can be used to compare Ethereum addresses.
|
|
2417
|
+
*
|
|
2418
|
+
* @param a - First address to compare
|
|
2419
|
+
* @param b - Second address to compare
|
|
2420
|
+
* @returns True if the addresses are equal (case-insensitive)
|
|
2421
|
+
*
|
|
2422
|
+
* @example
|
|
2423
|
+
* ```typescript
|
|
2424
|
+
* const sdk = new PalindromeEscrowSDK(...);
|
|
2425
|
+
* const areEqual = sdk.addressEquals(
|
|
2426
|
+
* "0xabc...",
|
|
2427
|
+
* "0xABC..."
|
|
2428
|
+
* ); // true
|
|
2429
|
+
* ```
|
|
2430
|
+
*/
|
|
2431
|
+
addressEquals(a, b) {
|
|
2432
|
+
return addressEquals(a, b);
|
|
2433
|
+
}
|
|
2434
|
+
// ============================================================================
|
|
2435
|
+
// GAS ESTIMATION HELPERS
|
|
2436
|
+
// ============================================================================
|
|
2437
|
+
/**
|
|
2438
|
+
* Get current gas price information from the network.
|
|
2439
|
+
* Returns standard, fast, and instant gas price estimates in gwei.
|
|
2440
|
+
*
|
|
2441
|
+
* @returns Object containing gas price estimates
|
|
2442
|
+
*
|
|
2443
|
+
* @example
|
|
2444
|
+
* ```typescript
|
|
2445
|
+
* const gasPrice = await sdk.getCurrentGasPrice();
|
|
2446
|
+
* console.log(`Standard: ${gasPrice.standard} gwei`);
|
|
2447
|
+
* console.log(`Fast: ${gasPrice.fast} gwei`);
|
|
2448
|
+
* console.log(`Instant: ${gasPrice.instant} gwei`);
|
|
2449
|
+
* ```
|
|
2450
|
+
*/
|
|
2451
|
+
async getCurrentGasPrice() {
|
|
2452
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2453
|
+
// Estimate different speed tiers (standard, fast, instant)
|
|
2454
|
+
// Standard: base price
|
|
2455
|
+
// Fast: +20%
|
|
2456
|
+
// Instant: +50%
|
|
2457
|
+
const standard = gasPrice;
|
|
2458
|
+
const fast = (gasPrice * 120n) / 100n;
|
|
2459
|
+
const instant = (gasPrice * 150n) / 100n;
|
|
2460
|
+
return {
|
|
2461
|
+
standard,
|
|
2462
|
+
fast,
|
|
2463
|
+
instant,
|
|
2464
|
+
wei: gasPrice
|
|
2465
|
+
};
|
|
2466
|
+
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Estimate gas cost for creating an escrow.
|
|
2469
|
+
*
|
|
2470
|
+
* @param params - Create escrow parameters
|
|
2471
|
+
* @returns Gas estimation details
|
|
2472
|
+
*
|
|
2473
|
+
* @example
|
|
2474
|
+
* ```typescript
|
|
2475
|
+
* const estimate = await sdk.estimateGasForCreateEscrow({
|
|
2476
|
+
* token: tokenAddress,
|
|
2477
|
+
* buyer: buyerAddress,
|
|
2478
|
+
* amount: 1000000n,
|
|
2479
|
+
* maturityDays: 7n,
|
|
2480
|
+
* arbiter: zeroAddress,
|
|
2481
|
+
* title: 'Test',
|
|
2482
|
+
* ipfsHash: ''
|
|
2483
|
+
* });
|
|
2484
|
+
* console.log(`Gas limit: ${estimate.gasLimit}`);
|
|
2485
|
+
* console.log(`Cost: ${estimate.estimatedCostEth} ETH`);
|
|
2486
|
+
* ```
|
|
2487
|
+
*/
|
|
2488
|
+
async estimateGasForCreateEscrow(params) {
|
|
2489
|
+
if (!this.walletClient) {
|
|
2490
|
+
throw new Error('Wallet client required for gas estimation');
|
|
2491
|
+
}
|
|
2492
|
+
try {
|
|
2493
|
+
const gasLimit = await this.publicClient.estimateContractGas({
|
|
2494
|
+
address: this.contractAddress,
|
|
2495
|
+
abi: this.abiEscrow,
|
|
2496
|
+
functionName: 'createEscrow',
|
|
2497
|
+
args: [
|
|
2498
|
+
params.token,
|
|
2499
|
+
params.buyer,
|
|
2500
|
+
params.amount,
|
|
2501
|
+
params.maturityDays,
|
|
2502
|
+
params.arbiter,
|
|
2503
|
+
params.title,
|
|
2504
|
+
params.ipfsHash,
|
|
2505
|
+
(0, viem_1.pad)('0x00', { size: 65 }) // Empty seller wallet sig
|
|
2506
|
+
],
|
|
2507
|
+
account: this.walletClient.account
|
|
2508
|
+
});
|
|
2509
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2510
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2511
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2512
|
+
return {
|
|
2513
|
+
gasLimit,
|
|
2514
|
+
estimatedCostWei,
|
|
2515
|
+
estimatedCostEth
|
|
2516
|
+
};
|
|
2517
|
+
}
|
|
2518
|
+
catch (error) {
|
|
2519
|
+
// If estimation fails, return conservative estimate
|
|
2520
|
+
const gasLimit = 300000n; // Conservative estimate
|
|
2521
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2522
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2523
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2524
|
+
return {
|
|
2525
|
+
gasLimit,
|
|
2526
|
+
estimatedCostWei,
|
|
2527
|
+
estimatedCostEth
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Estimate gas cost for depositing to an escrow.
|
|
2533
|
+
*
|
|
2534
|
+
* @param escrowId - The escrow ID
|
|
2535
|
+
* @returns Gas estimation details
|
|
2536
|
+
*
|
|
2537
|
+
* @example
|
|
2538
|
+
* ```typescript
|
|
2539
|
+
* const estimate = await sdk.estimateGasForDeposit(escrowId);
|
|
2540
|
+
* console.log(`Gas limit: ${estimate.gasLimit}`);
|
|
2541
|
+
* ```
|
|
2542
|
+
*/
|
|
2543
|
+
async estimateGasForDeposit(escrowId) {
|
|
2544
|
+
if (!this.walletClient) {
|
|
2545
|
+
throw new Error('Wallet client required for gas estimation');
|
|
2546
|
+
}
|
|
2547
|
+
try {
|
|
2548
|
+
const gasLimit = await this.publicClient.estimateContractGas({
|
|
2549
|
+
address: this.contractAddress,
|
|
2550
|
+
abi: this.abiEscrow,
|
|
2551
|
+
functionName: 'deposit',
|
|
2552
|
+
args: [escrowId, (0, viem_1.pad)('0x00', { size: 65 })],
|
|
2553
|
+
account: this.walletClient.account
|
|
2554
|
+
});
|
|
2555
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2556
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2557
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2558
|
+
return {
|
|
2559
|
+
gasLimit,
|
|
2560
|
+
estimatedCostWei,
|
|
2561
|
+
estimatedCostEth
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
catch {
|
|
2565
|
+
const gasLimit = 200000n;
|
|
2566
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2567
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2568
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2569
|
+
return {
|
|
2570
|
+
gasLimit,
|
|
2571
|
+
estimatedCostWei,
|
|
2572
|
+
estimatedCostEth
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
/**
|
|
2577
|
+
* Estimate gas cost for confirming delivery.
|
|
2578
|
+
*
|
|
2579
|
+
* @param escrowId - The escrow ID
|
|
2580
|
+
* @returns Gas estimation details
|
|
2581
|
+
*
|
|
2582
|
+
* @example
|
|
2583
|
+
* ```typescript
|
|
2584
|
+
* const estimate = await sdk.estimateGasForConfirmDelivery(escrowId);
|
|
2585
|
+
* console.log(`Cost: ${estimate.estimatedCostEth} ETH`);
|
|
2586
|
+
* ```
|
|
2587
|
+
*/
|
|
2588
|
+
async estimateGasForConfirmDelivery(escrowId) {
|
|
2589
|
+
if (!this.walletClient) {
|
|
2590
|
+
throw new Error('Wallet client required for gas estimation');
|
|
2591
|
+
}
|
|
2592
|
+
try {
|
|
2593
|
+
const gasLimit = await this.publicClient.estimateContractGas({
|
|
2594
|
+
address: this.contractAddress,
|
|
2595
|
+
abi: this.abiEscrow,
|
|
2596
|
+
functionName: 'confirmDelivery',
|
|
2597
|
+
args: [escrowId, (0, viem_1.pad)('0x00', { size: 65 })],
|
|
2598
|
+
account: this.walletClient.account
|
|
2599
|
+
});
|
|
2600
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2601
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2602
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2603
|
+
return {
|
|
2604
|
+
gasLimit,
|
|
2605
|
+
estimatedCostWei,
|
|
2606
|
+
estimatedCostEth
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
catch {
|
|
2610
|
+
const gasLimit = 150000n;
|
|
2611
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2612
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2613
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2614
|
+
return {
|
|
2615
|
+
gasLimit,
|
|
2616
|
+
estimatedCostWei,
|
|
2617
|
+
estimatedCostEth
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
/**
|
|
2622
|
+
* Estimate gas cost for withdrawing from escrow wallet.
|
|
2623
|
+
*
|
|
2624
|
+
* @returns Gas estimation details
|
|
2625
|
+
*
|
|
2626
|
+
* @example
|
|
2627
|
+
* ```typescript
|
|
2628
|
+
* const estimate = await sdk.estimateGasForWithdraw();
|
|
2629
|
+
* ```
|
|
2630
|
+
*/
|
|
2631
|
+
async estimateGasForWithdraw() {
|
|
2632
|
+
const gasLimit = 100000n; // Withdraw is typically cheaper
|
|
2633
|
+
const gasPrice = await this.publicClient.getGasPrice();
|
|
2634
|
+
const estimatedCostWei = gasLimit * gasPrice;
|
|
2635
|
+
const estimatedCostEth = (Number(estimatedCostWei) / 1e18).toFixed(6);
|
|
2636
|
+
return {
|
|
2637
|
+
gasLimit,
|
|
2638
|
+
estimatedCostWei,
|
|
2639
|
+
estimatedCostEth
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
// ============================================================================
|
|
2643
|
+
// FEE UTILITIES
|
|
2644
|
+
// ============================================================================
|
|
2645
|
+
/**
|
|
2646
|
+
* Get fee receiver address (cached - rarely changes)
|
|
2647
|
+
* @param forceRefresh - Set to true to bypass cache and fetch fresh value
|
|
2648
|
+
*/
|
|
2649
|
+
async getFeeReceiver(forceRefresh = false) {
|
|
2650
|
+
if (!forceRefresh && this.feeReceiverCache) {
|
|
2651
|
+
return this.feeReceiverCache;
|
|
2652
|
+
}
|
|
2653
|
+
this.feeReceiverCache = await this.publicClient.readContract({
|
|
2654
|
+
address: this.contractAddress,
|
|
2655
|
+
abi: this.abiEscrow,
|
|
2656
|
+
functionName: "feeReceiver",
|
|
2657
|
+
});
|
|
2658
|
+
return this.feeReceiverCache;
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* Get fee percentage in basis points.
|
|
2662
|
+
* Tries to read FEE_BPS from contract, falls back to default (100 = 1%).
|
|
2663
|
+
* Result is cached for performance.
|
|
2664
|
+
*/
|
|
2665
|
+
async getFeeBps() {
|
|
2666
|
+
if (this.cachedFeeBps !== null) {
|
|
2667
|
+
return this.cachedFeeBps;
|
|
2668
|
+
}
|
|
2669
|
+
try {
|
|
2670
|
+
// Try to read FEE_BPS if it's public in the contract
|
|
2671
|
+
const feeBps = await this.publicClient.readContract({
|
|
2672
|
+
address: this.contractAddress,
|
|
2673
|
+
abi: this.abiEscrow,
|
|
2674
|
+
functionName: "FEE_BPS",
|
|
2675
|
+
});
|
|
2676
|
+
this.cachedFeeBps = feeBps;
|
|
2677
|
+
return feeBps;
|
|
2678
|
+
}
|
|
2679
|
+
catch (e) {
|
|
2680
|
+
// Fall back to default if not readable (private constant)
|
|
2681
|
+
const message = this.extractErrorMessage(e);
|
|
2682
|
+
this.log('warn', 'Could not read FEE_BPS from contract, using default', {
|
|
2683
|
+
error: message
|
|
2684
|
+
});
|
|
2685
|
+
this.cachedFeeBps = 100n; // 1% fee
|
|
2686
|
+
return this.cachedFeeBps;
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* Calculate fee for an amount.
|
|
2691
|
+
* Uses the same logic as the contract's _computeFeeAndNet.
|
|
2692
|
+
*/
|
|
2693
|
+
async calculateFee(amount, tokenDecimals = 6) {
|
|
2694
|
+
const feeBps = await this.getFeeBps();
|
|
2695
|
+
const minFee = 10n ** BigInt(tokenDecimals > 2 ? tokenDecimals - 2 : 0);
|
|
2696
|
+
const calculatedFee = (amount * feeBps) / 10000n;
|
|
2697
|
+
const fee = calculatedFee >= minFee ? calculatedFee : minFee;
|
|
2698
|
+
return { fee, net: amount - fee };
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
exports.PalindromeEscrowSDK = PalindromeEscrowSDK;
|
|
2702
|
+
/**
|
|
2703
|
+
* Maximum nonce word index to prevent infinite loops.
|
|
2704
|
+
* 100 words = 25,600 nonces per escrow per signer.
|
|
2705
|
+
*/
|
|
2706
|
+
PalindromeEscrowSDK.MAX_NONCE_WORDS = 100n;
|
|
2707
|
+
// ============================================================================
|
|
2708
|
+
// EXPORT DEFAULT
|
|
2709
|
+
// ============================================================================
|
|
2710
|
+
exports.default = PalindromeEscrowSDK;
|