diff --git a/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs b/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs index 0195d91478..2cc26af259 100644 --- a/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs +++ b/core/bin/zksync_api/src/bin/providers/dev_price_provider.rs @@ -5,7 +5,7 @@ use actix_web::{web, HttpRequest, HttpResponse, Result}; use bigdecimal::BigDecimal; -use chrono::Utc; +use chrono::{SecondsFormat, Utc}; use serde::{Deserialize, Serialize}; use serde_json::json; use std::{collections::HashMap, fs::read_to_string, path::Path}; @@ -50,6 +50,45 @@ macro_rules! make_sloppy { }}; } +async fn handle_coinmarketcap_token_price_query( + query: web::Query, + _data: web::Data>, +) -> Result { + let symbol = query.symbol.clone(); + let base_price = match symbol.as_str() { + "RBTC" => BigDecimal::from(1800), + "wBTC" => BigDecimal::from(9000), + // Even though these tokens have their base price equal to + // the default one, we still keep them here so that in the future it would + // be easier to change the default price without affecting the important tokens + "DAI" => BigDecimal::from(1), + "tGLM" => BigDecimal::from(1), + "GLM" => BigDecimal::from(1), + + "RIF" => BigDecimal::try_from(0.053533).unwrap(), + _ => BigDecimal::from(1), + }; + let random_multiplier = thread_rng().gen_range(0.9, 1.1); + + let price = base_price * BigDecimal::try_from(random_multiplier).unwrap(); + + let last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true); + let resp = json!({ + "data": { + symbol: { + "quote": { + "USD": { + "price": price.to_string(), + "last_updated": last_updated + } + } + } + } + }); + vlog::info!("1.0 {} = {} USD", query.symbol, price); + Ok(HttpResponse::Ok().json(resp)) +} + #[derive(Debug, Deserialize)] struct Token { pub address: Address, @@ -140,22 +179,33 @@ pub fn create_price_service(sloppy_mode: bool) -> actix_web::Scope { .chain(testnet_tokens.into_iter()) .collect(); if sloppy_mode { - web::scope(API_PATH) + web::scope("") .app_data(web::Data::new(data)) .route( - "/coins/list", + "/cryptocurrency/quotes/latest", + web::get().to(make_sloppy!(handle_coinmarketcap_token_price_query)), + ) + .route( + format!("{}/coins/list", API_PATH).as_str(), web::get().to(make_sloppy!(handle_coingecko_token_list)), ) .route( - "/coins/{coin_id}/market_chart", + format!("{}/coins/{{coin_id}}/market_chart", API_PATH).as_str(), web::get().to(make_sloppy!(handle_coingecko_token_price_query)), ) } else { - web::scope(API_PATH) + web::scope("") .app_data(web::Data::new(data)) - .route("/coins/list", web::get().to(handle_coingecko_token_list)) .route( - "/coins/{coin_id}/market_chart", + "/cryptocurrency/quotes/latest", + web::get().to(handle_coinmarketcap_token_price_query), + ) + .route( + format!("{}/coins/list", API_PATH).as_str(), + web::get().to(handle_coingecko_token_list), + ) + .route( + format!("{}/coins/{{coin_id}}/market_chart", API_PATH).as_str(), web::get().to(handle_coingecko_token_price_query), ) } diff --git a/core/lib/wallet_creator/README.md b/core/lib/wallet_creator/README.md index 9dc22cfc63..eb947395dc 100644 --- a/core/lib/wallet_creator/README.md +++ b/core/lib/wallet_creator/README.md @@ -44,7 +44,7 @@ zk test i wallet-generator Or to do it manually: -Navigate to `ri-aggregation/core/lib/wallet_creator` and run: +Navigate to `./core/lib/wallet_creator` and run: Unit tests diff --git a/docker-compose.yml b/docker-compose.yml index cd1c89b969..1a087e6f28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -35,8 +35,9 @@ services: start_period: 15s dev-ticker: - image: "rsksmart/rollup-dev-ticker:1.0.0-beta" + image: "rsksmart/rollup-dev-ticker:1.1.0-beta" env_file: + - ./etc/env/${ZKSYNC_ENV-dev}.env - ./etc/env/${ENV_OVERRIDE-deploy}.env ports: - "9876:9876" diff --git a/docker/environment/Dockerfile b/docker/environment/Dockerfile index c5abd04d4a..8735fe2a4c 100644 --- a/docker/environment/Dockerfile +++ b/docker/environment/Dockerfile @@ -26,6 +26,7 @@ ENV RUSTUP_HOME=/usr/local/rustup \ PATH=/usr/local/cargo/bin:$PATH RUN curl https://sh.rustup.rs -sSf | bash -s -- -y RUN rustup install 1.69.0 +RUN rustup override set 1.69.0 RUN cargo install diesel_cli --version=1.4.0 --no-default-features --features postgres RUN cargo install --version=0.5.6 sqlx-cli RUN cargo install wasm-pack --git https://github.com/d3lm/wasm-pack --rev 713868b204f151acd1989c3f29ff9d3bc944c306 diff --git a/docs/architecture.md b/docs/architecture.md index 6feb5a9636..c878b1111e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -83,7 +83,6 @@ This section provides an overview on folders / sub-projects that exist in this r - `/docker`: Dockerfiles used for development of zkSync and for packaging zkSync for a production environment. - `/etc`: Configration files. - `/env`: `.env` files that contain environment variables for different configuration of zkSync Server / Prover. - - `/js`: Configuration files for JavaScript applications (such as Explorer). - `/tokens`: Configuration of supported Rootstock ERC-20 tokens. - `/infrastructure`: Application that aren't naturally a part of zkSync core, but are related to it. - `/keys`: Verification keys for `circuit` module. diff --git a/etc/js/env-config.js b/etc/js/env-config.js deleted file mode 100644 index 090bc8a97a..0000000000 --- a/etc/js/env-config.js +++ /dev/null @@ -1,26 +0,0 @@ -export default { - 'http://localhost': { - API_SERVER: 'https://localhost:3001', - WALLET_ADDRESS: 'http://localhost:3000', - EXPLORER: 'http://localhost:7001', - ETH_NETWORK: 'localhost', - WS_API_ADDR: 'ws://localhost:3031', - HTTP_RPC_API_ADDR: 'http://localhost:3030' - }, - 'https://explorer.dev.aggregation.rifcomputing.net': { - API_SERVER: 'https://dev.aggregation.rifcomputing.net:3029', - WALLET_ADDRESS: 'https://wallet.dev.aggregation.rifcomputing.net', - EXPLORER: 'https://explorer.testnet.rsk.co', - ETH_NETWORK: 'testnet', - WS_API_ADDR: 'https://dev.aggregation.rifcomputing.net:3031', - HTTP_RPC_API_ADDR: 'https://dev.aggregation.rifcomputing.net:3030' - }, - 'https://explorer.aggregation.rifcomputing.net': { - API_SERVER: 'https://aggregation.rifcomputing.net:3029', - WALLET_ADDRESS: 'https://wallet.aggregation.rifcomputing.net', - EXPLORER: 'https://explorer.rsk.co', - ETH_NETWORK: 'rsk_mainnet', - WS_API_ADDR: 'https://aggregation.rifcomputing.net:3031', - HTTP_RPC_API_ADDR: 'https://aggregation.rifcomputing.net:3030' - } -}[`${location.protocol}//${location.hostname}`]; diff --git a/infrastructure/zk/src/docker.ts b/infrastructure/zk/src/docker.ts index 3900fb5998..1f27ff9ac2 100644 --- a/infrastructure/zk/src/docker.ts +++ b/infrastructure/zk/src/docker.ts @@ -36,9 +36,6 @@ async function dockerCommand(command: 'push' | 'build', image: string, tag: stri } async function _build(image: string, tag: string) { - if (image == 'nginx') { - await utils.spawn('yarn explorer build'); - } if (image == 'server' || image == 'prover') { await contract.build(); } diff --git a/infrastructure/zk/src/init.ts b/infrastructure/zk/src/init.ts index af0b4ab973..326fbcc32c 100644 --- a/infrastructure/zk/src/init.ts +++ b/infrastructure/zk/src/init.ts @@ -53,7 +53,7 @@ async function createVolumes() { export const initCommand = new Command('init') .description('perform zksync network initialization for development') - .option('--no-crypto', 'not include sdk packages') + .option('--no-crypto', 'not include crypto packages') .option('--with-docker', 'use docker container instead of local environment') .action(async (cmd: Command) => { const { crypto, withDocker } = cmd; diff --git a/infrastructure/zk/src/run/forced-exit.ts b/infrastructure/zk/src/run/forced-exit.ts new file mode 100644 index 0000000000..d46d2893a2 --- /dev/null +++ b/infrastructure/zk/src/run/forced-exit.ts @@ -0,0 +1,207 @@ +import { Command } from 'commander'; +import fetch from 'node-fetch'; +import { BigNumber, BigNumberish, ethers, providers } from 'ethers'; +import * as utils from '../utils'; + +type State = { + balances: { + [token: string]: BigNumberish; + }; + nonce: number; + pubKeyHash: string; +}; + +type RestAccountState = { + finalized: State | undefined; +}; + +type RpcAccountState = { + verified: State; +}; + +const SYNC = 'sync:0000000000000000000000000000000000000000'; + +async function isForcedExitSenderAccountReady(nodeUrl: string, nodeType: string): Promise { + if (nodeType !== 'REST' && nodeType !== 'JSONRPC') { + console.log('Node type must be either REST or JSONRPC'); + return false; + } + + const forcedExitAccount = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS as string; + const state = + nodeType === 'REST' + ? (await processRestRequest(nodeUrl, forcedExitAccount)).finalized + : (await processJsonRpcRequest(nodeUrl, forcedExitAccount)).verified; + + if (state?.pubKeyHash !== SYNC) { + console.log('Forced exit sender account is ready'); + return true; + } + + if (state?.balances['RBTC']) { + const balance = BigNumber.from(state.balances['RBTC']); + if (!balance.isZero()) { + console.log(`Forced exit sender account balance is ${balance.toString()} RBTC`); + console.log('Wait until the preparation of the forced exit sender account is completed'); + return true; + } + } + + console.log('Forced exit sender account is not ready'); + return false; +} + +async function processRestRequest(nodeUrl: string, forcedExitAccount: string): Promise { + const response = await fetch(`${nodeUrl}/accounts/${forcedExitAccount}`); + const { result } = await response.json(); + + return result; +} + +async function processJsonRpcRequest(nodeUrl: string, forcedExitAccount: string): Promise { + const body = { + jsonrpc: '2.0', + method: 'account_info', + params: [forcedExitAccount], + id: 1 + }; + const response = await fetch(nodeUrl, { + method: 'POST', + body: JSON.stringify(body), + headers: { + Accept: 'application/json', + 'Content-type': 'application/json' + } + }); + const { result } = await response.json(); + + return result; +} + +async function prepareForcedExitSenderAccount( + l1Address: string, + privateKey: string, + amount: string | undefined, + nodeUrl: string, + nodeType: string +): Promise { + if (await isForcedExitSenderAccountReady(nodeUrl, nodeType)) { + return; + } + + await depositToForcedExitSenderAccount(l1Address, privateKey, amount); +} + +export async function depositToForcedExitSenderAccount(l1Address?: string, privateKey?: string, amount = '0.0001') { + console.log('Depositing to the forced exit sender account sender'); + + const parsedAmount = ethers.utils.parseEther(amount); + + const signer = await retrieveSigner(parsedAmount, l1Address, privateKey); + + if (!signer) { + console.log('Must provide an L1 address and L1 private key that matches'); + return; + } + + const mainZkSyncContract = new ethers.Contract( + process.env.CONTRACTS_CONTRACT_ADDR as string, + await utils.readZkSyncAbi(), + signer + ); + + const forcedExitAccount = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS as string; + const gasPrice = await signer.getGasPrice(); + const depositTransaction = (await mainZkSyncContract.depositRBTC(forcedExitAccount, { + value: parsedAmount, + gasPrice + })) as ethers.ContractTransaction; + + console.log(`Deposit transaction hash: ${depositTransaction.hash}`); + + await depositTransaction.wait(); + + console.log('Deposit to the forced exit sender account has been successfully completed'); +} + +async function retrieveSigner( + amount: BigNumberish, + l1Address?: string, + privateKey?: string +): Promise { + const provider = new providers.JsonRpcProvider( + process.env.FORCED_EXIT_REQUESTS_WEB3_URL ?? process.env.ETH_CLIENT_WEB3_URL + ); + + let signer: ethers.Signer | undefined; + if (l1Address && privateKey) { + signer = new ethers.Wallet(privateKey, provider); + + const address = await signer.getAddress(); + + if (l1Address.toLowerCase() !== address.toLowerCase()) { + console.log('L1 address does not match the provided private key'); + return undefined; + } + } + + if (!signer && process.env.ZKSYNC_ENV === 'dev') { + signer = await findWealthyAccount(amount, provider); + } + + return signer; +} + +async function findWealthyAccount( + requiredBalance: BigNumberish, + provider: providers.JsonRpcProvider +): Promise { + let accounts: string[] = []; + try { + accounts = await provider.listAccounts(); + + for (let i = accounts.length - 1; i >= 0; i--) { + const signer = provider.getSigner(i); + const balance = await signer.getBalance(); + if (balance.gte(requiredBalance)) { + console.log(`Found funded account ${await signer.getAddress()}`); + + return signer; + } + } + } catch (error) { + console.log('Failed to retrieve accounts and balances:', error); + } + console.log(`could not find unlocked account with sufficient balance; all accounts:\n - ${accounts.join('\n - ')}`); +} + +export const command = new Command('forced-exit') + .description('prepare forced exit sender account') + .requiredOption('-n, --nodeUrl ', 'Node url') + .requiredOption('-t, --nodeType ', 'Node type (REST or JSONRPC)'); + +command + .command('check') + .description('check forced exit sender account balance') + .action(async (cmd: Command) => { + const { + parent: { nodeUrl, nodeType } + } = cmd; + await isForcedExitSenderAccountReady(nodeUrl, nodeType); + }); + +command + .command('prepare') + .description('deposit to forced exit sender account if necessary') + .requiredOption('--address ', 'L1 address') + .requiredOption('-p, --privateKey ', 'Private key of the L1 address') + .option('--amount ', 'Amount of RBTC to deposit (default: 0.0001') + .action(async (cmd: Command) => { + const { + address, + privateKey, + amount, + parent: { nodeUrl, nodeType } + } = cmd; + await prepareForcedExitSenderAccount(address, privateKey, amount, nodeUrl, nodeType); + }); diff --git a/infrastructure/zk/src/run/run.ts b/infrastructure/zk/src/run/run.ts index 0503235a97..7bce375afa 100644 --- a/infrastructure/zk/src/run/run.ts +++ b/infrastructure/zk/src/run/run.ts @@ -9,6 +9,7 @@ import * as eventListener from './event-listener'; import * as dataRestore from './data-restore'; import * as docker from '../docker'; import * as walletGenerator from './generate-wallet'; +import * as forcedExit from './forced-exit'; export { verifyKeys, dataRestore }; @@ -96,7 +97,7 @@ export async function tokenInfo(address: string) { } // installs all dependencies and builds our js packages -export async function yarn(crypto = true) { +export async function yarn(crypto: boolean) { await utils.spawn('yarn'); if (crypto) { await utils.spawn('yarn build:crypto'); @@ -142,10 +143,6 @@ export async function revertReason(txHash: string, web3url?: string) { await utils.spawn(`yarn contracts ts-node scripts/revert-reason.ts ${txHash} ${web3url || ''}`); } -export async function explorer() { - await utils.spawn('yarn explorer serve'); -} - export async function exitProof(...args: string[]) { await utils.spawn(`cargo run --example generate_exit_proof --release -- ${args.join(' ')}`); } @@ -240,17 +237,17 @@ export const command = new Command('run') .addCommand(verifyKeys.command) .addCommand(dataRestore.command) .addCommand(eventListener.command) - .addCommand(walletGenerator.command); + .addCommand(walletGenerator.command) + .addCommand(forcedExit.command); command.command('test-accounts').description('print rootstock test accounts').action(testAccounts); -command.command('explorer').description('run zksync explorer locally').action(explorer); command .command('yarn') .description('install all JS dependencies') - .option('--no-sdk', 'not include sdk packages') + .option('--no-crypto', 'not include crypto packages') .action(async (cmd: Command) => { - const { sdk } = cmd; - await yarn(!!sdk); + const { crypto } = cmd; + await yarn(!!crypto); }); command.command('test-upgrade ').action(testUpgrade); command.command('cat-logs [exit_code]').description('print server and prover logs').action(catLogs); diff --git a/infrastructure/zk/src/server.ts b/infrastructure/zk/src/server.ts index 88214a35fc..818a702d48 100644 --- a/infrastructure/zk/src/server.ts +++ b/infrastructure/zk/src/server.ts @@ -4,12 +4,10 @@ import * as env from './env'; import fs from 'fs'; import * as db from './db/db'; import * as docker from './docker'; - -import { ethers } from 'ethers'; +import * as forcedExit from './run/forced-exit'; export async function core(withDocker = false) { - prepareForcedExitRequestAccount(); - + forcedExit.depositToForcedExitSenderAccount(); if (withDocker) { await docker.deployUp('server-core'); return; @@ -41,11 +39,7 @@ export async function apiNode(withDocker = false) { } export async function server(withDocker = false) { - // By the time this function is run the server is most likely not be running yet - // However, it does not matter, since the only thing the function does is depositing - // to the forced exit sender account, and server should be capable of recognizing - // priority operaitons that happened before it was booted - prepareForcedExitRequestAccount(); + forcedExit.depositToForcedExitSenderAccount(); if (withDocker) { await docker.deployUp('server'); return; @@ -79,64 +73,6 @@ export async function genesis(withDocker = false) { env.modify_contracts_toml('CONTRACTS_GENESIS_ROOT', genesisRoot); } -// This functions deposits funds onto the forced exit sender account -// This is needed to make sure that it has the account id -async function prepareForcedExitRequestAccount() { - console.log('Depositing to the forced exit sender account'); - const forcedExitAccount = process.env.FORCED_EXIT_REQUESTS_SENDER_ACCOUNT_ADDRESS as string; - - const ethProvider = getEthProvider(); - - // This is the private key of the first test account () - const ethRichWallet = new ethers.Wallet( - '0x20e4a6381bd3826a14f8da63653d94e7102b38eb5f929c7a94652f41fa7ba323', - ethProvider - ); - - const gasPrice = await ethProvider.getGasPrice(); - - const topupTransaction = await ethRichWallet.sendTransaction({ - to: forcedExitAccount, - // The amount for deposit should be enough to send at least - // one transaction to retrieve the funds form the forced exit smart contract - value: ethers.utils.parseEther('100.0'), - gasPrice - }); - - await topupTransaction.wait(); - - const mainZkSyncContract = new ethers.Contract( - process.env.CONTRACTS_CONTRACT_ADDR as string, - await utils.readZkSyncAbi(), - ethRichWallet - ); - - const depositTransaction = (await mainZkSyncContract.depositRBTC(forcedExitAccount, { - // Here the amount to deposit does not really matter, as it is done purely - // to guarantee that the account exists in the network - value: ethers.utils.parseEther('1.0'), - gasLimit: ethers.BigNumber.from('200000'), - gasPrice - })) as ethers.ContractTransaction; - - await depositTransaction.wait(); - - console.log('Deposit to the forced exit sender account has been successfully completed'); -} - -function getEthProvider() { - return new ethers.providers.JsonRpcProvider( - process.env.FORCED_EXIT_REQUESTS_WEB3_URL ?? process.env.ETH_CLIENT_WEB3_URL - ); -} - -async function testWeb3Network() { - const ethProvider = getEthProvider(); - - const network = await ethProvider.getNetwork(); - console.log(`current network chain id ${network.chainId}`); -} - export const command = new Command('server') .description('start zksync server') .option('--genesis', 'generate genesis data via server') @@ -178,8 +114,3 @@ command } = cmd; await core(withDocker); }); - -command - .command('test-web3-network') - .description('test if web3 node can be reached to prepare the forced exit requests account') - .action(testWeb3Network); diff --git a/infrastructure/zk/src/test/test.ts b/infrastructure/zk/src/test/test.ts index 59accede93..5e1ef5660c 100644 --- a/infrastructure/zk/src/test/test.ts +++ b/infrastructure/zk/src/test/test.ts @@ -76,7 +76,7 @@ async function rustCryptoTests() { } export async function serverRust() { - await utils.spawn(`${CARGO_FLAGS} cargo test --release`); + await utils.spawn(`${CARGO_FLAGS} cargo test --release -- --test-threads=1`); await db(true); await rustApi(true); await prover();