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
package/dist/withdraw.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { BigNumber, ethers } from 'ethers';
|
|
2
|
+
import ERCPoolAbi from './utils/ERCPool.abi.json' with { type: 'json' };
|
|
3
|
+
import EtherPoolAbi from './utils/EtherPool.abi.json' with { type: 'json' };
|
|
4
|
+
import { deriveKeys } from './utils/encryption.js';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
import { getErc20TokenConfig, resolveNetwork, resolvePrivacyToken } from './utils/networkConfig.js';
|
|
7
|
+
import { getRemoteConfig } from './utils/remoteConfig.js';
|
|
8
|
+
import { findUnspentUtxos, prepareTransaction, toFixedHex } from './utils/utils.js';
|
|
9
|
+
import { Utxo } from './utils/utxo.js';
|
|
10
|
+
function formatTokenAmount(value, isErc20, tokenDecimals) {
|
|
11
|
+
return isErc20 ? ethers.utils.formatUnits(value, tokenDecimals) : ethers.utils.formatEther(value);
|
|
12
|
+
}
|
|
13
|
+
function assertFeeFitsWithdrawal(fee, withdrawAmount, isErc20, tokenDecimals, tokenSymbol) {
|
|
14
|
+
if (fee.mul(2).gte(withdrawAmount)) {
|
|
15
|
+
throw new Error(`Withdrawal amount must be more than twice the total fee. Fee is ${formatTokenAmount(fee, isErc20, tokenDecimals)} ${tokenSymbol}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function validateFeeSnapshot({ net, token, feeSnapshot, expiredAsMissing = false, }) {
|
|
19
|
+
const snapshot = feeSnapshot ?? null;
|
|
20
|
+
if (!snapshot || net.chainKey !== 'eth')
|
|
21
|
+
return null;
|
|
22
|
+
if (snapshot.chain !== 'eth') {
|
|
23
|
+
throw new Error(`Unsupported fee snapshot chain: ${snapshot.chain}`);
|
|
24
|
+
}
|
|
25
|
+
if (Date.now() > snapshot.expiresAt) {
|
|
26
|
+
if (expiredAsMissing)
|
|
27
|
+
return null;
|
|
28
|
+
throw new Error('Fee snapshot expired. Please refresh the quote and try again.');
|
|
29
|
+
}
|
|
30
|
+
if (!snapshot.rentFeeUnits?.[token]) {
|
|
31
|
+
throw new Error(`${token.toUpperCase()} rent fee is missing from fee snapshot`);
|
|
32
|
+
}
|
|
33
|
+
return snapshot;
|
|
34
|
+
}
|
|
35
|
+
async function resolveFeeSnapshot({ net, token, remoteConfig, feeSnapshot, }) {
|
|
36
|
+
const providedSnapshot = validateFeeSnapshot({ net, token, feeSnapshot });
|
|
37
|
+
if (providedSnapshot || net.chainKey !== 'eth') {
|
|
38
|
+
return { remoteConfig, feeSnapshot: providedSnapshot };
|
|
39
|
+
}
|
|
40
|
+
const configSnapshot = validateFeeSnapshot({
|
|
41
|
+
net,
|
|
42
|
+
token,
|
|
43
|
+
feeSnapshot: remoteConfig.feeSnapshot,
|
|
44
|
+
expiredAsMissing: true,
|
|
45
|
+
});
|
|
46
|
+
if (configSnapshot) {
|
|
47
|
+
return { remoteConfig, feeSnapshot: configSnapshot };
|
|
48
|
+
}
|
|
49
|
+
logger.info('ETH fee snapshot missing from cached config, refreshing remote config');
|
|
50
|
+
const refreshedConfig = await getRemoteConfig(net, { forceRefresh: true });
|
|
51
|
+
const refreshedSnapshot = validateFeeSnapshot({
|
|
52
|
+
net,
|
|
53
|
+
token,
|
|
54
|
+
feeSnapshot: refreshedConfig.feeSnapshot,
|
|
55
|
+
});
|
|
56
|
+
if (!refreshedSnapshot) {
|
|
57
|
+
throw new Error('ETH fee snapshot is required. Please refresh the quote and try again.');
|
|
58
|
+
}
|
|
59
|
+
return { remoteConfig: refreshedConfig, feeSnapshot: refreshedSnapshot };
|
|
60
|
+
}
|
|
61
|
+
function getFlatFeeUnits({ rentFee, feeSnapshot, token, isErc20, tokenDecimals, }) {
|
|
62
|
+
const snapshotRentFee = feeSnapshot?.rentFeeUnits?.[token];
|
|
63
|
+
if (snapshotRentFee)
|
|
64
|
+
return BigNumber.from(snapshotRentFee);
|
|
65
|
+
return isErc20
|
|
66
|
+
? ethers.utils.parseUnits(rentFee.toFixed(tokenDecimals), tokenDecimals)
|
|
67
|
+
: ethers.utils.parseEther(rentFee.toFixed(18));
|
|
68
|
+
}
|
|
69
|
+
function logWithdrawFeeBreakdown({ flatFee, rateFee, totalFee, withdrawAmount, feeRate, isErc20, tokenDecimals, tokenSymbol, }) {
|
|
70
|
+
logger.info(`Withdraw fee breakdown: rent/network fee=${formatTokenAmount(flatFee, isErc20, tokenDecimals)} ${tokenSymbol}, ` +
|
|
71
|
+
`protocol fee=${formatTokenAmount(rateFee, isErc20, tokenDecimals)} ${tokenSymbol} (${feeRate / 100}% of ${formatTokenAmount(withdrawAmount, isErc20, tokenDecimals)} ${tokenSymbol}), ` +
|
|
72
|
+
`total fee=${formatTokenAmount(totalFee, isErc20, tokenDecimals)} ${tokenSymbol}`);
|
|
73
|
+
}
|
|
74
|
+
export async function withdraw({ withdrawAmountInput, recipient, keyBasePath, signature, address, token, network, feeSnapshot }) {
|
|
75
|
+
if (!ethers.utils.isAddress(recipient)) {
|
|
76
|
+
throw new Error(`Invalid recipient address: ${recipient}`);
|
|
77
|
+
}
|
|
78
|
+
const net = resolveNetwork(network);
|
|
79
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
80
|
+
const erc20Token = getErc20TokenConfig(net, resolvedToken);
|
|
81
|
+
const isErc20 = erc20Token !== null;
|
|
82
|
+
const tokenSymbol = erc20Token?.symbol ?? net.nativeSymbol ?? (net.chainKey === 'bnb' ? 'BNB' : 'ETH');
|
|
83
|
+
const tokenDecimals = erc20Token?.decimals ?? 18;
|
|
84
|
+
let remoteConfig = await getRemoteConfig(net);
|
|
85
|
+
const snapshotResolution = await resolveFeeSnapshot({
|
|
86
|
+
net,
|
|
87
|
+
token: resolvedToken,
|
|
88
|
+
remoteConfig,
|
|
89
|
+
feeSnapshot,
|
|
90
|
+
});
|
|
91
|
+
remoteConfig = snapshotResolution.remoteConfig;
|
|
92
|
+
const activeFeeSnapshot = snapshotResolution.feeSnapshot;
|
|
93
|
+
const minWithdrawal = remoteConfig.minimum_withdrawal[resolvedToken];
|
|
94
|
+
const rentFee = remoteConfig.rent_fees[resolvedToken];
|
|
95
|
+
const feeRate = activeFeeSnapshot?.feeRateBps ?? remoteConfig.fee_rate;
|
|
96
|
+
if (isErc20) {
|
|
97
|
+
if (withdrawAmountInput < minWithdrawal) {
|
|
98
|
+
throw new Error(`Withdrawal amount must be at least ${minWithdrawal} ${tokenSymbol}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
if (withdrawAmountInput < minWithdrawal) {
|
|
103
|
+
throw new Error(`Withdrawal amount must be at least ${minWithdrawal} ${tokenSymbol}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const poolAddress = ethers.utils.getAddress(erc20Token ? erc20Token.poolAddress : net.etherPoolAddress);
|
|
107
|
+
const abi = isErc20 ? ERCPoolAbi : EtherPoolAbi;
|
|
108
|
+
const feeRecipient = net.feeRecipientAddress;
|
|
109
|
+
logger.debug(`Withdrawing ${withdrawAmountInput} ${tokenSymbol} to recipient: ${recipient}`);
|
|
110
|
+
const { encryptionKey, keypair } = deriveKeys(signature);
|
|
111
|
+
logger.debug(`UTXO pubkey: ${toFixedHex(keypair.pubkey)}`);
|
|
112
|
+
const readProvider = new ethers.providers.JsonRpcProvider(net.rpcUrl, {
|
|
113
|
+
name: net.chainKey,
|
|
114
|
+
chainId: net.chainId,
|
|
115
|
+
});
|
|
116
|
+
const pool = new ethers.Contract(poolAddress, abi, readProvider);
|
|
117
|
+
const withdrawAmount = isErc20
|
|
118
|
+
? ethers.utils.parseUnits(withdrawAmountInput.toString(), tokenDecimals)
|
|
119
|
+
: ethers.utils.parseEther(withdrawAmountInput.toString());
|
|
120
|
+
// Scan on-chain events to find unspent UTXOs
|
|
121
|
+
logger.info('loading utxos');
|
|
122
|
+
const unspent = await findUnspentUtxos({
|
|
123
|
+
etherPool: pool,
|
|
124
|
+
encryptionKey,
|
|
125
|
+
keypair,
|
|
126
|
+
address,
|
|
127
|
+
token: resolvedToken,
|
|
128
|
+
network: net,
|
|
129
|
+
});
|
|
130
|
+
logger.debug(`Unspent UTXOs found: ${unspent.length}`);
|
|
131
|
+
if (unspent.length === 0) {
|
|
132
|
+
throw new Error('No unspent UTXOs available to withdraw.');
|
|
133
|
+
}
|
|
134
|
+
let inputs;
|
|
135
|
+
if (unspent.length >= 2) {
|
|
136
|
+
inputs = [unspent[0], unspent[1]];
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
inputs = [unspent[0]];
|
|
140
|
+
}
|
|
141
|
+
const inputSum = inputs.reduce((sum, u) => sum.add(u.amount), BigNumber.from(0));
|
|
142
|
+
if (inputSum.lt(withdrawAmount)) {
|
|
143
|
+
const have = isErc20 ? ethers.utils.formatUnits(inputSum, tokenDecimals) : ethers.utils.formatEther(inputSum);
|
|
144
|
+
const need = isErc20 ? ethers.utils.formatUnits(withdrawAmount, tokenDecimals) : ethers.utils.formatEther(withdrawAmount);
|
|
145
|
+
throw new Error(`Insufficient balance. Have ${have}, need ${need} (${withdrawAmountInput}).`);
|
|
146
|
+
}
|
|
147
|
+
const changeAmount = inputSum.sub(withdrawAmount);
|
|
148
|
+
const outputs = [];
|
|
149
|
+
if (changeAmount.gt(0)) {
|
|
150
|
+
// Change UTXOs for ERC20 pools must carry the same mintAddress.
|
|
151
|
+
const mintAddress = erc20Token ? BigNumber.from(erc20Token.tokenAddress) : BigNumber.from(0);
|
|
152
|
+
outputs.push(new Utxo({ amount: changeAmount, keypair, mintAddress }));
|
|
153
|
+
const formattedChange = isErc20
|
|
154
|
+
? `${ethers.utils.formatUnits(changeAmount, tokenDecimals)} ${tokenSymbol}`
|
|
155
|
+
: `${ethers.utils.formatEther(changeAmount)} ${tokenSymbol}`;
|
|
156
|
+
logger.debug(`Change UTXO: ${formattedChange}`);
|
|
157
|
+
}
|
|
158
|
+
const fixedFlatFee = getFlatFeeUnits({
|
|
159
|
+
rentFee,
|
|
160
|
+
feeSnapshot: activeFeeSnapshot,
|
|
161
|
+
token: resolvedToken,
|
|
162
|
+
isErc20,
|
|
163
|
+
tokenDecimals,
|
|
164
|
+
});
|
|
165
|
+
let flatFee = fixedFlatFee;
|
|
166
|
+
const rateFee = withdrawAmount.mul(feeRate).div(10000);
|
|
167
|
+
let fee = flatFee.add(rateFee);
|
|
168
|
+
logWithdrawFeeBreakdown({
|
|
169
|
+
flatFee,
|
|
170
|
+
rateFee,
|
|
171
|
+
totalFee: fee,
|
|
172
|
+
withdrawAmount,
|
|
173
|
+
feeRate,
|
|
174
|
+
isErc20,
|
|
175
|
+
tokenDecimals,
|
|
176
|
+
tokenSymbol,
|
|
177
|
+
});
|
|
178
|
+
assertFeeFitsWithdrawal(fee, withdrawAmount, isErc20, tokenDecimals, tokenSymbol);
|
|
179
|
+
if (activeFeeSnapshot) {
|
|
180
|
+
logger.info(`using ETH fee snapshot ${activeFeeSnapshot.id}`);
|
|
181
|
+
}
|
|
182
|
+
if (net.chainKey === 'eth' && !activeFeeSnapshot) {
|
|
183
|
+
throw new Error('ETH fee snapshot is required. Please refresh the quote and try again.');
|
|
184
|
+
}
|
|
185
|
+
if (isErc20) {
|
|
186
|
+
logger.debug(`Input UTXOs: ${inputs.length} (total: ${ethers.utils.formatUnits(inputSum, tokenDecimals)} ${tokenSymbol})`);
|
|
187
|
+
logger.debug(`Fee: ${ethers.utils.formatUnits(fee, tokenDecimals)} ${tokenSymbol} (${ethers.utils.formatUnits(flatFee, tokenDecimals)} ${tokenSymbol} + ${feeRate / 100}%)`);
|
|
188
|
+
logger.debug(`Amount to arrive at recipient: ${ethers.utils.formatUnits(withdrawAmount.sub(fee), tokenDecimals)} ${tokenSymbol}`);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
logger.debug(`Input UTXOs: ${inputs.length} (total: ${ethers.utils.formatEther(inputSum)} ${tokenSymbol})`);
|
|
192
|
+
logger.debug(`Fee: ${ethers.utils.formatEther(fee)} ${tokenSymbol} (${ethers.utils.formatEther(flatFee)} + ${feeRate / 100}%)`);
|
|
193
|
+
logger.debug(`Amount to arrive at recipient: ${ethers.utils.formatEther(withdrawAmount.sub(fee))} ${tokenSymbol}`);
|
|
194
|
+
}
|
|
195
|
+
logger.info('generating ZK proof');
|
|
196
|
+
const { args, extData } = await prepareTransaction({
|
|
197
|
+
inputs,
|
|
198
|
+
outputs,
|
|
199
|
+
recipient,
|
|
200
|
+
fee,
|
|
201
|
+
feeRecipient,
|
|
202
|
+
encryptionKey,
|
|
203
|
+
keyBasePath,
|
|
204
|
+
token: resolvedToken,
|
|
205
|
+
network: net,
|
|
206
|
+
});
|
|
207
|
+
logger.info('submitting transaction to relayer...');
|
|
208
|
+
const response = await fetch(`${net.indexerUrl}/relayer/withdraw`, {
|
|
209
|
+
method: 'POST',
|
|
210
|
+
headers: { 'Content-Type': 'application/json' },
|
|
211
|
+
body: JSON.stringify({ args, extData, token: resolvedToken, chain: net.chainKey, feeSnapshotId: activeFeeSnapshot?.id }),
|
|
212
|
+
});
|
|
213
|
+
const result = await response.json();
|
|
214
|
+
if (response.ok && result.success) {
|
|
215
|
+
logger.debug(`Transaction relayed successfully: ${result.txHash}`);
|
|
216
|
+
logger.debug(`Confirmed in block ${result.blockNumber}`);
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
throw new Error(`Relayer error: ${result.error || response.statusText}`);
|
|
220
|
+
}
|
|
221
|
+
if (changeAmount.gt(0)) {
|
|
222
|
+
const formattedChange = isErc20
|
|
223
|
+
? `${ethers.utils.formatUnits(changeAmount, tokenDecimals)} ${tokenSymbol}`
|
|
224
|
+
: `${ethers.utils.formatEther(changeAmount)} ${tokenSymbol}`;
|
|
225
|
+
logger.debug(`\nChange UTXO created (${formattedChange})`);
|
|
226
|
+
}
|
|
227
|
+
logger.info('confirming transaction');
|
|
228
|
+
let retryTimes = 0;
|
|
229
|
+
const itv = 3;
|
|
230
|
+
let start = Date.now();
|
|
231
|
+
while (true) {
|
|
232
|
+
logger.debug('Confirming transaction..');
|
|
233
|
+
logger.debug(`retryTimes: ${retryTimes}`);
|
|
234
|
+
await new Promise(resolve => setTimeout(resolve, itv * 1000));
|
|
235
|
+
logger.debug('Fetching updated onchain state...');
|
|
236
|
+
let res = await fetch(net.indexerUrl + '/check_encrypted_output', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ encryptedOutput: extData.encryptedOutput1, token: resolvedToken, chain: net.chainKey }),
|
|
240
|
+
});
|
|
241
|
+
let resJson = await res.json();
|
|
242
|
+
if (resJson.exists) {
|
|
243
|
+
logger.debug(`Withdrawal confirmed in ${((Date.now() - start) / 1000).toFixed(2)} seconds!`);
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
if (retryTimes >= 10) {
|
|
247
|
+
throw new Error('Refresh the page to see latest balance.');
|
|
248
|
+
}
|
|
249
|
+
retryTimes++;
|
|
250
|
+
}
|
|
251
|
+
logger.debug('\nwithdrawal successful!');
|
|
252
|
+
return result.txHash;
|
|
253
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "testprivacycash-evm",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/Privacy-Cash/privacy-cash-sdk-evm.git"
|
|
7
|
+
},
|
|
8
|
+
"description": "",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"author": "",
|
|
19
|
+
"license": "ISC",
|
|
20
|
+
"type": "module",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"ethers": "^5.0.0",
|
|
23
|
+
"ffjavascript": "^0.2.36",
|
|
24
|
+
"poseidon-lite": "^0.3.0",
|
|
25
|
+
"snarkjs": "^0.7.5",
|
|
26
|
+
"sqlite3": "^6.0.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^25.3.3",
|
|
30
|
+
"@types/snarkjs": "^0.7.9",
|
|
31
|
+
"babel-eslint": "^10.1.0",
|
|
32
|
+
"eslint": "^7.28.0",
|
|
33
|
+
"eslint-config-prettier": "^8.3.0",
|
|
34
|
+
"eslint-plugin-prettier": "^3.4.0",
|
|
35
|
+
"typescript": "^6.0.3"
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/balance.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { BigNumber, ethers } from 'ethers';
|
|
2
|
+
import ERCPoolAbi from './utils/ERCPool.abi.json' with { type: 'json' };
|
|
3
|
+
import EtherPoolAbi from './utils/EtherPool.abi.json' with { type: 'json' };
|
|
4
|
+
import { deriveKeys } from './utils/encryption.js';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
import { NetworkConfig, PrivacyToken, getErc20TokenConfig, resolveNetwork, resolvePrivacyToken } from './utils/networkConfig.js';
|
|
7
|
+
import { findUnspentUtxos, toFixedHex } from './utils/utils.js';
|
|
8
|
+
|
|
9
|
+
export async function getBalance({ signature, address, offset, token, network }: {
|
|
10
|
+
signature: string,
|
|
11
|
+
address: string,
|
|
12
|
+
offset?: number,
|
|
13
|
+
token?: PrivacyToken,
|
|
14
|
+
network?: NetworkConfig | number,
|
|
15
|
+
}) {
|
|
16
|
+
const net = resolveNetwork(network);
|
|
17
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
18
|
+
const readProvider = new ethers.providers.JsonRpcProvider(net.rpcUrl, {
|
|
19
|
+
name: net.chainKey,
|
|
20
|
+
chainId: net.chainId,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const erc20Token = getErc20TokenConfig(net, resolvedToken);
|
|
24
|
+
const isErc20 = erc20Token !== null;
|
|
25
|
+
const tokenSymbol = erc20Token?.symbol ?? net.nativeSymbol ?? (net.chainKey === 'bnb' ? 'BNB' : 'ETH');
|
|
26
|
+
const tokenDecimals = erc20Token?.decimals ?? 18;
|
|
27
|
+
const contractAddress = ethers.utils.getAddress(erc20Token ? erc20Token.poolAddress : net.etherPoolAddress);
|
|
28
|
+
const abi = isErc20 ? ERCPoolAbi : EtherPoolAbi;
|
|
29
|
+
|
|
30
|
+
logger.debug(`Address: ${address}`);
|
|
31
|
+
|
|
32
|
+
logger.debug('Signing in to derive keys...');
|
|
33
|
+
const { encryptionKey, keypair } = deriveKeys(signature);
|
|
34
|
+
logger.debug(`UTXO pubkey: ${toFixedHex(keypair.pubkey)}`);
|
|
35
|
+
|
|
36
|
+
const pool = new ethers.Contract(contractAddress, abi, readProvider);
|
|
37
|
+
|
|
38
|
+
if (!isErc20) {
|
|
39
|
+
const poolBalance = await readProvider.getBalance(pool.address);
|
|
40
|
+
logger.debug(`EtherPool: ${pool.address}`);
|
|
41
|
+
logger.debug(`Pool on-chain balance: ${ethers.utils.formatEther(poolBalance)} ${tokenSymbol}`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Scan on-chain events and decrypt to find our UTXOs
|
|
45
|
+
logger.debug('Scanning on-chain events...');
|
|
46
|
+
const unspent = await findUnspentUtxos({
|
|
47
|
+
etherPool: pool,
|
|
48
|
+
encryptionKey,
|
|
49
|
+
keypair,
|
|
50
|
+
address,
|
|
51
|
+
start: offset || 0,
|
|
52
|
+
token: resolvedToken,
|
|
53
|
+
network: net,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
logger.debug(`Unspent UTXOs: ${unspent.length}`);
|
|
57
|
+
let total = BigNumber.from(0);
|
|
58
|
+
for (let i = 0; i < unspent.length; i++) {
|
|
59
|
+
const utxo = unspent[i];
|
|
60
|
+
const formatted = isErc20
|
|
61
|
+
? ethers.utils.formatUnits(utxo.amount, tokenDecimals)
|
|
62
|
+
: ethers.utils.formatEther(utxo.amount);
|
|
63
|
+
logger.debug(` #${i}: ${formatted} ${tokenSymbol} (index: ${utxo.index})`);
|
|
64
|
+
total = total.add(utxo.amount);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const balance = isErc20
|
|
68
|
+
? ethers.utils.formatUnits(total, tokenDecimals)
|
|
69
|
+
: ethers.utils.formatEther(total);
|
|
70
|
+
|
|
71
|
+
logger.debug(`\nTotal unspent: ${balance} ${tokenSymbol}`);
|
|
72
|
+
|
|
73
|
+
return { balance };
|
|
74
|
+
}
|