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,55 @@
|
|
|
1
|
+
name: Publish to npm when version changes
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
- master
|
|
8
|
+
paths:
|
|
9
|
+
- "package.json"
|
|
10
|
+
|
|
11
|
+
permissions:
|
|
12
|
+
id-token: write
|
|
13
|
+
contents: read
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
publish:
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
|
|
19
|
+
steps:
|
|
20
|
+
- name: Checkout repository
|
|
21
|
+
uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: 24
|
|
27
|
+
registry-url: https://registry.npmjs.org/
|
|
28
|
+
|
|
29
|
+
- name: Get current package version
|
|
30
|
+
id: pkg
|
|
31
|
+
run: echo "version=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
|
|
32
|
+
|
|
33
|
+
- name: Check if version already exists on npm
|
|
34
|
+
id: check
|
|
35
|
+
run: |
|
|
36
|
+
PKG_NAME=$(node -p 'require("./package.json").name')
|
|
37
|
+
PUBLISHED=$(npm view $PKG_NAME versions --json | grep -o "\"${{ steps.pkg.outputs.version }}\"" || true)
|
|
38
|
+
if [ -n "$PUBLISHED" ]; then
|
|
39
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
40
|
+
echo "Version ${{ steps.pkg.outputs.version }} already exists on npm. Skipping publish."
|
|
41
|
+
else
|
|
42
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
- name: Install dependencies
|
|
46
|
+
run: npm install
|
|
47
|
+
|
|
48
|
+
- name: Build source
|
|
49
|
+
run: npm run build || echo "No build script defined."
|
|
50
|
+
|
|
51
|
+
- name: Publish to npm
|
|
52
|
+
if: steps.check.outputs.exists == 'false'
|
|
53
|
+
run: |
|
|
54
|
+
echo "Publishing version ${{ steps.pkg.outputs.version }}..."
|
|
55
|
+
npm publish --access public
|
package/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Base SDK
|
|
2
|
+
This is the SDK for Privacy Cash on Base. For documentation, please check: https://privacycash.mintlify.app/basesdk/frontend
|
|
3
|
+
|
|
4
|
+
The SDK also supports BNB mainnet native BNB through `BNB_NETWORK` (chain ID
|
|
5
|
+
56). Pass `token: 'bnb'` explicitly, or omit `token` when `network` is
|
|
6
|
+
`BNB_NETWORK`. BNB USDT is not supported yet.
|
|
7
|
+
|
|
8
|
+
Run `bun example/bnb-bnb.ts balance` for the BNB SDK example. Deposit and
|
|
9
|
+
withdraw actions require an amount and `BNB_PRIVATE_KEY`. The Base and Ethereum
|
|
10
|
+
examples continue to use `PRIVATE_KEY`.
|
|
11
|
+
|
|
12
|
+
### Disclaimer
|
|
13
|
+
This SDK powers Privacy Cash's frontend, assuming the single wallet use case. It is NOT supposed to support hardware wallet.
|
|
14
|
+
|
|
15
|
+
If you use it or published npm library from this repo, please fully test and beware of the inherent software risks or potential bugs. Our team is not responsible for any losses.
|
|
16
|
+
|
|
17
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NetworkConfig, PrivacyToken } from './utils/networkConfig.js';
|
|
2
|
+
export declare function getBalance({ signature, address, offset, token, network }: {
|
|
3
|
+
signature: string;
|
|
4
|
+
address: string;
|
|
5
|
+
offset?: number;
|
|
6
|
+
token?: PrivacyToken;
|
|
7
|
+
network?: NetworkConfig | number;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
balance: string;
|
|
10
|
+
}>;
|
package/dist/balance.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
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 { findUnspentUtxos, toFixedHex } from './utils/utils.js';
|
|
8
|
+
export async function getBalance({ signature, address, offset, token, network }) {
|
|
9
|
+
const net = resolveNetwork(network);
|
|
10
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
11
|
+
const readProvider = new ethers.providers.JsonRpcProvider(net.rpcUrl, {
|
|
12
|
+
name: net.chainKey,
|
|
13
|
+
chainId: net.chainId,
|
|
14
|
+
});
|
|
15
|
+
const erc20Token = getErc20TokenConfig(net, resolvedToken);
|
|
16
|
+
const isErc20 = erc20Token !== null;
|
|
17
|
+
const tokenSymbol = erc20Token?.symbol ?? net.nativeSymbol ?? (net.chainKey === 'bnb' ? 'BNB' : 'ETH');
|
|
18
|
+
const tokenDecimals = erc20Token?.decimals ?? 18;
|
|
19
|
+
const contractAddress = ethers.utils.getAddress(erc20Token ? erc20Token.poolAddress : net.etherPoolAddress);
|
|
20
|
+
const abi = isErc20 ? ERCPoolAbi : EtherPoolAbi;
|
|
21
|
+
logger.debug(`Address: ${address}`);
|
|
22
|
+
logger.debug('Signing in to derive keys...');
|
|
23
|
+
const { encryptionKey, keypair } = deriveKeys(signature);
|
|
24
|
+
logger.debug(`UTXO pubkey: ${toFixedHex(keypair.pubkey)}`);
|
|
25
|
+
const pool = new ethers.Contract(contractAddress, abi, readProvider);
|
|
26
|
+
if (!isErc20) {
|
|
27
|
+
const poolBalance = await readProvider.getBalance(pool.address);
|
|
28
|
+
logger.debug(`EtherPool: ${pool.address}`);
|
|
29
|
+
logger.debug(`Pool on-chain balance: ${ethers.utils.formatEther(poolBalance)} ${tokenSymbol}`);
|
|
30
|
+
}
|
|
31
|
+
// Scan on-chain events and decrypt to find our UTXOs
|
|
32
|
+
logger.debug('Scanning on-chain events...');
|
|
33
|
+
const unspent = await findUnspentUtxos({
|
|
34
|
+
etherPool: pool,
|
|
35
|
+
encryptionKey,
|
|
36
|
+
keypair,
|
|
37
|
+
address,
|
|
38
|
+
start: offset || 0,
|
|
39
|
+
token: resolvedToken,
|
|
40
|
+
network: net,
|
|
41
|
+
});
|
|
42
|
+
logger.debug(`Unspent UTXOs: ${unspent.length}`);
|
|
43
|
+
let total = BigNumber.from(0);
|
|
44
|
+
for (let i = 0; i < unspent.length; i++) {
|
|
45
|
+
const utxo = unspent[i];
|
|
46
|
+
const formatted = isErc20
|
|
47
|
+
? ethers.utils.formatUnits(utxo.amount, tokenDecimals)
|
|
48
|
+
: ethers.utils.formatEther(utxo.amount);
|
|
49
|
+
logger.debug(` #${i}: ${formatted} ${tokenSymbol} (index: ${utxo.index})`);
|
|
50
|
+
total = total.add(utxo.amount);
|
|
51
|
+
}
|
|
52
|
+
const balance = isErc20
|
|
53
|
+
? ethers.utils.formatUnits(total, tokenDecimals)
|
|
54
|
+
: ethers.utils.formatEther(total);
|
|
55
|
+
logger.debug(`\nTotal unspent: ${balance} ${tokenSymbol}`);
|
|
56
|
+
return { balance };
|
|
57
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { NetworkConfig, PrivacyToken } from './utils/networkConfig.js';
|
|
2
|
+
export declare function deposit({ depositAmountInput, keyBasePath, signature, address, txSender, token, network }: {
|
|
3
|
+
depositAmountInput: number;
|
|
4
|
+
keyBasePath: string;
|
|
5
|
+
signature: string;
|
|
6
|
+
address: string;
|
|
7
|
+
txSender: any;
|
|
8
|
+
token?: PrivacyToken;
|
|
9
|
+
network?: NetworkConfig | number;
|
|
10
|
+
}): Promise<any>;
|
package/dist/deposit.js
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
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 getFeeBumpPercent() {
|
|
11
|
+
const value = Number(process.env.NEXT_PUBLIC_EVM_FEE_BUMP_PERCENT || process.env.EVM_FEE_BUMP_PERCENT);
|
|
12
|
+
if (Number.isFinite(value) && value >= 100)
|
|
13
|
+
return Math.floor(value);
|
|
14
|
+
return 110;
|
|
15
|
+
}
|
|
16
|
+
function bumpFee(value, percent = getFeeBumpPercent()) {
|
|
17
|
+
return value.mul(percent).div(100);
|
|
18
|
+
}
|
|
19
|
+
const MAX_PRIORITY_FEE_GWEI = process.env.NEXT_PUBLIC_MAX_PRIORITY_FEE_GWEI || process.env.MAX_PRIORITY_FEE_GWEI || '0.6';
|
|
20
|
+
const MAX_PRIORITY_FEE = ethers.utils.parseUnits(MAX_PRIORITY_FEE_GWEI, 'gwei');
|
|
21
|
+
function formatGwei(value) {
|
|
22
|
+
return value ? ethers.utils.formatUnits(value, 'gwei') : 'null';
|
|
23
|
+
}
|
|
24
|
+
function getGasLimitBumpPercent() {
|
|
25
|
+
const value = Number(process.env.NEXT_PUBLIC_EVM_GAS_LIMIT_BUMP_PERCENT || process.env.EVM_GAS_LIMIT_BUMP_PERCENT);
|
|
26
|
+
if (Number.isFinite(value) && value >= 100)
|
|
27
|
+
return Math.floor(value);
|
|
28
|
+
return 120;
|
|
29
|
+
}
|
|
30
|
+
function bumpGasLimit(value, percent = getGasLimitBumpPercent()) {
|
|
31
|
+
return value.mul(percent).add(99).div(100);
|
|
32
|
+
}
|
|
33
|
+
function getMinPriorityFee(net) {
|
|
34
|
+
const configured = process.env.NEXT_PUBLIC_ETH_MIN_PRIORITY_FEE_GWEI || process.env.ETH_MIN_PRIORITY_FEE_GWEI;
|
|
35
|
+
const gwei = configured || (net.chainKey === 'eth' ? '0.2' : '');
|
|
36
|
+
return gwei ? ethers.utils.parseUnits(gwei, 'gwei') : null;
|
|
37
|
+
}
|
|
38
|
+
function selectPriorityFee(providerPriorityFee, minPriorityFee) {
|
|
39
|
+
const priorityFee = providerPriorityFee && !providerPriorityFee.isZero()
|
|
40
|
+
? providerPriorityFee
|
|
41
|
+
: (minPriorityFee ?? BigNumber.from(0));
|
|
42
|
+
return priorityFee.gt(MAX_PRIORITY_FEE) ? MAX_PRIORITY_FEE : priorityFee;
|
|
43
|
+
}
|
|
44
|
+
async function getFeeOverrides(net, provider, feeData) {
|
|
45
|
+
feeData ??= await provider.getFeeData();
|
|
46
|
+
// BNB Smart Chain's legacy gasPrice is the most widely supported path.
|
|
47
|
+
if (net.chainKey === 'bnb' && feeData.gasPrice) {
|
|
48
|
+
return { gasPrice: bumpFee(feeData.gasPrice) };
|
|
49
|
+
}
|
|
50
|
+
if (feeData.maxFeePerGas || feeData.maxPriorityFeePerGas) {
|
|
51
|
+
const minPriorityFee = getMinPriorityFee(net);
|
|
52
|
+
logger.debug(`Deposit feeData: gasPrice=${formatGwei(feeData.gasPrice)} gwei, ` +
|
|
53
|
+
`maxFeePerGas=${formatGwei(feeData.maxFeePerGas)} gwei, ` +
|
|
54
|
+
`maxPriorityFeePerGas=${formatGwei(feeData.maxPriorityFeePerGas)} gwei, ` +
|
|
55
|
+
`lastBaseFeePerGas=${formatGwei(feeData.lastBaseFeePerGas)} gwei, ` +
|
|
56
|
+
`defaultMinPriorityFee=${formatGwei(minPriorityFee)} gwei, ` +
|
|
57
|
+
`maxPriorityFeeCap=${MAX_PRIORITY_FEE_GWEI} gwei, ` +
|
|
58
|
+
`feeBumpPercent=${getFeeBumpPercent()}`);
|
|
59
|
+
const priorityFee = selectPriorityFee(feeData.maxPriorityFeePerGas, minPriorityFee);
|
|
60
|
+
if (priorityFee.isZero()) {
|
|
61
|
+
throw new Error('Failed to fetch network priority fee data');
|
|
62
|
+
}
|
|
63
|
+
let maxFeeBase = feeData.maxFeePerGas;
|
|
64
|
+
let fallbackBaseFee = null;
|
|
65
|
+
if (!maxFeeBase) {
|
|
66
|
+
const latestBlock = await provider.getBlock('latest');
|
|
67
|
+
fallbackBaseFee = latestBlock?.baseFeePerGas ?? null;
|
|
68
|
+
maxFeeBase = latestBlock?.baseFeePerGas
|
|
69
|
+
? latestBlock.baseFeePerGas.mul(2).add(priorityFee)
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
if (!maxFeeBase) {
|
|
73
|
+
throw new Error('Failed to fetch network max fee data');
|
|
74
|
+
}
|
|
75
|
+
const maxFeePerGas = bumpFee(maxFeeBase);
|
|
76
|
+
const finalMaxFeePerGas = maxFeePerGas.gt(priorityFee) ? maxFeePerGas : priorityFee.mul(2);
|
|
77
|
+
logger.debug(`Deposit fee overrides: selectedPriorityFee=${formatGwei(priorityFee)} gwei, ` +
|
|
78
|
+
`fallbackLatestBaseFee=${formatGwei(fallbackBaseFee)} gwei, ` +
|
|
79
|
+
`maxFeeBase=${formatGwei(maxFeeBase)} gwei, ` +
|
|
80
|
+
`finalMaxFeePerGas=${formatGwei(finalMaxFeePerGas)} gwei`);
|
|
81
|
+
return {
|
|
82
|
+
type: 2,
|
|
83
|
+
maxPriorityFeePerGas: priorityFee,
|
|
84
|
+
maxFeePerGas: finalMaxFeePerGas,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (feeData.gasPrice) {
|
|
88
|
+
const gasPrice = bumpFee(feeData.gasPrice);
|
|
89
|
+
logger.debug(`Deposit legacy feeData: providerGasPrice=${formatGwei(feeData.gasPrice)} gwei, ` +
|
|
90
|
+
`finalGasPrice=${formatGwei(gasPrice)} gwei, feeBumpPercent=${getFeeBumpPercent()}`);
|
|
91
|
+
return { gasPrice };
|
|
92
|
+
}
|
|
93
|
+
throw new Error('Failed to fetch network fee data');
|
|
94
|
+
}
|
|
95
|
+
async function estimateTransactGasLimit({ pool, args, extData, estimateOverrides, fallback, }) {
|
|
96
|
+
try {
|
|
97
|
+
const estimated = await pool.estimateGas.transact(args, extData, estimateOverrides);
|
|
98
|
+
const gasLimit = bumpGasLimit(estimated);
|
|
99
|
+
logger.debug(`Estimated transact gas: ${estimated.toString()} (using limit ${gasLimit.toString()})`);
|
|
100
|
+
return gasLimit;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.warn(`Failed to estimate transact gas; using fallback ${fallback.toString()}. Error:`, err);
|
|
104
|
+
return fallback;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
export async function deposit({ depositAmountInput, keyBasePath, signature, address, txSender, token, network }) {
|
|
108
|
+
const net = resolveNetwork(network);
|
|
109
|
+
const resolvedToken = resolvePrivacyToken(net, token);
|
|
110
|
+
const readProvider = new ethers.providers.JsonRpcProvider(net.rpcUrl, {
|
|
111
|
+
name: net.chainKey,
|
|
112
|
+
chainId: net.chainId,
|
|
113
|
+
});
|
|
114
|
+
const erc20Token = getErc20TokenConfig(net, resolvedToken);
|
|
115
|
+
const isErc20 = erc20Token !== null;
|
|
116
|
+
const tokenSymbol = erc20Token?.symbol ?? net.nativeSymbol ?? (net.chainKey === 'bnb' ? 'BNB' : 'ETH');
|
|
117
|
+
const tokenDecimals = erc20Token?.decimals ?? 18;
|
|
118
|
+
const remoteConfig = await getRemoteConfig(net);
|
|
119
|
+
const minDeposit = remoteConfig.minimum_deposit[resolvedToken];
|
|
120
|
+
if (isErc20) {
|
|
121
|
+
if (depositAmountInput < minDeposit) {
|
|
122
|
+
throw new Error(`Deposit amount must be at least ${minDeposit} ${tokenSymbol}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (depositAmountInput < minDeposit) {
|
|
127
|
+
throw new Error(`Deposit amount must be at least ${minDeposit} ${tokenSymbol}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const poolAddress = ethers.utils.getAddress(erc20Token ? erc20Token.poolAddress : net.etherPoolAddress);
|
|
131
|
+
const abi = isErc20 ? ERCPoolAbi : EtherPoolAbi;
|
|
132
|
+
logger.debug(`Depositor: ${address}`);
|
|
133
|
+
const { encryptionKey, keypair } = deriveKeys(signature);
|
|
134
|
+
logger.debug(`UTXO pubkey: ${toFixedHex(keypair.pubkey)}`);
|
|
135
|
+
const pool = new ethers.Contract(poolAddress, abi, readProvider);
|
|
136
|
+
if (!isErc20) {
|
|
137
|
+
const poolBalance = await readProvider.getBalance(pool.address);
|
|
138
|
+
logger.debug(`EtherPool: ${pool.address}`);
|
|
139
|
+
logger.debug(`Pool balance: ${ethers.utils.formatEther(poolBalance)} ${tokenSymbol}`);
|
|
140
|
+
}
|
|
141
|
+
const depositAmount = isErc20
|
|
142
|
+
? ethers.utils.parseUnits(depositAmountInput.toString(), tokenDecimals)
|
|
143
|
+
: ethers.utils.parseEther(depositAmountInput.toString());
|
|
144
|
+
const maxDeposit = await pool.maximumDepositAmount();
|
|
145
|
+
if (depositAmount.gt(maxDeposit)) {
|
|
146
|
+
const formatted = isErc20
|
|
147
|
+
? `${ethers.utils.formatUnits(maxDeposit, tokenDecimals)} ${tokenSymbol}`
|
|
148
|
+
: `${ethers.utils.formatEther(maxDeposit)} ${tokenSymbol}`;
|
|
149
|
+
throw new Error(`Please deposit less than ${formatted}`);
|
|
150
|
+
}
|
|
151
|
+
// post address to /screen_address of indexer to check if it's blacklisted
|
|
152
|
+
logger.info('screening wallet');
|
|
153
|
+
logger.debug(`screening address ${address}`);
|
|
154
|
+
try {
|
|
155
|
+
let res = await fetch(net.indexerUrl + '/screen_address', {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/json' },
|
|
158
|
+
body: JSON.stringify({ address }),
|
|
159
|
+
});
|
|
160
|
+
let resJson = await res.json();
|
|
161
|
+
if (resJson.isRisk) {
|
|
162
|
+
throw new Error('Your wallet address is risky. Service rejected.', { cause: { type: 'RISKY_ADDRESS' } });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
if (err.cause?.type === 'RISKY_ADDRESS') {
|
|
167
|
+
throw err;
|
|
168
|
+
}
|
|
169
|
+
logger.error('Failed to screen address, but proceeding with deposit. Error:', err);
|
|
170
|
+
}
|
|
171
|
+
logger.debug('Address screening passed');
|
|
172
|
+
// Scan on-chain events to find unspent UTXOs
|
|
173
|
+
logger.debug('Scanning on-chain events for unspent UTXOs...');
|
|
174
|
+
const unspent = await findUnspentUtxos({
|
|
175
|
+
etherPool: pool,
|
|
176
|
+
encryptionKey,
|
|
177
|
+
keypair,
|
|
178
|
+
address,
|
|
179
|
+
token: resolvedToken,
|
|
180
|
+
network: net,
|
|
181
|
+
});
|
|
182
|
+
logger.debug(`Unspent UTXOs found: ${unspent.length}`);
|
|
183
|
+
let inputs = [];
|
|
184
|
+
let inputSum = BigNumber.from(0);
|
|
185
|
+
if (unspent.length >= 2) {
|
|
186
|
+
inputs = [unspent[0], unspent[1]];
|
|
187
|
+
inputSum = inputs[0].amount.add(inputs[1].amount);
|
|
188
|
+
}
|
|
189
|
+
else if (unspent.length === 1) {
|
|
190
|
+
inputs = [unspent[0]];
|
|
191
|
+
inputSum = inputs[0].amount;
|
|
192
|
+
}
|
|
193
|
+
const outputAmount = inputSum.add(depositAmount);
|
|
194
|
+
// For ERC20 pools, embed the token contract address as mintAddress in the UTXO.
|
|
195
|
+
const mintAddress = erc20Token ? BigNumber.from(erc20Token.tokenAddress) : BigNumber.from(0);
|
|
196
|
+
const outputUtxo = new Utxo({ amount: outputAmount, keypair, mintAddress });
|
|
197
|
+
const formattedOutput = isErc20
|
|
198
|
+
? `${ethers.utils.formatUnits(outputAmount, tokenDecimals)} ${tokenSymbol}`
|
|
199
|
+
: `${ethers.utils.formatEther(outputAmount)} ${tokenSymbol}`;
|
|
200
|
+
logger.debug(`Depositing ${depositAmountInput} ${tokenSymbol} (new output: ${formattedOutput})`);
|
|
201
|
+
if (isErc20 && erc20Token) {
|
|
202
|
+
// Step 1: send ERC20 approve tx first, BEFORE generating the ZK proof.
|
|
203
|
+
// The proof fetches the Merkle root; if we generate it before the approve
|
|
204
|
+
// is mined the root can advance while waiting, causing proof verification
|
|
205
|
+
// to fail on-chain.
|
|
206
|
+
const erc20Abi = [
|
|
207
|
+
'function allowance(address owner, address spender) view returns (uint256)',
|
|
208
|
+
'function approve(address spender, uint256 amount) returns (bool)',
|
|
209
|
+
];
|
|
210
|
+
const erc20 = new ethers.Contract(erc20Token.tokenAddress, erc20Abi, readProvider);
|
|
211
|
+
const [network, feeData] = await Promise.all([
|
|
212
|
+
readProvider.getNetwork(),
|
|
213
|
+
readProvider.getFeeData(),
|
|
214
|
+
]);
|
|
215
|
+
const feeOverrides = await getFeeOverrides(net, readProvider, feeData);
|
|
216
|
+
const allowance = await erc20.allowance(address, poolAddress);
|
|
217
|
+
logger.debug(`Current ${tokenSymbol} allowance: ${ethers.utils.formatUnits(allowance, tokenDecimals)} ${tokenSymbol}`);
|
|
218
|
+
if (allowance.lt(depositAmount)) {
|
|
219
|
+
if (resolvedToken === 'usdt' && allowance.gt(0)) {
|
|
220
|
+
const resetApproveTx = await erc20.populateTransaction.approve(poolAddress, 0);
|
|
221
|
+
const unsignedResetApproveTx = {
|
|
222
|
+
...resetApproveTx,
|
|
223
|
+
chainId: network.chainId,
|
|
224
|
+
...feeOverrides,
|
|
225
|
+
};
|
|
226
|
+
logger.info('waiting for user signature [approve-reset]');
|
|
227
|
+
await txSender(unsignedResetApproveTx, { stage: 'approve-reset', token: resolvedToken, chain: net.chainKey });
|
|
228
|
+
}
|
|
229
|
+
const approveTx = await erc20.populateTransaction.approve(poolAddress, depositAmount);
|
|
230
|
+
const unsignedApproveTx = {
|
|
231
|
+
...approveTx,
|
|
232
|
+
chainId: network.chainId,
|
|
233
|
+
...feeOverrides,
|
|
234
|
+
};
|
|
235
|
+
logger.info('waiting for user signature [approve]');
|
|
236
|
+
await txSender(unsignedApproveTx, { stage: 'approve', token: resolvedToken, chain: net.chainKey });
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
logger.info(`${tokenSymbol} allowance is sufficient; skipping approve`);
|
|
240
|
+
}
|
|
241
|
+
// txSender is expected to wait for the approve to be mined before returning
|
|
242
|
+
// (the UI's txSender calls waitForTransactionReceipt for ERC20 deposits).
|
|
243
|
+
// Step 2: generate ZK proof with a fresh Merkle root now that approve is confirmed
|
|
244
|
+
logger.info('generating ZK proof');
|
|
245
|
+
const { args, extData } = await prepareTransaction({
|
|
246
|
+
inputs,
|
|
247
|
+
outputs: [outputUtxo],
|
|
248
|
+
encryptionKey,
|
|
249
|
+
keyBasePath,
|
|
250
|
+
token: resolvedToken,
|
|
251
|
+
network: net,
|
|
252
|
+
});
|
|
253
|
+
// Step 3: send transact tx (no ETH value for ERC pool)
|
|
254
|
+
const gasLimit = await estimateTransactGasLimit({
|
|
255
|
+
pool,
|
|
256
|
+
args,
|
|
257
|
+
extData,
|
|
258
|
+
estimateOverrides: { from: address },
|
|
259
|
+
fallback: BigNumber.from(2000000),
|
|
260
|
+
});
|
|
261
|
+
const partialTx = await pool.populateTransaction.transact(args, extData, { gasLimit });
|
|
262
|
+
const [network2, feeData2] = await Promise.all([
|
|
263
|
+
readProvider.getNetwork(),
|
|
264
|
+
readProvider.getFeeData(),
|
|
265
|
+
]);
|
|
266
|
+
const unsignedTx = {
|
|
267
|
+
...partialTx,
|
|
268
|
+
chainId: network2.chainId,
|
|
269
|
+
...(await getFeeOverrides(net, readProvider, feeData2)),
|
|
270
|
+
};
|
|
271
|
+
logger.info('waiting for user signature [deposit]');
|
|
272
|
+
const tx = await txSender(unsignedTx, { stage: 'deposit', token: resolvedToken, chain: net.chainKey });
|
|
273
|
+
logger.info('confirming transaction');
|
|
274
|
+
await confirmEncryptedOutput(extData.encryptedOutput1, resolvedToken, net);
|
|
275
|
+
return tx;
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
logger.info('generating ZK proof');
|
|
279
|
+
const { args, extData } = await prepareTransaction({
|
|
280
|
+
inputs,
|
|
281
|
+
outputs: [outputUtxo],
|
|
282
|
+
encryptionKey,
|
|
283
|
+
keyBasePath,
|
|
284
|
+
token: resolvedToken,
|
|
285
|
+
network: net,
|
|
286
|
+
});
|
|
287
|
+
const gasLimit = await estimateTransactGasLimit({
|
|
288
|
+
pool,
|
|
289
|
+
args,
|
|
290
|
+
extData,
|
|
291
|
+
estimateOverrides: {
|
|
292
|
+
from: address,
|
|
293
|
+
value: depositAmount,
|
|
294
|
+
},
|
|
295
|
+
fallback: BigNumber.from(3000000),
|
|
296
|
+
});
|
|
297
|
+
const partialTx = await pool.populateTransaction.transact(args, extData, {
|
|
298
|
+
value: depositAmount,
|
|
299
|
+
gasLimit,
|
|
300
|
+
});
|
|
301
|
+
const [network, nonce, feeData] = await Promise.all([
|
|
302
|
+
readProvider.getNetwork(),
|
|
303
|
+
readProvider.getTransactionCount(address, 'pending'),
|
|
304
|
+
readProvider.getFeeData(),
|
|
305
|
+
]);
|
|
306
|
+
const unsignedTx = {
|
|
307
|
+
...partialTx,
|
|
308
|
+
nonce,
|
|
309
|
+
chainId: network.chainId,
|
|
310
|
+
};
|
|
311
|
+
Object.assign(unsignedTx, await getFeeOverrides(net, readProvider, feeData));
|
|
312
|
+
logger.info('waiting for user signature [deposit]');
|
|
313
|
+
const tx = await txSender(unsignedTx);
|
|
314
|
+
logger.info('confirming transaction');
|
|
315
|
+
await confirmEncryptedOutput(extData.encryptedOutput1, resolvedToken, net);
|
|
316
|
+
return tx;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function confirmEncryptedOutput(encryptedOutput1, token, net) {
|
|
320
|
+
logger.debug('verifying transaction on indexer...', encryptedOutput1);
|
|
321
|
+
let retryTimes = 0;
|
|
322
|
+
const intervalMs = 3000;
|
|
323
|
+
const maxRetries = 10;
|
|
324
|
+
const start = Date.now();
|
|
325
|
+
while (true) {
|
|
326
|
+
logger.debug('Confirming transaction..');
|
|
327
|
+
logger.debug(`retryTimes: ${retryTimes}`);
|
|
328
|
+
await new Promise(resolve => setTimeout(resolve, intervalMs));
|
|
329
|
+
logger.debug('Fetching updated onchain state...');
|
|
330
|
+
let res = await fetch(net.indexerUrl + '/check_encrypted_output', {
|
|
331
|
+
method: 'POST',
|
|
332
|
+
headers: { 'Content-Type': 'application/json' },
|
|
333
|
+
body: JSON.stringify({ encryptedOutput: encryptedOutput1, token, chain: net.chainKey }),
|
|
334
|
+
});
|
|
335
|
+
let resJson = await res.json();
|
|
336
|
+
if (resJson.exists) {
|
|
337
|
+
logger.debug(`Top up successfully in ${((Date.now() - start) / 1000).toFixed(2)} seconds!`);
|
|
338
|
+
break;
|
|
339
|
+
}
|
|
340
|
+
if (retryTimes >= maxRetries) {
|
|
341
|
+
throw new Error('Refresh the page to see latest balance.');
|
|
342
|
+
}
|
|
343
|
+
retryTimes++;
|
|
344
|
+
}
|
|
345
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { getBalance } from './balance.js';
|
|
2
|
+
export { deposit } from './deposit.js';
|
|
3
|
+
export { setLogger } from './utils/logger.js';
|
|
4
|
+
export { getRemoteConfig } from './utils/remoteConfig.js';
|
|
5
|
+
export type { FeeSnapshot, RemoteConfig } from './utils/remoteConfig.js';
|
|
6
|
+
export { withdraw } from './withdraw.js';
|
|
7
|
+
export { FEE_RECIPIENT_ADDRESS, INDEXER_URL, PRIVATE_USDC_CONTRACT_ADDRESS, USDC_CONTRACT_ADDRESS, USDC_DECIMALS } from './utils/constants.js';
|
|
8
|
+
export { clearCache } from './utils/utils.js';
|
|
9
|
+
export { BASE_NETWORK, BNB_NETWORK, ETH_NETWORK, getDefaultNetworkConfig, getNetworkConfig, NETWORKS, resolveNetwork } from './utils/networkConfig.js';
|
|
10
|
+
export type { Erc20Token, NativeToken, NetworkConfig, PrivacyToken, SupportedChain } from './utils/networkConfig.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { getBalance } from './balance.js';
|
|
2
|
+
export { deposit } from './deposit.js';
|
|
3
|
+
export { setLogger } from './utils/logger.js';
|
|
4
|
+
export { getRemoteConfig } from './utils/remoteConfig.js';
|
|
5
|
+
export { withdraw } from './withdraw.js';
|
|
6
|
+
export { FEE_RECIPIENT_ADDRESS, INDEXER_URL, PRIVATE_USDC_CONTRACT_ADDRESS, USDC_CONTRACT_ADDRESS, USDC_DECIMALS } from './utils/constants.js';
|
|
7
|
+
export { clearCache } from './utils/utils.js';
|
|
8
|
+
export { BASE_NETWORK, BNB_NETWORK, ETH_NETWORK, getDefaultNetworkConfig, getNetworkConfig, NETWORKS, resolveNetwork } from './utils/networkConfig.js';
|