@fairblock/stabletrust 1.0.17 → 1.0.18

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.
@@ -11,203 +11,375 @@ dotenv.config();
11
11
  * https://docs.fairblock.network/docs/confidential_transfers/technical_overview
12
12
  */
13
13
 
14
- // Standard ERC20 token contract. Any ERC20 on this chain ID can be used.
15
- const TOKEN_ADDRESS =
16
- process.env.TOKEN_ADDRESS || "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
17
- const RPC_URL =
18
- process.env.ETHEREUM_RPC_URL || "https://rpc.testnet.stable.xyz";
19
- const EXPLORER_URL =
20
- process.env.EXPLORER_URL || "https://testnet.stablescan.xyz/tx/";
21
- const CHAIN_ID = process.env.CHAIN_ID || 2201;
14
+ const CHAINS = [
15
+ {
16
+ network: "Stable",
17
+ chainId: 2201,
18
+ tokenAddress: "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9",
19
+ rpcUrl: "https://rpc.testnet.stable.xyz",
20
+ explorerUrl: "https://testnet.stablescan.xyz/tx/",
21
+ },
22
+ {
23
+ network: "Arc",
24
+ chainId: 1244,
25
+ tokenAddress: "0x3600000000000000000000000000000000000000",
26
+ rpcUrl: "https://rpc.testnet.arc.network",
27
+ explorerUrl: "https://testnet.arcscan.app/tx/",
28
+ },
29
+ {
30
+ network: "Base",
31
+ chainId: 84532,
32
+ tokenAddress: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
33
+ rpcUrl: "https://base-testnet.api.pocket.network",
34
+ explorerUrl: "https://sepolia.basescan.org/tx/",
35
+ },
36
+ {
37
+ network: "Ethereum",
38
+ chainId: 11155111,
39
+ tokenAddress: "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238",
40
+ rpcUrl: "https://ethereum-sepolia-rpc.publicnode.com",
41
+ explorerUrl: "https://sepolia.etherscan.io/tx/",
42
+ },
43
+ {
44
+ network: "Arbitrum",
45
+ chainId: 421614,
46
+ tokenAddress: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
47
+ rpcUrl: "https://arbitrum-sepolia-testnet.api.pocket.network",
48
+ explorerUrl: "https://sepolia.arbiscan.io/tx/",
49
+ },
50
+ {
51
+ network: "Tempo",
52
+ chainId: 42431,
53
+ tokenAddress: "0x20c0000000000000000000000000000000000000",
54
+ rpcUrl: "https://rpc.moderato.tempo.xyz",
55
+ explorerUrl: "https://explore.tempo.xyz/tx/",
56
+ },
57
+ ];
58
+
59
+ const actionSummary = [];
22
60
 
23
61
  /**
24
- * Performance Utility: Tracks execution time and provides timestamps
62
+ * Performance Utility: Tracks execution time and provides timestamps and network congestion
25
63
  */
26
- async function trackPerformance(actionName, action) {
27
- const timestamp = new Date().toISOString();
64
+ async function trackPerformance(actionName, action, provider, chainNetwork) {
65
+ const startTimeMs = Date.now();
66
+ const startTimestamp = new Date(startTimeMs).toISOString();
28
67
  const start = performance.now();
68
+
29
69
  const result = await action();
70
+
30
71
  const end = performance.now();
72
+ const endTimeMs = Date.now();
73
+ const endTimestamp = new Date(endTimeMs).toISOString();
74
+
31
75
  const duration = ((end - start) / 1000).toFixed(3);
32
76
 
77
+ let congestionStr = "N/A";
78
+ let congestionInfo = "";
79
+ if (provider && result && (result.hash || result.transactionHash)) {
80
+ try {
81
+ const hash = result.hash || result.transactionHash;
82
+ // Wait for tx to be mined to be able to get block details
83
+ const receipt = await provider.waitForTransaction(hash, 1, 60000); // 60s timeout
84
+ if (receipt && receipt.blockNumber) {
85
+ const block = await provider.getBlock(receipt.blockNumber);
86
+ congestionStr = block.transactions.length;
87
+ congestionInfo = ` | Block TXs (Congestion): ${congestionStr}`;
88
+ }
89
+ } catch (e) {
90
+ congestionStr = "Unknown";
91
+ congestionInfo = " | Block TXs (Congestion): Unknown";
92
+ }
93
+ }
94
+
33
95
  console.log(`Action: ${actionName}`);
34
- console.log(`Timestamp: ${timestamp}`);
35
- console.log(`Duration: ${duration}s`);
96
+ console.log(`Started: ${startTimestamp}`);
97
+ console.log(`Duration: ${duration}s${congestionInfo}`);
98
+ console.log(`Completed: ${endTimestamp}`);
99
+
100
+ actionSummary.push({
101
+ Chain: chainNetwork,
102
+ Action: actionName,
103
+ "Duration (s)": duration,
104
+ Congestion: congestionStr,
105
+ "Started At": startTimestamp,
106
+ "Completed At": endTimestamp,
107
+ });
108
+
36
109
  return { result, duration };
37
110
  }
38
111
 
39
- async function main() {
40
- console.log("=== Starting Confidential Flow Performance Test ===\n");
112
+ async function runChainFlow(chain, senderPrivateKey, recipientPrivateKey) {
113
+ console.log(`\n======================================================`);
114
+ console.log(`=== Starting Confidential Flow for ${chain.network} ===`);
115
+ console.log(
116
+ `=== Chain ID: ${chain.chainId} | Token: ${chain.tokenAddress} ===`,
117
+ );
118
+ console.log(`======================================================\n`);
41
119
 
42
- const client = new ConfidentialTransferClient(RPC_URL, Number(CHAIN_ID));
120
+ try {
121
+ const rpcUrl = chain.rpcUrl;
122
+ const explorerBaseUrl = chain.explorerUrl;
123
+ const client = new ConfidentialTransferClient(
124
+ rpcUrl,
125
+ Number(chain.chainId),
126
+ );
43
127
 
44
- const provider = new ethers.JsonRpcProvider(RPC_URL);
45
- const sender = new ethers.Wallet(process.env.SENDER_PRIVATE_KEY, provider);
46
- const recipient = new ethers.Wallet(
47
- process.env.RECIPIENT_PRIVATE_KEY,
48
- provider,
49
- );
128
+ const provider = new ethers.JsonRpcProvider(rpcUrl);
129
+ const sender = new ethers.Wallet(senderPrivateKey, provider);
130
+ const recipient = new ethers.Wallet(recipientPrivateKey, provider);
50
131
 
51
- const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);
52
- const tokenDecimals = await tokenContract.decimals();
132
+ const tokenContract = new ethers.Contract(
133
+ chain.tokenAddress,
134
+ ERC20_ABI,
135
+ provider,
136
+ );
137
+ const tokenDecimals = await tokenContract.decimals();
53
138
 
54
- console.log("Sender Address:", sender.address);
55
- console.log("Recipient Address:", recipient.address);
139
+ console.log("Sender Address:", sender.address);
140
+ console.log("Recipient Address:", recipient.address);
56
141
 
57
- const senderKeys = await client.ensureAccount(sender);
58
- const recipientKeys = await client.ensureAccount(recipient);
142
+ const senderKeysRes = await trackPerformance(
143
+ "ENSURE_SENDER_ACCOUNT",
144
+ () => client.ensureAccount(sender),
145
+ provider,
146
+ chain.network,
147
+ );
148
+ const senderKeys = senderKeysRes.result;
59
149
 
60
- // 1. DEPOSIT PHASE
61
- const depositAmount = ethers.parseUnits("0.1", tokenDecimals);
62
- const senderConfidentialBalanceBefore = await client.getConfidentialBalance(
63
- sender.address,
64
- senderKeys.privateKey,
65
- TOKEN_ADDRESS,
66
- );
150
+ const recipientKeysRes = await trackPerformance(
151
+ "ENSURE_RECIPIENT_ACCOUNT",
152
+ () => client.ensureAccount(recipient),
153
+ provider,
154
+ chain.network,
155
+ );
156
+ const recipientKeys = recipientKeysRes.result;
67
157
 
68
- console.log(
69
- "Sender Confidential Balance (Pre-Deposit):",
70
- ethers.formatUnits(senderConfidentialBalanceBefore.amount, tokenDecimals),
71
- );
158
+ // 1. DEPOSIT PHASE
159
+ console.log("\n--- Phase 1: Deposit ---");
160
+ const depositAmount = ethers.parseUnits("0.1", tokenDecimals);
161
+ const senderConfidentialBalanceBefore = await client.getConfidentialBalance(
162
+ sender.address,
163
+ senderKeys.privateKey,
164
+ chain.tokenAddress,
165
+ );
72
166
 
73
- console.log("Depositing 0.1 tokens into confidential contract...");
167
+ console.log(
168
+ "Sender Confidential Balance (Pre-Deposit):",
169
+ ethers.formatUnits(senderConfidentialBalanceBefore.amount, tokenDecimals),
170
+ );
74
171
 
75
- const depRes = await trackPerformance("DEPOSIT_TOKENS", () =>
76
- client.confidentialDeposit(sender, TOKEN_ADDRESS, depositAmount),
77
- );
78
- // Fixed hash reference: checking for result.transactionHash or result.hash
79
- const depHash = depRes.result.hash;
80
- console.log(`Transaction Hash: ${depHash}`);
81
- console.log(`View Transaction: ${EXPLORER_URL}${depHash}`);
82
-
83
- const senderConfidentialBalanceAfterDep = await client.getConfidentialBalance(
84
- sender.address,
85
- senderKeys.privateKey,
86
- TOKEN_ADDRESS,
87
- );
88
- console.log(
89
- "Sender Confidential Balance (Post-Deposit):",
90
- ethers.formatUnits(senderConfidentialBalanceAfterDep.amount, tokenDecimals),
91
- );
172
+ console.log("Depositing 0.1 tokens into confidential contract...");
92
173
 
93
- // 2. TRANSFER PHASE
94
- const transferAmount = ethers.parseUnits("0.05", tokenDecimals);
95
- const senderConfidentialBalanceBeforeTx = await client.getConfidentialBalance(
96
- sender.address,
97
- senderKeys.privateKey,
98
- TOKEN_ADDRESS,
99
- );
174
+ const depRes = await trackPerformance(
175
+ "DEPOSIT_TOKENS",
176
+ () =>
177
+ client.confidentialDeposit(sender, chain.tokenAddress, depositAmount),
178
+ provider,
179
+ chain.network,
180
+ );
181
+ const depHash = depRes.result.hash || depRes.result.transactionHash;
182
+ console.log(`Transaction Hash: ${depHash}`);
183
+ console.log(`View Transaction: ${explorerBaseUrl}${depHash}`);
100
184
 
101
- const recipientConfidentialBalanceBeforeTx =
102
- await client.getConfidentialBalance(
103
- recipient.address,
104
- recipientKeys.privateKey,
105
- TOKEN_ADDRESS,
185
+ const senderConfidentialBalanceAfterDep =
186
+ await client.getConfidentialBalance(
187
+ sender.address,
188
+ senderKeys.privateKey,
189
+ chain.tokenAddress,
190
+ );
191
+ console.log(
192
+ "Sender Confidential Balance (Post-Deposit):",
193
+ ethers.formatUnits(
194
+ senderConfidentialBalanceAfterDep.amount,
195
+ tokenDecimals,
196
+ ),
106
197
  );
107
198
 
108
- console.log(
109
- "Sender Confidential Balance (Pre-Transfer):",
110
- ethers.formatUnits(senderConfidentialBalanceBeforeTx.amount, tokenDecimals),
111
- );
112
- console.log(
113
- "Recipient Confidential Balance (Pre-Transfer):",
114
- ethers.formatUnits(
115
- recipientConfidentialBalanceBeforeTx.amount,
116
- tokenDecimals,
117
- ),
118
- );
199
+ // 2. TRANSFER PHASE
200
+ console.log("\n--- Phase 2: Transfer ---");
201
+ const transferAmount = ethers.parseUnits("0.05", tokenDecimals);
202
+ const senderConfidentialBalanceBeforeTx =
203
+ await client.getConfidentialBalance(
204
+ sender.address,
205
+ senderKeys.privateKey,
206
+ chain.tokenAddress,
207
+ );
119
208
 
120
- console.log("Transferring 0.05 tokens confidentially to recipient...");
209
+ const recipientConfidentialBalanceBeforeTx =
210
+ await client.getConfidentialBalance(
211
+ recipient.address,
212
+ recipientKeys.privateKey,
213
+ chain.tokenAddress,
214
+ );
121
215
 
122
- // FIXED: Ensured all arguments are explicitly passed in the arrow function
123
- const txRes = await trackPerformance("CONFIDENTIAL_TRANSFER", () =>
124
- client.confidentialTransfer(
125
- sender,
126
- recipient.address,
127
- TOKEN_ADDRESS,
128
- transferAmount,
129
- ),
130
- );
216
+ console.log(
217
+ "Sender Confidential Balance (Pre-Transfer):",
218
+ ethers.formatUnits(
219
+ senderConfidentialBalanceBeforeTx.amount,
220
+ tokenDecimals,
221
+ ),
222
+ );
223
+ console.log(
224
+ "Recipient Confidential Balance (Pre-Transfer):",
225
+ ethers.formatUnits(
226
+ recipientConfidentialBalanceBeforeTx.amount,
227
+ tokenDecimals,
228
+ ),
229
+ );
131
230
 
132
- const txHash = txRes.result.hash;
133
- console.log(
134
- "Status:Confidential Transfer is completed. Transfer amount is hidden on-chain.",
135
- );
136
- console.log(`Transaction Hash: ${txHash}`);
137
- console.log(`View Transaction: ${EXPLORER_URL}${txHash}`);
231
+ console.log("Transferring 0.05 tokens confidentially to recipient...");
138
232
 
139
- const senderConfidentialBalanceAfterTx = await client.getConfidentialBalance(
140
- sender.address,
141
- senderKeys.privateKey,
142
- TOKEN_ADDRESS,
143
- );
233
+ const txRes = await trackPerformance(
234
+ "CONFIDENTIAL_TRANSFER",
235
+ () =>
236
+ client.confidentialTransfer(
237
+ sender,
238
+ recipient.address,
239
+ chain.tokenAddress,
240
+ transferAmount,
241
+ ),
242
+ provider,
243
+ chain.network,
244
+ );
144
245
 
145
- const recipientConfidentialBalanceAfterTx =
146
- await client.getConfidentialBalance(
147
- recipient.address,
148
- recipientKeys.privateKey,
149
- TOKEN_ADDRESS,
246
+ const txHash = txRes.result.hash || txRes.result.transactionHash;
247
+ console.log(
248
+ "Status: Confidential Transfer is completed. Transfer amount is hidden on-chain.",
150
249
  );
250
+ console.log(`Transaction Hash: ${txHash}`);
251
+ console.log(`View Transaction: ${explorerBaseUrl}${txHash}`);
151
252
 
152
- console.log(
153
- "Sender Confidential Balance (Post-Transfer):",
154
- ethers.formatUnits(senderConfidentialBalanceAfterTx.amount, tokenDecimals),
155
- );
156
- console.log(
157
- "Recipient Confidential Balance (Post-Transfer):",
158
- ethers.formatUnits(
159
- recipientConfidentialBalanceAfterTx.amount,
160
- tokenDecimals,
161
- ),
162
- "\n",
163
- );
253
+ const senderConfidentialBalanceAfterTx =
254
+ await client.getConfidentialBalance(
255
+ sender.address,
256
+ senderKeys.privateKey,
257
+ chain.tokenAddress,
258
+ );
164
259
 
165
- // 3. WITHDRAW PHASE
166
- const withdrawAmount = ethers.parseUnits("0.025", tokenDecimals);
167
- console.log(
168
- "Withdrawing 0.025 tokens from confidential contract to recipient's public balance...",
169
- );
170
- let recipientConfidentialBalanceBeforeWithdraw =
171
- await client.getConfidentialBalance(
172
- recipient.address,
173
- recipientKeys.privateKey,
174
- TOKEN_ADDRESS,
260
+ const recipientConfidentialBalanceAfterTx =
261
+ await client.getConfidentialBalance(
262
+ recipient.address,
263
+ recipientKeys.privateKey,
264
+ chain.tokenAddress,
265
+ );
266
+
267
+ console.log(
268
+ "Sender Confidential Balance (Post-Transfer):",
269
+ ethers.formatUnits(
270
+ senderConfidentialBalanceAfterTx.amount,
271
+ tokenDecimals,
272
+ ),
273
+ );
274
+ console.log(
275
+ "Recipient Confidential Balance (Post-Transfer):",
276
+ ethers.formatUnits(
277
+ recipientConfidentialBalanceAfterTx.amount,
278
+ tokenDecimals,
279
+ ),
175
280
  );
176
281
 
177
- console.log(
178
- "Recipient Confidential Balance (Pre-Withdraw):",
179
- ethers.formatUnits(
180
- recipientConfidentialBalanceBeforeWithdraw.amount,
181
- tokenDecimals,
182
- ),
183
- );
184
- const withdrawRes = await trackPerformance("WITHDRAW_TOKENS", () =>
185
- client.withdraw(recipient, TOKEN_ADDRESS, withdrawAmount),
186
- );
282
+ // 3. WITHDRAW PHASE
283
+ console.log("\n--- Phase 3: Withdraw ---");
284
+ const withdrawAmount = ethers.parseUnits("0.025", tokenDecimals);
285
+ console.log(
286
+ "Withdrawing 0.025 tokens from confidential contract to recipient's public balance...",
287
+ );
187
288
 
188
- const withdrawHash = withdrawRes.result.hash;
189
- console.log(`Transaction Hash: ${withdrawHash}`);
190
- console.log(`View Transaction: ${EXPLORER_URL}${withdrawHash}`);
289
+ const recipientConfidentialBalanceBeforeWithdraw =
290
+ await client.getConfidentialBalance(
291
+ recipient.address,
292
+ recipientKeys.privateKey,
293
+ chain.tokenAddress,
294
+ );
191
295
 
192
- const recipientConfidentialBalanceAfterWithdraw =
193
- await client.getConfidentialBalance(
194
- recipient.address,
195
- recipientKeys.privateKey,
196
- TOKEN_ADDRESS,
296
+ console.log(
297
+ "Recipient Confidential Balance (Pre-Withdraw):",
298
+ ethers.formatUnits(
299
+ recipientConfidentialBalanceBeforeWithdraw.amount,
300
+ tokenDecimals,
301
+ ),
197
302
  );
198
303
 
304
+ const withdrawRes = await trackPerformance(
305
+ "WITHDRAW_TOKENS",
306
+ () => client.withdraw(recipient, chain.tokenAddress, withdrawAmount),
307
+ provider,
308
+ chain.network,
309
+ );
310
+
311
+ const withdrawHash =
312
+ withdrawRes.result.hash || withdrawRes.result.transactionHash;
313
+ console.log(`Transaction Hash: ${withdrawHash}`);
314
+ console.log(`View Transaction: ${explorerBaseUrl}${withdrawHash}`);
315
+
316
+ const recipientConfidentialBalanceAfterWithdraw =
317
+ await client.getConfidentialBalance(
318
+ recipient.address,
319
+ recipientKeys.privateKey,
320
+ chain.tokenAddress,
321
+ );
322
+
323
+ console.log(
324
+ "Recipient Confidential Balance (Final):",
325
+ ethers.formatUnits(
326
+ recipientConfidentialBalanceAfterWithdraw.amount,
327
+ tokenDecimals,
328
+ ),
329
+ );
330
+
331
+ console.log(`\n=== Successfully completed flow for ${chain.network} ===`);
332
+
333
+ const currentChainSummary = actionSummary
334
+ .filter((s) => s.Chain === chain.network)
335
+ .map((s) => ({
336
+ Action: s.Action,
337
+ "Duration (s)": s["Duration (s)"],
338
+ Congestion: s.Congestion,
339
+ "Started At": s["Started At"],
340
+ "Completed At": s["Completed At"],
341
+ }));
342
+
343
+ if (currentChainSummary.length > 0) {
344
+ console.log(`\n=== Execution Summary for ${chain.network} ===`);
345
+ console.table(currentChainSummary);
346
+ }
347
+ } catch (err) {
348
+ console.error(
349
+ `\n[!] Error running flow on ${chain.network} (Chain ID: ${chain.chainId}):`,
350
+ );
351
+ console.error(err.message || err);
352
+ console.log(`Skipping ${chain.network} and moving to the next chain...\n`);
353
+ }
354
+ }
355
+
356
+ async function main() {
199
357
  console.log(
200
- "Recipient Confidential Balance (Final):",
201
- ethers.formatUnits(
202
- recipientConfidentialBalanceAfterWithdraw.amount,
203
- tokenDecimals,
204
- ),
358
+ "=== Starting Multi-Chain Confidential Flow Performance Test ===\n",
205
359
  );
206
360
 
207
- console.log("\n=== Complete Flow Execution Finished ===");
361
+ const senderPrivateKey = process.env.SENDER_PRIVATE_KEY;
362
+ const recipientPrivateKey = process.env.RECIPIENT_PRIVATE_KEY;
363
+
364
+ if (!senderPrivateKey || !recipientPrivateKey) {
365
+ console.error(
366
+ "Missing SENDER_PRIVATE_KEY or RECIPIENT_PRIVATE_KEY in .env",
367
+ );
368
+ process.exit(1);
369
+ }
370
+
371
+ for (const chain of CHAINS) {
372
+ await runChainFlow(chain, senderPrivateKey, recipientPrivateKey);
373
+ }
374
+
375
+ console.log("\n=== All Chain Flows Execution Finished ===");
376
+ if (actionSummary.length > 0) {
377
+ console.log("\n=== Actions Summary Table ===");
378
+ console.table(actionSummary);
379
+ }
208
380
  }
209
381
 
210
382
  main().catch((error) => {
211
- console.error("Execution Error:", error);
383
+ console.error("Critical Execution Error:", error);
212
384
  process.exit(1);
213
385
  });
@@ -1,6 +1,7 @@
1
1
  import { ethers } from "ethers";
2
2
  import dotenv from "dotenv";
3
3
  import { ConfidentialTransferClient } from "@fairblock/stabletrust";
4
+ import { ERC20_ABI } from "./constants.js";
4
5
  dotenv.config();
5
6
  const RPC_URL = process.env.RPC_URL || "https://rpc.testnet.stable.xyz";
6
7
  const EXPLORER_URL =
@@ -19,9 +20,9 @@ async function minimalFlow() {
19
20
  // 2. Initialize Confidential Accounts (View Keys)
20
21
  await client.ensureAccount(sender);
21
22
  await client.ensureAccount(recipient);
22
- const tokenContract = new ethers.Contract(TOKEN_ADDRESS, ERC20_ABI, provider);
23
- const tokenDecimals = await tokenContract.decimals();
24
23
  const TOKEN = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
24
+ const tokenContract = new ethers.Contract(TOKEN, ERC20_ABI, provider);
25
+ const tokenDecimals = await tokenContract.decimals();
25
26
  const amount = ethers.parseUnits("0.1", tokenDecimals);
26
27
  let res;
27
28
  // 3. DEPOSIT: Move public ERC20 into the confidential contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fairblock/stabletrust",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "SDK for confidential transfers using homomorphic encryption and zero-knowledge proofs",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
package/src/client.js CHANGED
@@ -3,10 +3,14 @@ import {
3
3
  CONTRACT_ABI,
4
4
  ERC20_ABI,
5
5
  getStableTrustContractAddress,
6
- TEMPO_FEE_TOKEN_ADDRESS,
7
6
  } from "./constants.js";
8
7
  import { deriveKeys, decryptCiphertext, combineCiphertext } from "./crypto.js";
9
- import { encodeTransferProof, encodeWithdrawProof, sleep } from "./utils.js";
8
+ import {
9
+ encodeTransferProof,
10
+ encodeWithdrawProof,
11
+ sleep,
12
+ uploadBytesToIpfs,
13
+ } from "./utils.js";
10
14
  import { initializeWasm } from "./wasm-loader.js";
11
15
 
12
16
  // Auto-initialize WASM on first use
@@ -545,30 +549,49 @@ export class ConfidentialTransferClient {
545
549
 
546
550
  if (!proof.success) {
547
551
  throw new Error(
548
- `Proof generation failed: ${proof.error || "Unknown error. Check your balance and amount."}`,
552
+ `Proof generation failed: ${
553
+ proof.error || "Unknown error. Check your balance and amount."
554
+ }`,
549
555
  );
550
556
  }
557
+ const encodedProof = ethers.getBytes(encodeTransferProof(proof.data));
558
+ let transferZkpArg;
559
+ let txOverrides;
560
+
561
+ if (this.config.chainId === 42431) {
562
+ // Tempo: upload proof to IPFS and pass an ipfs:// pointer; no native fee
563
+ try {
564
+ const cid = await uploadBytesToIpfs(
565
+ encodedProof,
566
+ "transfer-proof.bin",
567
+ );
568
+ transferZkpArg = ethers.toUtf8Bytes(`ipfs://${cid}`);
569
+ } catch (ipfsError) {
570
+ throw new Error(
571
+ `Failed to upload proof to IPFS: ${ipfsError.message}`,
572
+ );
573
+ }
574
+ txOverrides = { value: 0n };
575
+ } else {
576
+ // Standard chains: pass encoded proof bytes with native fee
577
+ transferZkpArg = encodedProof;
578
+ txOverrides = { value: await this.contract.feeAmount() };
579
+ }
580
+
581
+ const tx = await this.contract
582
+ .connect(senderWallet)
583
+ .transferConfidential(
584
+ recipientAddress,
585
+ tokenAddress,
586
+ transferZkpArg,
587
+ useOffchainVerify,
588
+ txOverrides,
589
+ );
551
590
 
552
- // Execute transfer based on chain type
553
- // Tempo chain (42431) uses token-based(fee token for the current contract is pathUSD)fees instead of native currency
554
- const receipt =
555
- this.config.chainId === 42431
556
- ? await this._executeTempoTransfer(
557
- senderWallet,
558
- senderAddress,
559
- recipientAddress,
560
- tokenAddress,
561
- proof,
562
- useOffchainVerify,
563
- fee,
564
- )
565
- : await this._executeStandardTransfer(
566
- senderWallet,
567
- recipientAddress,
568
- tokenAddress,
569
- proof,
570
- useOffchainVerify,
571
- );
591
+ const receipt = await tx.wait();
592
+ if (!receipt || receipt.status === 0) {
593
+ throw new Error("Transfer transaction failed");
594
+ }
572
595
 
573
596
  if (waitForFinalization) {
574
597
  await this._waitForGlobalState(senderAddress, "transfer");
@@ -587,159 +610,6 @@ export class ConfidentialTransferClient {
587
610
  }
588
611
  }
589
612
 
590
- /**
591
- * Execute confidential transfer on Tempo chain (chainId 42431)
592
- *
593
- * Tempo is a stablecoin-focused chain without native currency for smart contracts.
594
- * This method handles token-based fee payment using PathUSD, requiring:
595
- * - Fee token approval before transfer
596
- * - Fee token balance validation
597
- * - Gas estimation with fallback for estimation failures
598
- *
599
- * @private
600
- */
601
- async _executeTempoTransfer(
602
- senderWallet,
603
- senderAddress,
604
- recipientAddress,
605
- tokenAddress,
606
- proof,
607
- useOffchainVerify,
608
- fee,
609
- ) {
610
- const feeTokenAddress = TEMPO_FEE_TOKEN_ADDRESS;
611
- let tx;
612
-
613
- // Check if feeTokenAddress is configured - this indicates token-based fee payment
614
- if (feeTokenAddress) {
615
- // Approve fee token for the contract
616
- const feeTokenContract = this._getTokenContract(feeTokenAddress);
617
-
618
- // Check balance first
619
- const feeTokenBalance = await feeTokenContract.balanceOf(senderAddress);
620
- if (feeTokenBalance < fee) {
621
- throw new Error(
622
- `Insufficient fee token ${TEMPO_FEE_TOKEN_ADDRESS} balance. Required: ${fee}, Available: ${feeTokenBalance}`,
623
- );
624
- }
625
-
626
- const allowance = await feeTokenContract.allowance(
627
- senderAddress,
628
- this.config.contractAddress,
629
- );
630
-
631
- if (allowance < fee) {
632
- const approveTx = await feeTokenContract
633
- .connect(senderWallet)
634
- .approve(this.config.contractAddress, ethers.MaxUint256);
635
-
636
- const approveReceipt = await approveTx.wait();
637
- if (!approveReceipt || approveReceipt.status === 0) {
638
- throw new Error("Fee token approval failed");
639
- }
640
- }
641
-
642
- // Try to estimate gas first to catch any revert reasons early
643
- let gasLimit = 2_000_000n;
644
- try {
645
- const estimatedGas = await this.contract
646
- .connect(senderWallet)
647
- .transferConfidential.estimateGas(
648
- recipientAddress,
649
- tokenAddress,
650
- ethers.getBytes(encodeTransferProof(proof.data)),
651
- useOffchainVerify,
652
- { value: 0 },
653
- );
654
- // Add 20% buffer to estimated gas
655
- gasLimit = (estimatedGas * 120n) / 100n;
656
- } catch (gasEstError) {
657
- // If gas estimation fails, use default gas limit
658
- console.warn(
659
- `Gas estimation failed, using default gas limit: ${gasEstError?.message || String(gasEstError)}`,
660
- );
661
- }
662
-
663
- tx = await this.contract
664
- .connect(senderWallet)
665
- .transferConfidential(
666
- recipientAddress,
667
- tokenAddress,
668
- ethers.getBytes(encodeTransferProof(proof.data)),
669
- useOffchainVerify,
670
- { value: 0, gasLimit },
671
- );
672
- } else {
673
- try {
674
- tx = await this.contract
675
- .connect(senderWallet)
676
- .transferConfidential(
677
- recipientAddress,
678
- tokenAddress,
679
- ethers.getBytes(encodeTransferProof(proof.data)),
680
- useOffchainVerify,
681
- { value: fee },
682
- );
683
- } catch (gasError) {
684
- if (
685
- gasError?.code === "CALL_EXCEPTION" ||
686
- gasError?.code === "UNKNOWN_ERROR" ||
687
- gasError?.message?.includes("estimateGas") ||
688
- gasError?.message?.includes("missing revert data")
689
- ) {
690
- const gasLimit = 2_000_000n;
691
- tx = await this.contract
692
- .connect(senderWallet)
693
- .transferConfidential(
694
- recipientAddress,
695
- tokenAddress,
696
- ethers.getBytes(encodeTransferProof(proof.data)),
697
- useOffchainVerify,
698
- { value: fee, gasLimit },
699
- );
700
- } else {
701
- throw gasError;
702
- }
703
- }
704
- }
705
-
706
- const receipt = await tx.wait();
707
- if (!receipt || receipt.status === 0) {
708
- throw new Error("Transfer transaction failed");
709
- }
710
-
711
- return receipt;
712
- }
713
-
714
- /**
715
- * Execute confidential transfer on standard chains
716
- * @private
717
- */
718
- async _executeStandardTransfer(
719
- senderWallet,
720
- recipientAddress,
721
- tokenAddress,
722
- proof,
723
- useOffchainVerify,
724
- ) {
725
- const fee = await this.contract.feeAmount();
726
- const tx = await this.contract
727
- .connect(senderWallet)
728
- .transferConfidential(
729
- recipientAddress,
730
- tokenAddress,
731
- ethers.getBytes(encodeTransferProof(proof.data)),
732
- useOffchainVerify,
733
- { value: fee },
734
- );
735
-
736
- const receipt = await tx.wait();
737
- if (!receipt || receipt.status === 0) {
738
- throw new Error("Transfer transaction failed");
739
- }
740
-
741
- return receipt;
742
- }
743
613
  /**
744
614
  * Withdraw confidential tokens to public ERC20
745
615
  *
@@ -778,6 +648,7 @@ export class ConfidentialTransferClient {
778
648
  }
779
649
 
780
650
  const address = await wallet.getAddress();
651
+
781
652
  // Fetch balance first — only apply pending if available is insufficient
782
653
  let balanceSummary = await this.getConfidentialBalance(
783
654
  address,
@@ -838,17 +709,39 @@ export class ConfidentialTransferClient {
838
709
 
839
710
  if (!proof.success) {
840
711
  throw new Error(
841
- `Withdrawal proof generation failed: ${proof.error || "Unknown error. Check your balance and amount."}`,
712
+ `Withdrawal proof generation failed: ${
713
+ proof.error || "Unknown error. Check your balance and amount."
714
+ }`,
842
715
  );
843
716
  }
844
717
 
845
718
  // Execute withdrawal
719
+ const encodedProof = ethers.getBytes(encodeWithdrawProof(proof.data));
720
+ let withdrawZkpArg;
721
+
722
+ if (this.config.chainId === 42431) {
723
+ // Tempo: upload proof to IPFS and pass an ipfs:// pointer
724
+ try {
725
+ const cid = await uploadBytesToIpfs(
726
+ encodedProof,
727
+ "withdraw-proof.bin",
728
+ );
729
+ withdrawZkpArg = ethers.toUtf8Bytes(`ipfs://${cid}`);
730
+ } catch (ipfsError) {
731
+ throw new Error(
732
+ `Failed to upload proof to IPFS: ${ipfsError.message}`,
733
+ );
734
+ }
735
+ } else {
736
+ withdrawZkpArg = encodedProof;
737
+ }
738
+
846
739
  const tx = await this.contract
847
740
  .connect(wallet)
848
741
  .withdraw(
849
742
  tokenAddress,
850
743
  BigInt(withdrawAmount),
851
- ethers.getBytes(encodeWithdrawProof(proof.data)),
744
+ withdrawZkpArg,
852
745
  useOffchainVerify,
853
746
  );
854
747
 
@@ -930,7 +823,9 @@ export class ConfidentialTransferClient {
930
823
  } catch (error) {
931
824
  // If we can't get account info, wait and retry
932
825
  console.warn(
933
- `Warning: Failed to check account state (attempt ${attempts + 1}): ${error.message}`,
826
+ `Warning: Failed to check account state (attempt ${attempts + 1}): ${
827
+ error.message
828
+ }`,
934
829
  );
935
830
  }
936
831
 
package/src/constants.js CHANGED
@@ -25,12 +25,12 @@ export const TEMPO_FEE_TOKEN_ADDRESS =
25
25
  "0x20c0000000000000000000000000000000000000";
26
26
 
27
27
  export const STABLETRUST_CONTRACTS_BY_CHAIN_ID = Object.freeze({
28
- 2201: "0xB96aa42b246a956B170fE426A72fB610E4976f9E", //Stable
29
- 1244: "0xb20aB54e1c6AE55B0DD11FEB7FFf3fF1E9631f19", //Arc
30
- 84532: "0x962a8A7CD28BfFBb17C4F6Ec388782cca3ffd618", //Base
31
- 11155111: "0x2E48F3D9b8F4aCA9E6AF0630eaB2ceB7A3f5eEd1", //Ethereum
32
- 421614: "0x14Afd604971bee5b5fac52df2d56CaE421519Cc5", //Arbitrum
33
- 42431: "0x08B6563C95dfe3a4F5533CAA6F7D55a74FCb4F6c", //Tempo
28
+ 2201: "0xdf43bC80B5b22A858860947d383b6F4d6C81d8EC", //Stable
29
+ 1244: "0x5d43CE5269Cd46badC67B3664369862F20eC5649", //Arc
30
+ 84532: "0xFBEa2AbCf1208E09dd90266fE94Fb76d8BfC34d9", //Base
31
+ 11155111: "0x72B87207791996F416D2F3B0dcAcbb07F445C496", //Ethereum
32
+ 421614: "0x6C9eDBDd028Fe610b054187A34712714E341D112", //Arbitrum
33
+ 42431: "0xE559fB936C69c46E216bf61B07C16bF1a6d444aa", //Tempo
34
34
  });
35
35
 
36
36
  export function getStableTrustContractAddress(chainId) {
package/src/utils.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { ethers } from "ethers";
2
-
2
+ // Pinata uploads API endpoint
3
+ // Docs: https://docs.pinata.cloud
4
+ const PINATA_UPLOAD_URL = "https://uploads.pinata.cloud/v3/files";
5
+ const PINATA_JWT = process.env.PINATA_JWT;
3
6
  /**
4
7
  * Encodes the ZK-Proof data for a transfer into a format the Solidity contract expects.
5
8
  *
@@ -82,3 +85,78 @@ export async function waitForCondition(
82
85
  }
83
86
  throw new Error(`Timeout waiting for ${actionLabel}`);
84
87
  }
88
+
89
+ /**
90
+ * Upload a JSON-serializable object to IPFS and return its CID (as a string).
91
+ * The object will be stored as a UTF-8 JSON blob.
92
+ *
93
+ * @param {any} data - JSON-serializable data to store.
94
+ * @returns {Promise<string>} The CID string.
95
+ */
96
+ export async function uploadJsonToIpfs(data) {
97
+ if (!PINATA_JWT) {
98
+ throw new Error("REACT_APP_PINATA_JWT is not set");
99
+ }
100
+
101
+ const json = JSON.stringify(data);
102
+ const blob = new Blob([json], { type: "application/json" });
103
+
104
+ const form = new FormData();
105
+ form.append("file", blob, "proof.json");
106
+ form.append("network", "public");
107
+ form.append("name", "zk-proof");
108
+
109
+ const res = await fetch(PINATA_UPLOAD_URL, {
110
+ method: "POST",
111
+ headers: {
112
+ Authorization: `Bearer ${PINATA_JWT}`,
113
+ },
114
+ body: form,
115
+ });
116
+
117
+ if (!res.ok) {
118
+ const text = await res.text();
119
+ throw new Error(`Pinata upload failed: ${text}`);
120
+ }
121
+
122
+ const jsonRes = await res.json();
123
+ return jsonRes?.data?.cid?.toString();
124
+ }
125
+
126
+ /**
127
+ * Upload raw bytes to IPFS (Pinata) and return the CID.
128
+ *
129
+ * @param {Uint8Array|ArrayBuffer} bytes - Raw proof bytes.
130
+ * @param {string} [name='proof.bin'] - Optional file name metadata.
131
+ * @returns {Promise<string>} The CID string.
132
+ */
133
+ export async function uploadBytesToIpfs(bytes, name = "proof.bin") {
134
+ if (!PINATA_JWT) {
135
+ throw new Error("REACT_APP_PINATA_JWT is not set");
136
+ }
137
+
138
+ const uint8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
139
+
140
+ const blob = new Blob([uint8], { type: "application/octet-stream" });
141
+
142
+ const form = new FormData();
143
+ form.append("file", blob, name);
144
+ form.append("network", "public");
145
+ form.append("name", name);
146
+
147
+ const res = await fetch(PINATA_UPLOAD_URL, {
148
+ method: "POST",
149
+ headers: {
150
+ Authorization: `Bearer ${PINATA_JWT}`,
151
+ },
152
+ body: form,
153
+ });
154
+
155
+ if (!res.ok) {
156
+ const text = await res.text();
157
+ throw new Error(`Pinata upload failed: ${text}`);
158
+ }
159
+
160
+ const jsonRes = await res.json();
161
+ return jsonRes?.data?.cid?.toString();
162
+ }