bstest001 0.0.4 → 0.0.5
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/package.json +3 -2
- package/src/balance.ts +2 -1
- package/src/deposit.ts +1 -1
- package/src/utils/db.ts +151 -0
- package/src/utils/utils.ts +61 -23
- package/src/withdraw.ts +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bstest001",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"ethers": "^5.0.0",
|
|
18
18
|
"ffjavascript": "^0.2.36",
|
|
19
19
|
"poseidon-lite": "^0.3.0",
|
|
20
|
-
"snarkjs": "^0.7.5"
|
|
20
|
+
"snarkjs": "^0.7.5",
|
|
21
|
+
"sqlite3": "^6.0.1"
|
|
21
22
|
},
|
|
22
23
|
"devDependencies": {
|
|
23
24
|
"@types/node": "^25.3.3",
|
package/src/balance.ts
CHANGED
package/src/deposit.ts
CHANGED
package/src/utils/db.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
|
|
2
|
+
export interface StorageAdapter {
|
|
3
|
+
setup(): Promise<void>;
|
|
4
|
+
set(key: string, value: any): Promise<void>;
|
|
5
|
+
get(key: string): Promise<any | null>;
|
|
6
|
+
getAll(): Promise<any[]>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const isBrowser = typeof window !== 'undefined' && typeof indexedDB !== 'undefined';
|
|
10
|
+
export class UniversalStorage {
|
|
11
|
+
private adapter!: StorageAdapter;
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
private dbName: string = 'PrivacyCash',
|
|
15
|
+
private storeName: string = 'evm'
|
|
16
|
+
) { }
|
|
17
|
+
|
|
18
|
+
async init(): Promise<UniversalStorage> {
|
|
19
|
+
if (isBrowser) {
|
|
20
|
+
this.adapter = new BrowserIDBAdapter(this.dbName, this.storeName);
|
|
21
|
+
} else {
|
|
22
|
+
// Dynamically load adapter in Node.js environment
|
|
23
|
+
this.adapter = new NodeSQLiteAdapter(this.dbName);
|
|
24
|
+
}
|
|
25
|
+
await this.adapter.setup();
|
|
26
|
+
return this;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async set(key: string, value: any): Promise<void> {
|
|
30
|
+
return await this.adapter.set(key, value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async get<T = any>(key: string): Promise<T | null> {
|
|
34
|
+
return await this.adapter.get(key);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async getAll<T = any>(): Promise<T[]> {
|
|
38
|
+
return await this.adapter.getAll();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class BrowserIDBAdapter implements StorageAdapter {
|
|
43
|
+
private db: IDBDatabase | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(private dbName: string, private storeName: string) { }
|
|
46
|
+
|
|
47
|
+
setup(): Promise<void> {
|
|
48
|
+
return new Promise((resolve, reject) => {
|
|
49
|
+
const request = indexedDB.open(this.dbName, 1);
|
|
50
|
+
request.onupgradeneeded = (e: any) => {
|
|
51
|
+
const db = e.target.result as IDBDatabase;
|
|
52
|
+
if (!db.objectStoreNames.contains(this.storeName)) {
|
|
53
|
+
db.createObjectStore(this.storeName);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
request.onsuccess = (e: any) => {
|
|
57
|
+
this.db = e.target.result;
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
request.onerror = () => reject(new Error("IndexedDB initialization failed"));
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async set(key: string, value: any): Promise<void> {
|
|
65
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
66
|
+
const tx = this.db.transaction(this.storeName, 'readwrite');
|
|
67
|
+
const store = tx.objectStore(this.storeName);
|
|
68
|
+
return new Promise((res, rej) => {
|
|
69
|
+
const req = store.put(value, key);
|
|
70
|
+
tx.oncomplete = () => res();
|
|
71
|
+
tx.onerror = () => rej(tx.error);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async get(key: string): Promise<any | null> {
|
|
76
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
77
|
+
const tx = this.db.transaction(this.storeName, 'readonly');
|
|
78
|
+
const store = tx.objectStore(this.storeName);
|
|
79
|
+
return new Promise((res) => {
|
|
80
|
+
const req = store.get(key);
|
|
81
|
+
req.onsuccess = () => res(req.result || null);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async getAll(): Promise<any[]> {
|
|
86
|
+
if (!this.db) throw new Error("Database not initialized");
|
|
87
|
+
const tx = this.db.transaction(this.storeName, 'readonly');
|
|
88
|
+
const store = tx.objectStore(this.storeName);
|
|
89
|
+
return new Promise((res) => {
|
|
90
|
+
const req = store.getAll();
|
|
91
|
+
req.onsuccess = () => res(req.result || []);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
class NodeSQLiteAdapter implements StorageAdapter {
|
|
97
|
+
private db: any;
|
|
98
|
+
private fileName: string;
|
|
99
|
+
|
|
100
|
+
constructor(dbName: string) {
|
|
101
|
+
this.fileName = `./cache/${dbName}.db`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async setup(): Promise<void> {
|
|
105
|
+
const sqlite3 = await import('sqlite3');
|
|
106
|
+
const { Database } = sqlite3.default;
|
|
107
|
+
this.db = new Database(this.fileName);
|
|
108
|
+
await this.run('CREATE TABLE IF NOT EXISTS storage (key TEXT PRIMARY KEY, value TEXT)');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private run(sql: string, ...params: any[]): Promise<void> {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
this.db.run(sql, params, (err: any) => {
|
|
114
|
+
if (err) reject(err);
|
|
115
|
+
else resolve();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private queryGet(sql: string, ...params: any[]): Promise<any> {
|
|
121
|
+
return new Promise((resolve, reject) => {
|
|
122
|
+
this.db.get(sql, params, (err: any, row: any) => {
|
|
123
|
+
if (err) reject(err);
|
|
124
|
+
else resolve(row);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private queryAll(sql: string, ...params: any[]): Promise<any[]> {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
this.db.all(sql, params, (err: any, rows: any[]) => {
|
|
132
|
+
if (err) reject(err);
|
|
133
|
+
else resolve(rows);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async set(key: string, value: any): Promise<void> {
|
|
139
|
+
await this.run('INSERT OR REPLACE INTO storage (key, value) VALUES (?, ?)', key, JSON.stringify(value));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async get(key: string): Promise<any | null> {
|
|
143
|
+
const row = await this.queryGet('SELECT value FROM storage WHERE key = ?', key);
|
|
144
|
+
return row ? JSON.parse(row.value) : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async getAll(): Promise<any[]> {
|
|
148
|
+
const rows = await this.queryAll('SELECT value FROM storage');
|
|
149
|
+
return rows.map((row: any) => JSON.parse(row.value));
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/utils/utils.ts
CHANGED
|
@@ -2,10 +2,13 @@ import crypto from 'crypto';
|
|
|
2
2
|
import { BigNumber, ethers } from 'ethers';
|
|
3
3
|
import { poseidon1, poseidon2, poseidon3, poseidon4 } from 'poseidon-lite';
|
|
4
4
|
import { INDEXER_URL } from './constants.js';
|
|
5
|
+
import { UniversalStorage } from './db.js';
|
|
5
6
|
import { logger } from './logger.js';
|
|
6
7
|
import { prove } from './prover.js';
|
|
7
8
|
import { Utxo } from './utxo.js';
|
|
8
9
|
|
|
10
|
+
const storage = new UniversalStorage('PrivacyCashDB', 'evm');
|
|
11
|
+
|
|
9
12
|
export const FIELD_SIZE = BigNumber.from(
|
|
10
13
|
'21888242871839275222246405745257275088548364400416034343698204186575808495617',
|
|
11
14
|
);
|
|
@@ -230,51 +233,86 @@ export async function prepareTransaction({
|
|
|
230
233
|
}
|
|
231
234
|
|
|
232
235
|
export async function findUnspentUtxos({
|
|
236
|
+
address,
|
|
233
237
|
etherPool,
|
|
234
238
|
encryptionKey,
|
|
235
239
|
keypair,
|
|
240
|
+
start = 0,
|
|
236
241
|
}: {
|
|
242
|
+
address: string;
|
|
237
243
|
etherPool: ethers.Contract;
|
|
238
244
|
encryptionKey: Buffer;
|
|
239
245
|
keypair: any;
|
|
246
|
+
start?: number;
|
|
240
247
|
}) {
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
const
|
|
248
|
+
await storage.init();
|
|
249
|
+
const offsetKey = `offset_${address}`;
|
|
250
|
+
const knownKey = `keo_${address}`;
|
|
251
|
+
const cachedOffset = (await storage.get(offsetKey)) || start;
|
|
252
|
+
const cachedKnown = (await storage.get(knownKey)) || [];
|
|
253
|
+
let knownEncryptedOutputs = new Map<string, number>();
|
|
254
|
+
const candidates: { utxo: Utxo; nullifier: string }[] = [];
|
|
255
|
+
|
|
256
|
+
// print number of knownEncryptedOutputs
|
|
257
|
+
logger.debug(`Cached offset: ${cachedOffset}`);
|
|
258
|
+
logger.debug(`Cached known encrypted outputs: ${cachedKnown.length}`);
|
|
259
|
+
|
|
260
|
+
// Process cached known encrypted outputs
|
|
261
|
+
for (const item of cachedKnown) {
|
|
262
|
+
knownEncryptedOutputs.set(item.encrypted, item.index);
|
|
263
|
+
try {
|
|
264
|
+
const utxo = Utxo.decrypt(encryptionKey, item.encrypted, item.index, keypair);
|
|
265
|
+
if (!utxo.amount.isZero()) {
|
|
266
|
+
utxo.index = item.index;
|
|
267
|
+
const nullifier = toFixedHex(utxo.getNullifier());
|
|
268
|
+
candidates.push({ utxo, nullifier });
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Decryption failed — this output belongs to someone else
|
|
272
|
+
}
|
|
273
|
+
}
|
|
245
274
|
|
|
275
|
+
let startIndex = cachedOffset;
|
|
276
|
+
const limit = 1000;
|
|
277
|
+
let hasMore = true;
|
|
246
278
|
while (hasMore) {
|
|
247
|
-
let url = `${INDEXER_URL}/get_encrypted?start=${
|
|
248
|
-
logger.debug(`Fetching encrypted UTXOs from indexer: ${url}`)
|
|
249
|
-
const res = await fetch(url)
|
|
279
|
+
let url = `${INDEXER_URL}/get_encrypted?start=${startIndex}&end=${startIndex + limit}`;
|
|
280
|
+
logger.debug(`Fetching encrypted UTXOs from indexer: ${url}`);
|
|
281
|
+
const res = await fetch(url);
|
|
250
282
|
if (!res.ok) {
|
|
251
|
-
throw new Error(`Failed to fetch encrypted UTXOs: ${res.statusText}`)
|
|
283
|
+
throw new Error(`Failed to fetch encrypted UTXOs: ${res.statusText}`);
|
|
252
284
|
}
|
|
253
|
-
const data = await res.json()
|
|
254
|
-
const { encrypted_outputs, hasMore: more, start: realStart } = data
|
|
285
|
+
const data = await res.json();
|
|
286
|
+
const { encrypted_outputs, hasMore: more, start: realStart } = data;
|
|
255
287
|
|
|
256
288
|
if (encrypted_outputs && encrypted_outputs.length > 0) {
|
|
257
289
|
encrypted_outputs.forEach((encryptedOutput: string, i: number) => {
|
|
258
|
-
const index = (realStart ??
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
utxo.
|
|
263
|
-
|
|
264
|
-
|
|
290
|
+
const index = (realStart ?? startIndex) + i;
|
|
291
|
+
if (!knownEncryptedOutputs.has(encryptedOutput)) {
|
|
292
|
+
try {
|
|
293
|
+
const utxo = Utxo.decrypt(encryptionKey, encryptedOutput, index, keypair);
|
|
294
|
+
if (!utxo.amount.isZero()) {
|
|
295
|
+
knownEncryptedOutputs.set(encryptedOutput, index);
|
|
296
|
+
utxo.index = index;
|
|
297
|
+
const nullifier = toFixedHex(utxo.getNullifier());
|
|
298
|
+
candidates.push({ utxo, nullifier });
|
|
299
|
+
}
|
|
300
|
+
} catch {
|
|
301
|
+
// Decryption failed — this output belongs to someone else
|
|
265
302
|
}
|
|
266
|
-
} catch {
|
|
267
|
-
// Decryption failed — this output belongs to someone else
|
|
268
303
|
}
|
|
269
|
-
})
|
|
304
|
+
});
|
|
270
305
|
}
|
|
271
306
|
|
|
272
|
-
hasMore = more
|
|
273
|
-
|
|
307
|
+
hasMore = more;
|
|
308
|
+
startIndex += encrypted_outputs.length;
|
|
274
309
|
}
|
|
310
|
+
// Cache the updated offset and known encrypted outputs
|
|
311
|
+
await storage.set(offsetKey, startIndex);
|
|
312
|
+
await storage.set(knownKey, Array.from(knownEncryptedOutputs.entries()).map(([encrypted, index]) => ({ encrypted, index })));
|
|
275
313
|
|
|
276
314
|
// log candidates length
|
|
277
|
-
logger.debug(`Found ${candidates.length} candidate UTXOs, checking which are unspent...`)
|
|
315
|
+
logger.debug(`Found ${candidates.length} candidate UTXOs, checking which are unspent...`);
|
|
278
316
|
|
|
279
317
|
const unspent: Utxo[] = []
|
|
280
318
|
const spentCheckChunkSize = 256
|
package/src/withdraw.ts
CHANGED
|
@@ -8,12 +8,13 @@ import { Utxo } from './utils/utxo.js';
|
|
|
8
8
|
|
|
9
9
|
const FLAT_FEE = ethers.utils.parseEther(RENT_FEE.toString())
|
|
10
10
|
|
|
11
|
-
export async function withdraw({ provider: rawProvider, withdrawAmountInput, recipient, keyBasePath, signature }: {
|
|
11
|
+
export async function withdraw({ provider: rawProvider, withdrawAmountInput, recipient, keyBasePath, signature, address }: {
|
|
12
12
|
provider: any,
|
|
13
13
|
withdrawAmountInput: number,
|
|
14
14
|
recipient: string,
|
|
15
15
|
keyBasePath: string,
|
|
16
16
|
signature: string,
|
|
17
|
+
address: string
|
|
17
18
|
}) {
|
|
18
19
|
const provider: ethers.providers.Provider = rawProvider?.request
|
|
19
20
|
? new ethers.providers.Web3Provider(rawProvider)
|
|
@@ -40,7 +41,8 @@ export async function withdraw({ provider: rawProvider, withdrawAmountInput, rec
|
|
|
40
41
|
const unspent = await findUnspentUtxos({
|
|
41
42
|
etherPool,
|
|
42
43
|
encryptionKey,
|
|
43
|
-
keypair
|
|
44
|
+
keypair,
|
|
45
|
+
address
|
|
44
46
|
});
|
|
45
47
|
logger.debug(`Unspent UTXOs found: ${unspent.length}`);
|
|
46
48
|
|