@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
package/README.md
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
The
|
|
5
|
+
The stabletrust SDK by Fairblock provides a robust interface for executing confidential and unlinkable transfers using homomorphic encryption and zero-knowledge proofs. This package enables developers to integrate confidentiality features directly into their applications, allowing for secure token deposits, private transfers, and withdrawals while maintaining the integrity and auditability of the underlying blockchain transactions.
|
|
6
6
|
|
|
7
7
|
For a comprehensive technical understanding of the architecture and cryptographic primitives, please refer to the following documentation:
|
|
8
8
|
|
|
9
9
|
- **Technical Overview**: [Fairblock Confidential Transfers](https://docs.fairblock.network/docs/confidential_transfers/technical_overview)
|
|
10
|
-
- **
|
|
10
|
+
- **stabletrust Protocol**: [stabletrust Documentation](https://docs.fairblock.network/docs/confidential_transfers/stabletrust)
|
|
11
11
|
- **Confidential Transactions**: [Transaction Mechanics](https://docs.fairblock.network/docs/confidential_transfers/confidential_transactions)
|
|
12
12
|
|
|
13
13
|
## Requirements
|
|
@@ -41,9 +41,9 @@ The SDK ships two clients that serve different privacy models:
|
|
|
41
41
|
| Feature | `ConfidentialTransferClient` | `AnonymousTransferClient` |
|
|
42
42
|
| :----------------- | :----------------------------------------------- | :----------------------------------------------------------- |
|
|
43
43
|
| **Identity** | On-chain wallet address is visible | Account ID only — wallet address is hidden |
|
|
44
|
-
| **Gas** | User pays gas for all transactions |
|
|
44
|
+
| **Gas** | User pays gas for all transactions | Relay pays gas (except user-funded raw-tx ops: deposit, fee top-up/withdraw, native-ETH create) |
|
|
45
45
|
| **Setup** | Direct RPC connection | Requires a running Fairycloak relay server |
|
|
46
|
-
| **Recipient** | Must have a confidential account | Can receive to another
|
|
46
|
+
| **Recipient** | Must have a confidential account | Can receive to another unlinkable account or a public address |
|
|
47
47
|
| **Key management** | Keys auto-derived from wallet signature | Keys derived per-account; store the private key yourself |
|
|
48
48
|
| **Best for** | Privacy over balances/amounts with known senders | Full sender anonymity |
|
|
49
49
|
|
|
@@ -51,9 +51,9 @@ The SDK ships two clients that serve different privacy models:
|
|
|
51
51
|
|
|
52
52
|
Transactions are submitted on-chain directly from your wallet. The **amount and balance are encrypted** (hidden), but the **sender's address is visible** on-chain. Ideal when you want confidential balances without hiding your identity.
|
|
53
53
|
|
|
54
|
-
### `AnonymousTransferClient` —
|
|
54
|
+
### `AnonymousTransferClient` — Unlinkable Transfers
|
|
55
55
|
|
|
56
|
-
Transactions are routed through the **Fairycloak relay**, which submits them on-chain and pays gas. The sender's wallet address is **never revealed** on-chain
|
|
56
|
+
Transactions are routed through the **Fairycloak relay**, which submits them on-chain and pays gas (recouped from the account's prepaid fee reserve). The sender's wallet address is **never revealed** on-chain - only the caller-chosen account ID is associated with the transfer. Use this client when full sender anonymity is required.
|
|
57
57
|
|
|
58
58
|
---
|
|
59
59
|
|
|
@@ -63,12 +63,12 @@ The following contract addresses are available for confidential transfers on tes
|
|
|
63
63
|
|
|
64
64
|
| Network(Testnet) | Chain ID | Contract Address |
|
|
65
65
|
| :--------------- | :------- | :------------------------------------------- |
|
|
66
|
-
| Stable | 2201 | `
|
|
67
|
-
| Arc | 5042002 | `
|
|
68
|
-
| Base | 84532 | `
|
|
69
|
-
| Ethereum | 11155111 | `
|
|
70
|
-
| Arbitrum | 421614 | `
|
|
71
|
-
| Tempo | 42431 | `
|
|
66
|
+
| Stable | 2201 | `0xe1c1456CAb802312759a8cFc3976f88bf87082cf` |
|
|
67
|
+
| Arc | 5042002 | `0x1B4f05f67CC33788Da4C89a7cd0b2f8E0055E605` |
|
|
68
|
+
| Base | 84532 | `0x4a251C9D79faCa20b193630A4ee313af7cBCDD93` |
|
|
69
|
+
| Ethereum | 11155111 | `0x7507a13352AFAa79D33E994f86f2f62463ba8DE4` |
|
|
70
|
+
| Arbitrum | 421614 | `0x5acECCdeb5CbD3C727eCB49F8706Eb80EF2f977F` |
|
|
71
|
+
| Tempo | 42431 | `0xE559fB936C69c46E216bf61B07C16bF1a6d444aa` |
|
|
72
72
|
|
|
73
73
|
---
|
|
74
74
|
|
|
@@ -84,7 +84,7 @@ Import and initialize the client with your network configuration.
|
|
|
84
84
|
import { ConfidentialTransferClient } from "@fairblock/stabletrust";
|
|
85
85
|
import { ethers } from "ethers";
|
|
86
86
|
|
|
87
|
-
// Use SDK default
|
|
87
|
+
// Use SDK default stabletrust contract for a known chainId
|
|
88
88
|
const client = new ConfidentialTransferClient(
|
|
89
89
|
"https://sepolia.base.org",
|
|
90
90
|
84532,
|
|
@@ -96,7 +96,7 @@ If you are using a custom deployment, pass an explicit contract address:
|
|
|
96
96
|
```javascript
|
|
97
97
|
const customClient = new ConfidentialTransferClient(
|
|
98
98
|
"https://sepolia.base.org",
|
|
99
|
-
"
|
|
99
|
+
"0xYourCustomstabletrustContract",
|
|
100
100
|
84532,
|
|
101
101
|
);
|
|
102
102
|
```
|
|
@@ -116,14 +116,38 @@ const arcClient = new ConfidentialTransferClient(
|
|
|
116
116
|
5042002,
|
|
117
117
|
);
|
|
118
118
|
|
|
119
|
-
// Tempo (
|
|
119
|
+
// Tempo (defaults to offchainZKP/IPFS proof upload)
|
|
120
120
|
const tempoClient = new ConfidentialTransferClient(
|
|
121
121
|
"https://tempo-rpc.example.com",
|
|
122
122
|
42431,
|
|
123
123
|
);
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
-
**Note on
|
|
126
|
+
**Note on off-chain ZKP/IPFS proofs**: The latest confidential-transfer contract exposes `transferConfidential(address,address,bytes,bool)` and `withdraw(address,uint256,bytes,bool)`. By default the SDK submits inline ABI-encoded proof bytes with `offchainZKP=false`. Set `offchainZKP: true` on `confidentialTransfer` or `withdraw` to upload the ABI-encoded proof bytes to StableTrust IPFS and put only the bare CID bytes on-chain. Tempo (chainId 42431) keeps the old SDK behaviour and defaults to `offchainZKP=true`; pass `{ offchainZKP: false }` to force inline proofs.
|
|
127
|
+
|
|
128
|
+
```javascript
|
|
129
|
+
const client = new ConfidentialTransferClient(rpcUrl, chainId, {
|
|
130
|
+
ipfsUploadUrl: process.env.STABLETRUST_IPFS_UPLOAD_URL,
|
|
131
|
+
ipfsApiKey: process.env.STABLETRUST_IPFS_API_KEY,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
await client.confidentialTransfer(
|
|
135
|
+
signer,
|
|
136
|
+
recipientAddress,
|
|
137
|
+
tokenAddress,
|
|
138
|
+
amountToTransfer,
|
|
139
|
+
{ offchainZKP: true },
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
await client.withdraw(
|
|
143
|
+
signer,
|
|
144
|
+
tokenAddress,
|
|
145
|
+
amountToWithdraw,
|
|
146
|
+
{ offchainZKP: true },
|
|
147
|
+
);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The deprecated option name `useOffchainVerify` is still accepted as an alias for `offchainZKP`.
|
|
127
151
|
|
|
128
152
|
### Token Denomination
|
|
129
153
|
|
|
@@ -186,7 +210,7 @@ Deposits ERC-20 tokens into the confidential contract. Handles ERC-20 approval a
|
|
|
186
210
|
|
|
187
211
|
#### `confidentialTransfer(senderWallet, recipientAddress, tokenAddress, amount, options?)`
|
|
188
212
|
|
|
189
|
-
Transfers a confidential amount to a recipient. The recipient must have an existing confidential account.
|
|
213
|
+
Transfers a confidential amount to a recipient. The recipient must have an existing confidential account. Both inline and IPFS proof paths use the same live non-anonymous transfer fee. The SDK reads the active fee configuration automatically: native fees are attached as `msg.value`, while ERC-20 fees are approved for the diamond when necessary.
|
|
190
214
|
|
|
191
215
|
- **Returns**: Transaction receipt.
|
|
192
216
|
|
|
@@ -196,9 +220,14 @@ Withdraws from the confidential available balance back to public ERC-20.
|
|
|
196
220
|
|
|
197
221
|
- **Returns**: Transaction receipt.
|
|
198
222
|
|
|
199
|
-
####
|
|
223
|
+
#### Fee configuration getters
|
|
200
224
|
|
|
201
|
-
|
|
225
|
+
- `getFeeToken()` returns the active fee token; `ethers.ZeroAddress` means native currency.
|
|
226
|
+
- `getNonAnonymousTransferFee()` returns the fixed fee used by both inline and IPFS non-anonymous transfers.
|
|
227
|
+
- `getAnonymousIpfsTransferFee()` returns the fixed anonymous IPFS-path fee.
|
|
228
|
+
- `getAnonymousInlineTransferFee()` returns the fixed anonymous inline-path fee.
|
|
229
|
+
|
|
230
|
+
All amounts are returned as raw units of the active fee token.
|
|
202
231
|
|
|
203
232
|
#### `getPublicBalance(address, tokenAddress)`
|
|
204
233
|
|
|
@@ -208,11 +237,11 @@ Returns the public ERC-20 balance for an address.
|
|
|
208
237
|
|
|
209
238
|
## AnonymousTransferClient
|
|
210
239
|
|
|
211
|
-
The `AnonymousTransferClient` routes all operations through the **Fairycloak relay server**. The relay submits transactions on-chain and pays gas on your behalf (
|
|
240
|
+
The `AnonymousTransferClient` routes all operations through the **Fairycloak relay server**. The relay submits transactions on-chain and pays gas on your behalf, recovering the cost from the account's [prepaid fee reserve](#prepaid-fees). The exceptions are the **user-funded raw-tx operations** - `deposit`, `depositFees`, `withdrawFees`, `withdrawAllFees`, and native-ETH account creation - which you sign and pay gas for directly (and which are therefore not charged a prepaid fee). Your wallet address is never revealed - only your chosen unlinkable account ID appears on-chain.
|
|
212
241
|
|
|
213
|
-
> **Access Required** —
|
|
242
|
+
> **Access Required** — Unlinkable 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
243
|
|
|
215
|
-
###
|
|
244
|
+
### Unlinkable Account ID Rules
|
|
216
245
|
|
|
217
246
|
Every `accountId` (and `senderAccountId` / `recipientId`) you pass to `AnonymousTransferClient` must follow these rules, enforced both on-chain and by the Fairycloak relay:
|
|
218
247
|
|
|
@@ -235,9 +264,18 @@ const client = new AnonymousTransferClient({
|
|
|
235
264
|
});
|
|
236
265
|
```
|
|
237
266
|
|
|
267
|
+
**Config options** — `fairycloakUrl`, `chainId`, and `rpcUrl` are required. Optional:
|
|
268
|
+
|
|
269
|
+
| option | default | purpose |
|
|
270
|
+
|---|---|---|
|
|
271
|
+
| `diamondAddress` | auto-resolved by `chainId` | override the ConfidentialMirror contract address |
|
|
272
|
+
| `apiKey` | - | Fairycloak API key (sent as `X-API-Key`) |
|
|
273
|
+
| `ipfsUploadUrl` / `ipfsApiKey` / `ipfsGatewayUrl` | env fallback | off-chain ZKP proof uploads (see [Off-chain ZKP proofs](#off-chain-zkp-proofs-ipfs)) |
|
|
274
|
+
| `enableNativeFeeToken` | `false` | opt in to **native-ETH fee tokens**. Off by default - the SDK refuses account creation when the protocol fee token is native ETH unless this is `true`. See [Prepaid Fees](#prepaid-fees). |
|
|
275
|
+
|
|
238
276
|
### Key Derivation
|
|
239
277
|
|
|
240
|
-
|
|
278
|
+
Unlinkable accounts use a per-account ElGamal keypair derived from a wallet signature. **Store the returned `privateKey` securely** — it cannot be recovered without the original wallet and account ID.
|
|
241
279
|
|
|
242
280
|
```javascript
|
|
243
281
|
const keys = await client.deriveAnonymousKeys(authWallet, accountId);
|
|
@@ -249,13 +287,20 @@ const keys = await client.deriveAnonymousKeys(authWallet, accountId);
|
|
|
249
287
|
|
|
250
288
|
#### `createAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
251
289
|
|
|
252
|
-
Creates a new
|
|
290
|
+
Creates a new unlinkable account. `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 [Unlinkable Account ID Rules](#unlinkable-account-id-rules) above (non-empty, at most 20 characters, alphanumeric only, case-sensitive).
|
|
291
|
+
|
|
292
|
+
Creation also seeds the account's **prepaid fee reserve** (see [Prepaid Fees](#prepaid-fees)). It pulls an `initialFeeDeposit` in the active fee token, immediately charges the one-time `anonymousCreateAccountFee`, and credits the remainder to the reserve once creation finalises. The deposit defaults to the contract's `anonymousMinimumInitialFeeDeposit`; override it with `options.initialFeeDeposit`. The submission model is chosen automatically by the active fee token:
|
|
293
|
+
|
|
294
|
+
- **ERC-20 fee token** (default): the SDK checks/approves the deposit and the **relay submits + pays gas**.
|
|
295
|
+
- **Native ETH fee token** (`feeToken == address(0)`): ETH can't be pulled with `transferFrom`, so the fee payer **signs and funds the payable tx itself** (`msg.value == initialFeeDeposit`, `authWallet` pays gas). This path is **disabled unless** the client is constructed with `enableNativeFeeToken: true` — otherwise `createAccount` throws.
|
|
296
|
+
|
|
297
|
+
- **Options**: `deadlineOffset` (default `3600`), `initialFeeDeposit` (default = contract minimum), `gasLimit` (default `1000000n`, only used for the native raw tx)
|
|
253
298
|
|
|
254
299
|
#### `ensureAnonymousAccount(authWallet, accountId, elgamalPublicKey, options?)`
|
|
255
300
|
|
|
256
|
-
Idempotent helper that creates the
|
|
301
|
+
Idempotent helper that creates the unlinkable 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`.
|
|
257
302
|
|
|
258
|
-
> Note: an auth signer can only ever be bound to a single
|
|
303
|
+
> Note: an auth signer can only ever be bound to a single unlinkable account, so reusing a wallet across different `accountId`s will fail at creation time even via `ensureAnonymousAccount`.
|
|
259
304
|
|
|
260
305
|
```javascript
|
|
261
306
|
const { accountId, accountInfo, created } = await client.ensureAnonymousAccount(
|
|
@@ -270,7 +315,7 @@ const { accountId, accountInfo, created } = await client.ensureAnonymousAccount(
|
|
|
270
315
|
|
|
271
316
|
#### `updateAuthKeys(authWallet, accountId, { add, remove }, options?)`
|
|
272
317
|
|
|
273
|
-
Adds or removes authorised signers for an
|
|
318
|
+
Adds or removes authorised signers for an unlinkable account. Pass `ethers.Wallet` instances or raw uncompressed hex pubkey strings. The relay pays gas and charges the `anonymousUpdateKeysFee` from the account's [prepaid reserve](#prepaid-fees).
|
|
274
319
|
|
|
275
320
|
#### `getAnonymousAccountInfo(accountId)`
|
|
276
321
|
|
|
@@ -294,7 +339,7 @@ Returns decrypted balances including raw ciphertexts — useful when you need th
|
|
|
294
339
|
|
|
295
340
|
#### `deposit(authWallet, accountId, tokenAddress, amount, options?)`
|
|
296
341
|
|
|
297
|
-
Deposits tokens into an
|
|
342
|
+
Deposits tokens into an unlinkable account. The **user pays gas** for this operation. Handles ERC-20 approval automatically.
|
|
298
343
|
|
|
299
344
|
```javascript
|
|
300
345
|
const result = await client.deposit(
|
|
@@ -308,7 +353,7 @@ await client.waitForRequest(result.request_id);
|
|
|
308
353
|
|
|
309
354
|
#### `transferToPublic(authWallet, accountId, params, options?)`
|
|
310
355
|
|
|
311
|
-
Transfers from an
|
|
356
|
+
Transfers from an unlinkable account to a public EVM address. The relay pays gas and charges the inline or IPFS transfer fee (depending on `offchainZKP`) from the sender's [prepaid reserve](#prepaid-fees).
|
|
312
357
|
|
|
313
358
|
**Auto-proof mode** (recommended):
|
|
314
359
|
|
|
@@ -339,7 +384,7 @@ const result = await client.transferToPublic(wallet, accountId, {
|
|
|
339
384
|
|
|
340
385
|
#### `transferToAnonymous(authWallet, senderAccountId, params, options?)`
|
|
341
386
|
|
|
342
|
-
Transfers between two
|
|
387
|
+
Transfers between two unlinkable accounts. The relay pays gas and charges the inline or IPFS transfer fee (depending on `offchainZKP`) from the sender's [prepaid reserve](#prepaid-fees).
|
|
343
388
|
|
|
344
389
|
```javascript
|
|
345
390
|
const result = await client.transferToAnonymous(wallet, senderAccountId, {
|
|
@@ -352,11 +397,11 @@ const result = await client.transferToAnonymous(wallet, senderAccountId, {
|
|
|
352
397
|
|
|
353
398
|
#### `applyPending(authWallet, accountId, options?)`
|
|
354
399
|
|
|
355
|
-
Moves a pending incoming balance into available. Must be called after receiving an
|
|
400
|
+
Moves a pending incoming balance into available. Must be called after receiving an unlinkable-to-unlinkable transfer. The relay pays gas and charges the `anonymousApplyPendingFee` from the account's [prepaid reserve](#prepaid-fees).
|
|
356
401
|
|
|
357
402
|
#### `withdraw(authWallet, accountId, params, options?)`
|
|
358
403
|
|
|
359
|
-
Withdraws from an
|
|
404
|
+
Withdraws from an unlinkable account to a public EVM address. The relay pays gas and charges the inline or IPFS withdraw fee (depending on `offchainZKP`) from the account's [prepaid reserve](#prepaid-fees).
|
|
360
405
|
|
|
361
406
|
```javascript
|
|
362
407
|
const result = await client.withdraw(wallet, accountId, {
|
|
@@ -367,32 +412,110 @@ const result = await client.withdraw(wallet, accountId, {
|
|
|
367
412
|
});
|
|
368
413
|
```
|
|
369
414
|
|
|
415
|
+
### Off-chain ZKP proofs (IPFS)
|
|
416
|
+
|
|
417
|
+
By default an unlinkable proof is submitted **inline** through the Fairycloak relay. For `transferToPublic`, `transferToAnonymous`, and `withdraw` you can instead set **`offchainZKP: true`** to upload the proof to **StableTrust self-hosted IPFS** and put only the resulting **CID** on-chain. Fairycloak/Fairyport then fetches the full proof from IPFS and forwards the actual proof bytes to the CosmWasm contract. This keeps large (~2.5 KB) proofs out of calldata. The flag is optional and defaults to `false`, so the inline path is unchanged. These proof-carrying methods support it; `deposit` and `applyPending` do not (they carry no ZK proof).
|
|
418
|
+
|
|
419
|
+
**Providing StableTrust IPFS upload config.** When `offchainZKP` is enabled the SDK uploads proof bytes to the configured StableTrust IPFS upload endpoint. Provide it either via the client config (recommended, and required in browsers where `process.env` is unavailable):
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
const client = new AnonymousTransferClient({
|
|
423
|
+
fairycloakUrl: "http://127.0.0.1:8080",
|
|
424
|
+
chainId: 31337,
|
|
425
|
+
rpcUrl: "http://127.0.0.1:8545",
|
|
426
|
+
ipfsUploadUrl: process.env.STABLETRUST_IPFS_UPLOAD_URL,
|
|
427
|
+
ipfsApiKey: process.env.STABLETRUST_IPFS_API_KEY,
|
|
428
|
+
});
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
…or, in Node.js, by setting `STABLETRUST_IPFS_UPLOAD_URL` and `STABLETRUST_IPFS_API_KEY` environment variables, used as fallback values when they are not passed in client config.
|
|
432
|
+
|
|
433
|
+
**Usage** - add the flag to a normal transfer:
|
|
434
|
+
|
|
435
|
+
```javascript
|
|
436
|
+
// unlinkable -> unlinkable; proof stored on IPFS, CID on-chain
|
|
437
|
+
await client.transferToAnonymous(wallet, "alice", {
|
|
438
|
+
recipientId: "bob",
|
|
439
|
+
token: tokenAddress,
|
|
440
|
+
amount: ethers.parseUnits("5", 18),
|
|
441
|
+
elGamalPrivateKey: keys.privateKey,
|
|
442
|
+
offchainZKP: true,
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
// unlinkable -> public EVM address
|
|
446
|
+
await client.transferToPublic(wallet, "alice", {
|
|
447
|
+
recipient: "0xRecipientAddress",
|
|
448
|
+
token: tokenAddress,
|
|
449
|
+
amount: ethers.parseUnits("5", 18),
|
|
450
|
+
elGamalPrivateKey: keys.privateKey,
|
|
451
|
+
offchainZKP: true,
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// withdraw to a public address; withdraw proof stored on IPFS, CID on-chain
|
|
455
|
+
await client.withdraw(wallet, "alice", {
|
|
456
|
+
destination: "0xDestinationAddress",
|
|
457
|
+
token: tokenAddress,
|
|
458
|
+
plainAmount: ethers.parseUnits("5", 18),
|
|
459
|
+
elGamalPrivateKey: keys.privateKey,
|
|
460
|
+
offchainZKP: true,
|
|
461
|
+
});
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
In manual-proof mode (when you pass a pre-computed `proof`) with `offchainZKP: true`, the SDK treats `proof` as an already-uploaded CID rather than re-uploading it.
|
|
465
|
+
|
|
370
466
|
### Prepaid Fees
|
|
371
467
|
|
|
372
|
-
|
|
468
|
+
The protocol uses a **prepaid-fee-for-all-operations** model. Each unlinkable account holds a **prepaid fee reserve** (a pot in the active fee token), and every relay-submitted operation debits a fixed fee from it:
|
|
469
|
+
|
|
470
|
+
| operation | fee |
|
|
471
|
+
|---|---|
|
|
472
|
+
| create account | `anonymousCreateAccountFee` (charged once, at creation) |
|
|
473
|
+
| update auth keys | `anonymousUpdateKeysFee` |
|
|
474
|
+
| transfer (→ public / → anonymous) | `anonymousInlineTransferFee` or `anonymousIpfsTransferFee` |
|
|
475
|
+
| apply pending | `anonymousApplyPendingFee` |
|
|
476
|
+
| withdraw | `anonymousInlineWithdrawFee` or `anonymousIpfsWithdrawFee` |
|
|
477
|
+
|
|
478
|
+
The relay fronts the gas for these operations and is reimbursed from the pot. **User-funded raw-tx operations do _not_ charge a prepaid fee** - `deposit`, `depositFees` (top-up), `withdrawFees`, and `withdrawAllFees` are signed and paid for by the user's own wallet, so there is nothing to reimburse.
|
|
479
|
+
|
|
480
|
+
The reserve is seeded at [account creation](#createaccountauthwallet-accountid-elgamalpublickey-options) (`initialFeeDeposit − anonymousCreateAccountFee`) and topped up with `depositFees`. Transfers additionally pre-check the reserve before submitting and throw a descriptive error if it can't cover the fee.
|
|
481
|
+
|
|
482
|
+
**Fee token.** There is one active fee token for the whole protocol, set by the admin and read via `getFeeToken()`. It can be an **ERC-20** (the default) or **native ETH** (`address(0)`) - fee amounts and the prepaid pot are denominated in whichever it is. Using a native-ETH fee token requires opting in with `enableNativeFeeToken: true` in the client config (see [createAccount](#createaccountauthwallet-accountid-elgamalpublickey-options)).
|
|
373
483
|
|
|
374
484
|
#### `getFeeToken()`
|
|
375
485
|
|
|
376
|
-
Returns the currently configured
|
|
486
|
+
Returns the currently configured fee token address (checksummed). `ethers.ZeroAddress` represents native currency. `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.
|
|
487
|
+
|
|
488
|
+
#### Fee-config getters
|
|
489
|
+
|
|
490
|
+
All getters read live from the protocol and return **raw units of the active fee token** (`0n` when a pathway has no configured fee).
|
|
377
491
|
|
|
378
|
-
|
|
492
|
+
Per-operation fees:
|
|
379
493
|
|
|
380
|
-
|
|
494
|
+
- `getAnonymousCreateAccountFee()` - one-time account-creation fee.
|
|
495
|
+
- `getAnonymousMinimumInitialFeeDeposit()` - minimum `initialFeeDeposit` accepted at creation.
|
|
496
|
+
- `getAnonymousUpdateKeysFee()` - auth-key update.
|
|
497
|
+
- `getAnonymousApplyPendingFee()` - apply-pending.
|
|
498
|
+
- `getAnonymousInlineTransferFee()` / `getAnonymousIpfsTransferFee()` - inline / IPFS transfer.
|
|
499
|
+
- `getAnonymousInlineWithdrawFee()` / `getAnonymousIpfsWithdrawFee()` - inline / IPFS withdraw.
|
|
500
|
+
- `getNonAnonymousTransferFee()` - the shared non-anonymous transfer fee.
|
|
381
501
|
|
|
382
502
|
#### `getPrepaidFeeBalance(accountId, token)`
|
|
383
503
|
|
|
384
|
-
Returns the prepaid fee balance
|
|
504
|
+
Returns the prepaid fee balance in raw units of the selected fee token. For native currency reserves, the amount is in wei.
|
|
385
505
|
|
|
386
506
|
#### `depositFees(authWallet, accountId, amount, options?)`
|
|
387
507
|
|
|
388
|
-
Deposits tokens into the prepaid fee reserve for an
|
|
508
|
+
Deposits tokens into the prepaid fee reserve for an unlinkable account. **The user pays gas** for this operation (mirrors `deposit()`). Handles ERC-20 approval against the active fee token automatically.
|
|
389
509
|
|
|
390
510
|
```javascript
|
|
511
|
+
const offchainZKP = true;
|
|
391
512
|
const feeToken = await client.getFeeToken();
|
|
392
|
-
const
|
|
513
|
+
const inlineFee = await client.getAnonymousInlineTransferFee();
|
|
514
|
+
const ipfsFee = await client.getAnonymousIpfsTransferFee();
|
|
515
|
+
const requiredFee = offchainZKP ? ipfsFee : inlineFee;
|
|
393
516
|
const feeBalance = await client.getPrepaidFeeBalance(accountId, feeToken);
|
|
394
517
|
|
|
395
|
-
if (feeBalance <
|
|
518
|
+
if (feeBalance < requiredFee) {
|
|
396
519
|
const result = await client.depositFees(
|
|
397
520
|
wallet,
|
|
398
521
|
accountId,
|
|
@@ -404,11 +527,11 @@ if (feeBalance < feeAmount) {
|
|
|
404
527
|
|
|
405
528
|
#### `withdrawFees(authWallet, accountId, { token, destination, amount }, options?)`
|
|
406
529
|
|
|
407
|
-
|
|
530
|
+
Reclaims a specific amount from the prepaid fee reserve for a given fee token. **User-funded raw-tx** model (like `deposit`/`depositFees`): the anonymous signer authorises the withdrawal via EIP-712 and the same wallet signs and funds the outer transaction, so **the user pays gas and no prepaid fee is charged** — the full amount is returned. Use this to reclaim a specific (possibly historical) fee token.
|
|
408
531
|
|
|
409
532
|
#### `withdrawAllFees(authWallet, accountId, { destination }, options?)`
|
|
410
533
|
|
|
411
|
-
|
|
534
|
+
Drains all prepaid fee balances across all (including historical) fee tokens to `destination`. Same user-funded raw-tx model as `withdrawFees` — the user pays gas, no prepaid fee.
|
|
412
535
|
|
|
413
536
|
#### Request Tracking
|
|
414
537
|
|
|
@@ -475,7 +598,7 @@ const withdrawAmount = ethers.parseUnits("2", tokenDecimals);
|
|
|
475
598
|
await client.withdraw(wallet, tokenAddress, withdrawAmount);
|
|
476
599
|
```
|
|
477
600
|
|
|
478
|
-
###
|
|
601
|
+
### Unlinkable Flow (AnonymousTransferClient)
|
|
479
602
|
|
|
480
603
|
```javascript
|
|
481
604
|
import { AnonymousTransferClient } from "@fairblock/stabletrust";
|
|
@@ -512,12 +635,13 @@ const balance = await client.getBalance(
|
|
|
512
635
|
);
|
|
513
636
|
console.log("Available:", balance.available, "Pending:", balance.pending);
|
|
514
637
|
|
|
515
|
-
// 4. Top up the prepaid fee reserve if needed (user pays gas)
|
|
516
|
-
//
|
|
638
|
+
// 4. Top up the prepaid fee reserve if needed (user pays gas; no fee on the top-up itself)
|
|
639
|
+
// The reserve was seeded at account creation; every relay op (update-keys/transfer/apply/withdraw)
|
|
640
|
+
// draws a fixed fee from it. Here we make sure it can cover the next transfer.
|
|
517
641
|
const feeToken = await client.getFeeToken();
|
|
518
|
-
const
|
|
642
|
+
const inlineFee = await client.getAnonymousInlineTransferFee();
|
|
519
643
|
const feeBalance = await client.getPrepaidFeeBalance(accountId, feeToken);
|
|
520
|
-
if (feeBalance <
|
|
644
|
+
if (feeBalance < inlineFee) {
|
|
521
645
|
const feeResult = await client.depositFees(
|
|
522
646
|
wallet,
|
|
523
647
|
accountId,
|
|
@@ -526,7 +650,7 @@ if (feeBalance < feeAmount) {
|
|
|
526
650
|
await client.waitForRequest(feeResult.request_id);
|
|
527
651
|
}
|
|
528
652
|
|
|
529
|
-
// 5. Transfer to a public address (relay pays gas)
|
|
653
|
+
// 5. Transfer to a public address (relay pays gas; a transfer fee is drawn from the prepaid reserve)
|
|
530
654
|
const transferResult = await client.transferToPublic(wallet, accountId, {
|
|
531
655
|
recipient: "0xRecipientAddress",
|
|
532
656
|
token: tokenAddress,
|
|
@@ -535,7 +659,7 @@ const transferResult = await client.transferToPublic(wallet, accountId, {
|
|
|
535
659
|
});
|
|
536
660
|
await client.waitForRequest(transferResult.request_id);
|
|
537
661
|
|
|
538
|
-
// 6. Withdraw (relay pays gas)
|
|
662
|
+
// 6. Withdraw (relay pays gas; a withdraw fee is drawn from the prepaid reserve)
|
|
539
663
|
const withdrawResult = await client.withdraw(wallet, accountId, {
|
|
540
664
|
destination: wallet.address,
|
|
541
665
|
token: tokenAddress,
|
|
@@ -579,19 +703,19 @@ try {
|
|
|
579
703
|
| "Account finalization timeout" | Account creation is still processing | Wait a few minutes and retry |
|
|
580
704
|
| "Proof generation failed" | Invalid inputs or cryptographic operation error | Verify all parameters and ensure sufficient balance |
|
|
581
705
|
| "Amount too small" | Amount rounds to 0 in contract scale | Use a larger amount (minimum depends on token decimals) |
|
|
582
|
-
| "Insufficient prepaid fee balance" |
|
|
706
|
+
| "Insufficient prepaid fee balance" | Unlinkable account's prepaid fee reserve is below the selected inline/IPFS transfer fee | Call `depositFees(wallet, accountId, amount)` to top up; use `getFeeToken()` and the matching anonymous fee getter |
|
|
583
707
|
| Fairycloak HTTP error | Relay unreachable or request rejected | Check the relay URL, API key, and account authorization |
|
|
584
708
|
|
|
585
709
|
---
|
|
586
710
|
|
|
587
711
|
## Security Considerations
|
|
588
712
|
|
|
589
|
-
When using the
|
|
713
|
+
When using the stabletrust SDK, follow these best practices to ensure the security of your confidential transactions:
|
|
590
714
|
|
|
591
715
|
1. **Private Key Management**
|
|
592
716
|
- Never expose or log private keys or seed phrases
|
|
593
717
|
- Store private keys securely (e.g., hardware wallets, encrypted vaults)
|
|
594
|
-
-
|
|
718
|
+
- Unlinkable account `privateKey` values are sensitive — treat them the same as wallet private keys
|
|
595
719
|
|
|
596
720
|
2. **Signer Security**
|
|
597
721
|
- Use secure signer implementations (e.g., hardware wallets, encrypted key stores)
|
|
@@ -609,7 +733,7 @@ When using the StableTrust SDK, follow these best practices to ensure the securi
|
|
|
609
733
|
|
|
610
734
|
5. **Balance Verification**
|
|
611
735
|
- Check available balance before initiating transfers
|
|
612
|
-
-
|
|
736
|
+
- Unlinkable transfers between unlinkable accounts require `applyPending()` before the recipient can spend
|
|
613
737
|
|
|
614
738
|
6. **Error Handling**
|
|
615
739
|
- Implement comprehensive error handling for all SDK operations
|
|
@@ -232,25 +232,31 @@ async function main() {
|
|
|
232
232
|
console.log("\n── [5] Top up prepaid fees for anon1 ───────────────────");
|
|
233
233
|
|
|
234
234
|
const feeToken = await client.getFeeToken();
|
|
235
|
-
const
|
|
235
|
+
const inlineFee = await client.getAnonymousInlineTransferFee();
|
|
236
236
|
log(" fee token :", feeToken);
|
|
237
|
-
log(" fee
|
|
237
|
+
log(" inline fee :", fmt(inlineFee, TOKEN_DECIMALS), "tokens");
|
|
238
238
|
|
|
239
|
-
if (
|
|
239
|
+
if (inlineFee === 0n) {
|
|
240
240
|
log(" no fee configured — transactions are free, skipping top-up.");
|
|
241
241
|
} else {
|
|
242
242
|
const feeBalance = await client.getPrepaidFeeBalance(anon1Id, feeToken);
|
|
243
243
|
log(" fee balance:", fmt(feeBalance, TOKEN_DECIMALS), "tokens");
|
|
244
244
|
|
|
245
|
-
|
|
245
|
+
const requiredFeeBalance =
|
|
246
|
+
inlineFee > MIN_FEE_BALANCE ? inlineFee : MIN_FEE_BALANCE;
|
|
247
|
+
|
|
248
|
+
if (feeBalance < requiredFeeBalance) {
|
|
246
249
|
const currency = feeToken === ethers.ZeroAddress ? "native" : "ERC-20";
|
|
250
|
+
const deficit = requiredFeeBalance - feeBalance;
|
|
251
|
+
const topUpAmount =
|
|
252
|
+
FEE_DEPOSIT_AMOUNT > deficit ? FEE_DEPOSIT_AMOUNT : deficit;
|
|
247
253
|
log(
|
|
248
|
-
` balance < ${fmt(
|
|
254
|
+
` balance < ${fmt(requiredFeeBalance, TOKEN_DECIMALS)}, depositing ${fmt(topUpAmount, TOKEN_DECIMALS)} ${currency} fee tokens (anon1 pays gas) …`,
|
|
249
255
|
);
|
|
250
256
|
const fd = await client.depositFees(
|
|
251
257
|
anon1Wallet,
|
|
252
258
|
anon1Id,
|
|
253
|
-
|
|
259
|
+
topUpAmount,
|
|
254
260
|
);
|
|
255
261
|
log(" request_id:", fd.request_id, " status:", fd.status);
|
|
256
262
|
if (fd.error) log(" [!] Error:", fd.error);
|
|
@@ -266,7 +272,7 @@ async function main() {
|
|
|
266
272
|
log(" new fee balance:", fmt(newFeeBalance, TOKEN_DECIMALS), "tokens");
|
|
267
273
|
} else {
|
|
268
274
|
log(
|
|
269
|
-
` fee balance sufficient (${fmt(feeBalance, TOKEN_DECIMALS)} ≥ ${fmt(
|
|
275
|
+
` fee balance sufficient (${fmt(feeBalance, TOKEN_DECIMALS)} ≥ ${fmt(requiredFeeBalance, TOKEN_DECIMALS)}), skipping deposit.`,
|
|
270
276
|
);
|
|
271
277
|
}
|
|
272
278
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fairblock/stabletrust",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.11",
|
|
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",
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"url": "https://github.com/Fairblock/stabletrust-sdk"
|
|
29
29
|
},
|
|
30
30
|
"scripts": {
|
|
31
|
-
"test": "
|
|
31
|
+
"test": "node --test test/*.test.mjs",
|
|
32
|
+
"test:live": "node --test test/ipfs-live.mjs",
|
|
33
|
+
"check:abi": "node scripts/check-contract-abi.mjs",
|
|
34
|
+
"check": "npm run check:abi && npm test"
|
|
32
35
|
},
|
|
33
36
|
"keywords": [
|
|
34
37
|
"confidential",
|
|
@@ -41,10 +44,14 @@
|
|
|
41
44
|
"author": "",
|
|
42
45
|
"license": "Apache-2.0",
|
|
43
46
|
"dependencies": {
|
|
47
|
+
"@noble/bls12-381": "^1.4.0",
|
|
48
|
+
"@noble/hashes": "^1.3.2",
|
|
49
|
+
"@stablelib/chacha20poly1305": "^1.0.1",
|
|
50
|
+
"axios": "^1.16.0",
|
|
44
51
|
"dotenv": "^17.2.3",
|
|
45
52
|
"ethers": "^6.16.0"
|
|
46
53
|
},
|
|
47
54
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
55
|
+
"node": ">=18.0.0"
|
|
49
56
|
}
|
|
50
57
|
}
|