@fairblock/stabletrust 2.0.4 → 2.0.6
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 +12 -1
- package/examples/anonymous-flow.js +3 -2
- package/examples/complete-flow.js +1 -2
- package/examples/simple-snippets.js +1 -2
- package/package.json +2 -3
- package/src/anonymous-client.js +39 -3
- package/src/confidential-client.js +0 -46
- package/src/constants.js +5 -5
- package/src/index.d.ts +38 -8
- package/src/index.js +6 -1
- package/src/utils.js +34 -0
- package/examples/constants.js +0 -6
package/README.md
CHANGED
|
@@ -212,6 +212,17 @@ The `AnonymousTransferClient` routes all operations through the **Fairycloak rel
|
|
|
212
212
|
|
|
213
213
|
> **Access Required** — Anonymous transfers are available to teams building privacy-critical applications. To obtain a Fairycloak relay URL and API key, reach out to the Fairblock team at [hello@fairblock.network](mailto:hello@fairblock.network).
|
|
214
214
|
|
|
215
|
+
### Anonymous Account ID Rules
|
|
216
|
+
|
|
217
|
+
Every `accountId` (and `senderAccountId` / `recipientId`) you pass to `AnonymousTransferClient` must follow these rules, enforced both on-chain and by the Fairycloak relay:
|
|
218
|
+
|
|
219
|
+
- **Non-empty** string
|
|
220
|
+
- **At most 20 characters** long
|
|
221
|
+
- **Alphanumeric characters only** (`A-Z`, `a-z`, `0-9`) — no spaces, punctuation, or Unicode
|
|
222
|
+
- **Case-sensitive** — `"MyAccount"` and `"myaccount"` are different accounts; no normalization is applied
|
|
223
|
+
|
|
224
|
+
The SDK validates `accountId` (and related fields) before making any request and throws a descriptive `Error` if it doesn't follow these rules.
|
|
225
|
+
|
|
215
226
|
### Initialization
|
|
216
227
|
|
|
217
228
|
```javascript
|
|
@@ -238,7 +249,7 @@ const keys = await client.deriveAnonymousKeys(authWallet, accountId);
|
|
|
238
249
|
|
|
239
250
|
#### `createAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
240
251
|
|
|
241
|
-
Creates a new anonymous account via the relay. The relay pays gas. `accountId` is a **caller-chosen string**
|
|
252
|
+
Creates a new anonymous account via the relay. The relay pays gas. `accountId` is a **caller-chosen string** — pick any unique identifier you like (e.g. a short UUID fragment or a human-readable name), as long as it follows the [Anonymous Account ID Rules](#anonymous-account-id-rules) above (non-empty, at most 20 characters, alphanumeric only, case-sensitive).
|
|
242
253
|
|
|
243
254
|
#### `ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
244
255
|
|
|
@@ -128,8 +128,9 @@ async function main() {
|
|
|
128
128
|
// An auth signer can only ever be bound to a single anonymous account, so the
|
|
129
129
|
// account ID must be stable per-wallet across runs (random UUIDs would cause
|
|
130
130
|
// "auth signer already used" failures on every re-run with the same .env keys).
|
|
131
|
-
|
|
132
|
-
const
|
|
131
|
+
// Anonymous account IDs must be alphanumeric and at most 20 characters.
|
|
132
|
+
const anon1Id = `anon${anon1Wallet.address.slice(2, 6).toLowerCase()}`;
|
|
133
|
+
const anon2Id = `anon${anon2Wallet.address.slice(2, 6).toLowerCase()}`;
|
|
133
134
|
log("anon1 id:", anon1Id);
|
|
134
135
|
log("anon2 id:", anon2Id);
|
|
135
136
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ethers } from "ethers";
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
-
import { ConfidentialTransferClient } from "@fairblock/stabletrust";
|
|
4
|
-
import { ERC20_ABI } from "./constants.js";
|
|
3
|
+
import { ConfidentialTransferClient, ERC20_ABI } from "@fairblock/stabletrust";
|
|
5
4
|
dotenv.config();
|
|
6
5
|
|
|
7
6
|
/**
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ethers } from "ethers";
|
|
2
2
|
import dotenv from "dotenv";
|
|
3
|
-
import { ConfidentialTransferClient } from "@fairblock/stabletrust";
|
|
4
|
-
import { ERC20_ABI } from "./constants.js";
|
|
3
|
+
import { ConfidentialTransferClient, ERC20_ABI } from "@fairblock/stabletrust";
|
|
5
4
|
dotenv.config();
|
|
6
5
|
const RPC_URL = process.env.RPC_URL || "https://rpc.testnet.stable.xyz";
|
|
7
6
|
const EXPLORER_URL =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairblock/stabletrust",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "SDK for confidential transfers using homomorphic encryption and zero-knowledge proofs",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "src/index.d.ts",
|
|
@@ -21,8 +21,7 @@
|
|
|
21
21
|
"src/",
|
|
22
22
|
"pkg/",
|
|
23
23
|
"examples/",
|
|
24
|
-
"README.md"
|
|
25
|
-
"CHANGELOG.md"
|
|
24
|
+
"README.md"
|
|
26
25
|
],
|
|
27
26
|
"repository": {
|
|
28
27
|
"type": "git",
|
package/src/anonymous-client.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { ethers } from "ethers";
|
|
2
2
|
import { initializeWasm } from "./wasm-loader.js";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
encodeTransferProof,
|
|
5
|
+
encodeWithdrawProof,
|
|
6
|
+
validateAnonymousAccountId,
|
|
7
|
+
} from "./utils.js";
|
|
4
8
|
import { getStabletrustContractAddress } from "./constants.js";
|
|
5
9
|
|
|
6
10
|
// EIP-712 domain for anonymous operations (LibAnonAuth domain name)
|
|
@@ -38,6 +42,16 @@ function _parseBigInt(v) {
|
|
|
38
42
|
* Anonymous transfers are routed through the Fairycloak HTTP relay server instead of being
|
|
39
43
|
* submitted directly on-chain. The relay pays gas for all managed transactions; the user only
|
|
40
44
|
* signs EIP-712 authorisation payloads (and a raw EVM transaction for deposits).
|
|
45
|
+
*
|
|
46
|
+
* ### Anonymous account ID rules
|
|
47
|
+
* Every `accountId` / `senderAccountId` / `recipientId` accepted by this client must:
|
|
48
|
+
* - be a non-empty string
|
|
49
|
+
* - be at most 20 characters long
|
|
50
|
+
* - contain only alphanumeric characters (A-Z, a-z, 0-9)
|
|
51
|
+
* - be treated as case-sensitive (no normalization is applied)
|
|
52
|
+
*
|
|
53
|
+
* IDs that don't follow these rules are rejected with a descriptive error before any
|
|
54
|
+
* network request is made.
|
|
41
55
|
*/
|
|
42
56
|
export class AnonymousTransferClient {
|
|
43
57
|
/**
|
|
@@ -191,6 +205,7 @@ export class AnonymousTransferClient {
|
|
|
191
205
|
* @returns {Promise<{exists:boolean, finalized:boolean, hasPendingAction:boolean, txId:bigint, elgamalPubkey:string, authNonce:bigint}>}
|
|
192
206
|
*/
|
|
193
207
|
async getAnonymousAccountInfo(accountId) {
|
|
208
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
194
209
|
try {
|
|
195
210
|
const data = await this._fetch(
|
|
196
211
|
"GET",
|
|
@@ -234,6 +249,7 @@ export class AnonymousTransferClient {
|
|
|
234
249
|
* @returns {Promise<boolean>}
|
|
235
250
|
*/
|
|
236
251
|
async isAuthorizedSigner(accountId, signerAddress) {
|
|
252
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
237
253
|
const data = await this._fetch(
|
|
238
254
|
"GET",
|
|
239
255
|
`/v1/views/anonymous/accounts/${accountId}/authorized-signers/${signerAddress}`,
|
|
@@ -312,6 +328,7 @@ export class AnonymousTransferClient {
|
|
|
312
328
|
* @returns {Promise<{available: {amount: number, ciphertext: string|null}, pending: {amount: number, ciphertext: string|null}}>}
|
|
313
329
|
*/
|
|
314
330
|
async getAnonymousBalance(accountId, tokenAddress, elGamalPrivateKey) {
|
|
331
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
315
332
|
if (!ethers.isAddress(tokenAddress))
|
|
316
333
|
throw new Error(`Invalid token address: ${tokenAddress}`);
|
|
317
334
|
if (!elGamalPrivateKey) throw new Error("elGamalPrivateKey is required");
|
|
@@ -347,6 +364,7 @@ export class AnonymousTransferClient {
|
|
|
347
364
|
* @returns {Promise<{publicKey:string, privateKey:string}>} Base-64 encoded keypair
|
|
348
365
|
*/
|
|
349
366
|
async deriveAnonymousKeys(authWallet, accountId) {
|
|
367
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
350
368
|
try {
|
|
351
369
|
const wasm = await this._getWasm();
|
|
352
370
|
const address = await authWallet.getAddress();
|
|
@@ -508,13 +526,16 @@ export class AnonymousTransferClient {
|
|
|
508
526
|
* The relay submits the `createAnonymousAccount` transaction and pays gas.
|
|
509
527
|
*
|
|
510
528
|
* @param {ethers.Wallet|ethers.Signer} authWallet - Initial auth signer
|
|
511
|
-
* @param {string} accountId - Caller-chosen account ID
|
|
529
|
+
* @param {string} accountId - Caller-chosen account ID. Must be a non-empty, case-sensitive,
|
|
530
|
+
* alphanumeric string of at most 20 characters.
|
|
512
531
|
* @param {string} elgamalPublicKey - ElGamal public key as base64 or "0x"-prefixed hex (32 bytes)
|
|
513
532
|
* @param {Object} [options]
|
|
514
533
|
* @param {number} [options.deadlineOffset=3600] - Deadline in seconds from now
|
|
515
534
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string, action:string}>}
|
|
535
|
+
* @throws {Error} If `accountId` doesn't follow the anonymous account ID rules (see class docs).
|
|
516
536
|
*/
|
|
517
537
|
async createAccount(authWallet, accountId, elgamalPublicKey, options = {}) {
|
|
538
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
518
539
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
519
540
|
try {
|
|
520
541
|
const authAddress = await authWallet.getAddress();
|
|
@@ -581,7 +602,8 @@ export class AnonymousTransferClient {
|
|
|
581
602
|
* so reusing a wallet across different `accountId`s will still fail at creation time.
|
|
582
603
|
*
|
|
583
604
|
* @param {ethers.Wallet|ethers.Signer} authWallet - Initial auth signer
|
|
584
|
-
* @param {string} accountId - Caller-chosen account ID
|
|
605
|
+
* @param {string} accountId - Caller-chosen account ID. Must be a non-empty, case-sensitive,
|
|
606
|
+
* alphanumeric string of at most 20 characters.
|
|
585
607
|
* @param {string} elgamalPublicKey - ElGamal public key as base64 or "0x"-prefixed hex (32 bytes)
|
|
586
608
|
* @param {Object} [options]
|
|
587
609
|
* @param {number} [options.deadlineOffset=3600] - Deadline in seconds from now
|
|
@@ -589,8 +611,10 @@ export class AnonymousTransferClient {
|
|
|
589
611
|
* @param {number} [options.timeoutMs=180000] - Max time to wait for creation/readiness
|
|
590
612
|
* @param {number} [options.pollIntervalMs=2000]
|
|
591
613
|
* @returns {Promise<{accountId:string, accountInfo:Object, created:boolean}>}
|
|
614
|
+
* @throws {Error} If `accountId` doesn't follow the anonymous account ID rules (see class docs).
|
|
592
615
|
*/
|
|
593
616
|
async ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options = {}) {
|
|
617
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
594
618
|
const {
|
|
595
619
|
deadlineOffset = DEFAULT_DEADLINE_OFFSET,
|
|
596
620
|
waitUntilReady = true,
|
|
@@ -681,6 +705,7 @@ export class AnonymousTransferClient {
|
|
|
681
705
|
{ add = [], remove = [] } = {},
|
|
682
706
|
options = {},
|
|
683
707
|
) {
|
|
708
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
684
709
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
685
710
|
try {
|
|
686
711
|
const authNonce = await this.getAuthNonce(accountId);
|
|
@@ -760,6 +785,7 @@ export class AnonymousTransferClient {
|
|
|
760
785
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
761
786
|
*/
|
|
762
787
|
async deposit(authWallet, accountId, tokenAddress, amount, options = {}) {
|
|
788
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
763
789
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET, gasLimit = 600000n } =
|
|
764
790
|
options;
|
|
765
791
|
try {
|
|
@@ -933,6 +959,7 @@ export class AnonymousTransferClient {
|
|
|
933
959
|
},
|
|
934
960
|
options = {},
|
|
935
961
|
) {
|
|
962
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
936
963
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
937
964
|
try {
|
|
938
965
|
if (!ethers.isAddress(recipient))
|
|
@@ -1069,6 +1096,8 @@ export class AnonymousTransferClient {
|
|
|
1069
1096
|
},
|
|
1070
1097
|
options = {},
|
|
1071
1098
|
) {
|
|
1099
|
+
validateAnonymousAccountId(senderAccountId, "senderAccountId");
|
|
1100
|
+
validateAnonymousAccountId(recipientId, "recipientId");
|
|
1072
1101
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1073
1102
|
try {
|
|
1074
1103
|
if (!ethers.isAddress(token))
|
|
@@ -1181,6 +1210,7 @@ export class AnonymousTransferClient {
|
|
|
1181
1210
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1182
1211
|
*/
|
|
1183
1212
|
async applyPending(authWallet, accountId, options = {}) {
|
|
1213
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1184
1214
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1185
1215
|
try {
|
|
1186
1216
|
const authNonce = await this.getAuthNonce(accountId);
|
|
@@ -1248,6 +1278,7 @@ export class AnonymousTransferClient {
|
|
|
1248
1278
|
},
|
|
1249
1279
|
options = {},
|
|
1250
1280
|
) {
|
|
1281
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1251
1282
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1252
1283
|
try {
|
|
1253
1284
|
if (!ethers.isAddress(destination))
|
|
@@ -1357,6 +1388,7 @@ export class AnonymousTransferClient {
|
|
|
1357
1388
|
* @returns {Promise<{ amount: number, available: number, pending: number }>}
|
|
1358
1389
|
*/
|
|
1359
1390
|
async getBalance(accountId, tokenAddress, elGamalPrivateKey) {
|
|
1391
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1360
1392
|
try {
|
|
1361
1393
|
if (!ethers.isAddress(tokenAddress)) {
|
|
1362
1394
|
throw new Error(`Invalid token address: ${tokenAddress}`);
|
|
@@ -1430,6 +1462,7 @@ export class AnonymousTransferClient {
|
|
|
1430
1462
|
* @returns {Promise<bigint>}
|
|
1431
1463
|
*/
|
|
1432
1464
|
async getPrepaidFeeBalance(accountId, token) {
|
|
1465
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1433
1466
|
try {
|
|
1434
1467
|
if (!ethers.isAddress(token))
|
|
1435
1468
|
throw new Error(`Invalid token address: ${token}`);
|
|
@@ -1493,6 +1526,7 @@ export class AnonymousTransferClient {
|
|
|
1493
1526
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1494
1527
|
*/
|
|
1495
1528
|
async depositFees(authWallet, accountId, amount, options = {}) {
|
|
1529
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1496
1530
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET, gasLimit = 300000n } =
|
|
1497
1531
|
options;
|
|
1498
1532
|
try {
|
|
@@ -1643,6 +1677,7 @@ export class AnonymousTransferClient {
|
|
|
1643
1677
|
{ token, destination, amount },
|
|
1644
1678
|
options = {},
|
|
1645
1679
|
) {
|
|
1680
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1646
1681
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1647
1682
|
try {
|
|
1648
1683
|
if (!ethers.isAddress(token))
|
|
@@ -1706,6 +1741,7 @@ export class AnonymousTransferClient {
|
|
|
1706
1741
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
1707
1742
|
*/
|
|
1708
1743
|
async withdrawAllFees(authWallet, accountId, { destination }, options = {}) {
|
|
1744
|
+
validateAnonymousAccountId(accountId, "accountId");
|
|
1709
1745
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
1710
1746
|
try {
|
|
1711
1747
|
if (!ethers.isAddress(destination))
|
|
@@ -840,52 +840,6 @@ export class ConfidentialTransferClient {
|
|
|
840
840
|
);
|
|
841
841
|
}
|
|
842
842
|
|
|
843
|
-
/**
|
|
844
|
-
* Wait for pending balance to appear
|
|
845
|
-
*
|
|
846
|
-
* @param {string} address - Account address
|
|
847
|
-
* @param {string} privateKey - Private key for decryption
|
|
848
|
-
* @param {string} tokenAddress - Token address
|
|
849
|
-
* @param {Object} [options] - Options
|
|
850
|
-
* @param {number} [options.maxAttempts=225] - Maximum polling attempts
|
|
851
|
-
* @param {number} [options.intervalMs=400] - Polling interval in milliseconds
|
|
852
|
-
* @returns {Promise<{amount: number, ciphertext: string}>}
|
|
853
|
-
*/
|
|
854
|
-
async _waitForPendingBalance(
|
|
855
|
-
address,
|
|
856
|
-
privateKey,
|
|
857
|
-
tokenAddress,
|
|
858
|
-
options = {},
|
|
859
|
-
) {
|
|
860
|
-
const { maxAttempts = 225, intervalMs = 400 } = options;
|
|
861
|
-
await sleep(1000);
|
|
862
|
-
|
|
863
|
-
try {
|
|
864
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
865
|
-
const pending = await this._getPendingBalance(
|
|
866
|
-
address,
|
|
867
|
-
privateKey,
|
|
868
|
-
tokenAddress,
|
|
869
|
-
);
|
|
870
|
-
|
|
871
|
-
if (pending.amount > 0n) {
|
|
872
|
-
return pending;
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
await sleep(intervalMs);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
throw new Error(
|
|
879
|
-
`Timeout waiting for pending balance after ${maxAttempts} attempts. The transfer may still be processing.`,
|
|
880
|
-
);
|
|
881
|
-
} catch (error) {
|
|
882
|
-
if (error.message.includes("Timeout waiting for pending balance")) {
|
|
883
|
-
throw error;
|
|
884
|
-
}
|
|
885
|
-
throw new Error(`Failed to wait for pending balance: ${error.message}`);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
|
|
889
843
|
/**
|
|
890
844
|
* Get the current fee amount for confidential transfers
|
|
891
845
|
*
|
package/src/constants.js
CHANGED
|
@@ -24,11 +24,11 @@ export const ERC20_ABI = [
|
|
|
24
24
|
export const TEMPO_FEE_TOKEN_ADDRESS =
|
|
25
25
|
"0x20c0000000000000000000000000000000000000";
|
|
26
26
|
export const STABLETRUST_CONTRACTS_BY_CHAIN_ID = Object.freeze({
|
|
27
|
-
2201: "
|
|
28
|
-
5042002: "
|
|
29
|
-
84532: "
|
|
30
|
-
11155111: "
|
|
31
|
-
421614: "
|
|
27
|
+
2201: "0xe1c1456CAb802312759a8cFc3976f88bf87082cf", //Stable
|
|
28
|
+
5042002: "0x1B4f05f67CC33788Da4C89a7cd0b2f8E0055E605", //Arc
|
|
29
|
+
84532: "0x4a251C9D79faCa20b193630A4ee313af7cBCDD93", //Base
|
|
30
|
+
11155111: "0x7507a13352AFAa79D33E994f86f2f62463ba8DE4", //Ethereum
|
|
31
|
+
421614: "0x5acECCdeb5CbD3C727eCB49F8706Eb80EF2f977F", //Arbitrum
|
|
32
32
|
42431: "0xE559fB936C69c46E216bf61B07C16bF1a6d444aa", //Tempo
|
|
33
33
|
});
|
|
34
34
|
|
package/src/index.d.ts
CHANGED
|
@@ -169,6 +169,16 @@ declare module "@fairblock/stabletrust" {
|
|
|
169
169
|
*
|
|
170
170
|
* All operations (except `deposit`) are gas-free for the user — Fairycloak pays gas.
|
|
171
171
|
* `deposit` requires the user to sign and pay for a raw EVM transaction.
|
|
172
|
+
*
|
|
173
|
+
* ### Anonymous account ID rules
|
|
174
|
+
* Every `accountId` / `senderAccountId` / `recipientId` passed to this client must:
|
|
175
|
+
* - be a non-empty string
|
|
176
|
+
* - be at most {@link MAX_ANONYMOUS_ACCOUNT_ID_LENGTH} (20) characters
|
|
177
|
+
* - contain only alphanumeric characters (A-Z, a-z, 0-9)
|
|
178
|
+
* - be treated as case-sensitive (no normalization is applied)
|
|
179
|
+
*
|
|
180
|
+
* IDs that violate these rules are rejected with a descriptive error before any
|
|
181
|
+
* network request is made. See `validateAnonymousAccountId`.
|
|
172
182
|
*/
|
|
173
183
|
export class AnonymousTransferClient {
|
|
174
184
|
constructor(config: AnonymousTransferClientConfig);
|
|
@@ -178,9 +188,7 @@ declare module "@fairblock/stabletrust" {
|
|
|
178
188
|
/**
|
|
179
189
|
* Get on-chain core state for an anonymous account.
|
|
180
190
|
*/
|
|
181
|
-
getAnonymousAccountInfo(
|
|
182
|
-
accountId: number | bigint,
|
|
183
|
-
): Promise<AnonymousAccountInfo>;
|
|
191
|
+
getAnonymousAccountInfo(accountId: string): Promise<AnonymousAccountInfo>;
|
|
184
192
|
|
|
185
193
|
/**
|
|
186
194
|
* Read the current authNonce for an anonymous account.
|
|
@@ -201,7 +209,7 @@ declare module "@fairblock/stabletrust" {
|
|
|
201
209
|
* Use `.available` and `.pending` for individual balances, `.amount` for the total.
|
|
202
210
|
*/
|
|
203
211
|
getBalance(
|
|
204
|
-
accountId:
|
|
212
|
+
accountId: string,
|
|
205
213
|
tokenAddress: string,
|
|
206
214
|
elGamalPrivateKey: string,
|
|
207
215
|
): Promise<AnonymousAccountBalance>;
|
|
@@ -211,7 +219,7 @@ declare module "@fairblock/stabletrust" {
|
|
|
211
219
|
* Use `getBalance` for a simpler numeric summary.
|
|
212
220
|
*/
|
|
213
221
|
getAnonymousBalance(
|
|
214
|
-
accountId:
|
|
222
|
+
accountId: string,
|
|
215
223
|
tokenAddress: string,
|
|
216
224
|
elGamalPrivateKey: string,
|
|
217
225
|
): Promise<AnonymousBalance>;
|
|
@@ -312,7 +320,8 @@ declare module "@fairblock/stabletrust" {
|
|
|
312
320
|
|
|
313
321
|
/**
|
|
314
322
|
* Create a new anonymous account. The relay pays gas.
|
|
315
|
-
* @param accountId Caller-chosen account ID
|
|
323
|
+
* @param accountId Caller-chosen account ID. Must be a non-empty, case-sensitive,
|
|
324
|
+
* alphanumeric string of at most {@link MAX_ANONYMOUS_ACCOUNT_ID_LENGTH} (20) characters.
|
|
316
325
|
* @param elgamalPublicKey Base64 or "0x"-prefixed hex (32 bytes)
|
|
317
326
|
*/
|
|
318
327
|
createAccount(
|
|
@@ -473,7 +482,7 @@ declare module "@fairblock/stabletrust" {
|
|
|
473
482
|
/**
|
|
474
483
|
* Deposit options
|
|
475
484
|
*/
|
|
476
|
-
export interface
|
|
485
|
+
export interface ConfidentialDepositOptions {
|
|
477
486
|
waitForFinalization?: boolean;
|
|
478
487
|
}
|
|
479
488
|
|
|
@@ -549,7 +558,7 @@ declare module "@fairblock/stabletrust" {
|
|
|
549
558
|
wallet: ethers.Wallet | ethers.Signer,
|
|
550
559
|
tokenAddress: string,
|
|
551
560
|
amount: bigint | string | number,
|
|
552
|
-
options?:
|
|
561
|
+
options?: ConfidentialDepositOptions,
|
|
553
562
|
): Promise<ethers.ContractTransactionReceipt>;
|
|
554
563
|
|
|
555
564
|
/**
|
|
@@ -637,6 +646,27 @@ declare module "@fairblock/stabletrust" {
|
|
|
637
646
|
*/
|
|
638
647
|
export function encodeWithdrawProof(proof: any): string;
|
|
639
648
|
|
|
649
|
+
/**
|
|
650
|
+
* Maximum allowed length for an anonymous account ID (20 characters).
|
|
651
|
+
*/
|
|
652
|
+
export const MAX_ANONYMOUS_ACCOUNT_ID_LENGTH: number;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Validate an anonymous account ID.
|
|
656
|
+
*
|
|
657
|
+
* Rules (must match the Fairycloak relay and on-chain contract):
|
|
658
|
+
* - Non-empty string
|
|
659
|
+
* - At most {@link MAX_ANONYMOUS_ACCOUNT_ID_LENGTH} (20) characters
|
|
660
|
+
* - Alphanumeric characters only (A-Z, a-z, 0-9)
|
|
661
|
+
* - Case-sensitive (no normalization is applied)
|
|
662
|
+
*
|
|
663
|
+
* @throws {Error} If `accountId` violates any of the above rules.
|
|
664
|
+
*/
|
|
665
|
+
export function validateAnonymousAccountId(
|
|
666
|
+
accountId: string,
|
|
667
|
+
fieldName?: string,
|
|
668
|
+
): void;
|
|
669
|
+
|
|
640
670
|
/**
|
|
641
671
|
* Constants
|
|
642
672
|
*/
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export { ConfidentialTransferClient } from "./confidential-client.js";
|
|
2
2
|
export { AnonymousTransferClient } from "./anonymous-client.js";
|
|
3
3
|
export { deriveKeys, decryptCiphertext, combineCiphertext } from "./crypto.js";
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
encodeTransferProof,
|
|
6
|
+
encodeWithdrawProof,
|
|
7
|
+
validateAnonymousAccountId,
|
|
8
|
+
MAX_ANONYMOUS_ACCOUNT_ID_LENGTH,
|
|
9
|
+
} from "./utils.js";
|
|
5
10
|
export {
|
|
6
11
|
CONTRACT_ABI,
|
|
7
12
|
ERC20_ABI,
|
package/src/utils.js
CHANGED
|
@@ -3,6 +3,40 @@ import { ethers } from "ethers";
|
|
|
3
3
|
// Docs: https://docs.pinata.cloud
|
|
4
4
|
const PINATA_UPLOAD_URL = "https://uploads.pinata.cloud/v3/files";
|
|
5
5
|
const PINATA_JWT = process.env.PINATA_JWT;
|
|
6
|
+
|
|
7
|
+
// Anonymous account ID constraints (must match the Fairycloak relay and on-chain contract rules)
|
|
8
|
+
export const MAX_ANONYMOUS_ACCOUNT_ID_LENGTH = 20;
|
|
9
|
+
const ANONYMOUS_ACCOUNT_ID_REGEX = /^[A-Za-z0-9]+$/;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validate an anonymous account ID against the rules enforced by the Fairycloak
|
|
13
|
+
* relay and the on-chain contract:
|
|
14
|
+
* - non-empty
|
|
15
|
+
* - at most 20 characters
|
|
16
|
+
* - alphanumeric characters only (A-Z, a-z, 0-9)
|
|
17
|
+
* - case-sensitive (no normalization is applied)
|
|
18
|
+
*
|
|
19
|
+
* @param {string} accountId - The anonymous account ID to validate.
|
|
20
|
+
* @param {string} [fieldName="accountId"] - Name of the field, used in error messages.
|
|
21
|
+
* @throws {Error} If the account ID violates any of the above rules.
|
|
22
|
+
*/
|
|
23
|
+
export function validateAnonymousAccountId(accountId, fieldName = "accountId") {
|
|
24
|
+
const RULES =
|
|
25
|
+
`${fieldName} must be a non-empty string of at most ${MAX_ANONYMOUS_ACCOUNT_ID_LENGTH} characters, ` +
|
|
26
|
+
`containing only alphanumeric characters (A-Z, a-z, 0-9). IDs are case-sensitive.`;
|
|
27
|
+
|
|
28
|
+
if (typeof accountId !== "string" || accountId.length === 0) {
|
|
29
|
+
throw new Error(`Invalid ${fieldName}: ${RULES}`);
|
|
30
|
+
}
|
|
31
|
+
if (accountId.length > MAX_ANONYMOUS_ACCOUNT_ID_LENGTH) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Invalid ${fieldName} "${accountId}": exceeds ${MAX_ANONYMOUS_ACCOUNT_ID_LENGTH} characters (got ${accountId.length}). ${RULES}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
if (!ANONYMOUS_ACCOUNT_ID_REGEX.test(accountId)) {
|
|
37
|
+
throw new Error(`Invalid ${fieldName} "${accountId}": contains non-alphanumeric characters. ${RULES}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
6
40
|
/**
|
|
7
41
|
* Encodes the ZK-Proof data for a transfer into a format the Solidity contract expects.
|
|
8
42
|
*
|
package/examples/constants.js
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export const ERC20_ABI = [
|
|
2
|
-
"function approve(address spender, uint256 amount) external returns (bool)",
|
|
3
|
-
"function allowance(address owner, address spender) external view returns (uint256)",
|
|
4
|
-
"function balanceOf(address account) external view returns (uint256)",
|
|
5
|
-
"function decimals() external view returns (uint8)",
|
|
6
|
-
];
|