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