testprivacycash-evm 1.2.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.
- package/.github/workflows/npm-publish.yml +55 -0
- package/README.md +17 -0
- package/circuits/transaction2.wasm +0 -0
- package/circuits/transaction2.zkey +0 -0
- package/dist/balance.d.ts +10 -0
- package/dist/balance.js +57 -0
- package/dist/deposit.d.ts +10 -0
- package/dist/deposit.js +345 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +8 -0
- package/dist/utils/ERCPool.abi.json +929 -0
- package/dist/utils/EtherPool.abi.json +798 -0
- package/dist/utils/constants.d.ts +8 -0
- package/dist/utils/constants.js +11 -0
- package/dist/utils/db.d.ts +17 -0
- package/dist/utils/db.js +276 -0
- package/dist/utils/encryption.d.ts +8 -0
- package/dist/utils/encryption.js +34 -0
- package/dist/utils/keypair.d.ts +9 -0
- package/dist/utils/keypair.js +19 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/networkConfig.d.ts +54 -0
- package/dist/utils/networkConfig.js +126 -0
- package/dist/utils/prover.d.ts +15 -0
- package/dist/utils/prover.js +30 -0
- package/dist/utils/remoteConfig.d.ts +21 -0
- package/dist/utils/remoteConfig.js +24 -0
- package/dist/utils/utils.d.ts +83 -0
- package/dist/utils/utils.js +275 -0
- package/dist/utils/utxo.d.ts +20 -0
- package/dist/utils/utxo.js +55 -0
- package/dist/withdraw.d.ts +12 -0
- package/dist/withdraw.js +253 -0
- package/package.json +37 -0
- package/src/balance.ts +74 -0
- package/src/deposit.ts +406 -0
- package/src/index.ts +16 -0
- package/src/utils/ERCPool.abi.json +929 -0
- package/src/utils/EtherPool.abi.json +798 -0
- package/src/utils/constants.ts +15 -0
- package/src/utils/db.ts +311 -0
- package/src/utils/encryption.ts +40 -0
- package/src/utils/keypair.ts +24 -0
- package/src/utils/logger.ts +42 -0
- package/src/utils/networkConfig.ts +169 -0
- package/src/utils/prover.ts +39 -0
- package/src/utils/remoteConfig.ts +49 -0
- package/src/utils/utils.ts +387 -0
- package/src/utils/utxo.ts +74 -0
- package/src/withdraw.ts +351 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NetworkConfig, PrivacyToken } from './networkConfig.js';
|
|
2
|
+
export interface FeeSnapshot {
|
|
3
|
+
id: string;
|
|
4
|
+
chain: 'eth';
|
|
5
|
+
feeRateBps: number;
|
|
6
|
+
validFrom: number;
|
|
7
|
+
expiresAt: number;
|
|
8
|
+
rentFeeUnits: Partial<Record<PrivacyToken, string>>;
|
|
9
|
+
rentFees?: Partial<Record<PrivacyToken, number>>;
|
|
10
|
+
}
|
|
11
|
+
export interface RemoteConfig {
|
|
12
|
+
prices: Partial<Record<PrivacyToken, number>>;
|
|
13
|
+
minimum_withdrawal: Record<PrivacyToken, number>;
|
|
14
|
+
minimum_deposit: Record<PrivacyToken, number>;
|
|
15
|
+
rent_fees: Record<PrivacyToken, number>;
|
|
16
|
+
fee_rate: number;
|
|
17
|
+
feeSnapshot?: FeeSnapshot | null;
|
|
18
|
+
}
|
|
19
|
+
export declare function getRemoteConfig(network?: NetworkConfig | number, options?: {
|
|
20
|
+
forceRefresh?: boolean;
|
|
21
|
+
}): Promise<RemoteConfig>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
import { resolveNetwork } from './networkConfig.js';
|
|
3
|
+
const CACHE_TTL_MS = 60 * 1000; // 1 minute
|
|
4
|
+
const configCache = new Map();
|
|
5
|
+
export async function getRemoteConfig(network, options) {
|
|
6
|
+
const net = resolveNetwork(network);
|
|
7
|
+
const now = Date.now();
|
|
8
|
+
const cached = configCache.get(net.chainId);
|
|
9
|
+
if (!options?.forceRefresh && cached && now < cached.expiresAt)
|
|
10
|
+
return cached.config;
|
|
11
|
+
if (!net.indexerUrl) {
|
|
12
|
+
throw new Error(`No indexer URL configured for chain ${net.chainId} (${net.chainKey})`);
|
|
13
|
+
}
|
|
14
|
+
const configUrl = new URL(`${net.indexerUrl.replace(/\/$/, '')}/config`);
|
|
15
|
+
configUrl.searchParams.set('chain', net.chainKey);
|
|
16
|
+
const res = await fetch(configUrl.toString());
|
|
17
|
+
if (!res.ok) {
|
|
18
|
+
throw new Error(`Failed to fetch remote config: HTTP ${res.status}`);
|
|
19
|
+
}
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
configCache.set(net.chainId, { config: data, expiresAt: now + CACHE_TTL_MS });
|
|
22
|
+
logger.debug('Remote config loaded successfully');
|
|
23
|
+
return data;
|
|
24
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { BigNumber, ethers } from 'ethers';
|
|
2
|
+
import { NetworkConfig, PrivacyToken } from './networkConfig.js';
|
|
3
|
+
import { Utxo } from './utxo.js';
|
|
4
|
+
export declare const FIELD_SIZE: BigNumber;
|
|
5
|
+
export declare function poseidonHash(items: any[]): BigNumber;
|
|
6
|
+
export declare const poseidonHash2: (a: any, b: any) => BigNumber;
|
|
7
|
+
/** Generate random number of specified byte length */
|
|
8
|
+
export declare const randomBN: (nbytes?: number) => BigNumber;
|
|
9
|
+
/** BigNumber to hex string of specified length */
|
|
10
|
+
export declare function toFixedHex(number: any, length?: number): string;
|
|
11
|
+
/** Convert value into buffer of specified byte length */
|
|
12
|
+
export declare const toBuffer: (value: any, length: number) => Buffer<ArrayBuffer>;
|
|
13
|
+
export declare function getProof({ inputs, outputs, extAmount, fee, recipient, feeRecipient, encryptionKey, keyBasePath, token, network }: {
|
|
14
|
+
inputs: Utxo[];
|
|
15
|
+
outputs: Utxo[];
|
|
16
|
+
extAmount: BigNumber;
|
|
17
|
+
fee: BigNumber;
|
|
18
|
+
recipient: string;
|
|
19
|
+
feeRecipient: string;
|
|
20
|
+
encryptionKey: Buffer;
|
|
21
|
+
keyBasePath: string;
|
|
22
|
+
token?: PrivacyToken;
|
|
23
|
+
network?: NetworkConfig | number;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
extData: {
|
|
26
|
+
recipient: string;
|
|
27
|
+
extAmount: string;
|
|
28
|
+
feeRecipient: string;
|
|
29
|
+
fee: string;
|
|
30
|
+
encryptedOutput1: string;
|
|
31
|
+
encryptedOutput2: string;
|
|
32
|
+
};
|
|
33
|
+
args: {
|
|
34
|
+
pA: string[];
|
|
35
|
+
pB: string[][];
|
|
36
|
+
pC: string[];
|
|
37
|
+
root: string;
|
|
38
|
+
inputNullifiers: string[];
|
|
39
|
+
outputCommitments: string[];
|
|
40
|
+
publicAmount: string;
|
|
41
|
+
extDataHash: string;
|
|
42
|
+
};
|
|
43
|
+
}>;
|
|
44
|
+
export declare function prepareTransaction({ inputs, outputs, fee, recipient, feeRecipient, encryptionKey, keyBasePath, token, network }: {
|
|
45
|
+
inputs?: Utxo[];
|
|
46
|
+
outputs?: Utxo[];
|
|
47
|
+
fee?: BigNumber;
|
|
48
|
+
recipient?: string;
|
|
49
|
+
feeRecipient?: string;
|
|
50
|
+
encryptionKey: Buffer;
|
|
51
|
+
keyBasePath: string;
|
|
52
|
+
token?: PrivacyToken;
|
|
53
|
+
network?: NetworkConfig | number;
|
|
54
|
+
}): Promise<{
|
|
55
|
+
extData: {
|
|
56
|
+
recipient: string;
|
|
57
|
+
extAmount: string;
|
|
58
|
+
feeRecipient: string;
|
|
59
|
+
fee: string;
|
|
60
|
+
encryptedOutput1: string;
|
|
61
|
+
encryptedOutput2: string;
|
|
62
|
+
};
|
|
63
|
+
args: {
|
|
64
|
+
pA: string[];
|
|
65
|
+
pB: string[][];
|
|
66
|
+
pC: string[];
|
|
67
|
+
root: string;
|
|
68
|
+
inputNullifiers: string[];
|
|
69
|
+
outputCommitments: string[];
|
|
70
|
+
publicAmount: string;
|
|
71
|
+
extDataHash: string;
|
|
72
|
+
};
|
|
73
|
+
}>;
|
|
74
|
+
export declare function findUnspentUtxos({ address, etherPool, encryptionKey, keypair, start, token, network }: {
|
|
75
|
+
address: string;
|
|
76
|
+
etherPool: ethers.Contract;
|
|
77
|
+
encryptionKey: Buffer;
|
|
78
|
+
keypair: any;
|
|
79
|
+
start?: number;
|
|
80
|
+
token?: PrivacyToken;
|
|
81
|
+
network?: NetworkConfig | number;
|
|
82
|
+
}): Promise<Utxo[]>;
|
|
83
|
+
export declare function clearCache(address: string, token?: PrivacyToken, network?: NetworkConfig | number): Promise<void>;
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import crypto from 'crypto';
|
|
2
|
+
import { BigNumber, ethers } from 'ethers';
|
|
3
|
+
import { poseidon1, poseidon2, poseidon3, poseidon4 } from 'poseidon-lite';
|
|
4
|
+
import { UniversalStorage } from './db.js';
|
|
5
|
+
import { logger } from './logger.js';
|
|
6
|
+
import { getErc20TokenConfig, isNativeToken, resolveNetwork, resolvePrivacyToken } from './networkConfig.js';
|
|
7
|
+
import { prove } from './prover.js';
|
|
8
|
+
import { Utxo } from './utxo.js';
|
|
9
|
+
const storageCache = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Returns the local UTXO cache store for the given chain+token combination.
|
|
12
|
+
* Base keeps legacy store names ('evmProd', 'evmUsdcProd') to preserve existing caches.
|
|
13
|
+
* Ethereum and future chains use prefixed names ('eth_evmProd', 'eth_evmUsdtProd').
|
|
14
|
+
*/
|
|
15
|
+
function getStorage(token, net) {
|
|
16
|
+
getErc20TokenConfig(net, token);
|
|
17
|
+
const baseName = isNativeToken(net, token) ? 'evmProd' : token === 'usdc' ? 'evmUsdcProd' : 'evmUsdtProd';
|
|
18
|
+
const storeName = net.chainId === 8453 ? baseName : `${net.cachePrefix}_${baseName}`;
|
|
19
|
+
const key = `PrivacyCashDB_${storeName}`;
|
|
20
|
+
if (!storageCache.has(key)) {
|
|
21
|
+
storageCache.set(key, new UniversalStorage('PrivacyCashDB', storeName));
|
|
22
|
+
}
|
|
23
|
+
return storageCache.get(key);
|
|
24
|
+
}
|
|
25
|
+
export const FIELD_SIZE = BigNumber.from('21888242871839275222246405745257275088548364400416034343698204186575808495617');
|
|
26
|
+
export function poseidonHash(items) {
|
|
27
|
+
if (items.length === 1)
|
|
28
|
+
return BigNumber.from(poseidon1([items[0]]));
|
|
29
|
+
if (items.length === 2)
|
|
30
|
+
return BigNumber.from(poseidon2([items[0], items[1]]));
|
|
31
|
+
if (items.length === 3)
|
|
32
|
+
return BigNumber.from(poseidon3([items[0], items[1], items[2]]));
|
|
33
|
+
if (items.length === 4)
|
|
34
|
+
return BigNumber.from(poseidon4([items[0], items[1], items[2], items[3]]));
|
|
35
|
+
throw new Error(`Unsupported poseidon input length: ${items.length}`);
|
|
36
|
+
}
|
|
37
|
+
export const poseidonHash2 = (a, b) => poseidonHash([a, b]);
|
|
38
|
+
/** Generate random number of specified byte length */
|
|
39
|
+
export const randomBN = (nbytes = 31) => BigNumber.from(crypto.randomBytes(nbytes));
|
|
40
|
+
/** BigNumber to hex string of specified length */
|
|
41
|
+
export function toFixedHex(number, length = 32) {
|
|
42
|
+
let result = '0x' +
|
|
43
|
+
(number instanceof Buffer
|
|
44
|
+
? number.toString('hex')
|
|
45
|
+
: BigNumber.from(number).toHexString().replace('0x', '')).padStart(length * 2, '0');
|
|
46
|
+
if (result.indexOf('-') > -1) {
|
|
47
|
+
result = '-' + result.replace('-', '');
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/** Convert value into buffer of specified byte length */
|
|
52
|
+
export const toBuffer = (value, length) => Buffer.from(BigNumber.from(value)
|
|
53
|
+
.toHexString()
|
|
54
|
+
.slice(2)
|
|
55
|
+
.padStart(length * 2, '0'), 'hex');
|
|
56
|
+
function getExtDataHash({ recipient, extAmount, feeRecipient, fee, encryptedOutput1, encryptedOutput2, }) {
|
|
57
|
+
const abi = new ethers.utils.AbiCoder();
|
|
58
|
+
const encodedData = abi.encode([
|
|
59
|
+
'tuple(address recipient,int256 extAmount,address feeRecipient,uint256 fee,bytes encryptedOutput1,bytes encryptedOutput2)',
|
|
60
|
+
], [
|
|
61
|
+
{
|
|
62
|
+
recipient: toFixedHex(recipient, 20),
|
|
63
|
+
extAmount: toFixedHex(extAmount),
|
|
64
|
+
feeRecipient,
|
|
65
|
+
fee: toFixedHex(fee),
|
|
66
|
+
encryptedOutput1: encryptedOutput1,
|
|
67
|
+
encryptedOutput2: encryptedOutput2,
|
|
68
|
+
},
|
|
69
|
+
]);
|
|
70
|
+
const hash = ethers.utils.keccak256(encodedData);
|
|
71
|
+
return BigNumber.from(hash).mod(FIELD_SIZE);
|
|
72
|
+
}
|
|
73
|
+
export async function getProof({ inputs, outputs, extAmount, fee, recipient, feeRecipient, encryptionKey, keyBasePath, token = 'eth', network, }) {
|
|
74
|
+
const net = resolveNetwork(network);
|
|
75
|
+
let inputMerklePathIndices = [];
|
|
76
|
+
let inputMerklePathElements = [];
|
|
77
|
+
// fetch /merkle/root from indexer url
|
|
78
|
+
const res = await fetch(`${net.indexerUrl}/merkle/root?token=${token}&chain=${net.chainKey}`);
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
throw new Error(`Failed to fetch merkle root: ${res.status} ${res.statusText}`);
|
|
81
|
+
}
|
|
82
|
+
const { root, nextIndex: initialNextIndex } = await res.json();
|
|
83
|
+
let nextIndex = initialNextIndex;
|
|
84
|
+
for (const input of inputs) {
|
|
85
|
+
if (input.amount.gt(0)) {
|
|
86
|
+
let res = await fetch(`${net.indexerUrl}/commitment/`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
headers: { 'Content-Type': 'application/json' },
|
|
89
|
+
body: JSON.stringify({ commitment: toFixedHex(input.getCommitment()), token, chain: net.chainKey }),
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
throw new Error(`Failed to fetch commitment info`);
|
|
93
|
+
}
|
|
94
|
+
const { index, pathElements } = await res.json();
|
|
95
|
+
input.index = index;
|
|
96
|
+
if (input.index == null) {
|
|
97
|
+
throw new Error(`Input commitment ${toFixedHex(input.getCommitment())} was not found`);
|
|
98
|
+
}
|
|
99
|
+
inputMerklePathIndices.push(input.index);
|
|
100
|
+
inputMerklePathElements.push(pathElements);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
inputMerklePathIndices.push(0);
|
|
104
|
+
inputMerklePathElements.push(new Array(26).fill(0));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// update nextIndex to outputs
|
|
108
|
+
for (let output of outputs) {
|
|
109
|
+
output.index = nextIndex++;
|
|
110
|
+
}
|
|
111
|
+
const extData = {
|
|
112
|
+
recipient,
|
|
113
|
+
extAmount: toFixedHex(extAmount),
|
|
114
|
+
feeRecipient,
|
|
115
|
+
fee: toFixedHex(fee),
|
|
116
|
+
encryptedOutput1: outputs[0].encrypt(encryptionKey),
|
|
117
|
+
encryptedOutput2: (outputs[1] || new Utxo()).encrypt(encryptionKey),
|
|
118
|
+
};
|
|
119
|
+
const extDataHash = getExtDataHash(extData);
|
|
120
|
+
let input = {
|
|
121
|
+
root,
|
|
122
|
+
inputNullifier: inputs.map((x) => x.getNullifier().toString()),
|
|
123
|
+
outputCommitment: outputs.map((x) => x.getCommitment().toString()),
|
|
124
|
+
publicAmount: BigNumber.from(extAmount).sub(fee).add(FIELD_SIZE).mod(FIELD_SIZE).toString(),
|
|
125
|
+
extDataHash: extDataHash.toString(),
|
|
126
|
+
mintAddress: inputs[0].mintAddress.toString(),
|
|
127
|
+
inAmount: inputs.map((x) => x.amount.toString()),
|
|
128
|
+
inPrivateKey: inputs.map((x) => x.keypair.privkey.toString()),
|
|
129
|
+
inBlinding: inputs.map((x) => x.blinding.toString()),
|
|
130
|
+
inPathIndices: inputMerklePathIndices,
|
|
131
|
+
inPathElements: inputMerklePathElements,
|
|
132
|
+
outAmount: outputs.map((x) => x.amount.toString()),
|
|
133
|
+
outBlinding: outputs.map((x) => x.blinding.toString()),
|
|
134
|
+
outPubkey: outputs.map((x) => x.keypair.pubkey.toString()),
|
|
135
|
+
};
|
|
136
|
+
const { pA, pB, pC } = await prove(input, `${keyBasePath}${inputs.length}`);
|
|
137
|
+
const args = {
|
|
138
|
+
pA,
|
|
139
|
+
pB,
|
|
140
|
+
pC,
|
|
141
|
+
root: toFixedHex(input.root),
|
|
142
|
+
inputNullifiers: inputs.map((x) => toFixedHex(x.getNullifier())),
|
|
143
|
+
outputCommitments: outputs.map((x) => toFixedHex(x.getCommitment())),
|
|
144
|
+
publicAmount: toFixedHex(input.publicAmount),
|
|
145
|
+
extDataHash: toFixedHex(extDataHash),
|
|
146
|
+
};
|
|
147
|
+
return { extData, args };
|
|
148
|
+
}
|
|
149
|
+
export async function prepareTransaction({ inputs = [], outputs = [], fee = BigNumber.from(0), recipient = '0x44eb9939cfdE7C394f1632C6890191d695f0a3ce', feeRecipient = '0x44eb9939cfdE7C394f1632C6890191d695f0a3ce', encryptionKey, keyBasePath, token, network, }) {
|
|
150
|
+
const net = resolveNetwork(network);
|
|
151
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
152
|
+
if (inputs.length > 2 || outputs.length > 2) {
|
|
153
|
+
throw new Error('Only 2 inputs and 2 outputs are supported');
|
|
154
|
+
}
|
|
155
|
+
// Derive mintAddress from actual UTXOs so dummy padding doesn't break circuit constraints
|
|
156
|
+
const effectiveMintAddress = inputs.find(u => u.amount.gt(0))?.mintAddress ??
|
|
157
|
+
outputs.find(u => u.amount.gt(0))?.mintAddress ??
|
|
158
|
+
BigNumber.from(0);
|
|
159
|
+
while (inputs.length < 2)
|
|
160
|
+
inputs.push(new Utxo({ mintAddress: effectiveMintAddress }));
|
|
161
|
+
while (outputs.length < 2)
|
|
162
|
+
outputs.push(new Utxo({ mintAddress: effectiveMintAddress }));
|
|
163
|
+
let extAmount = outputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0))
|
|
164
|
+
.sub(inputs.reduce((sum, x) => sum.add(x.amount), BigNumber.from(0)))
|
|
165
|
+
.add(fee);
|
|
166
|
+
const { extData, args } = await getProof({
|
|
167
|
+
inputs,
|
|
168
|
+
outputs,
|
|
169
|
+
extAmount,
|
|
170
|
+
fee,
|
|
171
|
+
recipient,
|
|
172
|
+
feeRecipient,
|
|
173
|
+
encryptionKey,
|
|
174
|
+
keyBasePath,
|
|
175
|
+
token: resolvedToken,
|
|
176
|
+
network: net,
|
|
177
|
+
});
|
|
178
|
+
return { extData, args };
|
|
179
|
+
}
|
|
180
|
+
export async function findUnspentUtxos({ address, etherPool, encryptionKey, keypair, start = 0, token, network, }) {
|
|
181
|
+
const net = resolveNetwork(network);
|
|
182
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
183
|
+
const storage = getStorage(resolvedToken, net);
|
|
184
|
+
await storage.init();
|
|
185
|
+
const offsetKey = `offset_${address}`;
|
|
186
|
+
const knownKey = `keo_${address}`;
|
|
187
|
+
const cachedOffset = (await storage.get(offsetKey)) || start;
|
|
188
|
+
const cachedKnown = (await storage.get(knownKey)) || [];
|
|
189
|
+
let knownEncryptedOutputs = new Map();
|
|
190
|
+
const candidates = [];
|
|
191
|
+
// print number of knownEncryptedOutputs
|
|
192
|
+
logger.debug(`Cached offset: ${cachedOffset}`);
|
|
193
|
+
logger.debug(`Cached known encrypted outputs: ${cachedKnown.length}`);
|
|
194
|
+
// Process cached known encrypted outputs
|
|
195
|
+
for (const item of cachedKnown) {
|
|
196
|
+
knownEncryptedOutputs.set(item.index, item.encrypted);
|
|
197
|
+
try {
|
|
198
|
+
const utxo = Utxo.decrypt(encryptionKey, item.encrypted, item.index, keypair);
|
|
199
|
+
if (!utxo.amount.isZero()) {
|
|
200
|
+
utxo.index = item.index;
|
|
201
|
+
const nullifier = toFixedHex(utxo.getNullifier());
|
|
202
|
+
candidates.push({ utxo, nullifier });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Decryption failed — this output belongs to someone else
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
let startIndex = cachedOffset;
|
|
210
|
+
const limit = 1000;
|
|
211
|
+
let hasMore = true;
|
|
212
|
+
while (hasMore) {
|
|
213
|
+
let url = `${net.indexerUrl}/get_encrypted?start=${startIndex}&end=${startIndex + limit}&token=${resolvedToken}&chain=${net.chainKey}`;
|
|
214
|
+
logger.debug(`Fetching encrypted UTXOs from indexer: ${url}`);
|
|
215
|
+
const res = await fetch(url);
|
|
216
|
+
if (!res.ok) {
|
|
217
|
+
throw new Error(`Failed to fetch encrypted UTXOs: ${res.statusText}`);
|
|
218
|
+
}
|
|
219
|
+
const data = await res.json();
|
|
220
|
+
const { encrypted_outputs, hasMore: more, start: realStart, total } = data;
|
|
221
|
+
logger.info(`decrypting utxo ${startIndex}/${total}`);
|
|
222
|
+
if (encrypted_outputs && encrypted_outputs.length > 0) {
|
|
223
|
+
encrypted_outputs.forEach((encryptedOutput, i) => {
|
|
224
|
+
const index = (realStart ?? startIndex) + i;
|
|
225
|
+
if (!knownEncryptedOutputs.has(index)) {
|
|
226
|
+
try {
|
|
227
|
+
const utxo = Utxo.decrypt(encryptionKey, encryptedOutput, index, keypair);
|
|
228
|
+
if (!utxo.amount.isZero()) {
|
|
229
|
+
knownEncryptedOutputs.set(index, encryptedOutput);
|
|
230
|
+
utxo.index = index;
|
|
231
|
+
const nullifier = toFixedHex(utxo.getNullifier());
|
|
232
|
+
candidates.push({ utxo, nullifier });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Decryption failed — this output belongs to someone else
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
hasMore = more;
|
|
242
|
+
startIndex += encrypted_outputs.length;
|
|
243
|
+
}
|
|
244
|
+
// Cache the updated offset and known encrypted outputs
|
|
245
|
+
await storage.set(offsetKey, startIndex);
|
|
246
|
+
await storage.set(knownKey, Array.from(knownEncryptedOutputs.entries()).map(([index, encrypted]) => ({ encrypted, index })));
|
|
247
|
+
// log candidates length
|
|
248
|
+
logger.debug(`Found ${candidates.length} candidate UTXOs, checking which are unspent...`);
|
|
249
|
+
const unspent = [];
|
|
250
|
+
const spentCheckChunkSize = 256;
|
|
251
|
+
for (let i = 0; i < candidates.length; i += spentCheckChunkSize) {
|
|
252
|
+
const chunk = candidates.slice(i, i + spentCheckChunkSize);
|
|
253
|
+
const nullifiers = chunk.map((x) => x.nullifier);
|
|
254
|
+
try {
|
|
255
|
+
const spentFlags = await etherPool.isSpentArray(nullifiers);
|
|
256
|
+
for (let j = 0; j < chunk.length; j++) {
|
|
257
|
+
if (!spentFlags[j]) {
|
|
258
|
+
unspent.push(chunk[j].utxo);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
throw new Error(`Failed to check spent status of UTXOs: ${error instanceof Error ? error.message : String(error)}`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return unspent;
|
|
267
|
+
}
|
|
268
|
+
export async function clearCache(address, token, network) {
|
|
269
|
+
const net = resolveNetwork(network);
|
|
270
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
271
|
+
const storage = getStorage(resolvedToken, net);
|
|
272
|
+
await storage.init();
|
|
273
|
+
await storage.set(`offset_${address}`, 0);
|
|
274
|
+
await storage.set(`keo_${address}`, []);
|
|
275
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { BigNumber } from 'ethers';
|
|
2
|
+
import { Keypair } from './keypair.js';
|
|
3
|
+
export declare class Utxo {
|
|
4
|
+
amount: BigNumber;
|
|
5
|
+
blinding: BigNumber;
|
|
6
|
+
keypair: Keypair;
|
|
7
|
+
index: number | null;
|
|
8
|
+
mintAddress: BigNumber;
|
|
9
|
+
constructor({ amount, keypair, blinding, index, mintAddress }?: {
|
|
10
|
+
amount?: any;
|
|
11
|
+
keypair?: Keypair;
|
|
12
|
+
blinding?: any;
|
|
13
|
+
index?: number | null;
|
|
14
|
+
mintAddress?: any;
|
|
15
|
+
});
|
|
16
|
+
getCommitment(): BigNumber;
|
|
17
|
+
getNullifier(): BigNumber;
|
|
18
|
+
encrypt(encryptionKey: Buffer): string;
|
|
19
|
+
static decrypt(encryptionKey: Buffer, data: string, index: number, keypair: Keypair): Utxo;
|
|
20
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { BigNumber } from 'ethers';
|
|
2
|
+
import { decrypt, encrypt } from './encryption.js';
|
|
3
|
+
import { Keypair } from './keypair.js';
|
|
4
|
+
import { poseidonHash, randomBN } from './utils.js';
|
|
5
|
+
const DUMMY_MINT = BigNumber.from(0);
|
|
6
|
+
export class Utxo {
|
|
7
|
+
amount;
|
|
8
|
+
blinding;
|
|
9
|
+
keypair;
|
|
10
|
+
index;
|
|
11
|
+
mintAddress;
|
|
12
|
+
constructor({ amount = 0, keypair = new Keypair(), blinding = randomBN(), index = null, mintAddress = DUMMY_MINT, } = {}) {
|
|
13
|
+
this.amount = BigNumber.from(amount);
|
|
14
|
+
this.blinding = BigNumber.from(blinding);
|
|
15
|
+
this.keypair = keypair;
|
|
16
|
+
this.index = index;
|
|
17
|
+
this.mintAddress = BigNumber.from(mintAddress);
|
|
18
|
+
}
|
|
19
|
+
getCommitment() {
|
|
20
|
+
return poseidonHash([this.amount, this.keypair.pubkey, this.blinding, this.mintAddress]);
|
|
21
|
+
}
|
|
22
|
+
getNullifier() {
|
|
23
|
+
if (this.amount.gt(0)) {
|
|
24
|
+
if (this.index == null) {
|
|
25
|
+
throw new Error('Can not compute nullifier without utxo index');
|
|
26
|
+
}
|
|
27
|
+
if (this.keypair.privkey == null) {
|
|
28
|
+
throw new Error('Can not compute nullifier without utxo private key');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const commitment = this.getCommitment();
|
|
32
|
+
const idx = this.index || 0;
|
|
33
|
+
const signature = this.keypair.sign(commitment, idx);
|
|
34
|
+
return poseidonHash([commitment, idx, signature]);
|
|
35
|
+
}
|
|
36
|
+
encrypt(encryptionKey) {
|
|
37
|
+
const utxoString = `${this.amount.toString()}|${this.blinding.toString()}|${this.index || 0}|${this.mintAddress.toString()}`;
|
|
38
|
+
return encrypt(utxoString, encryptionKey);
|
|
39
|
+
}
|
|
40
|
+
static decrypt(encryptionKey, data, index, keypair) {
|
|
41
|
+
const decrypted = decrypt(data, encryptionKey);
|
|
42
|
+
const parts = decrypted.toString().split('|');
|
|
43
|
+
if (parts.length !== 4) {
|
|
44
|
+
throw new Error('Invalid UTXO format after decryption');
|
|
45
|
+
}
|
|
46
|
+
const [amount, blinding, , mintAddress] = parts;
|
|
47
|
+
return new Utxo({
|
|
48
|
+
amount: BigNumber.from(amount),
|
|
49
|
+
blinding: BigNumber.from(blinding),
|
|
50
|
+
keypair,
|
|
51
|
+
index,
|
|
52
|
+
mintAddress: BigNumber.from(mintAddress),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { NetworkConfig, PrivacyToken } from './utils/networkConfig.js';
|
|
2
|
+
import type { FeeSnapshot } from './utils/remoteConfig.js';
|
|
3
|
+
export declare function withdraw({ withdrawAmountInput, recipient, keyBasePath, signature, address, token, network, feeSnapshot }: {
|
|
4
|
+
withdrawAmountInput: number;
|
|
5
|
+
recipient: string;
|
|
6
|
+
keyBasePath: string;
|
|
7
|
+
signature: string;
|
|
8
|
+
address: string;
|
|
9
|
+
token?: PrivacyToken;
|
|
10
|
+
network?: NetworkConfig | number;
|
|
11
|
+
feeSnapshot?: FeeSnapshot | null;
|
|
12
|
+
}): Promise<any>;
|