@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.
- package/examples/complete-flow.js +325 -153
- package/examples/simple-snippets.js +3 -2
- package/package.json +1 -1
- package/src/client.js +74 -179
- package/src/constants.js +6 -6
- package/src/utils.js +79 -1
|
@@ -11,203 +11,375 @@ dotenv.config();
|
|
|
11
11
|
* https://docs.fairblock.network/docs/confidential_transfers/technical_overview
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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(`
|
|
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
|
|
40
|
-
console.log(
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
132
|
+
const tokenContract = new ethers.Contract(
|
|
133
|
+
chain.tokenAddress,
|
|
134
|
+
ERC20_ABI,
|
|
135
|
+
provider,
|
|
136
|
+
);
|
|
137
|
+
const tokenDecimals = await tokenContract.decimals();
|
|
53
138
|
|
|
54
|
-
|
|
55
|
-
|
|
139
|
+
console.log("Sender Address:", sender.address);
|
|
140
|
+
console.log("Recipient Address:", recipient.address);
|
|
56
141
|
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
69
|
-
"
|
|
70
|
-
ethers.
|
|
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
|
-
|
|
167
|
+
console.log(
|
|
168
|
+
"Sender Confidential Balance (Pre-Deposit):",
|
|
169
|
+
ethers.formatUnits(senderConfidentialBalanceBefore.amount, tokenDecimals),
|
|
170
|
+
);
|
|
74
171
|
|
|
75
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
109
|
-
"
|
|
110
|
-
ethers.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
209
|
+
const recipientConfidentialBalanceBeforeTx =
|
|
210
|
+
await client.getConfidentialBalance(
|
|
211
|
+
recipient.address,
|
|
212
|
+
recipientKeys.privateKey,
|
|
213
|
+
chain.tokenAddress,
|
|
214
|
+
);
|
|
121
215
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
178
|
-
"
|
|
179
|
-
ethers.
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
289
|
+
const recipientConfidentialBalanceBeforeWithdraw =
|
|
290
|
+
await client.getConfidentialBalance(
|
|
291
|
+
recipient.address,
|
|
292
|
+
recipientKeys.privateKey,
|
|
293
|
+
chain.tokenAddress,
|
|
294
|
+
);
|
|
191
295
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
"
|
|
201
|
-
ethers.formatUnits(
|
|
202
|
-
recipientConfidentialBalanceAfterWithdraw.amount,
|
|
203
|
-
tokenDecimals,
|
|
204
|
-
),
|
|
358
|
+
"=== Starting Multi-Chain Confidential Flow Performance Test ===\n",
|
|
205
359
|
);
|
|
206
360
|
|
|
207
|
-
|
|
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
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 {
|
|
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: ${
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
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: ${
|
|
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
|
-
|
|
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}): ${
|
|
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: "
|
|
29
|
-
1244: "
|
|
30
|
-
84532: "
|
|
31
|
-
11155111: "
|
|
32
|
-
421614: "
|
|
33
|
-
42431: "
|
|
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
|
+
}
|