@fairblock/stabletrust 2.0.1 → 2.0.3
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 +5 -16
- package/examples/anonymous-flow.js +48 -30
- package/package.json +1 -1
- package/src/anonymous-client.js +112 -24
- package/src/constants.js +1 -1
package/README.md
CHANGED
|
@@ -236,19 +236,9 @@ const keys = await client.deriveAnonymousKeys(authWallet, accountId);
|
|
|
236
236
|
|
|
237
237
|
### Key Methods
|
|
238
238
|
|
|
239
|
-
#### `
|
|
239
|
+
#### `createAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
240
240
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
```javascript
|
|
244
|
-
const count = await client.getNextAccountId();
|
|
245
|
-
const myNewId = Number(count) + 1;
|
|
246
|
-
await client.createAccount(wallet, keys.publicKey);
|
|
247
|
-
```
|
|
248
|
-
|
|
249
|
-
#### `createAccount(authWallet, elgamalPublicKey, options?)`
|
|
250
|
-
|
|
251
|
-
Creates a new anonymous account via the relay. The relay pays gas.
|
|
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).
|
|
252
242
|
|
|
253
243
|
#### `updateAuthKeys(authWallet, accountId, { add, remove }, options?)`
|
|
254
244
|
|
|
@@ -429,11 +419,10 @@ const wallet = new ethers.Wallet(process.env.PRIVATE_KEY);
|
|
|
429
419
|
const tokenAddress = "0xYourTokenAddress";
|
|
430
420
|
const tokenDecimals = 6;
|
|
431
421
|
|
|
432
|
-
// 1.
|
|
433
|
-
const
|
|
434
|
-
const accountId = Number(count) + 1;
|
|
422
|
+
// 1. Choose a unique account ID, derive keys, then create the account
|
|
423
|
+
const accountId = "my-unique-account-id"; // any non-empty string you choose
|
|
435
424
|
const keys = await client.deriveAnonymousKeys(wallet, accountId);
|
|
436
|
-
await client.createAccount(wallet, keys.publicKey);
|
|
425
|
+
await client.createAccount(wallet, accountId, keys.publicKey);
|
|
437
426
|
|
|
438
427
|
// 2. Deposit (user pays gas)
|
|
439
428
|
const depositResult = await client.deposit(
|
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { ethers } from "ethers";
|
|
17
|
-
import { randomUUID } from "crypto";
|
|
18
17
|
import dotenv from "dotenv";
|
|
19
18
|
import { AnonymousTransferClient } from "@fairblock/stabletrust";
|
|
20
19
|
|
|
@@ -30,9 +29,9 @@ const TOKEN_ADDRESS =
|
|
|
30
29
|
const TOKEN_DECIMALS = Number(process.env.TOKEN_DECIMALS ?? "6");
|
|
31
30
|
|
|
32
31
|
// Amounts in raw ERC-20 units (6 decimals — matches complete-flow.js)
|
|
33
|
-
const DEPOSIT_AMOUNT
|
|
34
|
-
const TRANSFER_AMOUNT =
|
|
35
|
-
const WITHDRAW_AMOUNT =
|
|
32
|
+
const DEPOSIT_AMOUNT = 100_000n; // 0.1 USDC
|
|
33
|
+
const TRANSFER_AMOUNT = 50_000n; // 0.05 USDC
|
|
34
|
+
const WITHDRAW_AMOUNT = 50_000n; // 0.025 USDC
|
|
36
35
|
|
|
37
36
|
// Anvil test accounts — override via .env
|
|
38
37
|
const ANON1_PK =
|
|
@@ -121,8 +120,11 @@ async function main() {
|
|
|
121
120
|
|
|
122
121
|
console.log("\n── [1] Generate anonymous account IDs ──────────────────");
|
|
123
122
|
|
|
124
|
-
|
|
125
|
-
|
|
123
|
+
// An auth signer can only ever be bound to a single anonymous account, so the
|
|
124
|
+
// account ID must be stable per-wallet across runs (random UUIDs would cause
|
|
125
|
+
// "auth signer already used" failures on every re-run with the same .env keys).
|
|
126
|
+
const anon1Id = `anon-${anon1Wallet.address.toLowerCase()}`;
|
|
127
|
+
const anon2Id = `anon-${anon2Wallet.address.toLowerCase()}`;
|
|
126
128
|
log("anon1 id:", anon1Id);
|
|
127
129
|
log("anon2 id:", anon2Id);
|
|
128
130
|
|
|
@@ -142,26 +144,25 @@ async function main() {
|
|
|
142
144
|
|
|
143
145
|
console.log("\n── [3] Create anonymous accounts ───────────────────────");
|
|
144
146
|
|
|
145
|
-
log(`
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
log("Waiting for Fairycloak to confirm account creations …");
|
|
156
|
-
const [r1, r2] = await Promise.all([
|
|
157
|
-
client.waitForRequest(cr1.request_id, { timeoutMs: 180_000 }),
|
|
158
|
-
client.waitForRequest(cr2.request_id, { timeoutMs: 180_000 }),
|
|
159
|
-
]);
|
|
160
|
-
if (r1.error) log(" [!] create anon1 failed:", r1.error);
|
|
161
|
-
if (r2.error) log(" [!] create anon2 failed:", r2.error);
|
|
147
|
+
log(` ensuring anon1 (id=${anon1Id}) …`);
|
|
148
|
+
const e1 = await client.ensureAnonymousAccount(
|
|
149
|
+
anon1Wallet,
|
|
150
|
+
anon1Id,
|
|
151
|
+
anon1Keys.publicKey,
|
|
152
|
+
);
|
|
153
|
+
log(
|
|
154
|
+
` created=${e1.created} exists=${e1.accountInfo.exists} finalized=${e1.accountInfo.finalized}`,
|
|
155
|
+
);
|
|
162
156
|
|
|
163
|
-
|
|
164
|
-
|
|
157
|
+
log(` ensuring anon2 (id=${anon2Id}) …`);
|
|
158
|
+
const e2 = await client.ensureAnonymousAccount(
|
|
159
|
+
anon2Wallet,
|
|
160
|
+
anon2Id,
|
|
161
|
+
anon2Keys.publicKey,
|
|
162
|
+
);
|
|
163
|
+
log(
|
|
164
|
+
` created=${e2.created} exists=${e2.accountInfo.exists} finalized=${e2.accountInfo.finalized}`,
|
|
165
|
+
);
|
|
165
166
|
|
|
166
167
|
const [i1, i2] = await Promise.all([
|
|
167
168
|
client.getAnonymousAccountInfo(anon1Id),
|
|
@@ -190,7 +191,9 @@ async function main() {
|
|
|
190
191
|
// ── [4] Deposit ──────────────────────────────────────────────────────────────
|
|
191
192
|
|
|
192
193
|
console.log("\n── [4] Deposit into anon1 ──────────────────────────────");
|
|
193
|
-
log(
|
|
194
|
+
log(
|
|
195
|
+
`Depositing ${fmt(DEPOSIT_AMOUNT, TOKEN_DECIMALS)} tokens into anon1 (anon1 pays gas) …`,
|
|
196
|
+
);
|
|
194
197
|
|
|
195
198
|
const dep = await client.deposit(
|
|
196
199
|
anon1Wallet,
|
|
@@ -292,7 +295,9 @@ async function main() {
|
|
|
292
295
|
// ── [7] Withdraw anon2 → public address ──────────────────────────────────────
|
|
293
296
|
|
|
294
297
|
console.log("\n── [7] Withdraw from anon2 ─────────────────────────────");
|
|
295
|
-
log(
|
|
298
|
+
log(
|
|
299
|
+
`Withdrawing ${fmt(WITHDRAW_AMOUNT, TOKEN_DECIMALS)} tokens → ${withdrawDest} …`,
|
|
300
|
+
);
|
|
296
301
|
|
|
297
302
|
const wd = await client.withdraw(anon2Wallet, anon2Id, {
|
|
298
303
|
destination: withdrawDest,
|
|
@@ -327,9 +332,22 @@ async function main() {
|
|
|
327
332
|
console.log("═══════════════════════════════════════════════════════");
|
|
328
333
|
log("anon1 id :", anon1Id);
|
|
329
334
|
log("anon2 id :", anon2Id);
|
|
330
|
-
log(
|
|
331
|
-
|
|
332
|
-
|
|
335
|
+
log(
|
|
336
|
+
"Deposited :",
|
|
337
|
+
ethers.formatUnits(DEPOSIT_AMOUNT, TOKEN_DECIMALS),
|
|
338
|
+
"tokens → anon1",
|
|
339
|
+
);
|
|
340
|
+
log(
|
|
341
|
+
"Transferred:",
|
|
342
|
+
ethers.formatUnits(TRANSFER_AMOUNT, TOKEN_DECIMALS),
|
|
343
|
+
"tokens → anon2",
|
|
344
|
+
);
|
|
345
|
+
log(
|
|
346
|
+
"Withdrawn :",
|
|
347
|
+
ethers.formatUnits(WITHDRAW_AMOUNT, TOKEN_DECIMALS),
|
|
348
|
+
"tokens →",
|
|
349
|
+
withdrawDest,
|
|
350
|
+
);
|
|
333
351
|
}
|
|
334
352
|
|
|
335
353
|
main().catch((err) => {
|
package/package.json
CHANGED
package/src/anonymous-client.js
CHANGED
|
@@ -123,22 +123,21 @@ export class AnonymousTransferClient {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
|
-
* Resolve a pubkey
|
|
127
|
-
* Accepts: ethers.Wallet,
|
|
126
|
+
* Resolve a wallet/address/pubkey to a checksummed EVM address.
|
|
127
|
+
* Accepts: ethers.Wallet/Signer, an address string, or an uncompressed hex pubkey string.
|
|
128
128
|
* @private
|
|
129
129
|
*/
|
|
130
|
-
|
|
131
|
-
if (typeof
|
|
132
|
-
|
|
130
|
+
async _resolveSignerAddress(walletOrAddress) {
|
|
131
|
+
if (typeof walletOrAddress === "string") {
|
|
132
|
+
if (ethers.isAddress(walletOrAddress))
|
|
133
|
+
return ethers.getAddress(walletOrAddress);
|
|
134
|
+
return ethers.computeAddress(walletOrAddress); // hex pubkey → address
|
|
133
135
|
}
|
|
134
|
-
if (
|
|
135
|
-
return
|
|
136
|
-
}
|
|
137
|
-
if (walletOrPubkey.publicKey) {
|
|
138
|
-
return walletOrPubkey.publicKey; // ethers.SigningKey
|
|
136
|
+
if (typeof walletOrAddress.getAddress === "function") {
|
|
137
|
+
return await walletOrAddress.getAddress();
|
|
139
138
|
}
|
|
140
139
|
throw new Error(
|
|
141
|
-
"Expected an ethers.Wallet or uncompressed hex public-key string",
|
|
140
|
+
"Expected an ethers.Wallet/Signer, an address string, or an uncompressed hex public-key string",
|
|
142
141
|
);
|
|
143
142
|
}
|
|
144
143
|
|
|
@@ -515,7 +514,6 @@ export class AnonymousTransferClient {
|
|
|
515
514
|
const { deadlineOffset = DEFAULT_DEADLINE_OFFSET } = options;
|
|
516
515
|
try {
|
|
517
516
|
const authAddress = await authWallet.getAddress();
|
|
518
|
-
const authPubkey = this._resolvePubkey(authWallet); // uncompressed 65-byte pubkey
|
|
519
517
|
|
|
520
518
|
// Normalise elgamal key to hex
|
|
521
519
|
const elgamalHex = elgamalPublicKey.startsWith("0x")
|
|
@@ -557,7 +555,7 @@ export class AnonymousTransferClient {
|
|
|
557
555
|
return await this._fetch("POST", "/v1/anonymous/accounts", {
|
|
558
556
|
account_id: accountId,
|
|
559
557
|
elgamal_pubkey: elgamalHex,
|
|
560
|
-
|
|
558
|
+
auth_signers: [authAddress],
|
|
561
559
|
client_request_id: clientRequestId,
|
|
562
560
|
deadline: String(deadline),
|
|
563
561
|
signature,
|
|
@@ -569,14 +567,106 @@ export class AnonymousTransferClient {
|
|
|
569
567
|
}
|
|
570
568
|
}
|
|
571
569
|
|
|
570
|
+
/**
|
|
571
|
+
* Create an anonymous account if it doesn't already exist, and (optionally) wait
|
|
572
|
+
* until it has settled (no pending CW→EVM action) before returning.
|
|
573
|
+
*
|
|
574
|
+
* Looks the account up by `accountId` first — if it already exists on-chain this
|
|
575
|
+
* skips account creation entirely (idempotent re-runs with a stable `accountId`).
|
|
576
|
+
* Note that an auth signer can only ever be bound to a single anonymous account,
|
|
577
|
+
* so reusing a wallet across different `accountId`s will still fail at creation time.
|
|
578
|
+
*
|
|
579
|
+
* @param {ethers.Wallet|ethers.Signer} authWallet - Initial auth signer
|
|
580
|
+
* @param {string} accountId - Caller-chosen account ID (non-empty string, case-sensitive)
|
|
581
|
+
* @param {string} elgamalPublicKey - ElGamal public key as base64 or "0x"-prefixed hex (32 bytes)
|
|
582
|
+
* @param {Object} [options]
|
|
583
|
+
* @param {number} [options.deadlineOffset=3600] - Deadline in seconds from now
|
|
584
|
+
* @param {boolean} [options.waitUntilReady=true] - Poll until `hasPendingAction` is false
|
|
585
|
+
* @param {number} [options.timeoutMs=180000] - Max time to wait for creation/readiness
|
|
586
|
+
* @param {number} [options.pollIntervalMs=2000]
|
|
587
|
+
* @returns {Promise<{accountId:string, accountInfo:Object, created:boolean}>}
|
|
588
|
+
*/
|
|
589
|
+
async ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options = {}) {
|
|
590
|
+
const {
|
|
591
|
+
deadlineOffset = DEFAULT_DEADLINE_OFFSET,
|
|
592
|
+
waitUntilReady = true,
|
|
593
|
+
timeoutMs = 180_000,
|
|
594
|
+
pollIntervalMs = 2000,
|
|
595
|
+
} = options;
|
|
596
|
+
|
|
597
|
+
const cutoff = Date.now() + timeoutMs;
|
|
598
|
+
const waitForReady = async () => {
|
|
599
|
+
let info = await this.getAnonymousAccountInfo(accountId);
|
|
600
|
+
while (waitUntilReady && info.hasPendingAction) {
|
|
601
|
+
if (Date.now() > cutoff) {
|
|
602
|
+
throw new Error(
|
|
603
|
+
`Timeout waiting for anonymous account ${accountId} to become ready`,
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
607
|
+
info = await this.getAnonymousAccountInfo(accountId);
|
|
608
|
+
}
|
|
609
|
+
return info;
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
let accountInfo = await this.getAnonymousAccountInfo(accountId);
|
|
613
|
+
if (accountInfo.exists) {
|
|
614
|
+
accountInfo = await waitForReady();
|
|
615
|
+
return { accountId, accountInfo, created: false };
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const failIfAlreadyUsed = async (err) => {
|
|
619
|
+
if (typeof err === "string" && err.includes("already used")) {
|
|
620
|
+
const address = await authWallet.getAddress();
|
|
621
|
+
throw new Error(
|
|
622
|
+
`Signer ${address} is already bound to a different anonymous account, ` +
|
|
623
|
+
`but no account exists with accountId "${accountId}". ` +
|
|
624
|
+
`The provided accountId is likely incorrect — each auth signer can only ` +
|
|
625
|
+
`ever be registered to a single anonymous account.`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const created = await this.createAccount(authWallet, accountId, elgamalPublicKey, {
|
|
631
|
+
deadlineOffset,
|
|
632
|
+
});
|
|
633
|
+
if (created.status === "failed" || created.error) {
|
|
634
|
+
await failIfAlreadyUsed(created.error);
|
|
635
|
+
throw new Error(
|
|
636
|
+
`Failed to create anonymous account ${accountId}: ${created.error ?? "request failed"}`,
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const finalRequest = await this.waitForRequest(created.request_id, { timeoutMs });
|
|
641
|
+
if (finalRequest.status === "failed" || finalRequest.error) {
|
|
642
|
+
await failIfAlreadyUsed(finalRequest.error);
|
|
643
|
+
throw new Error(
|
|
644
|
+
`Failed to create anonymous account ${accountId}: ${finalRequest.error ?? "request failed"}`,
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
while (!accountInfo.exists) {
|
|
649
|
+
if (Date.now() > cutoff) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`Timeout waiting for anonymous account ${accountId} to be created`,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
655
|
+
accountInfo = await this.getAnonymousAccountInfo(accountId);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
accountInfo = await waitForReady();
|
|
659
|
+
return { accountId, accountInfo, created: true };
|
|
660
|
+
}
|
|
661
|
+
|
|
572
662
|
/**
|
|
573
663
|
* Update the set of authorised signers for an anonymous account via Fairycloak.
|
|
574
664
|
*
|
|
575
665
|
* @param {ethers.Wallet|ethers.Signer} authWallet - A currently authorised signer
|
|
576
666
|
* @param {string} accountId
|
|
577
667
|
* @param {Object} keys
|
|
578
|
-
* @param {Array<string|ethers.Wallet>} [keys.add=[]] -
|
|
579
|
-
* @param {Array<string|ethers.Wallet>} [keys.remove=[]] -
|
|
668
|
+
* @param {Array<string|ethers.Wallet|ethers.Signer>} [keys.add=[]] - Addresses/wallets/pubkeys to add
|
|
669
|
+
* @param {Array<string|ethers.Wallet|ethers.Signer>} [keys.remove=[]] - Addresses/wallets/pubkeys to remove
|
|
580
670
|
* @param {Object} [options]
|
|
581
671
|
* @param {number} [options.deadlineOffset=3600]
|
|
582
672
|
* @returns {Promise<{request_id:string, tx_hash:string, status:string}>}
|
|
@@ -592,13 +682,11 @@ export class AnonymousTransferClient {
|
|
|
592
682
|
const authNonce = await this.getAuthNonce(accountId);
|
|
593
683
|
const deadline = this._makeDeadline(deadlineOffset);
|
|
594
684
|
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
const removeAddresses = removePubkeys.map((pk) =>
|
|
601
|
-
ethers.computeAddress(pk),
|
|
685
|
+
const addAddresses = await Promise.all(
|
|
686
|
+
add.map((w) => this._resolveSignerAddress(w)),
|
|
687
|
+
);
|
|
688
|
+
const removeAddresses = await Promise.all(
|
|
689
|
+
remove.map((w) => this._resolveSignerAddress(w)),
|
|
602
690
|
);
|
|
603
691
|
|
|
604
692
|
const addSignersHash = ethers.keccak256(
|
|
@@ -633,8 +721,8 @@ export class AnonymousTransferClient {
|
|
|
633
721
|
|
|
634
722
|
return await this._fetch("POST", "/v1/anonymous/keys/update", {
|
|
635
723
|
account_id: accountId,
|
|
636
|
-
|
|
637
|
-
|
|
724
|
+
add_signers: addAddresses,
|
|
725
|
+
remove_signers: removeAddresses,
|
|
638
726
|
auth_nonce: String(authNonce),
|
|
639
727
|
deadline: String(deadline),
|
|
640
728
|
signature,
|
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: "0xdA4a410F600E1C245DC1188Ee559833879f62Fbb", //Arc
|
|
29
29
|
84532: "0xF66A0a1670F14AE5D1852B4E7d1e4C693b2Accfd", //Base
|
|
30
30
|
11155111: "0xa066b5C30382110d19925108BBa1Eef613a3A041", //Ethereum
|
|
31
31
|
421614: "0xbda65d65A7833D28F9391FF01d0b212B75538Cf2", //Arbitrum
|