@fairblock/stabletrust 2.0.7 → 2.0.10
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/README.md +177 -53
- package/examples/anonymous-flow.js +13 -7
- package/package.json +10 -3
- package/src/anonymous-client.js +531 -76
- package/src/backend.js +19 -0
- package/src/confidential-client.js +497 -82
- package/src/constants.js +36 -9
- package/src/ibe.js +59 -0
- package/src/index.d.ts +85 -18
- package/src/index.js +6 -0
- package/src/utils.js +169 -62
- package/src/vendor/ts-ibe/age/age-encrypt-decrypt.js +127 -0
- package/src/vendor/ts-ibe/age/age-reader-writer.js +94 -0
- package/src/vendor/ts-ibe/age/armor.js +52 -0
- package/src/vendor/ts-ibe/age/no-op-encdec.js +72 -0
- package/src/vendor/ts-ibe/age/stream-cipher.js +99 -0
- package/src/vendor/ts-ibe/age/utils-crypto.js +60 -0
- package/src/vendor/ts-ibe/age/utils.js +59 -0
- package/src/vendor/ts-ibe/crypto/ibe.js +198 -0
- package/src/vendor/ts-ibe/crypto/utils.js +50 -0
- package/src/vendor/ts-ibe/index.js +87 -0
- package/src/vendor/ts-ibe/types/index.js +2 -0
- package/src/vendor/ts-ibe/version.js +2 -0
- package/src/wasm-loader.js +3 -3
- package/pkg/README.md +0 -310
|
@@ -2,6 +2,12 @@ import { ethers } from "ethers";
|
|
|
2
2
|
import {
|
|
3
3
|
CONTRACT_ABI,
|
|
4
4
|
ERC20_ABI,
|
|
5
|
+
TRANSFER_CONFIDENTIAL_SIGNATURE,
|
|
6
|
+
WITHDRAW_CONFIDENTIAL_SIGNATURE,
|
|
7
|
+
FEE_TOKEN_SIGNATURE,
|
|
8
|
+
NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE,
|
|
9
|
+
ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE,
|
|
10
|
+
ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE,
|
|
5
11
|
getStabletrustContractAddress,
|
|
6
12
|
} from "./constants.js";
|
|
7
13
|
import { deriveKeys, decryptCiphertext, combineCiphertext } from "./crypto.js";
|
|
@@ -9,9 +15,12 @@ import {
|
|
|
9
15
|
encodeTransferProof,
|
|
10
16
|
encodeWithdrawProof,
|
|
11
17
|
sleep,
|
|
18
|
+
toContractScale,
|
|
12
19
|
uploadBytesToIpfs,
|
|
13
20
|
} from "./utils.js";
|
|
14
21
|
import { initializeWasm } from "./wasm-loader.js";
|
|
22
|
+
import { encryptRandomness } from "./ibe.js";
|
|
23
|
+
import { createRequest, CHAIN_KEY_BY_ID } from "./backend.js";
|
|
15
24
|
|
|
16
25
|
// Auto-initialize WASM on first use
|
|
17
26
|
let wasmModulePromise = null;
|
|
@@ -33,8 +42,12 @@ export class ConfidentialTransferClient {
|
|
|
33
42
|
* @param {string} rpcUrl - RPC endpoint URL
|
|
34
43
|
* @param {string|number} contractAddressOrChainId - Contract address or chain ID
|
|
35
44
|
* @param {number} [chainId] - Chain ID when contract address is provided
|
|
45
|
+
* @param {Object} [options]
|
|
46
|
+
* @param {string} [options.ipfsUploadUrl] - StableTrust IPFS upload endpoint for off-chain proof uploads
|
|
47
|
+
* @param {string} [options.ipfsApiKey] - Optional bearer token for the StableTrust IPFS upload endpoint
|
|
48
|
+
* @param {string} [options.apiBaseUrl] - StableTrust backend API base URL for request persistence
|
|
36
49
|
*/
|
|
37
|
-
constructor(rpcUrl, contractAddressOrChainId, chainId) {
|
|
50
|
+
constructor(rpcUrl, contractAddressOrChainId, chainId, options = {}) {
|
|
38
51
|
// Validate required config
|
|
39
52
|
if (!rpcUrl) {
|
|
40
53
|
throw new Error("rpcUrl is required");
|
|
@@ -42,10 +55,15 @@ export class ConfidentialTransferClient {
|
|
|
42
55
|
|
|
43
56
|
let resolvedChainId;
|
|
44
57
|
let resolvedContractAddress;
|
|
58
|
+
let ipfsOptions = options || {};
|
|
45
59
|
|
|
46
|
-
if (
|
|
60
|
+
if (
|
|
61
|
+
typeof contractAddressOrChainId === "number" &&
|
|
62
|
+
(chainId === undefined || typeof chainId === "object")
|
|
63
|
+
) {
|
|
47
64
|
resolvedChainId = contractAddressOrChainId;
|
|
48
65
|
resolvedContractAddress = getStabletrustContractAddress(resolvedChainId);
|
|
66
|
+
ipfsOptions = typeof chainId === "object" && chainId !== null ? chainId : ipfsOptions;
|
|
49
67
|
} else {
|
|
50
68
|
resolvedChainId = chainId;
|
|
51
69
|
resolvedContractAddress =
|
|
@@ -73,6 +91,9 @@ export class ConfidentialTransferClient {
|
|
|
73
91
|
rpcUrl,
|
|
74
92
|
contractAddress: ethers.getAddress(resolvedContractAddress),
|
|
75
93
|
chainId: Number(resolvedChainId),
|
|
94
|
+
ipfsUploadUrl: ipfsOptions.ipfsUploadUrl || null,
|
|
95
|
+
ipfsApiKey: ipfsOptions.ipfsApiKey || null,
|
|
96
|
+
apiBaseUrl: ipfsOptions.apiBaseUrl || null,
|
|
76
97
|
};
|
|
77
98
|
|
|
78
99
|
// WASM will be auto-initialized on first use
|
|
@@ -88,6 +109,7 @@ export class ConfidentialTransferClient {
|
|
|
88
109
|
CONTRACT_ABI,
|
|
89
110
|
this.provider,
|
|
90
111
|
);
|
|
112
|
+
this._assertLatestContractAbi();
|
|
91
113
|
} catch (error) {
|
|
92
114
|
throw new Error(
|
|
93
115
|
`Failed to initialize contracts: ${error.message}. Check your RPC URL and contract addresses.`,
|
|
@@ -95,6 +117,132 @@ export class ConfidentialTransferClient {
|
|
|
95
117
|
}
|
|
96
118
|
}
|
|
97
119
|
|
|
120
|
+
/**
|
|
121
|
+
* Fire-and-forget request row creation
|
|
122
|
+
* @private
|
|
123
|
+
*/
|
|
124
|
+
_postRequest(data) {
|
|
125
|
+
const apiBaseUrl = this.config.apiBaseUrl || undefined;
|
|
126
|
+
const payload = {
|
|
127
|
+
chainKey: CHAIN_KEY_BY_ID[this.config.chainId] || String(this.config.chainId),
|
|
128
|
+
chainId: this.config.chainId,
|
|
129
|
+
...data,
|
|
130
|
+
};
|
|
131
|
+
createRequest(payload, apiBaseUrl).catch((err) => {
|
|
132
|
+
const status = err?.response?.status;
|
|
133
|
+
const body = err?.response?.data;
|
|
134
|
+
console.warn("[StableTrust] Failed to post request:", err.message);
|
|
135
|
+
if (status) console.warn("[StableTrust] Status:", status);
|
|
136
|
+
if (body) console.warn("[StableTrust] Response:", JSON.stringify(body, null, 2));
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract request/tx ID from receipt logs
|
|
142
|
+
* @private
|
|
143
|
+
*/
|
|
144
|
+
_getRequestIdFromReceipt(receipt, eventNames) {
|
|
145
|
+
try {
|
|
146
|
+
const iface = this.contract.interface;
|
|
147
|
+
const contractAddr = this.config.contractAddress.toLowerCase();
|
|
148
|
+
|
|
149
|
+
for (const log of receipt.logs) {
|
|
150
|
+
if (log.address.toLowerCase() === contractAddr) {
|
|
151
|
+
const txId = this._parseLogItem(iface, log, eventNames);
|
|
152
|
+
if (txId) return txId;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const log of receipt.logs) {
|
|
157
|
+
const txId = this._parseLogItem(iface, log, eventNames);
|
|
158
|
+
if (txId) return txId;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const name of eventNames) {
|
|
162
|
+
try {
|
|
163
|
+
const topicHash = iface.getEvent(name).topicHash;
|
|
164
|
+
for (const log of receipt.logs) {
|
|
165
|
+
if (log.topics[0] === topicHash && log.topics.length >= 3) {
|
|
166
|
+
const txId = BigInt(log.topics[2]).toString();
|
|
167
|
+
if (txId !== "0") return txId;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
} catch {
|
|
171
|
+
// Event not in ABI
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return null;
|
|
176
|
+
} catch {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Parse a single log item to extract txId
|
|
183
|
+
* @private
|
|
184
|
+
*/
|
|
185
|
+
_parseLogItem(iface, log, eventNames) {
|
|
186
|
+
try {
|
|
187
|
+
const parsed = iface.parseLog({
|
|
188
|
+
topics: [...log.topics],
|
|
189
|
+
data: log.data,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!parsed || !eventNames.includes(parsed.name)) return null;
|
|
193
|
+
|
|
194
|
+
if (parsed.args.txId !== undefined && parsed.args.txId !== null) {
|
|
195
|
+
return parsed.args.txId.toString();
|
|
196
|
+
}
|
|
197
|
+
if (parsed.args.requestId !== undefined && parsed.args.requestId !== null) {
|
|
198
|
+
return parsed.args.requestId.toString();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
let txId;
|
|
202
|
+
if (
|
|
203
|
+
parsed.name === "TransferRequested" ||
|
|
204
|
+
parsed.name === "TransferProcessed"
|
|
205
|
+
) {
|
|
206
|
+
txId = parsed.args[2];
|
|
207
|
+
} else {
|
|
208
|
+
txId = parsed.args[1];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return txId !== undefined && txId !== null ? txId.toString() : null;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get requestId from account core state (txId)
|
|
219
|
+
* @private
|
|
220
|
+
*/
|
|
221
|
+
async _getRequestIdFromAccountCore(address) {
|
|
222
|
+
try {
|
|
223
|
+
const account = await this.contract.getAccountCore(address);
|
|
224
|
+
const txId = account.txId?.toString?.() ?? String(account.txId);
|
|
225
|
+
return txId && txId !== "0" ? txId : null;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Resolve a request ID from receipt logs, then account state, then tx hash
|
|
233
|
+
* @private
|
|
234
|
+
*/
|
|
235
|
+
async _resolveRequestId(receipt, address, eventNames) {
|
|
236
|
+
let requestId = this._getRequestIdFromReceipt(receipt, eventNames);
|
|
237
|
+
if (!requestId) {
|
|
238
|
+
requestId = await this._getRequestIdFromAccountCore(address);
|
|
239
|
+
}
|
|
240
|
+
if (!requestId || requestId === "0") {
|
|
241
|
+
requestId = receipt.hash;
|
|
242
|
+
}
|
|
243
|
+
return requestId;
|
|
244
|
+
}
|
|
245
|
+
|
|
98
246
|
/**
|
|
99
247
|
* Get WASM module (auto-initializes if needed)
|
|
100
248
|
* @private
|
|
@@ -114,6 +262,121 @@ export class ConfidentialTransferClient {
|
|
|
114
262
|
return new ethers.Contract(tokenAddress, ERC20_ABI, this.provider);
|
|
115
263
|
}
|
|
116
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Fail fast when a stale SDK ABI is installed against the latest contract.
|
|
267
|
+
* This catches the common dependency-lock problem where the contract has
|
|
268
|
+
* `bytes,bool` proof methods but node_modules still contains the older ABI.
|
|
269
|
+
* @private
|
|
270
|
+
*/
|
|
271
|
+
_assertLatestContractAbi() {
|
|
272
|
+
const requiredSignatures = [
|
|
273
|
+
TRANSFER_CONFIDENTIAL_SIGNATURE,
|
|
274
|
+
WITHDRAW_CONFIDENTIAL_SIGNATURE,
|
|
275
|
+
FEE_TOKEN_SIGNATURE,
|
|
276
|
+
NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE,
|
|
277
|
+
ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE,
|
|
278
|
+
ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE,
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
const missingSignatures = requiredSignatures.filter(
|
|
282
|
+
(signature) => !this.contract.interface.getFunction(signature),
|
|
283
|
+
);
|
|
284
|
+
if (missingSignatures.length > 0) {
|
|
285
|
+
throw new Error(
|
|
286
|
+
`Installed @fairblock/stabletrust ABI is stale or incomplete. Missing ${missingSignatures.join(", ")}. Reinstall/update the SDK.`,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolve whether non-anonymous proof bytes should be submitted inline or via
|
|
293
|
+
* StableTrust IPFS. `offchainZKP` is the latest contract flag;
|
|
294
|
+
* `useOffchainVerify` is kept as a deprecated SDK alias for older callers.
|
|
295
|
+
* Tempo keeps the old SDK behaviour of using IPFS by default.
|
|
296
|
+
* @private
|
|
297
|
+
*/
|
|
298
|
+
_resolveOffchainZKP(options = {}) {
|
|
299
|
+
if (options.offchainZKP !== undefined) return Boolean(options.offchainZKP);
|
|
300
|
+
if (options.useOffchainVerify !== undefined) return Boolean(options.useOffchainVerify);
|
|
301
|
+
return this.config.chainId === 42431;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Prepare payment for a non-anonymous confidential transfer. Both inline and
|
|
306
|
+
* IPFS proof paths use the same live `nonAnonymousTransferFee`. Native fees
|
|
307
|
+
* are attached as msg.value; ERC-20 fees are approved for the diamond and
|
|
308
|
+
* collected by the contract with transferFrom.
|
|
309
|
+
* @private
|
|
310
|
+
*/
|
|
311
|
+
async _prepareNonAnonymousTransferFee(senderWallet) {
|
|
312
|
+
const [feeTokenRaw, feeAmountRaw] = await Promise.all([
|
|
313
|
+
this.contract.feeToken(),
|
|
314
|
+
this.contract.nonAnonymousTransferFee(),
|
|
315
|
+
]);
|
|
316
|
+
|
|
317
|
+
const feeToken = ethers.getAddress(feeTokenRaw);
|
|
318
|
+
const feeAmount = BigInt(feeAmountRaw);
|
|
319
|
+
|
|
320
|
+
if (feeAmount === 0n) {
|
|
321
|
+
return { value: 0n };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (feeToken === ethers.ZeroAddress) {
|
|
325
|
+
return { value: feeAmount };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const senderAddress = await senderWallet.getAddress();
|
|
329
|
+
const feeTokenContract = this._getTokenContract(feeToken);
|
|
330
|
+
const [balance, allowance] = await Promise.all([
|
|
331
|
+
feeTokenContract.balanceOf(senderAddress),
|
|
332
|
+
feeTokenContract.allowance(senderAddress, this.config.contractAddress),
|
|
333
|
+
]);
|
|
334
|
+
|
|
335
|
+
if (balance < feeAmount) {
|
|
336
|
+
throw new Error(
|
|
337
|
+
`Insufficient fee token balance. Required: ${feeAmount}, available: ${balance}, fee token: ${feeToken}`,
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (allowance < feeAmount) {
|
|
342
|
+
const approveTx = await feeTokenContract
|
|
343
|
+
.connect(senderWallet)
|
|
344
|
+
.approve(this.config.contractAddress, ethers.MaxUint256);
|
|
345
|
+
const approveReceipt = await approveTx.wait();
|
|
346
|
+
if (!approveReceipt || approveReceipt.status === 0) {
|
|
347
|
+
throw new Error("Fee token approval failed");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { value: 0n };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* For the latest contract, public transfer/withdraw always pass
|
|
356
|
+
* `(bytes proof, bool offchainZKP)`. When `offchainZKP=true`, the bytes arg is
|
|
357
|
+
* the bare CID encoded as UTF-8 bytes. When false, it is the full ABI proof.
|
|
358
|
+
* @private
|
|
359
|
+
*/
|
|
360
|
+
async _resolveContractProofArg(encodedProof, proofName, offchainZKP) {
|
|
361
|
+
if (!offchainZKP) {
|
|
362
|
+
return encodedProof;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
try {
|
|
366
|
+
const cid = await uploadBytesToIpfs(encodedProof, proofName, {
|
|
367
|
+
uploadUrl: this.config.ipfsUploadUrl,
|
|
368
|
+
apiKey: this.config.ipfsApiKey,
|
|
369
|
+
});
|
|
370
|
+
const bareCid = String(cid || "").trim().replace(/^ipfs:\/\//i, "");
|
|
371
|
+
if (!bareCid) {
|
|
372
|
+
throw new Error("IPFS upload returned no CID");
|
|
373
|
+
}
|
|
374
|
+
return ethers.toUtf8Bytes(bareCid);
|
|
375
|
+
} catch (ipfsError) {
|
|
376
|
+
throw new Error(`Failed to upload proof to IPFS: ${ipfsError.message}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
117
380
|
/**
|
|
118
381
|
* Derive encryption keys for a wallet
|
|
119
382
|
*
|
|
@@ -220,10 +483,13 @@ export class ConfidentialTransferClient {
|
|
|
220
483
|
/**
|
|
221
484
|
* Get total decrypted balance (available + pending) for an address
|
|
222
485
|
*
|
|
486
|
+
* Amounts are in raw token units (bigint). Note that this differs from the anonymous client, which returns
|
|
487
|
+
* contract scale (amount * 100 / 10^decimals).
|
|
488
|
+
*
|
|
223
489
|
* @param {string} address - Account address
|
|
224
490
|
* @param {string} privateKey - Private key for decryption
|
|
225
491
|
* @param {string} tokenAddress - Token address
|
|
226
|
-
* @returns {Promise<{amount:
|
|
492
|
+
* @returns {Promise<{amount: bigint, available: {amount: bigint, ciphertext: string|null}, pending: {amount: bigint, ciphertext: string|null}}>}
|
|
227
493
|
*/
|
|
228
494
|
async getConfidentialBalance(address, privateKey, tokenAddress) {
|
|
229
495
|
try {
|
|
@@ -329,7 +595,11 @@ export class ConfidentialTransferClient {
|
|
|
329
595
|
|
|
330
596
|
if (pendingBalance.amount > 0n) {
|
|
331
597
|
try {
|
|
332
|
-
await this._applyPending(wallet, {
|
|
598
|
+
await this._applyPending(wallet, {
|
|
599
|
+
waitForFinalization: true,
|
|
600
|
+
tokenAddress,
|
|
601
|
+
pendingAmount: pendingBalance.amount,
|
|
602
|
+
});
|
|
333
603
|
} catch (error) {
|
|
334
604
|
console.warn(
|
|
335
605
|
`Warning: Failed to apply pending balance before ${actionLabel}: ${error.message}. You may have pending balance that is not yet applied.`,
|
|
@@ -373,8 +643,7 @@ export class ConfidentialTransferClient {
|
|
|
373
643
|
|
|
374
644
|
const tokenContract = this._getTokenContract(tokenAddress);
|
|
375
645
|
const tokenDecimals = await tokenContract.decimals();
|
|
376
|
-
const depositAmount =
|
|
377
|
-
(BigInt(amount) * 100n) / 10n ** BigInt(tokenDecimals);
|
|
646
|
+
const depositAmount = toContractScale(amount, tokenDecimals);
|
|
378
647
|
|
|
379
648
|
// Check token balance and allowance in parallel
|
|
380
649
|
const [tokenBalance, allowance] = await Promise.all([
|
|
@@ -387,6 +656,7 @@ export class ConfidentialTransferClient {
|
|
|
387
656
|
);
|
|
388
657
|
}
|
|
389
658
|
|
|
659
|
+
const depositOverrides = {};
|
|
390
660
|
if (allowance < BigInt(amount)) {
|
|
391
661
|
const approveTx = await tokenContract
|
|
392
662
|
.connect(wallet)
|
|
@@ -396,11 +666,17 @@ export class ConfidentialTransferClient {
|
|
|
396
666
|
if (!approveReceipt || approveReceipt.status === 0) {
|
|
397
667
|
throw new Error("Token approval failed");
|
|
398
668
|
}
|
|
669
|
+
// Pin the deposit nonce to (approve nonce + 1). The approve was just
|
|
670
|
+
// broadcast from this same EOA; letting ethers resolve the deposit nonce
|
|
671
|
+
// via getTransactionCount can still return the pre-approve count on a fast
|
|
672
|
+
// node and assign the deposit the SAME nonce as the approve, which the node
|
|
673
|
+
// rejects as "nonce too low" (NONCE_EXPIRED).
|
|
674
|
+
depositOverrides.nonce = approveTx.nonce + 1;
|
|
399
675
|
}
|
|
400
676
|
// Perform deposit
|
|
401
677
|
const depositTx = await this.contract
|
|
402
678
|
.connect(wallet)
|
|
403
|
-
.deposit(tokenAddress, depositAmount);
|
|
679
|
+
.deposit(tokenAddress, depositAmount, depositOverrides);
|
|
404
680
|
|
|
405
681
|
const receipt = await depositTx.wait();
|
|
406
682
|
if (!receipt || receipt.status === 0) {
|
|
@@ -411,6 +687,27 @@ export class ConfidentialTransferClient {
|
|
|
411
687
|
await this._waitForGlobalState(address, "deposit");
|
|
412
688
|
}
|
|
413
689
|
|
|
690
|
+
const tokenSymbol = await tokenContract.symbol();
|
|
691
|
+
const requestId = await this._resolveRequestId(receipt, address, [
|
|
692
|
+
"DepositRequested",
|
|
693
|
+
"DepositProcessed",
|
|
694
|
+
]);
|
|
695
|
+
|
|
696
|
+
this._postRequest({
|
|
697
|
+
requestId,
|
|
698
|
+
userAddress: address.toLowerCase(),
|
|
699
|
+
operationKind: 1,
|
|
700
|
+
type: "deposit",
|
|
701
|
+
transactionHash: receipt.hash,
|
|
702
|
+
status: "pending",
|
|
703
|
+
details: {
|
|
704
|
+
token: tokenSymbol,
|
|
705
|
+
tokenAddress,
|
|
706
|
+
amount: depositAmount.toString(),
|
|
707
|
+
amountInWei: true,
|
|
708
|
+
},
|
|
709
|
+
});
|
|
710
|
+
|
|
414
711
|
return receipt;
|
|
415
712
|
} catch (error) {
|
|
416
713
|
if (error.message.includes("Insufficient token balance")) {
|
|
@@ -426,7 +723,7 @@ export class ConfidentialTransferClient {
|
|
|
426
723
|
* @param {ethers.Wallet|ethers.Signer} senderWallet - Sender's wallet
|
|
427
724
|
* @param {string} recipientAddress - Recipient's address
|
|
428
725
|
* @param {string} tokenAddress - Token address to transfer
|
|
429
|
-
* @param {number} amount - Amount to transfer
|
|
726
|
+
* @param {bigint|string|number} amount - Amount to transfer (in token units)
|
|
430
727
|
* @param {Object} [options] - Options
|
|
431
728
|
* @param {boolean} [options.waitForFinalization=true] - Wait for transfer finalization
|
|
432
729
|
* @returns {Promise<Object>} Transaction receipt
|
|
@@ -439,6 +736,7 @@ export class ConfidentialTransferClient {
|
|
|
439
736
|
options = {},
|
|
440
737
|
) {
|
|
441
738
|
const { waitForFinalization = true } = options;
|
|
739
|
+
const offchainZKP = this._resolveOffchainZKP(options);
|
|
442
740
|
|
|
443
741
|
try {
|
|
444
742
|
// Validate inputs
|
|
@@ -456,8 +754,7 @@ export class ConfidentialTransferClient {
|
|
|
456
754
|
}
|
|
457
755
|
const tokenContract = this._getTokenContract(tokenAddress);
|
|
458
756
|
const tokenDecimals = await tokenContract.decimals();
|
|
459
|
-
const transferAmount =
|
|
460
|
-
(BigInt(amount) * 100n) / 10n ** BigInt(tokenDecimals);
|
|
757
|
+
const transferAmount = toContractScale(amount, tokenDecimals);
|
|
461
758
|
|
|
462
759
|
const senderAddress = await senderWallet.getAddress();
|
|
463
760
|
|
|
@@ -497,14 +794,11 @@ export class ConfidentialTransferClient {
|
|
|
497
794
|
"transfer",
|
|
498
795
|
);
|
|
499
796
|
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
),
|
|
506
|
-
this.getFeeAmount(),
|
|
507
|
-
]);
|
|
797
|
+
const balanceSummary = await this.getConfidentialBalance(
|
|
798
|
+
senderAddress,
|
|
799
|
+
derivedSenderKeys.privateKey,
|
|
800
|
+
tokenAddress,
|
|
801
|
+
);
|
|
508
802
|
const derivedCurrentBalanceCiphertext =
|
|
509
803
|
balanceSummary.available.ciphertext;
|
|
510
804
|
const derivedCurrentBalance = balanceSummary.available.amount;
|
|
@@ -554,37 +848,20 @@ export class ConfidentialTransferClient {
|
|
|
554
848
|
);
|
|
555
849
|
}
|
|
556
850
|
const encodedProof = ethers.getBytes(encodeTransferProof(proof.data));
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const cid = await uploadBytesToIpfs(
|
|
564
|
-
encodedProof,
|
|
565
|
-
"transfer-proof.bin",
|
|
566
|
-
);
|
|
567
|
-
transferZkpArg = ethers.toUtf8Bytes(`ipfs://${cid}`);
|
|
568
|
-
} catch (ipfsError) {
|
|
569
|
-
throw new Error(
|
|
570
|
-
`Failed to upload proof to IPFS: ${ipfsError.message}`,
|
|
571
|
-
);
|
|
572
|
-
}
|
|
573
|
-
txOverrides = { value: 0n };
|
|
574
|
-
} else {
|
|
575
|
-
// Standard chains: pass encoded proof bytes with native fee
|
|
576
|
-
transferZkpArg = encodedProof;
|
|
577
|
-
txOverrides = { value: await this.contract.feeAmount() };
|
|
578
|
-
}
|
|
851
|
+
const transferZkpArg = await this._resolveContractProofArg(
|
|
852
|
+
encodedProof,
|
|
853
|
+
"transfer-proof.bin",
|
|
854
|
+
offchainZKP,
|
|
855
|
+
);
|
|
856
|
+
const txOverrides = await this._prepareNonAnonymousTransferFee(senderWallet);
|
|
579
857
|
|
|
580
|
-
const tx = await this.contract
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
);
|
|
858
|
+
const tx = await this.contract.connect(senderWallet).transferConfidential(
|
|
859
|
+
recipientAddress,
|
|
860
|
+
tokenAddress,
|
|
861
|
+
transferZkpArg,
|
|
862
|
+
offchainZKP,
|
|
863
|
+
txOverrides,
|
|
864
|
+
);
|
|
588
865
|
|
|
589
866
|
const receipt = await tx.wait();
|
|
590
867
|
if (!receipt || receipt.status === 0) {
|
|
@@ -595,6 +872,66 @@ export class ConfidentialTransferClient {
|
|
|
595
872
|
await this._waitForGlobalState(senderAddress, "transfer");
|
|
596
873
|
}
|
|
597
874
|
|
|
875
|
+
// IBE-encrypt randomness then persist both records (fire-and-forget, after finalization)
|
|
876
|
+
const txHash = receipt.hash;
|
|
877
|
+
const requestId = await this._resolveRequestId(receipt, senderAddress, [
|
|
878
|
+
"TransferRequested",
|
|
879
|
+
"TransferProcessed",
|
|
880
|
+
]);
|
|
881
|
+
const tokenSymbol = await tokenContract.symbol();
|
|
882
|
+
|
|
883
|
+
Promise.all([
|
|
884
|
+
encryptRandomness(senderAddress, proof.data.sender_randomness),
|
|
885
|
+
encryptRandomness(recipientAddress, proof.data.receiver_randomness),
|
|
886
|
+
])
|
|
887
|
+
.then(([senderEnc, receiverEnc]) => {
|
|
888
|
+
const transferDetails = {
|
|
889
|
+
sender: senderAddress.toLowerCase(),
|
|
890
|
+
recipient: recipientAddress.toLowerCase(),
|
|
891
|
+
token: tokenSymbol,
|
|
892
|
+
tokenAddress,
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
const queuedFields =
|
|
896
|
+
senderEnc.queuedEncrypted && receiverEnc.queuedEncrypted
|
|
897
|
+
? {
|
|
898
|
+
queuedEncryptedSenderRandomness: senderEnc.queuedEncrypted,
|
|
899
|
+
queuedEncryptedReceiverRandomness: receiverEnc.queuedEncrypted,
|
|
900
|
+
}
|
|
901
|
+
: {};
|
|
902
|
+
|
|
903
|
+
this._postRequest({
|
|
904
|
+
requestId,
|
|
905
|
+
userAddress: senderAddress.toLowerCase(),
|
|
906
|
+
operationKind: 2,
|
|
907
|
+
type: "transfer",
|
|
908
|
+
transactionHash: txHash,
|
|
909
|
+
status: "pending",
|
|
910
|
+
details: transferDetails,
|
|
911
|
+
encryptedSenderRandomness: senderEnc.encrypted,
|
|
912
|
+
encryptedReceiverRandomness: receiverEnc.encrypted,
|
|
913
|
+
...queuedFields,
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
this._postRequest({
|
|
917
|
+
requestId: `received-${requestId}-${recipientAddress.toLowerCase()}`,
|
|
918
|
+
userAddress: recipientAddress.toLowerCase(),
|
|
919
|
+
operationKind: 2,
|
|
920
|
+
type: "received",
|
|
921
|
+
transactionHash: txHash,
|
|
922
|
+
status: "pending",
|
|
923
|
+
details: transferDetails,
|
|
924
|
+
encryptedReceiverRandomness: receiverEnc.encrypted,
|
|
925
|
+
...(receiverEnc.queuedEncrypted
|
|
926
|
+
? {
|
|
927
|
+
queuedEncryptedReceiverRandomness:
|
|
928
|
+
receiverEnc.queuedEncrypted,
|
|
929
|
+
}
|
|
930
|
+
: {}),
|
|
931
|
+
});
|
|
932
|
+
})
|
|
933
|
+
.catch(() => {});
|
|
934
|
+
|
|
598
935
|
return receipt;
|
|
599
936
|
} catch (error) {
|
|
600
937
|
const message = error?.message ?? String(error);
|
|
@@ -613,13 +950,14 @@ export class ConfidentialTransferClient {
|
|
|
613
950
|
*
|
|
614
951
|
* @param {ethers.Wallet|ethers.Signer} wallet - The wallet to withdraw from
|
|
615
952
|
* @param {string} tokenAddress - Token address to withdraw
|
|
616
|
-
* @param {number} amount - Amount to withdraw
|
|
953
|
+
* @param {bigint|string|number} amount - Amount to withdraw (in token units)
|
|
617
954
|
* @param {Object} [options] - Options
|
|
618
955
|
* @param {boolean} [options.waitForFinalization=true] - Wait for withdrawal finalization
|
|
619
956
|
* @returns {Promise<Object>} Transaction receipt
|
|
620
957
|
*/
|
|
621
958
|
async withdraw(wallet, tokenAddress, amount, options = {}) {
|
|
622
959
|
const { waitForFinalization = true } = options;
|
|
960
|
+
const offchainZKP = this._resolveOffchainZKP(options);
|
|
623
961
|
|
|
624
962
|
try {
|
|
625
963
|
// Validate inputs
|
|
@@ -635,8 +973,7 @@ export class ConfidentialTransferClient {
|
|
|
635
973
|
|
|
636
974
|
const tokenContract = this._getTokenContract(tokenAddress);
|
|
637
975
|
const tokenDecimals = await tokenContract.decimals();
|
|
638
|
-
const withdrawAmount =
|
|
639
|
-
(BigInt(amount) * 100n) / 10n ** BigInt(tokenDecimals);
|
|
976
|
+
const withdrawAmount = toContractScale(amount, tokenDecimals);
|
|
640
977
|
|
|
641
978
|
// Auto-derive keys
|
|
642
979
|
const derivedKeys = await this._deriveKeys(wallet);
|
|
@@ -720,32 +1057,18 @@ export class ConfidentialTransferClient {
|
|
|
720
1057
|
|
|
721
1058
|
// Execute withdrawal
|
|
722
1059
|
const encodedProof = ethers.getBytes(encodeWithdrawProof(proof.data));
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const cid = await uploadBytesToIpfs(
|
|
729
|
-
encodedProof,
|
|
730
|
-
"withdraw-proof.bin",
|
|
731
|
-
);
|
|
732
|
-
withdrawZkpArg = ethers.toUtf8Bytes(`ipfs://${cid}`);
|
|
733
|
-
} catch (ipfsError) {
|
|
734
|
-
throw new Error(
|
|
735
|
-
`Failed to upload proof to IPFS: ${ipfsError.message}`,
|
|
736
|
-
);
|
|
737
|
-
}
|
|
738
|
-
} else {
|
|
739
|
-
withdrawZkpArg = encodedProof;
|
|
740
|
-
}
|
|
1060
|
+
const withdrawZkpArg = await this._resolveContractProofArg(
|
|
1061
|
+
encodedProof,
|
|
1062
|
+
"withdraw-proof.bin",
|
|
1063
|
+
offchainZKP,
|
|
1064
|
+
);
|
|
741
1065
|
|
|
742
|
-
const tx = await this.contract
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
);
|
|
1066
|
+
const tx = await this.contract.connect(wallet).withdraw(
|
|
1067
|
+
tokenAddress,
|
|
1068
|
+
BigInt(withdrawAmount),
|
|
1069
|
+
withdrawZkpArg,
|
|
1070
|
+
offchainZKP,
|
|
1071
|
+
);
|
|
749
1072
|
|
|
750
1073
|
const receipt = await tx.wait();
|
|
751
1074
|
if (!receipt || receipt.status === 0) {
|
|
@@ -756,6 +1079,27 @@ export class ConfidentialTransferClient {
|
|
|
756
1079
|
await this._waitForGlobalState(address, "withdraw");
|
|
757
1080
|
}
|
|
758
1081
|
|
|
1082
|
+
const requestId = await this._resolveRequestId(receipt, address, [
|
|
1083
|
+
"WithdrawRequested",
|
|
1084
|
+
"WithdrawProcessed",
|
|
1085
|
+
]);
|
|
1086
|
+
const tokenSymbol = await tokenContract.symbol();
|
|
1087
|
+
|
|
1088
|
+
this._postRequest({
|
|
1089
|
+
requestId,
|
|
1090
|
+
userAddress: address.toLowerCase(),
|
|
1091
|
+
operationKind: 4,
|
|
1092
|
+
type: "withdraw",
|
|
1093
|
+
transactionHash: receipt.hash,
|
|
1094
|
+
status: "pending",
|
|
1095
|
+
details: {
|
|
1096
|
+
token: tokenSymbol,
|
|
1097
|
+
tokenAddress,
|
|
1098
|
+
amount: withdrawAmount.toString(),
|
|
1099
|
+
amountInWei: true,
|
|
1100
|
+
},
|
|
1101
|
+
});
|
|
1102
|
+
|
|
759
1103
|
return receipt;
|
|
760
1104
|
} catch (error) {
|
|
761
1105
|
const message = error?.message ?? String(error);
|
|
@@ -778,7 +1122,7 @@ export class ConfidentialTransferClient {
|
|
|
778
1122
|
* @returns {Promise<Object>} Transaction receipt
|
|
779
1123
|
*/
|
|
780
1124
|
async _applyPending(wallet, options = {}) {
|
|
781
|
-
const { waitForFinalization = true } = options;
|
|
1125
|
+
const { waitForFinalization = true, tokenAddress, pendingAmount } = options;
|
|
782
1126
|
|
|
783
1127
|
try {
|
|
784
1128
|
if (!wallet) {
|
|
@@ -798,6 +1142,37 @@ export class ConfidentialTransferClient {
|
|
|
798
1142
|
await this._waitForGlobalState(address, "apply pending");
|
|
799
1143
|
}
|
|
800
1144
|
|
|
1145
|
+
const requestId = await this._resolveRequestId(receipt, address, [
|
|
1146
|
+
"ApplyPendingRequested",
|
|
1147
|
+
"ApplyPendingProcessed",
|
|
1148
|
+
]);
|
|
1149
|
+
|
|
1150
|
+
const tokenContract = tokenAddress
|
|
1151
|
+
? this._getTokenContract(tokenAddress)
|
|
1152
|
+
: null;
|
|
1153
|
+
const tokenSymbol = tokenContract
|
|
1154
|
+
? await tokenContract.symbol()
|
|
1155
|
+
: "USD₮0";
|
|
1156
|
+
|
|
1157
|
+
this._postRequest({
|
|
1158
|
+
requestId,
|
|
1159
|
+
userAddress: address.toLowerCase(),
|
|
1160
|
+
operationKind: 3,
|
|
1161
|
+
type: "secure claim",
|
|
1162
|
+
transactionHash: receipt.hash,
|
|
1163
|
+
status: "pending",
|
|
1164
|
+
details: {
|
|
1165
|
+
token: tokenSymbol,
|
|
1166
|
+
...(tokenAddress ? { tokenAddress } : {}),
|
|
1167
|
+
...(pendingAmount === undefined
|
|
1168
|
+
? {}
|
|
1169
|
+
: {
|
|
1170
|
+
pendingAmount: pendingAmount.toString(),
|
|
1171
|
+
amountInWei: true,
|
|
1172
|
+
}),
|
|
1173
|
+
},
|
|
1174
|
+
});
|
|
1175
|
+
|
|
801
1176
|
return receipt;
|
|
802
1177
|
} catch (error) {
|
|
803
1178
|
throw new Error(`Failed to apply pending: ${error.message}`);
|
|
@@ -841,15 +1216,55 @@ export class ConfidentialTransferClient {
|
|
|
841
1216
|
}
|
|
842
1217
|
|
|
843
1218
|
/**
|
|
844
|
-
* Get the
|
|
1219
|
+
* Get the currently configured fee token. address(0) means native currency.
|
|
1220
|
+
*
|
|
1221
|
+
* @returns {Promise<string>} Checksummed token address
|
|
1222
|
+
*/
|
|
1223
|
+
async getFeeToken() {
|
|
1224
|
+
try {
|
|
1225
|
+
return ethers.getAddress(await this.contract.feeToken());
|
|
1226
|
+
} catch (error) {
|
|
1227
|
+
throw new Error(`Failed to get fee token: ${error.message}`);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
/**
|
|
1232
|
+
* Get the fixed fee used by all non-anonymous confidential transfers,
|
|
1233
|
+
* regardless of whether the proof is inline or stored on IPFS.
|
|
1234
|
+
*
|
|
1235
|
+
* @returns {Promise<bigint>} Fee amount in raw fee-token units
|
|
1236
|
+
*/
|
|
1237
|
+
async getNonAnonymousTransferFee() {
|
|
1238
|
+
try {
|
|
1239
|
+
return await this.contract.nonAnonymousTransferFee();
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
throw new Error(`Failed to get non-anonymous transfer fee: ${error.message}`);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
/**
|
|
1246
|
+
* Get the fixed fee used by anonymous transfers whose proof is stored on IPFS.
|
|
1247
|
+
*
|
|
1248
|
+
* @returns {Promise<bigint>} Fee amount in raw fee-token units
|
|
1249
|
+
*/
|
|
1250
|
+
async getAnonymousIpfsTransferFee() {
|
|
1251
|
+
try {
|
|
1252
|
+
return await this.contract.anonymousIpfsTransferFee();
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
throw new Error(`Failed to get anonymous IPFS transfer fee: ${error.message}`);
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
/**
|
|
1259
|
+
* Get the fixed fee used by anonymous transfers whose proof is submitted inline.
|
|
845
1260
|
*
|
|
846
|
-
* @returns {Promise<bigint>} Fee amount in
|
|
1261
|
+
* @returns {Promise<bigint>} Fee amount in raw fee-token units
|
|
847
1262
|
*/
|
|
848
|
-
async
|
|
1263
|
+
async getAnonymousInlineTransferFee() {
|
|
849
1264
|
try {
|
|
850
|
-
return await this.contract.
|
|
1265
|
+
return await this.contract.anonymousInlineTransferFee();
|
|
851
1266
|
} catch (error) {
|
|
852
|
-
throw new Error(`Failed to get fee
|
|
1267
|
+
throw new Error(`Failed to get anonymous inline transfer fee: ${error.message}`);
|
|
853
1268
|
}
|
|
854
1269
|
}
|
|
855
1270
|
|