@fairblock/stabletrust 1.0.0

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.
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Contract ABIs and Constants
3
+ */
4
+
5
+ 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 hasPendingAction, uint256 lastUpdate, bytes pubkey, bytes availableC1, bytes availableC2, uint64 nonce, uint64 lastProcessedNonce))",
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, bool useOffchainVerify) external payable",
12
+ "function withdraw(address token, uint256 plainAmount, bytes proof, bool useOffchainVerify) external",
13
+ "function applyPending() external",
14
+ "function feeAmount() external view returns (uint256)",
15
+ ];
16
+
17
+ export const ERC20_ABI = [
18
+ "function approve(address spender, uint256 amount) external returns (bool)",
19
+ "function allowance(address owner, address spender) external view returns (uint256)",
20
+ "function balanceOf(address account) external view returns (uint256)",
21
+ ];
22
+
23
+ export const DEFAULT_CONFIG = {
24
+ CHAIN_ID: 421614,
25
+ EXPLORER_URL: "https://sepolia.arbiscan.io/tx/",
26
+ RPC_URL: "https://sepolia-rollup.arbitrum.io/rpc",
27
+ CONTRACT_ADDRESS: "0x30bAc8a17DCACbA7f70F305f4ad908C9fd6d3E2E",
28
+ TOKEN_ADDRESS: "0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d",
29
+ };
30
+
31
+ export const TEMPO_FEE_TOKEN_ADDRESS =
32
+ "0x20c0000000000000000000000000000000000000";
package/src/crypto.js ADDED
@@ -0,0 +1,90 @@
1
+ import { ethers } from "ethers";
2
+
3
+ /**
4
+ * Derives ElGamal encryption keys deterministically using the user's wallet signature.
5
+ * This ensures that the user's privacy keys stay tied to their Ethereum account.
6
+ *
7
+ * @param {ethers.Wallet} wallet - The wallet to derive keys for
8
+ * @param {Object} config - Configuration object with chainId and contractAddress
9
+ * @param {Function} generateKeypair - The WASM function for key generation
10
+ * @returns {Promise<{publicKey: string, privateKey: string}>}
11
+ */
12
+ export async function deriveKeys(wallet, config, generateKeypair) {
13
+ const domain = {
14
+ name: "Fairblock",
15
+ version: "1",
16
+ chainId: config.chainId,
17
+ verifyingContract: config.contractAddress,
18
+ };
19
+
20
+ const types = {
21
+ DeriveElGamalKey: [
22
+ { name: "purpose", type: "string" },
23
+ { name: "user", type: "address" },
24
+ { name: "context", type: "bytes32" },
25
+ ],
26
+ };
27
+
28
+ const contextHash = ethers.keccak256(
29
+ ethers.solidityPacked(
30
+ ["uint256", "address", "address", "string"],
31
+ [config.chainId, config.contractAddress, ethers.ZeroAddress, "main"],
32
+ ),
33
+ );
34
+
35
+ const message = {
36
+ purpose: "homomorphic-key-derive-v1",
37
+ user: wallet.address,
38
+ context: contextHash,
39
+ };
40
+
41
+ const signature = await wallet.signTypedData(domain, types, message);
42
+ const domainContext = JSON.stringify({
43
+ chainId: config.chainId.toString(),
44
+ verifyingContract: config.contractAddress,
45
+ user: wallet.address,
46
+ purpose: "homomorphic-key-derive-v1",
47
+ version: "1",
48
+ });
49
+
50
+ const keypair = JSON.parse(
51
+ generateKeypair(signature.slice(2), domainContext),
52
+ );
53
+
54
+ return {
55
+ publicKey: keypair.public_key,
56
+ privateKey: keypair.private_key,
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Decrypts a ciphertext using the private key
62
+ *
63
+ * @param {string} ciphertext - Base64 encoded ciphertext
64
+ * @param {string} privateKey - The private key for decryption
65
+ * @param {Function} decryptFn - The WASM decrypt function
66
+ * @returns {number} The decrypted amount
67
+ */
68
+ export function decryptCiphertext(ciphertext, privateKey, decryptFn) {
69
+ try {
70
+ const plainStr = decryptFn(ciphertext, privateKey);
71
+ const result = JSON.parse(plainStr);
72
+ return result.decrypted_amount || 0;
73
+ } catch (e) {
74
+ throw new Error("Decryption failed: " + e.message);
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Combines two elliptic curve points into a single ciphertext
80
+ *
81
+ * @param {string} c1 - First component (hex string)
82
+ * @param {string} c2 - Second component (hex string)
83
+ * @returns {string} Base64 encoded combined ciphertext
84
+ */
85
+ export function combineCiphertext(c1, c2) {
86
+ const combined = new Uint8Array(64);
87
+ combined.set(ethers.getBytes(c1), 0);
88
+ combined.set(ethers.getBytes(c2), 32);
89
+ return Buffer.from(combined).toString("base64");
90
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,275 @@
1
+ declare module "@fairblock/stabletrust" {
2
+ import { ethers } from "ethers";
3
+
4
+ /**
5
+ * SDK configuration
6
+ */
7
+ export interface SdkConfig {
8
+ rpcUrl: string;
9
+ contractAddress: string;
10
+ chainId: number;
11
+ }
12
+
13
+ /**
14
+ * Encryption keys
15
+ */
16
+ export interface Keys {
17
+ publicKey: string;
18
+ privateKey: string;
19
+ }
20
+
21
+ /**
22
+ * Balance information
23
+ */
24
+ export interface Balance {
25
+ amount: number;
26
+ ciphertext: string | null;
27
+ }
28
+
29
+ /**
30
+ * Account options
31
+ */
32
+ export interface AccountOptions {
33
+ waitForFinalization?: boolean;
34
+ maxAttempts?: number;
35
+ }
36
+
37
+ /**
38
+ * Deposit options
39
+ */
40
+ export interface DepositOptions {
41
+ waitForFinalization?: boolean;
42
+ }
43
+
44
+ /**
45
+ * Transfer options
46
+ */
47
+ export interface TransferOptions {
48
+ useOffchainVerify?: boolean;
49
+ waitForFinalization?: boolean;
50
+ }
51
+
52
+ /**
53
+ * Apply pending options
54
+ */
55
+ export interface ApplyPendingOptions {
56
+ waitForFinalization?: boolean;
57
+ }
58
+
59
+ /**
60
+ * Withdraw options
61
+ */
62
+ export interface WithdrawOptions {
63
+ useOffchainVerify?: boolean;
64
+ waitForFinalization?: boolean;
65
+ }
66
+
67
+ /**
68
+ * Balance options
69
+ */
70
+ export interface BalanceOptions {
71
+ type?: "available" | "pending";
72
+ }
73
+
74
+ /**
75
+ * Wait for pending balance options
76
+ */
77
+ export interface WaitForPendingOptions {
78
+ maxAttempts?: number;
79
+ intervalMs?: number;
80
+ }
81
+
82
+ /**
83
+ * Main SDK client class
84
+ * WASM auto-initializes on first use - no manual initialization required!
85
+ */
86
+ export class ConfidentialTransferClient {
87
+ /**
88
+ * Create a new ConfidentialTransferClient
89
+ * @param rpcUrl RPC endpoint URL
90
+ * @param contractAddress Confidential transfer contract address
91
+ * @param chainId Chain ID
92
+ */
93
+ constructor(rpcUrl: string, contractAddress: string, chainId?: number);
94
+
95
+ /**
96
+ * Derive encryption keys for a wallet
97
+ */
98
+ deriveKeys(wallet: ethers.Wallet | ethers.Signer): Promise<Keys>;
99
+
100
+ /**
101
+ * Get account information from the contract
102
+ */
103
+ getAccountInfo(address: string): Promise<any>;
104
+
105
+ /**
106
+ * Create a confidential account if it doesn't exist and wait for finalization
107
+ */
108
+ ensureAccount(
109
+ wallet: ethers.Wallet | ethers.Signer,
110
+ options?: AccountOptions,
111
+ ): Promise<Keys>;
112
+
113
+ /**
114
+ * Get decrypted balance for an address
115
+ * @param address Account address
116
+ * @param privateKey Private key for decryption
117
+ * @param tokenAddress Token contract address
118
+ * @param options Balance options
119
+ */
120
+ getBalance(
121
+ address: string,
122
+ privateKey: string,
123
+ tokenAddress: string,
124
+ options?: BalanceOptions,
125
+ ): Promise<Balance>;
126
+
127
+ /**
128
+ * Deposit tokens into confidential account
129
+ * @param wallet Wallet to deposit from
130
+ * @param tokenAddress Token contract address to deposit
131
+ * @param amount Amount to deposit
132
+ * @param options Deposit options
133
+ */
134
+ deposit(
135
+ wallet: ethers.Wallet | ethers.Signer,
136
+ tokenAddress: string,
137
+ amount: bigint | string | number,
138
+ options?: DepositOptions,
139
+ ): Promise<ethers.ContractTransactionReceipt>;
140
+
141
+ /**
142
+ * Transfer confidential tokens to another address
143
+ * All necessary data is derived automatically - you only need the wallet and recipient info
144
+ * @param senderWallet Sender's wallet
145
+ * @param recipientAddress Recipient's address
146
+ * @param tokenAddress Token contract address to transfer
147
+ * @param amount Amount to transfer
148
+ * @param options Transfer options
149
+ */
150
+ transfer(
151
+ senderWallet: ethers.Wallet | ethers.Signer,
152
+ recipientAddress: string,
153
+ tokenAddress: string,
154
+ amount: number,
155
+ options?: TransferOptions,
156
+ ): Promise<ethers.ContractTransactionReceipt>;
157
+
158
+ /**
159
+ * Apply pending balance to available balance
160
+ */
161
+ applyPending(
162
+ wallet: ethers.Wallet | ethers.Signer,
163
+ options?: ApplyPendingOptions,
164
+ ): Promise<ethers.ContractTransactionReceipt>;
165
+
166
+ /**
167
+ * Withdraw confidential tokens to public ERC20
168
+ * @param wallet Wallet to withdraw to
169
+ * @param tokenAddress Token contract address to withdraw
170
+ * @param amount Amount to withdraw
171
+ * @param keys Encryption keys
172
+ * @param currentBalanceCiphertext Current balance ciphertext
173
+ * @param currentBalance Current balance (decrypted)
174
+ * @param options Withdrawal options
175
+ */
176
+ withdraw(
177
+ wallet: ethers.Wallet | ethers.Signer,
178
+ tokenAddress: string,
179
+ amount: number,
180
+ keys: Keys,
181
+ currentBalanceCiphertext: string,
182
+ currentBalance: number,
183
+ options?: WithdrawOptions,
184
+ ): Promise<ethers.ContractTransactionReceipt>;
185
+
186
+ /**
187
+ * Wait for pending balance to appear
188
+ * @param address Account address
189
+ * @param privateKey Private key for decryption
190
+ * @param tokenAddress Token contract address
191
+ * @param options Wait options
192
+ */
193
+ waitForPendingBalance(
194
+ address: string,
195
+ privateKey: string,
196
+ tokenAddress: string,
197
+ options?: WaitForPendingOptions,
198
+ ): Promise<Balance>;
199
+
200
+ /**
201
+ * Get the current fee amount for confidential transfers
202
+ */
203
+ getFeeAmount(): Promise<bigint>;
204
+
205
+ /**
206
+ * Get ERC20 token balance
207
+ * @param address Account address
208
+ * @param tokenAddress Token contract address
209
+ */
210
+ getTokenBalance(address: string, tokenAddress: string): Promise<bigint>;
211
+ }
212
+
213
+ /**
214
+ * Cryptography utilities
215
+ */
216
+
217
+ /**
218
+ * Derive encryption keys for a wallet
219
+ */
220
+ export function deriveKeys(
221
+ wallet: ethers.Wallet | ethers.Signer,
222
+ domainContext: { chainId: number; contractAddress: string },
223
+ generateKeypairFn: (signature: string, context: string) => string,
224
+ ): Promise<Keys>;
225
+
226
+ /**
227
+ * Decrypt a ciphertext using a private key
228
+ */
229
+ export function decryptCiphertext(
230
+ ciphertext: string,
231
+ privateKey: string,
232
+ decryptFn: (ciphertext: string, privateKey: string) => string,
233
+ ): number;
234
+
235
+ /**
236
+ * Combine two ciphertext parts (c1, c2)
237
+ */
238
+ export function combineCiphertext(c1: string, c2: string): string;
239
+
240
+ /**
241
+ * Utilities
242
+ */
243
+
244
+ /**
245
+ * Encode a transfer proof for contract submission
246
+ */
247
+ export function encodeTransferProof(proof: any): string;
248
+
249
+ /**
250
+ * Encode a withdraw proof for contract submission
251
+ */
252
+ export function encodeWithdrawProof(proof: any): string;
253
+
254
+ /**
255
+ * Constants
256
+ */
257
+
258
+ /**
259
+ * Contract ABI
260
+ */
261
+ export const CONTRACT_ABI: any[];
262
+
263
+ /**
264
+ * ERC20 ABI
265
+ */
266
+ export const ERC20_ABI: any[];
267
+
268
+ /**
269
+ * Default configuration values
270
+ */
271
+ export const DEFAULT_CONFIG: {
272
+ CHAIN_ID: number;
273
+ EXPLORER_URL: string;
274
+ };
275
+ }
package/src/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { ConfidentialTransferClient } from "./client.js";
2
+ export { deriveKeys, decryptCiphertext, combineCiphertext } from "./crypto.js";
3
+ export { encodeTransferProof, encodeWithdrawProof } from "./utils.js";
4
+ export { CONTRACT_ABI, ERC20_ABI, DEFAULT_CONFIG } from "./constants.js";
5
+ // Note: initializeWasm is now internal - WASM auto-initializes on first client use
package/src/utils.js ADDED
@@ -0,0 +1,84 @@
1
+ import { ethers } from "ethers";
2
+
3
+ /**
4
+ * Encodes the ZK-Proof data for a transfer into a format the Solidity contract expects.
5
+ *
6
+ * @param {Object} proofData - The proof data object
7
+ * @returns {string} Encoded proof bytes
8
+ */
9
+ export function encodeTransferProof(proofData) {
10
+ const abiCoder = ethers.AbiCoder.defaultAbiCoder();
11
+ return abiCoder.encode(
12
+ ["string", "string", "string"],
13
+ [
14
+ proofData.equality_proof,
15
+ proofData.ciphertext_validity_proof,
16
+ proofData.range_proof,
17
+ ],
18
+ );
19
+ }
20
+
21
+ /**
22
+ * Encodes the ZK-Proof data for a withdrawal.
23
+ *
24
+ * @param {Object} proofData - The proof data object
25
+ * @returns {string} Encoded proof bytes
26
+ */
27
+ export function encodeWithdrawProof(proofData) {
28
+ const abiCoder = ethers.AbiCoder.defaultAbiCoder();
29
+ return abiCoder.encode(
30
+ ["string", "string"],
31
+ [proofData.equality_proof, proofData.range_proof],
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Delays execution for a specified time
37
+ *
38
+ * @param {number} ms - Milliseconds to wait
39
+ * @returns {Promise<void>}
40
+ */
41
+ export function sleep(ms) {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ /**
46
+ * Logs a transaction with optional privacy note
47
+ *
48
+ * @param {string} hash - Transaction hash
49
+ * @param {string} explorerUrl - Block explorer base URL
50
+ * @param {boolean} isConfidential - Whether the transaction is confidential
51
+ */
52
+ export function logTransaction(hash, explorerUrl, isConfidential = false) {
53
+ console.log(`Transaction submitted: ${explorerUrl}${hash}`);
54
+ if (isConfidential) {
55
+ console.log(
56
+ `Note: This is a confidential transaction - the amount is not visible on-chain.`,
57
+ );
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Waits for a condition to be true with timeout
63
+ *
64
+ * @param {Function} conditionFn - Async function that returns boolean
65
+ * @param {number} maxAttempts - Maximum number of attempts
66
+ * @param {number} intervalMs - Interval between attempts in milliseconds
67
+ * @param {string} actionLabel - Label for logging
68
+ * @returns {Promise<void>}
69
+ * @throws {Error} If timeout is reached
70
+ */
71
+ export async function waitForCondition(
72
+ conditionFn,
73
+ maxAttempts = 60,
74
+ intervalMs = 3000,
75
+ actionLabel = "operation",
76
+ ) {
77
+ for (let i = 0; i < maxAttempts; i++) {
78
+ if (await conditionFn()) {
79
+ return;
80
+ }
81
+ await sleep(intervalMs);
82
+ }
83
+ throw new Error(`Timeout waiting for ${actionLabel}`);
84
+ }
@@ -0,0 +1,85 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import init, {
5
+ generate_deterministic_keypair,
6
+ generate_transfer_proof,
7
+ generate_withdraw_proof,
8
+ decrypt_ciphertext,
9
+ } from "../pkg/confidential_transfer_proof_generation.js";
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ let isInitialized = false;
15
+
16
+ /**
17
+ * Initialize the WASM module
18
+ * This must be called before using the SDK
19
+ *
20
+ * @param {Buffer|Uint8Array|string} [wasmPath] - Optional custom WASM file path or buffer
21
+ * @returns {Promise<Object>} WASM module functions
22
+ */
23
+ export async function initializeWasm(wasmPath) {
24
+ if (isInitialized) {
25
+ return {
26
+ generate_deterministic_keypair,
27
+ generate_transfer_proof,
28
+ generate_withdraw_proof,
29
+ decrypt_ciphertext,
30
+ };
31
+ }
32
+
33
+ try {
34
+ let wasmBuffer;
35
+
36
+ if (wasmPath) {
37
+ // If a custom path or buffer is provided
38
+ if (typeof wasmPath === "string") {
39
+ wasmBuffer = fs.readFileSync(wasmPath);
40
+ } else {
41
+ wasmBuffer = wasmPath;
42
+ }
43
+ } else {
44
+ // Use the bundled WASM file
45
+ const defaultWasmPath = path.resolve(
46
+ __dirname,
47
+ "../pkg/confidential_transfer_proof_generation_bg.wasm",
48
+ );
49
+ wasmBuffer = fs.readFileSync(defaultWasmPath);
50
+ }
51
+
52
+ await init(wasmBuffer);
53
+ isInitialized = true;
54
+
55
+ return {
56
+ generate_deterministic_keypair,
57
+ generate_transfer_proof,
58
+ generate_withdraw_proof,
59
+ decrypt_ciphertext,
60
+ };
61
+ } catch (error) {
62
+ throw new Error(
63
+ `Failed to initialize WASM module: ${error.message}. ` +
64
+ `Make sure the WASM file exists at the expected location.`,
65
+ );
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Check if WASM module is initialized
71
+ * @returns {boolean}
72
+ */
73
+ export function isWasmInitialized() {
74
+ return isInitialized;
75
+ }
76
+
77
+ /**
78
+ * Export WASM functions (they will only work after initialization)
79
+ */
80
+ export {
81
+ generate_deterministic_keypair,
82
+ generate_transfer_proof,
83
+ generate_withdraw_proof,
84
+ decrypt_ciphertext,
85
+ };