@fairblock/stabletrust 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -4
- package/examples/anonymous-flow.js +54 -10
- package/package.json +1 -1
- package/src/anonymous-client.js +371 -1
- package/src/constants.js +1 -1
- package/src/index.d.ts +79 -0
package/README.md
CHANGED
|
@@ -240,6 +240,23 @@ const keys = await client.deriveAnonymousKeys(authWallet, accountId);
|
|
|
240
240
|
|
|
241
241
|
Creates a new anonymous account via the relay. The relay pays gas. `accountId` is a **caller-chosen string** (non-empty, case-sensitive) — pick any unique identifier you like (e.g. a UUID or a human-readable name).
|
|
242
242
|
|
|
243
|
+
#### `ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
244
|
+
|
|
245
|
+
Idempotent helper that creates the anonymous account if it doesn't already exist, and (by default) waits until it has settled (no pending CW→EVM action) before returning. If an account with `accountId` already exists on-chain, account creation is skipped entirely — safe to call on every run with a stable `accountId`.
|
|
246
|
+
|
|
247
|
+
> Note: an auth signer can only ever be bound to a single anonymous account, so reusing a wallet across different `accountId`s will fail at creation time even via `ensureAnonymousAccount`.
|
|
248
|
+
|
|
249
|
+
```javascript
|
|
250
|
+
const { accountId, accountInfo, created } = await client.ensureAnonymousAccount(
|
|
251
|
+
wallet,
|
|
252
|
+
accountId,
|
|
253
|
+
keys.publicKey,
|
|
254
|
+
);
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
- **Options**: `deadlineOffset` (default `3600`), `waitUntilReady` (default `true`), `timeoutMs` (default `180000`), `pollIntervalMs` (default `2000`)
|
|
258
|
+
- **Returns**: `{ accountId, accountInfo, created }`
|
|
259
|
+
|
|
243
260
|
#### `updateAuthKeys(authWallet, accountId, { add, remove }, options?)`
|
|
244
261
|
|
|
245
262
|
Adds or removes authorised signers for an anonymous account. Pass `ethers.Wallet` instances or raw uncompressed hex pubkey strings.
|
|
@@ -339,6 +356,49 @@ const result = await client.withdraw(wallet, accountId, {
|
|
|
339
356
|
});
|
|
340
357
|
```
|
|
341
358
|
|
|
359
|
+
### Prepaid Fees
|
|
360
|
+
|
|
361
|
+
Every `transferToPublic` and `transferToAnonymous` call deducts a protocol fee from the sender account's **prepaid fee reserve**. The SDK checks this balance before submitting a transfer and throws a descriptive error if it's insufficient — top up the reserve with `depositFees` before transferring.
|
|
362
|
+
|
|
363
|
+
#### `getFeeToken()`
|
|
364
|
+
|
|
365
|
+
Returns the currently configured ERC-20 fee token address (checksummed). `depositFees` deposits must use this token. The fee token can be rotated by the protocol over time, but existing balances in old fee tokens remain withdrawable.
|
|
366
|
+
|
|
367
|
+
#### `getFeeAmount()`
|
|
368
|
+
|
|
369
|
+
Returns the protocol fee amount (in raw ERC-20 units of the fee token) deducted from the prepaid reserve on each anonymous transfer. Returns `0n` if no fee is configured.
|
|
370
|
+
|
|
371
|
+
#### `getPrepaidFeeBalance(accountId, token)`
|
|
372
|
+
|
|
373
|
+
Returns the prepaid fee balance (in raw ERC-20 units) for an anonymous account and a given fee token.
|
|
374
|
+
|
|
375
|
+
#### `depositFees(authWallet, accountId, amount, options?)`
|
|
376
|
+
|
|
377
|
+
Deposits tokens into the prepaid fee reserve for an anonymous account. **The user pays gas** for this operation (mirrors `deposit()`). Handles ERC-20 approval against the active fee token automatically.
|
|
378
|
+
|
|
379
|
+
```javascript
|
|
380
|
+
const feeToken = await client.getFeeToken();
|
|
381
|
+
const feeAmount = await client.getFeeAmount();
|
|
382
|
+
const feeBalance = await client.getPrepaidFeeBalance(accountId, feeToken);
|
|
383
|
+
|
|
384
|
+
if (feeBalance < feeAmount) {
|
|
385
|
+
const result = await client.depositFees(
|
|
386
|
+
wallet,
|
|
387
|
+
accountId,
|
|
388
|
+
ethers.parseUnits("0.5", tokenDecimals),
|
|
389
|
+
);
|
|
390
|
+
await client.waitForRequest(result.request_id);
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
#### `withdrawFees(authWallet, accountId, { token, destination, amount }, options?)`
|
|
395
|
+
|
|
396
|
+
Withdraws a specific amount from the prepaid fee reserve for a given fee token. The relay pays gas. Use this to reclaim fees from a specific (possibly historical) fee token.
|
|
397
|
+
|
|
398
|
+
#### `withdrawAllFees(authWallet, accountId, { destination }, options?)`
|
|
399
|
+
|
|
400
|
+
Withdraws all prepaid fee balances across all (including historical) fee tokens to `destination`. The relay pays gas.
|
|
401
|
+
|
|
342
402
|
#### Request Tracking
|
|
343
403
|
|
|
344
404
|
All relay operations return a `{ request_id, tx_hash, status }` object. Use the following to track completion:
|
|
@@ -419,10 +479,10 @@ const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
|
|
|
419
479
|
const tokenAddress = "0xYourTokenAddress";
|
|
420
480
|
const tokenDecimals = 6;
|
|
421
481
|
|
|
422
|
-
// 1. Choose a unique account ID, derive keys, then
|
|
482
|
+
// 1. Choose a unique account ID, derive keys, then ensure the account exists
|
|
423
483
|
const accountId = "my-unique-account-id"; // any non-empty string you choose
|
|
424
484
|
const keys = await client.deriveAnonymousKeys(wallet, accountId);
|
|
425
|
-
await client.
|
|
485
|
+
await client.ensureAnonymousAccount(wallet, accountId, keys.publicKey);
|
|
426
486
|
|
|
427
487
|
// 2. Deposit (user pays gas)
|
|
428
488
|
const depositResult = await client.deposit(
|
|
@@ -441,7 +501,21 @@ const balance = await client.getBalance(
|
|
|
441
501
|
);
|
|
442
502
|
console.log("Available:", balance.available, "Pending:", balance.pending);
|
|
443
503
|
|
|
444
|
-
// 4.
|
|
504
|
+
// 4. Top up the prepaid fee reserve if needed (user pays gas)
|
|
505
|
+
// transferToPublic/transferToAnonymous deduct a protocol fee from this reserve.
|
|
506
|
+
const feeToken = await client.getFeeToken();
|
|
507
|
+
const feeAmount = await client.getFeeAmount();
|
|
508
|
+
const feeBalance = await client.getPrepaidFeeBalance(accountId, feeToken);
|
|
509
|
+
if (feeBalance < feeAmount) {
|
|
510
|
+
const feeResult = await client.depositFees(
|
|
511
|
+
wallet,
|
|
512
|
+
accountId,
|
|
513
|
+
ethers.parseUnits("0.5", tokenDecimals),
|
|
514
|
+
);
|
|
515
|
+
await client.waitForRequest(feeResult.request_id);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// 5. Transfer to a public address (relay pays gas)
|
|
445
519
|
const transferResult = await client.transferToPublic(wallet, accountId, {
|
|
446
520
|
recipient: "0xRecipientAddress",
|
|
447
521
|
token: tokenAddress,
|
|
@@ -450,7 +524,7 @@ const transferResult = await client.transferToPublic(wallet, accountId, {
|
|
|
450
524
|
});
|
|
451
525
|
await client.waitForRequest(transferResult.request_id);
|
|
452
526
|
|
|
453
|
-
//
|
|
527
|
+
// 6. Withdraw (relay pays gas)
|
|
454
528
|
const withdrawResult = await client.withdraw(wallet, accountId, {
|
|
455
529
|
destination: wallet.address,
|
|
456
530
|
token: tokenAddress,
|
|
@@ -494,6 +568,7 @@ try {
|
|
|
494
568
|
| "Account finalization timeout" | Account creation is still processing | Wait a few minutes and retry |
|
|
495
569
|
| "Proof generation failed" | Invalid inputs or cryptographic operation error | Verify all parameters and ensure sufficient balance |
|
|
496
570
|
| "Amount too small" | Amount rounds to 0 in contract scale | Use a larger amount (minimum depends on token decimals) |
|
|
571
|
+
| "Insufficient prepaid fee balance" | Anonymous account's prepaid fee reserve is below the per-transfer fee | Call `depositFees(wallet, accountId, amount)` to top up; use `getFeeToken()`/`getFeeAmount()` to check requirements |
|
|
497
572
|
| Fairycloak HTTP error | Relay unreachable or request rejected | Check the relay URL, API key, and account authorization |
|
|
498
573
|
|
|
499
574
|
---
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* [2] Derive ElGamal keypairs for both accounts
|
|
6
6
|
* [3] Create both anonymous accounts via Fairycloak
|
|
7
7
|
* [4] Deposit tokens into anon1
|
|
8
|
-
* [5]
|
|
9
|
-
* [6]
|
|
10
|
-
* [7]
|
|
8
|
+
* [5] Top up prepaid fees for anon1 (if balance < 0.1)
|
|
9
|
+
* [6] Transfer anon1 → anon2
|
|
10
|
+
* [7] Apply pending balance for anon2
|
|
11
|
+
* [8] Withdraw anon2 → public address
|
|
11
12
|
*
|
|
12
13
|
* Run:
|
|
13
14
|
* node examples/anonymous-flow.js
|
|
@@ -31,7 +32,11 @@ const TOKEN_DECIMALS = Number(process.env.TOKEN_DECIMALS ?? "6");
|
|
|
31
32
|
// Amounts in raw ERC-20 units (6 decimals — matches complete-flow.js)
|
|
32
33
|
const DEPOSIT_AMOUNT = 100_000n; // 0.1 USDC
|
|
33
34
|
const TRANSFER_AMOUNT = 50_000n; // 0.05 USDC
|
|
34
|
-
const WITHDRAW_AMOUNT =
|
|
35
|
+
const WITHDRAW_AMOUNT = 20_000n; // 0.02 USDC
|
|
36
|
+
|
|
37
|
+
// Prepaid fee thresholds (raw units, assumes fee token uses TOKEN_DECIMALS)
|
|
38
|
+
const MIN_FEE_BALANCE = 1_000_000n; // 0.1 — deposit if balance falls below this
|
|
39
|
+
const FEE_DEPOSIT_AMOUNT = 2_000_000n; // 0.5 — how much to top up in one shot
|
|
35
40
|
|
|
36
41
|
// Anvil test accounts — override via .env
|
|
37
42
|
const ANON1_PK =
|
|
@@ -221,9 +226,48 @@ async function main() {
|
|
|
221
226
|
"anon1:post-deposit",
|
|
222
227
|
);
|
|
223
228
|
|
|
224
|
-
// ── [5]
|
|
229
|
+
// ── [5] Top up prepaid fees for anon1 ───────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
console.log("\n── [5] Top up prepaid fees for anon1 ───────────────────");
|
|
232
|
+
|
|
233
|
+
const feeToken = await client.getFeeToken();
|
|
234
|
+
const feeAmount = await client.getFeeAmount();
|
|
235
|
+
log(" fee token :", feeToken);
|
|
236
|
+
log(" fee/xfer :", fmt(feeAmount, TOKEN_DECIMALS), "tokens");
|
|
237
|
+
|
|
238
|
+
const feeBalance = await client.getPrepaidFeeBalance(anon1Id, feeToken);
|
|
239
|
+
log(" fee balance:", fmt(feeBalance, TOKEN_DECIMALS), "tokens");
|
|
240
|
+
|
|
241
|
+
if (feeBalance < MIN_FEE_BALANCE) {
|
|
242
|
+
log(
|
|
243
|
+
` balance < ${fmt(MIN_FEE_BALANCE, TOKEN_DECIMALS)}, depositing ${fmt(FEE_DEPOSIT_AMOUNT, TOKEN_DECIMALS)} fee tokens (anon1 pays gas) …`,
|
|
244
|
+
);
|
|
245
|
+
const fd = await client.depositFees(
|
|
246
|
+
anon1Wallet,
|
|
247
|
+
anon1Id,
|
|
248
|
+
FEE_DEPOSIT_AMOUNT,
|
|
249
|
+
);
|
|
250
|
+
log(" request_id:", fd.request_id, " status:", fd.status);
|
|
251
|
+
if (fd.error) log(" [!] Error:", fd.error);
|
|
252
|
+
if (fd.tx_hash) log(" tx_hash :", fd.tx_hash);
|
|
253
|
+
|
|
254
|
+
const fdFinal = await client.waitForRequest(fd.request_id, {
|
|
255
|
+
timeoutMs: 180_000,
|
|
256
|
+
});
|
|
257
|
+
log(" status :", fdFinal.status, " tx_hash:", fdFinal.tx_hash ?? "—");
|
|
258
|
+
if (fdFinal.error) log(" [!] Error:", fdFinal.error);
|
|
259
|
+
|
|
260
|
+
const newFeeBalance = await client.getPrepaidFeeBalance(anon1Id, feeToken);
|
|
261
|
+
log(" new fee balance:", fmt(newFeeBalance, TOKEN_DECIMALS), "tokens");
|
|
262
|
+
} else {
|
|
263
|
+
log(
|
|
264
|
+
` fee balance sufficient (${fmt(feeBalance, TOKEN_DECIMALS)} ≥ ${fmt(MIN_FEE_BALANCE, TOKEN_DECIMALS)}), skipping deposit.`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── [6] Transfer anon1 → anon2 ───────────────────────────────────────────────
|
|
225
269
|
|
|
226
|
-
console.log("\n── [
|
|
270
|
+
console.log("\n── [6] Transfer anon1 → anon2 ──────────────────────────");
|
|
227
271
|
log(`Transferring ${fmt(TRANSFER_AMOUNT, TOKEN_DECIMALS)} tokens …`);
|
|
228
272
|
|
|
229
273
|
const txAnon = await client.transferToAnonymous(anon1Wallet, anon1Id, {
|
|
@@ -266,9 +310,9 @@ async function main() {
|
|
|
266
310
|
"anon2:post-transfer",
|
|
267
311
|
);
|
|
268
312
|
|
|
269
|
-
// ── [
|
|
313
|
+
// ── [7] Apply pending for anon2 ──────────────────────────────────────────────
|
|
270
314
|
|
|
271
|
-
console.log("\n── [
|
|
315
|
+
console.log("\n── [7] Apply pending balance for anon2 ─────────────────");
|
|
272
316
|
log("Moving incoming credit from pending → available …");
|
|
273
317
|
|
|
274
318
|
const ap = await client.applyPending(anon2Wallet, anon2Id);
|
|
@@ -292,9 +336,9 @@ async function main() {
|
|
|
292
336
|
"anon2:post-apply",
|
|
293
337
|
);
|
|
294
338
|
|
|
295
|
-
// ── [
|
|
339
|
+
// ── [8] Withdraw anon2 → public address ──────────────────────────────────────
|
|
296
340
|
|
|
297
|
-
console.log("\n── [
|
|
341
|
+
console.log("\n── [8] Withdraw from anon2 ─────────────────────────────");
|
|
298
342
|
log(
|
|
299
343
|
`Withdrawing ${fmt(WITHDRAW_AMOUNT, TOKEN_DECIMALS)} tokens → ${withdrawDest} …`,
|
|
300
344
|
);
|
package/package.json
CHANGED
package/src/anonymous-client.js
CHANGED
|
@@ -10,11 +10,15 @@ const ANON_DOMAIN_VERSION = "1";
|
|
|
10
10
|
// Default deadline offset: 1 hour
|
|
11
11
|
const DEFAULT_DEADLINE_OFFSET = 3600;
|
|
12
12
|
|
|
13
|
-
// Minimal ABI — only used for deposit calldata encoding (reads go through Fairycloak views)
|
|
13
|
+
// Minimal ABI — only used for deposit/fee calldata encoding (reads go through Fairycloak views)
|
|
14
14
|
const DEPOSIT_INTERFACE = new ethers.Interface([
|
|
15
15
|
"function depositAnonymous(string accountId, address token, uint256 plainAmount, uint256 authNonce, uint256 deadline, bytes authSig) external",
|
|
16
16
|
]);
|
|
17
17
|
|
|
18
|
+
const ADD_FEES_INTERFACE = new ethers.Interface([
|
|
19
|
+
"function addFees(string accountId, address token, uint256 amount, uint256 authNonce, uint256 deadline, bytes signature) external payable",
|
|
20
|
+
]);
|
|
21
|
+
|
|
18
22
|
/**
|
|
19
23
|
* Internal helper to parse values as BigInt, handling hex strings and null/undefined.
|
|
20
24
|
* @private
|
|
@@ -986,6 +990,8 @@ export class AnonymousTransferClient {
|
|
|
986
990
|
});
|
|
987
991
|
}
|
|
988
992
|
|
|
993
|
+
await this._assertSufficientPrepaidFees(accountId);
|
|
994
|
+
|
|
989
995
|
const authNonce = await this.getAuthNonce(accountId);
|
|
990
996
|
const deadline = this._makeDeadline(deadlineOffset);
|
|
991
997
|
const proofHash = ethers.keccak256(proofHex);
|
|
@@ -1118,6 +1124,8 @@ export class AnonymousTransferClient {
|
|
|
1118
1124
|
});
|
|
1119
1125
|
}
|
|
1120
1126
|
|
|
1127
|
+
await this._assertSufficientPrepaidFees(senderAccountId);
|
|
1128
|
+
|
|
1121
1129
|
const authNonce = await this.getAuthNonce(senderAccountId);
|
|
1122
1130
|
const deadline = this._makeDeadline(deadlineOffset);
|
|
1123
1131
|
const proofHash = ethers.keccak256(proofHex);
|
|
@@ -1376,6 +1384,368 @@ export class AnonymousTransferClient {
|
|
|
1376
1384
|
}
|
|
1377
1385
|
}
|
|
1378
1386
|
|
|
1387
|
+
// ─────────────────── prepaid fee reads ─────────────────────────
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Get the currently configured fee token address from the contract.
|
|
1391
|
+
*
|
|
1392
|
+
* Deposits via `depositFees` must use this token. Rotate-aware: the fee token
|
|
1393
|
+
* can change over time, but existing balances for old tokens remain withdrawable.
|
|
1394
|
+
*
|
|
1395
|
+
* @returns {Promise<string>} Checksummed ERC-20 token address (or address(0) if none set)
|
|
1396
|
+
*/
|
|
1397
|
+
async getFeeToken() {
|
|
1398
|
+
try {
|
|
1399
|
+
const data = await this._fetch("GET", "/v1/views/fee-token");
|
|
1400
|
+
const raw = data.result ?? data;
|
|
1401
|
+
const addr = typeof raw === "string" ? raw : raw.feeToken ?? raw;
|
|
1402
|
+
return ethers.getAddress(addr);
|
|
1403
|
+
} catch (e) {
|
|
1404
|
+
throw new Error(`Failed to get fee token: ${e.message}`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
/**
|
|
1409
|
+
* Get the currently configured protocol fee amount (in raw ERC-20 units of the fee token).
|
|
1410
|
+
*
|
|
1411
|
+
* This is the amount deducted from the prepaid reserve on each anonymous transfer.
|
|
1412
|
+
* Returns 0n if no fee is configured.
|
|
1413
|
+
*
|
|
1414
|
+
* @returns {Promise<bigint>}
|
|
1415
|
+
*/
|
|
1416
|
+
async getFeeAmount() {
|
|
1417
|
+
try {
|
|
1418
|
+
const data = await this._fetch("GET", "/v1/views/fee-amount");
|
|
1419
|
+
return _parseBigInt(data.result ?? data);
|
|
1420
|
+
} catch (e) {
|
|
1421
|
+
throw new Error(`Failed to get fee amount: ${e.message}`);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
/**
|
|
1426
|
+
* Get the prepaid fee balance for an anonymous account and token (in raw ERC-20 units).
|
|
1427
|
+
*
|
|
1428
|
+
* @param {string} accountId
|
|
1429
|
+
* @param {string} token - ERC-20 token address (use `getFeeToken()` to get the active fee token)
|
|
1430
|
+
* @returns {Promise<bigint>}
|
|
1431
|
+
*/
|
|
1432
|
+
async getPrepaidFeeBalance(accountId, token) {
|
|
1433
|
+
try {
|
|
1434
|
+
if (!ethers.isAddress(token))
|
|
1435
|
+
throw new Error(`Invalid token address: ${token}`);
|
|
1436
|
+
const data = await this._fetch(
|
|
1437
|
+
"GET",
|
|
1438
|
+
`/v1/views/anonymous/accounts/${accountId}/prepaid-fees/${token}`,
|
|
1439
|
+
);
|
|
1440
|
+
return _parseBigInt(data.result ?? data);
|
|
1441
|
+
} catch (e) {
|
|
1442
|
+
throw new Error(`Failed to get prepaid fee balance: ${e.message}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/**
|
|
1447
|
+
* Check that the anonymous account has sufficient prepaid fees to cover one transfer.
|
|
1448
|
+
* Throws a descriptive error if the balance is insufficient.
|
|
1449
|
+
* @private
|
|
1450
|
+
*/
|
|
1451
|
+
async _assertSufficientPrepaidFees(accountId) {
|
|
1452
|
+
const [feeToken, feeAmount] = await Promise.all([
|
|
1453
|
+
this.getFeeToken(),
|
|
1454
|
+
this.getFeeAmount(),
|
|
1455
|
+
]);
|
|
1456
|
+
if (feeAmount === 0n) return; // no fee configured
|
|
1457
|
+
|
|
1458
|
+
if (feeToken === ethers.ZeroAddress) return; // fee token not set
|
|
1459
|
+
|
|
1460
|
+
const balance = await this.getPrepaidFeeBalance(accountId, feeToken);
|
|
1461
|
+
if (balance < feeAmount) {
|
|
1462
|
+
throw new Error(
|
|
1463
|
+
`Insufficient prepaid fee balance for anonymous account "${accountId}". ` +
|
|
1464
|
+
`Required: ${feeAmount} (raw units of fee token ${feeToken}), ` +
|
|
1465
|
+
`available: ${balance}. ` +
|
|
1466
|
+
`Call depositFees(wallet, accountId, amount) to top up the prepaid fee reserve. ` +
|
|
1467
|
+
`Use getFeeToken() to get the active fee token address and getFeeAmount() to see the required deposit amount per transfer.`,
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// ─────────────────── prepaid fee actions ────────────────────────
|
|
1473
|
+
|
|
1474
|
+
/**
|
|
1475
|
+
* Deposit tokens into the prepaid fee reserve for an anonymous account.
|
|
1476
|
+
*
|
|
1477
|
+
* Mirrors `deposit()` from a transaction-signing perspective: the user wallet pays
|
|
1478
|
+
* gas and signs the raw EVM transaction. Fairycloak validates and broadcasts it.
|
|
1479
|
+
*
|
|
1480
|
+
* Steps:
|
|
1481
|
+
* 1. Fetch the active fee token from the contract
|
|
1482
|
+
* 2. ERC-20 approve if allowance is insufficient
|
|
1483
|
+
* 3. Sign EIP-712 AnonymousAddFees authorisation payload
|
|
1484
|
+
* 4. Build and sign the raw `addFees(...)` EVM transaction
|
|
1485
|
+
* 5. Submit to Fairycloak `/v1/anonymous/fees/add/raw-tx`
|
|
1486
|
+
*
|
|
1487
|
+
* @param {ethers.Wallet} authWallet - Wallet that owns the tokens and pays gas
|
|
1488
|
+
* @param {string} accountId - Target anonymous account ID
|
|
1489
|
+
* @param {bigint|string|number} amount - Amount in raw ERC-20 units of the active fee token
|
|
1490
|
+
* @param {Object} [options]
|
|
1491
|
+
* @param {number} [options.deadlineOffset=3600]
|
|
1492
|
+
* @param {bigint} [options.gasLimit=300000n]
|
|
1493
|
+
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1494
|
+
*/
|
|
1495
|
+
async depositFees(authWallet, accountId, amount, options = {}) {
|
|
1496
|
+
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET, gasLimit = 300000n } =
|
|
1497
|
+
options;
|
|
1498
|
+
try {
|
|
1499
|
+
const amountBig = BigInt(amount);
|
|
1500
|
+
if (amountBig <= 0n) throw new Error("Amount must be greater than 0");
|
|
1501
|
+
|
|
1502
|
+
const feeToken = await this.getFeeToken();
|
|
1503
|
+
if (feeToken === ethers.ZeroAddress)
|
|
1504
|
+
throw new Error(
|
|
1505
|
+
"No fee token is currently configured on the contract. Contact the protocol operator.",
|
|
1506
|
+
);
|
|
1507
|
+
|
|
1508
|
+
const depositorAddress = await authWallet.getAddress();
|
|
1509
|
+
const walletWithProvider = authWallet.connect
|
|
1510
|
+
? authWallet.connect(this.provider)
|
|
1511
|
+
: authWallet;
|
|
1512
|
+
|
|
1513
|
+
// 1. Approve if needed
|
|
1514
|
+
const tokenContract = new ethers.Contract(
|
|
1515
|
+
feeToken,
|
|
1516
|
+
[
|
|
1517
|
+
"function balanceOf(address) view returns (uint256)",
|
|
1518
|
+
"function allowance(address,address) view returns (uint256)",
|
|
1519
|
+
"function approve(address,uint256) returns (bool)",
|
|
1520
|
+
],
|
|
1521
|
+
walletWithProvider,
|
|
1522
|
+
);
|
|
1523
|
+
|
|
1524
|
+
const [balance, allowance] = await Promise.all([
|
|
1525
|
+
tokenContract.balanceOf(depositorAddress),
|
|
1526
|
+
tokenContract.allowance(depositorAddress, this.diamondAddress),
|
|
1527
|
+
]);
|
|
1528
|
+
|
|
1529
|
+
if (balance < amountBig)
|
|
1530
|
+
throw new Error(
|
|
1531
|
+
`Insufficient fee token balance. Required: ${amountBig}, available: ${balance}`,
|
|
1532
|
+
);
|
|
1533
|
+
|
|
1534
|
+
if (allowance < amountBig) {
|
|
1535
|
+
const approveTx = await tokenContract.approve(
|
|
1536
|
+
this.diamondAddress,
|
|
1537
|
+
ethers.MaxUint256,
|
|
1538
|
+
);
|
|
1539
|
+
const receipt = await approveTx.wait();
|
|
1540
|
+
if (!receipt || receipt.status === 0)
|
|
1541
|
+
throw new Error("Fee token approval failed");
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// 2. EIP-712 AnonymousAddFees signature
|
|
1545
|
+
const authNonce = await this.getAuthNonce(accountId);
|
|
1546
|
+
const deadline = this._makeDeadline(deadlineOffset);
|
|
1547
|
+
|
|
1548
|
+
const domain = this._buildDomain();
|
|
1549
|
+
const types = {
|
|
1550
|
+
AnonymousAddFees: [
|
|
1551
|
+
{ name: "accountId", type: "string" },
|
|
1552
|
+
{ name: "token", type: "address" },
|
|
1553
|
+
{ name: "amount", type: "uint256" },
|
|
1554
|
+
{ name: "authNonce", type: "uint256" },
|
|
1555
|
+
{ name: "deadline", type: "uint256" },
|
|
1556
|
+
],
|
|
1557
|
+
};
|
|
1558
|
+
const value = {
|
|
1559
|
+
accountId: String(accountId),
|
|
1560
|
+
token: feeToken,
|
|
1561
|
+
amount: amountBig,
|
|
1562
|
+
authNonce,
|
|
1563
|
+
deadline: BigInt(deadline),
|
|
1564
|
+
};
|
|
1565
|
+
|
|
1566
|
+
const signature = await authWallet.signTypedData(domain, types, value);
|
|
1567
|
+
|
|
1568
|
+
// 3. Build and sign the raw addFees transaction
|
|
1569
|
+
const calldata = ADD_FEES_INTERFACE.encodeFunctionData("addFees", [
|
|
1570
|
+
accountId,
|
|
1571
|
+
feeToken,
|
|
1572
|
+
amountBig,
|
|
1573
|
+
authNonce,
|
|
1574
|
+
BigInt(deadline),
|
|
1575
|
+
signature,
|
|
1576
|
+
]);
|
|
1577
|
+
|
|
1578
|
+
const [nonce, feeData] = await Promise.all([
|
|
1579
|
+
this.provider.getTransactionCount(depositorAddress),
|
|
1580
|
+
this.provider.getFeeData(),
|
|
1581
|
+
]);
|
|
1582
|
+
|
|
1583
|
+
let txObj;
|
|
1584
|
+
if (feeData.maxFeePerGas) {
|
|
1585
|
+
txObj = {
|
|
1586
|
+
to: this.diamondAddress,
|
|
1587
|
+
data: calldata,
|
|
1588
|
+
nonce,
|
|
1589
|
+
chainId: BigInt(this.chainId),
|
|
1590
|
+
gasLimit,
|
|
1591
|
+
maxFeePerGas: (feeData.maxFeePerGas * 12n) / 10n,
|
|
1592
|
+
maxPriorityFeePerGas: feeData.maxPriorityFeePerGas ?? 0n,
|
|
1593
|
+
value: 0n,
|
|
1594
|
+
};
|
|
1595
|
+
} else {
|
|
1596
|
+
txObj = {
|
|
1597
|
+
to: this.diamondAddress,
|
|
1598
|
+
data: calldata,
|
|
1599
|
+
nonce,
|
|
1600
|
+
chainId: BigInt(this.chainId),
|
|
1601
|
+
gasLimit,
|
|
1602
|
+
gasPrice: (feeData.gasPrice * 11n) / 10n,
|
|
1603
|
+
value: 0n,
|
|
1604
|
+
};
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
const signedTx = await authWallet.signTransaction(txObj);
|
|
1608
|
+
|
|
1609
|
+
// 4. Submit to Fairycloak
|
|
1610
|
+
return await this._fetch("POST", "/v1/anonymous/fees/add/raw-tx", {
|
|
1611
|
+
raw_tx: signedTx,
|
|
1612
|
+
});
|
|
1613
|
+
} catch (e) {
|
|
1614
|
+
const msg = e?.message ?? String(e);
|
|
1615
|
+
if (
|
|
1616
|
+
msg.startsWith("Fairycloak") ||
|
|
1617
|
+
msg.startsWith("Insufficient") ||
|
|
1618
|
+
msg.startsWith("No fee token")
|
|
1619
|
+
)
|
|
1620
|
+
throw e;
|
|
1621
|
+
throw new Error(`Failed to deposit fees: ${msg}`);
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1625
|
+
/**
|
|
1626
|
+
* Withdraw a specific amount from the prepaid fee reserve via Fairycloak. The relay pays gas.
|
|
1627
|
+
*
|
|
1628
|
+
* Use this to reclaim fees for a specific (possibly historical) fee token.
|
|
1629
|
+
*
|
|
1630
|
+
* @param {ethers.Wallet|ethers.Signer} authWallet - Authorised signer for the anonymous account
|
|
1631
|
+
* @param {string} accountId
|
|
1632
|
+
* @param {Object} params
|
|
1633
|
+
* @param {string} params.token - Fee token address to withdraw
|
|
1634
|
+
* @param {string} params.destination - Destination EVM address
|
|
1635
|
+
* @param {bigint|string|number} params.amount - Amount in raw ERC-20 units
|
|
1636
|
+
* @param {Object} [options]
|
|
1637
|
+
* @param {number} [options.deadlineOffset=3600]
|
|
1638
|
+
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1639
|
+
*/
|
|
1640
|
+
async withdrawFees(
|
|
1641
|
+
authWallet,
|
|
1642
|
+
accountId,
|
|
1643
|
+
{ token, destination, amount },
|
|
1644
|
+
options = {},
|
|
1645
|
+
) {
|
|
1646
|
+
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1647
|
+
try {
|
|
1648
|
+
if (!ethers.isAddress(token))
|
|
1649
|
+
throw new Error(`Invalid token address: ${token}`);
|
|
1650
|
+
if (!ethers.isAddress(destination))
|
|
1651
|
+
throw new Error(`Invalid destination address: ${destination}`);
|
|
1652
|
+
const amountBig = BigInt(amount);
|
|
1653
|
+
if (amountBig <= 0n) throw new Error("Amount must be greater than 0");
|
|
1654
|
+
|
|
1655
|
+
const authNonce = await this.getAuthNonce(accountId);
|
|
1656
|
+
const deadline = this._makeDeadline(deadlineOffset);
|
|
1657
|
+
|
|
1658
|
+
const domain = this._buildDomain();
|
|
1659
|
+
const types = {
|
|
1660
|
+
AnonymousWithdrawFees: [
|
|
1661
|
+
{ name: "accountId", type: "string" },
|
|
1662
|
+
{ name: "token", type: "address" },
|
|
1663
|
+
{ name: "destination", type: "address" },
|
|
1664
|
+
{ name: "amount", type: "uint256" },
|
|
1665
|
+
{ name: "authNonce", type: "uint256" },
|
|
1666
|
+
{ name: "deadline", type: "uint256" },
|
|
1667
|
+
],
|
|
1668
|
+
};
|
|
1669
|
+
const value = {
|
|
1670
|
+
accountId: String(accountId),
|
|
1671
|
+
token,
|
|
1672
|
+
destination,
|
|
1673
|
+
amount: amountBig,
|
|
1674
|
+
authNonce,
|
|
1675
|
+
deadline: BigInt(deadline),
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
const signature = await authWallet.signTypedData(domain, types, value);
|
|
1679
|
+
|
|
1680
|
+
return await this._fetch("POST", "/v1/anonymous/fees/withdraw", {
|
|
1681
|
+
account_id: accountId,
|
|
1682
|
+
token,
|
|
1683
|
+
destination,
|
|
1684
|
+
amount: String(amountBig),
|
|
1685
|
+
auth_nonce: String(authNonce),
|
|
1686
|
+
deadline: String(deadline),
|
|
1687
|
+
signature,
|
|
1688
|
+
});
|
|
1689
|
+
} catch (e) {
|
|
1690
|
+
const msg = e?.message ?? String(e);
|
|
1691
|
+
if (msg.startsWith("Fairycloak") || msg.startsWith("Invalid")) throw e;
|
|
1692
|
+
throw new Error(`Failed to withdraw fees: ${msg}`);
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
/**
|
|
1697
|
+
* Withdraw all prepaid fee balances across all (including historical) fee tokens via Fairycloak.
|
|
1698
|
+
* The relay pays gas.
|
|
1699
|
+
*
|
|
1700
|
+
* @param {ethers.Wallet|ethers.Signer} authWallet - Authorised signer for the anonymous account
|
|
1701
|
+
* @param {string} accountId
|
|
1702
|
+
* @param {Object} params
|
|
1703
|
+
* @param {string} params.destination - Destination EVM address
|
|
1704
|
+
* @param {Object} [options]
|
|
1705
|
+
* @param {number} [options.deadlineOffset=3600]
|
|
1706
|
+
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1707
|
+
*/
|
|
1708
|
+
async withdrawAllFees(authWallet, accountId, { destination }, options = {}) {
|
|
1709
|
+
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1710
|
+
try {
|
|
1711
|
+
if (!ethers.isAddress(destination))
|
|
1712
|
+
throw new Error(`Invalid destination address: ${destination}`);
|
|
1713
|
+
|
|
1714
|
+
const authNonce = await this.getAuthNonce(accountId);
|
|
1715
|
+
const deadline = this._makeDeadline(deadlineOffset);
|
|
1716
|
+
|
|
1717
|
+
const domain = this._buildDomain();
|
|
1718
|
+
const types = {
|
|
1719
|
+
AnonymousWithdrawAllFees: [
|
|
1720
|
+
{ name: "accountId", type: "string" },
|
|
1721
|
+
{ name: "destination", type: "address" },
|
|
1722
|
+
{ name: "authNonce", type: "uint256" },
|
|
1723
|
+
{ name: "deadline", type: "uint256" },
|
|
1724
|
+
],
|
|
1725
|
+
};
|
|
1726
|
+
const value = {
|
|
1727
|
+
accountId: String(accountId),
|
|
1728
|
+
destination,
|
|
1729
|
+
authNonce,
|
|
1730
|
+
deadline: BigInt(deadline),
|
|
1731
|
+
};
|
|
1732
|
+
|
|
1733
|
+
const signature = await authWallet.signTypedData(domain, types, value);
|
|
1734
|
+
|
|
1735
|
+
return await this._fetch("POST", "/v1/anonymous/fees/withdraw-all", {
|
|
1736
|
+
account_id: accountId,
|
|
1737
|
+
destination,
|
|
1738
|
+
auth_nonce: String(authNonce),
|
|
1739
|
+
deadline: String(deadline),
|
|
1740
|
+
signature,
|
|
1741
|
+
});
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
const msg = e?.message ?? String(e);
|
|
1744
|
+
if (msg.startsWith("Fairycloak") || msg.startsWith("Invalid")) throw e;
|
|
1745
|
+
throw new Error(`Failed to withdraw all fees: ${msg}`);
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1379
1749
|
// ──────────────────── request tracking ─────────────────────────
|
|
1380
1750
|
|
|
1381
1751
|
/**
|
package/src/constants.js
CHANGED
|
@@ -25,7 +25,7 @@ export const TEMPO_FEE_TOKEN_ADDRESS =
|
|
|
25
25
|
"0x20c0000000000000000000000000000000000000";
|
|
26
26
|
export const STABLETRUST_CONTRACTS_BY_CHAIN_ID = Object.freeze({
|
|
27
27
|
2201: "0xA98e5ba75Bb44459916D52Abf1604caB3d98CC4B", //Stable
|
|
28
|
-
5042002: "
|
|
28
|
+
5042002: "0x18c8aEc4Ce4fCdB6687Bb99a402277e82394AFF7", //Arc
|
|
29
29
|
84532: "0xF66A0a1670F14AE5D1852B4E7d1e4C693b2Accfd", //Base
|
|
30
30
|
11155111: "0xa066b5C30382110d19925108BBa1Eef613a3A041", //Ethereum
|
|
31
31
|
421614: "0xbda65d65A7833D28F9391FF01d0b212B75538Cf2", //Arbitrum
|
package/src/index.d.ts
CHANGED
|
@@ -146,6 +146,24 @@ declare module "@fairblock/stabletrust" {
|
|
|
146
146
|
gasLimit?: bigint;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
export interface DepositFeesOptions extends DeadlineOptions {
|
|
150
|
+
gasLimit?: bigint;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export interface WithdrawFeesParams {
|
|
154
|
+
/** Fee token address (use `getFeeToken()` to get the active token). */
|
|
155
|
+
token: string;
|
|
156
|
+
/** Destination EVM address to receive the withdrawn tokens. */
|
|
157
|
+
destination: string;
|
|
158
|
+
/** Amount in raw ERC-20 units of the fee token. */
|
|
159
|
+
amount: bigint | string | number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface WithdrawAllFeesParams {
|
|
163
|
+
/** Destination EVM address to receive all withdrawn fee tokens. */
|
|
164
|
+
destination: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
149
167
|
/**
|
|
150
168
|
* AnonymousTransferClient — SDK class for anonymous confidential transfers via the Fairycloak relay.
|
|
151
169
|
*
|
|
@@ -229,6 +247,67 @@ declare module "@fairblock/stabletrust" {
|
|
|
229
247
|
params: WithdrawProofParams,
|
|
230
248
|
): Promise<string>;
|
|
231
249
|
|
|
250
|
+
// ── prepaid fee reads ───────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Get the currently configured fee token address from the contract.
|
|
254
|
+
* Deposits via `depositFees` must use this token.
|
|
255
|
+
*/
|
|
256
|
+
getFeeToken(): Promise<string>;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get the protocol fee amount in raw ERC-20 units of the fee token.
|
|
260
|
+
* This is the amount deducted per anonymous transfer. Returns 0n if no fee is set.
|
|
261
|
+
*/
|
|
262
|
+
getFeeAmount(): Promise<bigint>;
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the prepaid fee balance for an anonymous account and token (raw ERC-20 units).
|
|
266
|
+
* Use `getFeeToken()` to get the active fee token address.
|
|
267
|
+
*/
|
|
268
|
+
getPrepaidFeeBalance(accountId: string, token: string): Promise<bigint>;
|
|
269
|
+
|
|
270
|
+
// ── prepaid fee actions ─────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Deposit tokens into the prepaid fee reserve for an anonymous account.
|
|
274
|
+
* The user wallet pays gas for this raw EVM transaction.
|
|
275
|
+
* Handles ERC-20 approval automatically.
|
|
276
|
+
*
|
|
277
|
+
* The fee token is read from the contract — use `getFeeToken()` to see it.
|
|
278
|
+
* Use `getFeeAmount()` to see how much is deducted per transfer.
|
|
279
|
+
*
|
|
280
|
+
* @param amount Amount in raw ERC-20 units of the active fee token
|
|
281
|
+
*/
|
|
282
|
+
depositFees(
|
|
283
|
+
authWallet: ethers.Wallet,
|
|
284
|
+
accountId: string,
|
|
285
|
+
amount: bigint | string | number,
|
|
286
|
+
options?: DepositFeesOptions,
|
|
287
|
+
): Promise<FairycloakResponse>;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Withdraw a specific amount from the prepaid fee reserve. The relay pays gas.
|
|
291
|
+
* Supports historical fee tokens (after a fee-token rotation).
|
|
292
|
+
*/
|
|
293
|
+
withdrawFees(
|
|
294
|
+
authWallet: ethers.Wallet | ethers.Signer,
|
|
295
|
+
accountId: string,
|
|
296
|
+
params: WithdrawFeesParams,
|
|
297
|
+
options?: DeadlineOptions,
|
|
298
|
+
): Promise<FairycloakResponse>;
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Withdraw all prepaid fee balances across all (including historical) fee tokens.
|
|
302
|
+
* The relay pays gas.
|
|
303
|
+
*/
|
|
304
|
+
withdrawAllFees(
|
|
305
|
+
authWallet: ethers.Wallet | ethers.Signer,
|
|
306
|
+
accountId: string,
|
|
307
|
+
params: WithdrawAllFeesParams,
|
|
308
|
+
options?: DeadlineOptions,
|
|
309
|
+
): Promise<FairycloakResponse>;
|
|
310
|
+
|
|
232
311
|
// ── Fairycloak operations ───────────────────────────────────────
|
|
233
312
|
|
|
234
313
|
/**
|