@fairblock/stabletrust 2.0.7 → 2.0.10

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/src/constants.js CHANGED
@@ -2,16 +2,42 @@
2
2
  * Contract ABIs and Constants
3
3
  */
4
4
 
5
+ const CREATE_CONFIDENTIAL_ACCOUNT_SIGNATURE = "createConfidentialAccount(bytes)";
6
+ const DEPOSIT_SIGNATURE = "deposit(address,uint256)";
7
+ const GET_ACCOUNT_CORE_SIGNATURE = "getAccountCore(address)";
8
+ const GET_AVAILABLE_SIGNATURE = "getAvailable(address,address)";
9
+ const GET_PENDING_SIGNATURE = "getPending(address,address)";
10
+ const APPLY_PENDING_SIGNATURE = "applyPending()";
11
+
12
+ export const TRANSFER_CONFIDENTIAL_SIGNATURE = "transferConfidential(address,address,bytes,bool)";
13
+ export const WITHDRAW_CONFIDENTIAL_SIGNATURE = "withdraw(address,uint256,bytes,bool)";
14
+ export const FEE_TOKEN_SIGNATURE = "feeToken()";
15
+ export const NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE = "nonAnonymousTransferFee()";
16
+ export const ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE = "anonymousIpfsTransferFee()";
17
+ export const ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE = "anonymousInlineTransferFee()";
18
+
5
19
  export const CONTRACT_ABI = [
6
- "function createConfidentialAccount(bytes elgamalPubkey) external",
7
- "function deposit(address token, uint256 plainAmount) external",
8
- "function getAccountCore(address ownerAddr) external view returns ((bool exists, bool finalized, bool pendingAction, uint256 txId, bytes elgamalPubkey, uint64 pendingCreditCounter))",
9
- "function getAvailable(address ownerAddr, address token) external view returns (bytes c1, bytes c2)",
10
- "function getPending(address ownerAddr, address token) external view returns (bytes c1, bytes c2)",
11
- "function transferConfidential(address recipient, address token, bytes proof) external payable",
12
- "function withdraw(address token, uint256 plainAmount, bytes proof) external",
13
- "function applyPending() external",
14
- "function feeAmount() external view returns (uint256)",
20
+ `function ${CREATE_CONFIDENTIAL_ACCOUNT_SIGNATURE} external`,
21
+ `function ${DEPOSIT_SIGNATURE} external`,
22
+ `function ${GET_ACCOUNT_CORE_SIGNATURE} external view returns ((bool exists, bool finalized, bool pendingAction, uint256 txId, bytes elgamalPubkey, uint64 pendingCreditCounter))`,
23
+ `function ${GET_AVAILABLE_SIGNATURE} external view returns (bytes c1, bytes c2)`,
24
+ `function ${GET_PENDING_SIGNATURE} external view returns (bytes c1, bytes c2)`,
25
+ `function ${TRANSFER_CONFIDENTIAL_SIGNATURE} external payable`,
26
+ `function ${WITHDRAW_CONFIDENTIAL_SIGNATURE} external`,
27
+ `function ${APPLY_PENDING_SIGNATURE} external`,
28
+ `function ${FEE_TOKEN_SIGNATURE} external view returns (address)`,
29
+ `function ${NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE} external view returns (uint256)`,
30
+ `function ${ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE} external view returns (uint256)`,
31
+ `function ${ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE} external view returns (uint256)`,
32
+ // Events used to extract request/tx IDs from receipts for backend persistence
33
+ "event DepositRequested(address indexed ownerAddr, uint256 indexed txId, address indexed token, uint256 plainAmount, bytes availableShare, bytes pendingShare)",
34
+ "event DepositProcessed(address indexed ownerAddr, uint256 indexed txId, bool ok, string errorMsg, address token, bytes newAvailC1, bytes newAvailC2, uint64 pendingCreditCounter, uint64 minimumPendingCreditCounter, bytes availableShare, bytes pendingShare)",
35
+ "event TransferRequested(address indexed sender, address indexed recipient, uint256 indexed txId, address token, bytes proof, uint256 feePaid, bool useOffchainVerify, bytes senderPubkey, bytes recipientPubkey, bytes senderCurrC1, bytes senderCurrC2)",
36
+ "event TransferProcessed(address indexed sender, address indexed recipient, uint256 indexed txId, bool ok, string errorMsg, address token, bytes senderNewAvailC1, bytes senderNewAvailC2, bytes recipientNewPendingC1, bytes recipientNewPendingC2, uint64 senderPendingCreditCounter, bytes senderAvailableShare, uint64 recipientPendingCreditCounter, uint64 recipientMinimumPendingCreditCounter, bytes recipientPendingShare)",
37
+ "event ApplyPendingRequested(address indexed ownerAddr, uint256 indexed txId)",
38
+ "event ApplyPendingProcessed(address indexed ownerAddr, uint256 indexed txId, bool ok, string errorMsg, address[] tokens, bytes[] newAvailC1, bytes[] newAvailC2, uint64 pendingCreditCounter, uint64 minimumPendingCreditCounter, bytes availableShare)",
39
+ "event WithdrawRequested(address indexed ownerAddr, uint256 indexed txId, address token, uint256 plainAmount, bytes proof, bool useOffchainVerify, bytes userPubkey, bytes userCurrC1, bytes userCurrC2)",
40
+ "event WithdrawProcessed(address indexed ownerAddr, uint256 indexed txId, bool ok, string errorMsg, address token, uint256 plainAmount, bytes newAvailC1, bytes newAvailC2, uint64 pendingCreditCounter, uint64 minimumPendingCreditCounter, bytes availableShare)",
15
41
  ];
16
42
 
17
43
  export const ERC20_ABI = [
@@ -19,6 +45,7 @@ export const ERC20_ABI = [
19
45
  "function allowance(address owner, address spender) external view returns (uint256)",
20
46
  "function balanceOf(address account) external view returns (uint256)",
21
47
  "function decimals() external view returns (uint8)",
48
+ "function symbol() external view returns (string)",
22
49
  ];
23
50
 
24
51
  export const TEMPO_FEE_TOKEN_ADDRESS =
package/src/ibe.js ADDED
@@ -0,0 +1,59 @@
1
+ import { timelockEncrypt } from "./vendor/ts-ibe/index.js";
2
+
3
+ const IBE_PUBKEY_URL =
4
+ "https://anon-testnet-api.fairblock.network/fairyring/keyshare/pubkey";
5
+
6
+ /** @type {{ active: string, queued: string | null } | null} */
7
+ let cachedIbePublicKeys = null;
8
+
9
+ function extractPublicKey(pubkeyObj) {
10
+ return pubkeyObj?.public_key || pubkeyObj?.pubkey || null;
11
+ }
12
+
13
+ async function getIbePublicKeys() {
14
+ if (cachedIbePublicKeys) return cachedIbePublicKeys;
15
+
16
+ const response = await fetch(IBE_PUBKEY_URL, {
17
+ headers: { Accept: "application/json" },
18
+ });
19
+
20
+ if (!response.ok) {
21
+ throw new Error(
22
+ `Failed to fetch IBE public key: ${response.status} ${response.statusText}`,
23
+ );
24
+ }
25
+
26
+ const data = await response.json();
27
+ const active = extractPublicKey(data?.active_pubkey);
28
+ const queued = extractPublicKey(data?.queued_pubkey);
29
+
30
+ if (!active) {
31
+ throw new Error("Active IBE public key not found in chain API response");
32
+ }
33
+
34
+ cachedIbePublicKeys = { active, queued: queued || null };
35
+ return cachedIbePublicKeys;
36
+ }
37
+
38
+ /**
39
+ * Encrypts a base64-encoded randomness (PedersenOpening) using IBE.
40
+ * Always encrypts under the active pubkey; also encrypts under the queued
41
+ * pubkey when the chain reports one (backup for key rotation).
42
+ *
43
+ * @param {string} userAddress - Wallet address used as the IBE identity
44
+ * @param {string} randomnessBase64 - Base64-encoded randomness
45
+ * @returns {Promise<{ encrypted: string, queuedEncrypted: string | null }>}
46
+ * Hex-encoded IBE ciphertexts
47
+ */
48
+ export async function encryptRandomness(userAddress, randomnessBase64) {
49
+ const { active, queued } = await getIbePublicKeys();
50
+ const id = userAddress.toLowerCase();
51
+ const randomnessBuffer = Buffer.from(randomnessBase64, "base64");
52
+
53
+ const [encrypted, queuedEncrypted] = await Promise.all([
54
+ timelockEncrypt(id, active, randomnessBuffer),
55
+ queued ? timelockEncrypt(id, queued, randomnessBuffer) : Promise.resolve(null),
56
+ ]);
57
+
58
+ return { encrypted, queuedEncrypted };
59
+ }
package/src/index.d.ts CHANGED
@@ -14,6 +14,16 @@ declare module "@fairblock/stabletrust" {
14
14
  diamondAddress?: string;
15
15
  /** Optional API key for Fairycloak */
16
16
  apiKey?: string;
17
+ /** StableTrust IPFS upload endpoint, used when a transfer sets `offchainZKP: true`.
18
+ * Falls back to `STABLETRUST_IPFS_UPLOAD_URL` in Node.js when omitted.
19
+ */
20
+ ipfsUploadUrl?: string;
21
+ /** Optional bearer token for the StableTrust IPFS upload endpoint.
22
+ * Falls back to `STABLETRUST_IPFS_API_KEY` in Node.js when omitted.
23
+ */
24
+ ipfsApiKey?: string;
25
+ /** Optional read gateway base URL, e.g. `https://ipfs.example.com`. */
26
+ ipfsGatewayUrl?: string;
17
27
  }
18
28
 
19
29
  export interface AnonymousAccountInfo {
@@ -81,6 +91,11 @@ declare module "@fairblock/stabletrust" {
81
91
  amount?: bigint | string | number;
82
92
  /** Recipient ElGamal public key (base64). Auto-resolved from the chain if omitted. */
83
93
  destinationPublicKey?: string;
94
+ /**
95
+ * When true, the ZK proof is uploaded to StableTrust IPFS and only its CID is put on-chain,
96
+ * instead of sending the full proof inline. Requires `ipfsUploadUrl`/`ipfsApiKey` config or the StableTrust IPFS environment variables.
97
+ */
98
+ offchainZKP?: boolean;
84
99
  }
85
100
 
86
101
  export interface TransferToAnonymousParams {
@@ -94,6 +109,11 @@ declare module "@fairblock/stabletrust" {
94
109
  amount?: bigint | string | number;
95
110
  /** Recipient ElGamal public key (base64). Auto-resolved from the chain if omitted. */
96
111
  destinationPublicKey?: string;
112
+ /**
113
+ * When true, the ZK proof is uploaded to StableTrust IPFS and only its CID is put on-chain,
114
+ * instead of sending the full proof inline. Requires `ipfsUploadUrl`/`ipfsApiKey` config or the StableTrust IPFS environment variables.
115
+ */
116
+ offchainZKP?: boolean;
97
117
  }
98
118
 
99
119
  export interface WithdrawParams {
@@ -105,6 +125,11 @@ declare module "@fairblock/stabletrust" {
105
125
  proof?: string;
106
126
  /** ElGamal private key (base64) for auto-proof generation. Contract scale is derived automatically. */
107
127
  elGamalPrivateKey?: string;
128
+ /**
129
+ * When true, the ZK proof is uploaded to StableTrust IPFS and only its CID is put on-chain,
130
+ * instead of sending the full proof inline. Requires `ipfsUploadUrl`/`ipfsApiKey` config or `STABLETRUST_IPFS_UPLOAD_URL` / `STABLETRUST_IPFS_API_KEY`. Defaults to false.
131
+ */
132
+ offchainZKP?: boolean;
108
133
  }
109
134
 
110
135
  export interface AnonymousAccountBalance {
@@ -263,14 +288,17 @@ declare module "@fairblock/stabletrust" {
263
288
  */
264
289
  getFeeToken(): Promise<string>;
265
290
 
266
- /**
267
- * Get the protocol fee amount in raw ERC-20 units of the fee token.
268
- * This is the amount deducted per anonymous transfer. Returns 0n if no fee is set.
269
- */
270
- getFeeAmount(): Promise<bigint>;
291
+ /** Get the fixed fee used by all non-anonymous confidential transfers. */
292
+ getNonAnonymousTransferFee(): Promise<bigint>;
293
+
294
+ /** Get the fixed fee used by anonymous transfers whose proof is stored on IPFS. */
295
+ getAnonymousIpfsTransferFee(): Promise<bigint>;
296
+
297
+ /** Get the fixed fee used by anonymous transfers whose proof is submitted inline. */
298
+ getAnonymousInlineTransferFee(): Promise<bigint>;
271
299
 
272
300
  /**
273
- * Get the prepaid fee balance for an anonymous account and token (raw ERC-20 units).
301
+ * Get the prepaid fee balance for an anonymous account and token (raw fee-token units).
274
302
  * Use `getFeeToken()` to get the active fee token address.
275
303
  */
276
304
  getPrepaidFeeBalance(accountId: string, token: string): Promise<bigint>;
@@ -283,9 +311,10 @@ declare module "@fairblock/stabletrust" {
283
311
  * Handles ERC-20 approval automatically.
284
312
  *
285
313
  * The fee token is read from the contract — use `getFeeToken()` to see it.
286
- * Use `getFeeAmount()` to see how much is deducted per transfer.
314
+ * Use `getAnonymousInlineTransferFee()` or `getAnonymousIpfsTransferFee()`
315
+ * to see how much is deducted for the selected proof path.
287
316
  *
288
- * @param amount Amount in raw ERC-20 units of the active fee token
317
+ * @param amount Amount in raw units of the active fee token (wei for native currency)
289
318
  */
290
319
  depositFees(
291
320
  authWallet: ethers.Wallet,
@@ -458,7 +487,7 @@ declare module "@fairblock/stabletrust" {
458
487
  * Balance information
459
488
  */
460
489
  export interface Balance {
461
- amount: number;
490
+ amount: bigint;
462
491
  ciphertext: string | null;
463
492
  }
464
493
 
@@ -466,7 +495,7 @@ declare module "@fairblock/stabletrust" {
466
495
  * Confidential balance summary
467
496
  */
468
497
  export interface ConfidentialBalance {
469
- amount: number;
498
+ amount: bigint;
470
499
  available: Balance;
471
500
  pending: Balance;
472
501
  }
@@ -490,6 +519,14 @@ declare module "@fairblock/stabletrust" {
490
519
  * Transfer options
491
520
  */
492
521
  export interface TransferOptions {
522
+ /**
523
+ * When true, upload the ABI-encoded transfer proof to StableTrust IPFS and
524
+ * submit only the bare CID bytes on-chain with offchainZKP=true.
525
+ * Defaults to true on Tempo (chainId 42431) for backwards compatibility,
526
+ * and false on other chains.
527
+ */
528
+ offchainZKP?: boolean;
529
+ /** @deprecated Use offchainZKP instead. */
493
530
  useOffchainVerify?: boolean;
494
531
  waitForFinalization?: boolean;
495
532
  }
@@ -498,10 +535,27 @@ declare module "@fairblock/stabletrust" {
498
535
  * Withdraw options
499
536
  */
500
537
  export interface WithdrawOptions {
538
+ /**
539
+ * When true, upload the ABI-encoded withdraw proof to StableTrust IPFS and
540
+ * submit only the bare CID bytes on-chain with offchainZKP=true.
541
+ * Defaults to true on Tempo (chainId 42431) for backwards compatibility,
542
+ * and false on other chains.
543
+ */
544
+ offchainZKP?: boolean;
545
+ /** @deprecated Use offchainZKP instead. */
501
546
  useOffchainVerify?: boolean;
502
547
  waitForFinalization?: boolean;
503
548
  }
504
549
 
550
+ export interface StableTrustIpfsOptions {
551
+ /** StableTrust IPFS upload endpoint. Falls back to STABLETRUST_IPFS_UPLOAD_URL in Node.js. */
552
+ ipfsUploadUrl?: string;
553
+ /** Optional bearer token for the StableTrust IPFS upload endpoint. Falls back to STABLETRUST_IPFS_API_KEY in Node.js. */
554
+ ipfsApiKey?: string;
555
+ /** StableTrust backend API base URL for persisting request rows. Defaults to the production API. */
556
+ apiBaseUrl?: string;
557
+ }
558
+
505
559
  /**
506
560
  * Main SDK client class
507
561
  * WASM auto-initializes on first use - no manual initialization required!
@@ -512,7 +566,7 @@ declare module "@fairblock/stabletrust" {
512
566
  * @param rpcUrl RPC endpoint URL
513
567
  * @param chainId Chain ID (uses default Stabletrust contract for known networks)
514
568
  */
515
- constructor(rpcUrl: string, chainId: number);
569
+ constructor(rpcUrl: string, chainId: number, options?: StableTrustIpfsOptions);
516
570
 
517
571
  /**
518
572
  * Create a new ConfidentialTransferClient
@@ -520,7 +574,7 @@ declare module "@fairblock/stabletrust" {
520
574
  * @param contractAddress Confidential transfer contract address
521
575
  * @param chainId Chain ID
522
576
  */
523
- constructor(rpcUrl: string, contractAddress: string, chainId: number);
577
+ constructor(rpcUrl: string, contractAddress: string, chainId: number, options?: StableTrustIpfsOptions);
524
578
 
525
579
  /**
526
580
  * Get account information from the contract
@@ -574,7 +628,7 @@ declare module "@fairblock/stabletrust" {
574
628
  senderWallet: ethers.Wallet | ethers.Signer,
575
629
  recipientAddress: string,
576
630
  tokenAddress: string,
577
- amount: number,
631
+ amount: bigint | string | number,
578
632
  options?: TransferOptions,
579
633
  ): Promise<ethers.ContractTransactionReceipt>;
580
634
 
@@ -588,14 +642,21 @@ declare module "@fairblock/stabletrust" {
588
642
  withdraw(
589
643
  wallet: ethers.Wallet | ethers.Signer,
590
644
  tokenAddress: string,
591
- amount: number,
645
+ amount: bigint | string | number,
592
646
  options?: WithdrawOptions,
593
647
  ): Promise<ethers.ContractTransactionReceipt>;
594
648
 
595
- /**
596
- * Get the current fee amount for confidential transfers
597
- */
598
- getFeeAmount(): Promise<bigint>;
649
+ /** Get the currently configured fee token. address(0) means native currency. */
650
+ getFeeToken(): Promise<string>;
651
+
652
+ /** Get the fixed fee used by all non-anonymous confidential transfers. */
653
+ getNonAnonymousTransferFee(): Promise<bigint>;
654
+
655
+ /** Get the fixed fee used by anonymous transfers whose proof is stored on IPFS. */
656
+ getAnonymousIpfsTransferFee(): Promise<bigint>;
657
+
658
+ /** Get the fixed fee used by anonymous transfers whose proof is submitted inline. */
659
+ getAnonymousInlineTransferFee(): Promise<bigint>;
599
660
 
600
661
  /**
601
662
  * Get the public ERC20 balance for an address (for comparison with confidential balance)
@@ -674,6 +735,12 @@ declare module "@fairblock/stabletrust" {
674
735
  /**
675
736
  * Contract ABI
676
737
  */
738
+ export const TRANSFER_CONFIDENTIAL_SIGNATURE: string;
739
+ export const WITHDRAW_CONFIDENTIAL_SIGNATURE: string;
740
+ export const FEE_TOKEN_SIGNATURE: string;
741
+ export const NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE: string;
742
+ export const ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE: string;
743
+ export const ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE: string;
677
744
  export const CONTRACT_ABI: any[];
678
745
 
679
746
  /**
package/src/index.js CHANGED
@@ -10,6 +10,12 @@ export {
10
10
  export {
11
11
  CONTRACT_ABI,
12
12
  ERC20_ABI,
13
+ TRANSFER_CONFIDENTIAL_SIGNATURE,
14
+ WITHDRAW_CONFIDENTIAL_SIGNATURE,
15
+ FEE_TOKEN_SIGNATURE,
16
+ NON_ANONYMOUS_TRANSFER_FEE_SIGNATURE,
17
+ ANONYMOUS_IPFS_TRANSFER_FEE_SIGNATURE,
18
+ ANONYMOUS_INLINE_TRANSFER_FEE_SIGNATURE,
13
19
  STABLETRUST_CONTRACTS_BY_CHAIN_ID,
14
20
  getStabletrustContractAddress,
15
21
  } from "./constants.js";
package/src/utils.js CHANGED
@@ -1,8 +1,4 @@
1
1
  import { ethers } from "ethers";
2
- // Pinata uploads API endpoint
3
- // Docs: https://docs.pinata.cloud
4
- const PINATA_UPLOAD_URL = "https://uploads.pinata.cloud/v3/files";
5
- const PINATA_JWT = process.env.PINATA_JWT;
6
2
 
7
3
  // Anonymous account ID constraints (must match the Fairycloak relay and on-chain contract rules)
8
4
  export const MAX_ANONYMOUS_ACCOUNT_ID_LENGTH = 20;
@@ -38,37 +34,96 @@ export function validateAnonymousAccountId(accountId, fieldName = "accountId") {
38
34
  }
39
35
  }
40
36
  /**
41
- * Encodes the ZK-Proof data for a transfer into a format the Solidity contract expects.
37
+ * Convert a proof component returned by the WASM/proof generator into the exact
38
+ * bytes carried inside the contract/Fairyport ABI proof tuple. The current proof
39
+ * generator returns base64 text for each proof component; these ASCII bytes are
40
+ * what the Go relayer and CosmWasm contract expect to receive.
41
+ *
42
+ * Hex strings are also accepted for callers that provide raw proof component
43
+ * bytes manually.
44
+ *
45
+ * @param {string|Uint8Array|ArrayBuffer|Array<number>} value
46
+ * @returns {Uint8Array}
47
+ * @private
48
+ */
49
+ function _proofPartToBytes(value) {
50
+ if (value instanceof Uint8Array) return value;
51
+ if (value instanceof ArrayBuffer) return new Uint8Array(value);
52
+ if (Array.isArray(value)) return new Uint8Array(value);
53
+ if (typeof value !== "string" || value.length === 0) {
54
+ throw new Error("invalid proof component: expected non-empty string or bytes");
55
+ }
56
+ if (value.startsWith("0x")) return ethers.getBytes(value);
57
+ return new TextEncoder().encode(value);
58
+ }
59
+
60
+ /**
61
+ * Encodes the ZK-Proof data for a transfer into the format expected by the
62
+ * latest EVM/Fairyport/CosmWasm flow: abi.encode(bytes equalityProof,
63
+ * bytes ciphertextValidityProof, bytes rangeProof).
42
64
  *
43
65
  * @param {Object} proofData - The proof data object
44
- * @returns {string} Encoded proof bytes
66
+ * @returns {string} ABI-encoded proof bytes
45
67
  */
46
68
  export function encodeTransferProof(proofData) {
47
69
  const abiCoder = ethers.AbiCoder.defaultAbiCoder();
48
70
  return abiCoder.encode(
49
- ["string", "string", "string"],
71
+ ["bytes", "bytes", "bytes"],
50
72
  [
51
- proofData.equality_proof,
52
- proofData.ciphertext_validity_proof,
53
- proofData.range_proof,
73
+ _proofPartToBytes(proofData.equality_proof),
74
+ _proofPartToBytes(proofData.ciphertext_validity_proof),
75
+ _proofPartToBytes(proofData.range_proof),
54
76
  ],
55
77
  );
56
78
  }
57
79
 
58
80
  /**
59
- * Encodes the ZK-Proof data for a withdrawal.
81
+ * Encodes the ZK-Proof data for a withdrawal into the format expected by the
82
+ * latest EVM/Fairyport/CosmWasm flow: abi.encode(bytes equalityProof,
83
+ * bytes rangeProof).
60
84
  *
61
85
  * @param {Object} proofData - The proof data object
62
- * @returns {string} Encoded proof bytes
86
+ * @returns {string} ABI-encoded proof bytes
63
87
  */
64
88
  export function encodeWithdrawProof(proofData) {
65
89
  const abiCoder = ethers.AbiCoder.defaultAbiCoder();
66
90
  return abiCoder.encode(
67
- ["string", "string"],
68
- [proofData.equality_proof, proofData.range_proof],
91
+ ["bytes", "bytes"],
92
+ [_proofPartToBytes(proofData.equality_proof), _proofPartToBytes(proofData.range_proof)],
69
93
  );
70
94
  }
71
95
 
96
+ /**
97
+ * Convert a raw ERC-20 token amount to the contract's 2-decimal scale
98
+ * (`rawAmount * 100 / 10^decimals`).
99
+ *
100
+ * Throws when the amount rounds to zero and warns when a sub-cent remainder
101
+ * is dropped, so callers never silently no-op or lose precision.
102
+ *
103
+ * @param {bigint|string|number} rawAmount - Amount in raw ERC-20 units
104
+ * @param {bigint|number} tokenDecimals - Token decimals
105
+ * @returns {bigint} Amount in contract scale
106
+ * @throws {Error} If the amount rounds to 0 in contract scale
107
+ */
108
+ export function toContractScale(rawAmount, tokenDecimals) {
109
+ const raw = BigInt(rawAmount);
110
+ const divisor = 10n ** BigInt(tokenDecimals);
111
+ const scaled = (raw * 100n) / divisor;
112
+
113
+ if (scaled <= 0n) {
114
+ throw new Error(
115
+ `Amount too small: ${raw} raw units rounds to 0 in contract scale`,
116
+ );
117
+ }
118
+ if ((raw * 100n) % divisor !== 0n) {
119
+ console.warn(
120
+ `Amount ${raw} is not fully representable in contract scale (2 decimals); the sub-cent remainder will be dropped.`,
121
+ );
122
+ }
123
+
124
+ return scaled;
125
+ }
126
+
72
127
  /**
73
128
  * Delays execution for a specified time
74
129
  *
@@ -121,76 +176,128 @@ export async function waitForCondition(
121
176
  }
122
177
 
123
178
  /**
124
- * Upload a JSON-serializable object to IPFS and return its CID (as a string).
125
- * The object will be stored as a UTF-8 JSON blob.
179
+ * Browser-safe environment lookup.
126
180
  *
127
- * @param {any} data - JSON-serializable data to store.
128
- * @returns {Promise<string>} The CID string.
181
+ * @param {string} name - Environment variable name.
182
+ * @returns {string|undefined}
183
+ * @private
129
184
  */
130
- export async function uploadJsonToIpfs(data) {
131
- if (!PINATA_JWT) {
132
- throw new Error("PINATA_JWT is not set");
185
+ function _env(name) {
186
+ if (typeof process !== "undefined" && process?.env?.[name]) {
187
+ return process.env[name];
133
188
  }
189
+ return undefined;
190
+ }
134
191
 
135
- const json = JSON.stringify(data);
136
- const blob = new Blob([json], { type: "application/json" });
137
-
138
- const form = new FormData();
139
- form.append("file", blob, "proof.json");
140
- form.append("network", "public");
141
- form.append("name", "zk-proof");
142
-
143
- const res = await fetch(PINATA_UPLOAD_URL, {
144
- method: "POST",
145
- headers: {
146
- Authorization: `Bearer ${PINATA_JWT}`,
147
- },
148
- body: form,
149
- });
192
+ /**
193
+ * Resolve StableTrust IPFS upload options from explicit options or environment variables.
194
+ *
195
+ * @param {Object} [options]
196
+ * @param {string} [options.uploadUrl] - StableTrust IPFS upload endpoint.
197
+ * @param {string} [options.ipfsUploadUrl] - Alias for uploadUrl.
198
+ * @param {string} [options.apiKey] - Bearer token for upload endpoint.
199
+ * @param {string} [options.ipfsApiKey] - Alias for apiKey.
200
+ * @returns {{uploadUrl:string, apiKey:string|null}}
201
+ * @private
202
+ */
203
+ function _resolveIpfsUploadOptions(options = {}) {
204
+ const uploadUrl =
205
+ options.uploadUrl ||
206
+ options.ipfsUploadUrl ||
207
+ _env("STABLETRUST_IPFS_UPLOAD_URL") ||
208
+ _env("IPFS_UPLOAD_URL");
150
209
 
151
- if (!res.ok) {
152
- const text = await res.text();
153
- throw new Error(`Pinata upload failed: ${text}`);
210
+ if (!uploadUrl) {
211
+ throw new Error(
212
+ "StableTrust IPFS upload URL is not set - pass `ipfsUploadUrl` in the client config or set STABLETRUST_IPFS_UPLOAD_URL",
213
+ );
154
214
  }
155
215
 
156
- const jsonRes = await res.json();
157
- return jsonRes?.data?.cid?.toString();
216
+ return {
217
+ uploadUrl: String(uploadUrl).trim(),
218
+ apiKey:
219
+ options.apiKey ||
220
+ options.ipfsApiKey ||
221
+ _env("STABLETRUST_IPFS_API_KEY") ||
222
+ _env("IPFS_UPLOAD_API_KEY") ||
223
+ null,
224
+ };
158
225
  }
159
226
 
160
227
  /**
161
- * Upload raw bytes to IPFS (Pinata) and return the CID.
228
+ * Upload a JSON-serializable object to the configured StableTrust IPFS upload endpoint
229
+ * and return its CID. The object is stored as UTF-8 JSON bytes.
162
230
  *
163
- * @param {Uint8Array|ArrayBuffer} bytes - Raw proof bytes.
164
- * @param {string} [name='proof.bin'] - Optional file name metadata.
165
- * @returns {Promise<string>} The CID string.
231
+ * @param {any} data - JSON-serializable data to store.
232
+ * @param {Object} [options]
233
+ * @param {string} [options.uploadUrl] - StableTrust IPFS upload endpoint.
234
+ * @param {string} [options.apiKey] - Bearer token for upload endpoint.
235
+ * @returns {Promise<string>} Bare CID string.
166
236
  */
167
- export async function uploadBytesToIpfs(bytes, name = "proof.bin") {
168
- if (!PINATA_JWT) {
169
- throw new Error("PINATA_JWT is not set");
170
- }
237
+ export async function uploadJsonToIpfs(data, options = {}) {
238
+ const json = JSON.stringify(data);
239
+ return uploadBytesToIpfs(new TextEncoder().encode(json), "proof.json", options);
240
+ }
171
241
 
242
+ /**
243
+ * Upload raw proof bytes to the configured StableTrust IPFS upload endpoint and return the bare CID.
244
+ *
245
+ * Expected upload endpoint contract:
246
+ * - POST raw bytes to `uploadUrl`
247
+ * - `Content-Type: application/octet-stream`
248
+ * - optional `Authorization: Bearer <apiKey>`
249
+ * - response JSON contains `{ "cid": "..." }` (also accepts `Hash`/`Cid` for compatibility)
250
+ *
251
+ * @param {Uint8Array|ArrayBuffer|Array<number>} bytes - Raw proof bytes.
252
+ * @param {string} [name='proof.bin'] - Optional proof name, sent as `X-StableTrust-Filename` metadata.
253
+ * @param {Object} [options]
254
+ * @param {string} [options.uploadUrl] - StableTrust IPFS upload endpoint.
255
+ * @param {string} [options.apiKey] - Bearer token for upload endpoint.
256
+ * @returns {Promise<string>} Bare CID string.
257
+ */
258
+ export async function uploadBytesToIpfs(bytes, name = "proof.bin", options = {}) {
259
+ const { uploadUrl, apiKey } = _resolveIpfsUploadOptions(options);
172
260
  const uint8 = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
173
261
 
174
- const blob = new Blob([uint8], { type: "application/octet-stream" });
262
+ if (uint8.length === 0) {
263
+ throw new Error("cannot upload an empty proof to IPFS");
264
+ }
175
265
 
176
- const form = new FormData();
177
- form.append("file", blob, name);
178
- form.append("network", "public");
179
- form.append("name", name);
266
+ const headers = {
267
+ "Content-Type": "application/octet-stream",
268
+ };
269
+ if (name) {
270
+ headers["X-StableTrust-Filename"] = String(name);
271
+ }
272
+ if (apiKey) {
273
+ headers.Authorization = `Bearer ${apiKey}`;
274
+ }
180
275
 
181
- const res = await fetch(PINATA_UPLOAD_URL, {
276
+ const res = await fetch(uploadUrl, {
182
277
  method: "POST",
183
- headers: {
184
- Authorization: `Bearer ${PINATA_JWT}`,
185
- },
186
- body: form,
278
+ headers,
279
+ body: uint8,
187
280
  });
188
281
 
189
282
  if (!res.ok) {
190
283
  const text = await res.text();
191
- throw new Error(`Pinata upload failed: ${text}`);
284
+ throw new Error(`StableTrust IPFS upload failed: ${text}`);
285
+ }
286
+
287
+ const contentType = res.headers.get("content-type") || "";
288
+ let cid;
289
+ if (contentType.includes("application/json")) {
290
+ const jsonRes = await res.json();
291
+ cid = jsonRes?.cid ?? jsonRes?.CID ?? jsonRes?.Cid ?? jsonRes?.Hash ?? jsonRes?.hash;
292
+ } else {
293
+ cid = await res.text();
192
294
  }
193
295
 
194
- const jsonRes = await res.json();
195
- return jsonRes?.data?.cid?.toString();
296
+ cid = String(cid || "").trim().replace(/^ipfs:\/\//i, "");
297
+ if (!cid) {
298
+ throw new Error("StableTrust IPFS upload returned no CID");
299
+ }
300
+
301
+ return cid;
196
302
  }
303
+