diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c202f23 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,33 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Added by cargo + +/target + +.vscode/ + +package-lock.json +package.json +node_modules/ + +contracts/ \ No newline at end of file diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml new file mode 100644 index 0000000..4e4e8fc --- /dev/null +++ b/.github/workflows/workflow.yml @@ -0,0 +1,93 @@ +name: CI + +on: + push: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci-workflow + CARGO_TERM_COLOR: always + +jobs: + contract-test: + name: "Contract Test" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + working-directory: contracts + + - name: Run Forge build + run: | + forge build --sizes + id: build + working-directory: contracts + + - name: Run Forge tests + run: | + forge test -vvv + id: test + working-directory: contracts + + server-test: + name: "Server Test" + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build + - name: Run tests + run: cargo test + + push-reg: + name: "Push Registry" + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v2 + - name: Run script + run: | + docker login $REGISTRY -u $REGISTRY_USER -p $REGISTRY_PASSWORD + docker build -t "$IMAGE:${GITHUB_SHA:0:7}" . + docker push "$IMAGE:${GITHUB_SHA:0:7}" + env: + REGISTRY: ${{ vars.REGISTRY }} + REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + REGISTRY_USER: ${{ secrets.REGISTRY_USER }} + APP_NAME: ${{ vars.APP_NAME }} + IMAGE: ${{ vars.REGISTRY }}/${{ vars.APP_NAME }} + + deploy-server: + name: "Deploy DARKUBE" + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/main' + + needs: + - push-reg + - server-test + + container: hamravesh.hamdocker.ir/public/darkube-cli:v1.1 + steps: + - name: Run script + run: darkube deploy --ref master --token ${DARKUBE_DEPLOY_TOKEN} --app-id ${DARKUBE_APP_ID} --image-tag "${GITHUB_SHA:0:7}" --job-id "$GITHUB_RUN_ID" --stateless-app true + env: + DARKUBE_DEPLOY_TOKEN: ${{ secrets.DARKUBE_DEPLOY_TOKEN }} + DARKUBE_APP_ID: ${{ vars.DARKUBE_APP_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ddd2278 --- /dev/null +++ b/.gitignore @@ -0,0 +1,31 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ +.env +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Added by cargo + +/target + +.vscode/ + +package-lock.json +package.json +node_modules/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..fa2c58c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[submodule "contracts/lib/forge-std"] + path = contracts/lib/forge-std + url = https://github.com/foundry-rs/forge-std +[submodule "contracts/lib/openzeppelin-foundry-upgrades"] + path = contracts/lib/openzeppelin-foundry-upgrades + url = https://github.com/OpenZeppelin/openzeppelin-foundry-upgrades +[submodule "contracts/lib/openzeppelin-contracts-upgradeable"] + path = contracts/lib/openzeppelin-contracts-upgradeable + url = https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable +[submodule "misc/foundry-project/lib/forge-std"] + path = misc/foundry-project/lib/forge-std + url = https://github.com/foundry-rs/forge-std \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..3837adb --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "owshen" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0" +alloy = { version = "0.3", features = ["full", "rlp"] } +serde = { version = "1.0", features = ["derive"] } +bincode = "1.3.3" +rlp = "0.5.2" +tokio = { version = "1.39.2", features = ["full"] } +rand = "0.8.5" +async-trait = "0.1.81" +ff = { version = "0.13", features = ["derive", "derive_bits"] } +lazy_static = "1.5.0" +num-bigint = "0.4" +num-integer = "0.1" +leveldb = "0.8.6" +db-key = "0.0.5" +axum = "0.6" +hyper = "0.14" +eyre = "0.6.12" +reqwest = "0.12.7" +log = "0.4" +serde_json = "1.0.125" +jsonrpsee = { version = "0.24.3", features = [ + "server", + "http-client", + "ws-client", + "macros", + "client-ws-transport-tls", +] } +tower = { version = "0.4.13", features = ["full"] } +tower-http = { version = "0.5.2", features = ["full"] } +tower_http_cors = { package = "tower-http", version = "0.4.4", features = ["full"] } +env_logger = "0.11" +structopt = { version = "0.3", default-features = false } +webbrowser = "0.6" +ethereum-types = "0.14.1" +sha3 = "0.10.8" +hex = "0.4.3" +evm = "0.41" +primitive-types = "0.12" +tinytemplate = "1.2.1" +criterion = "0.5.1" +css-style = "0.14.1" +mime_guess = "2.0.5" +once_cell = "1.19.0" +tracing-subscriber = "0.3.18" +alloy-sol-types = "0.8.3" + diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3defa02 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +# syntax=docker/dockerfile:1.4 +FROM rust:1.80-slim-bullseye AS base + +WORKDIR /code +RUN cargo init +COPY Cargo.toml /code/Cargo.toml +RUN cargo fetch +COPY . /code + +FROM base AS builder + +RUN apt update && apt-get install -y pkg-config libudev-dev libssl-dev cmake gcc g++ +RUN cargo build --release --offline + +FROM debian:11.10-slim + +COPY --from=builder /code/target/release/owshen /owshen +COPY --from=builder /code/GENESIS.json /GENESIS.json diff --git a/GENESIS.json b/GENESIS.json new file mode 100644 index 0000000..a5a5de2 --- /dev/null +++ b/GENESIS.json @@ -0,0 +1,23 @@ +[ + { + "token_type": "Native", + "balances": [ + { + "address": "0x0202020202020202020202020202020202020202", + "amount": "2" + } + ] + }, + { + "token_type": "Erc20", + "contract_address": "0x0303030303030303030303030303030303030303", + "decimal": "18", + "symbol": "USDT", + "balances": [ + { + "address": "0x0202020202020202020202020202020202020202", + "amount": "200" + } + ] + } +] diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca87583 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +# Owshen Network 🤏⛓️ + +- A minimalistic ***imaginary*** blockchain, controlled by a centralized party. +- Listens to the deposits that happen on the network's smart-contract. +- Implements Ethereum RPC endpoints, and the centralized owner may mint/burn tokens as he wish. +- Mints when someone deposits, and burns when someone withdraws. +- Zero fees! +- Literally a ***fake layer-2*** with ***super-cheap transactions***! + +## Vision + + +### Phase 1 +- Imagine there is a smart-contract hosted on the Ethereum blockchain, owned by the Owshen Network's maintainer (E.g Nobitex). + - The smart-contract has a very simple structure: + ```solidity= + contract OwshenEx { + address owner; + + constructor() { + owner = msg.sender; + } + + function withdraw_native(uint256 amount, uint8 v, bytes32 r, bytes32 s) {} + function withdraw_token(address token, uint256 amount, uint8 v, bytes32 r, bytes32 s) {} + } + ``` +- When someone deposits to that smart-contract, the Owshen Network's maintainer (The core application) will detect it, and will ***mint*** the amount deposited on the Owshen Network's imaginary blockchain. (No interactions with the smart-contract is needed). The user himself may let the Owshen Network's maintainer know that he has deposited some amount to the contract, by reporting his transaction-hash through a POST method: `https://api.owshen.io/deposit` +- Owshen Network's core application hosts RPC endpoints on: `https://rpc.owshen.io` +- Users may add Owshen Network's Network on their wallet (E.g Metamask) given the RPC endpoint and chain-id, and may see their minted balances. +- Users should be able to see their deposited tokens on their Metamask wallets when they migrate their funds. +- Users should be able to transfer their tokens to other users of the Owshen Network Network, using the Metamask wallet. The transfer requests are sent from Metamask, as signed Ethereum transactions, to the RPC endpoints, and ***the maintainer may verify the signature of the transaction himself***, and then apply it on the next block. User's should be able to transfer both native and erc-20 tokens. +- There are two kinds of transactions that can be executed on Owshen Network blockchain. The transactions are either: + 1. Regular Ethereum transactions: Transactions that are created/signed by an Ethereum wallet (E.g Metamask) + 2. Owshen Network's custom transactions: Customized operations that are only valid on Owshen Network (E.g when the Owshen Network's maintainer wants to mint some tokens for some user, he may create and sign a Owshen Network ***mint*** transaction) +- Owshen Network has a block explorer too, people may actually see their operations and verify Owshen Network maintainer's legitimate behavior. +- Whenever someone wants to migrate his tokens back to the Ethereum blockchain, he may ask the Owshen Network maintainer to give him a signature, allowing him to call `withdraw_*` functions. The users may request their withdrawals through a POST endpoint: `https://api.owshen.io/withdraw`. The output of this endpoint is a signature that may be submitted on the network's Ethereum smart-contract and funds can be withdrawn. + +API Summary / Definition of Done: + +- `POST https://api.owshen.io/deposit`: Letting the core software know that a deposit has happened, minting the appropriate amount on the Owshen Network's imaginary blockchain. +- `POST https://api.owshen.io/withdraw`: Asking the core software to sign a message that let's the user withdraw his tokens to the mainnet. +- `GET https://api.owshen.io/explore?since=&count=`: Get JSON list of `count` blocks starting from `since` index. Used by the explorer software. +- `https://rpc.owshen.io`: Metamask RPC endpoint that implements `eth_getBalance`, `eth_call`, `eth_sendRawTransaction` and etc. allowing the users to see his balances on Metamask, and send both native and etc-20 coins. + +### Phase 2 +- We may add a new Owshen Network custom transaction, used for putting spot buy/sell orders on Owshen Network's internal matching engine. They may sign their orders through RLP encoded messages via Metamask, and submit them through a POST endpoint: `https://api.owshen.io/deposit` +- We may add a rewarding system: according to the USD value you own on the network, you may receive DIVE amounts per block. +- Users may deploy smart-contracts. diff --git a/contracts/.gitignore b/contracts/.gitignore new file mode 100644 index 0000000..d1a5319 --- /dev/null +++ b/contracts/.gitignore @@ -0,0 +1,15 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ +broadcast + +# Docs +docs/ + +# Dotenv file +.env diff --git a/contracts/Makefile b/contracts/Makefile new file mode 100644 index 0000000..4d64e51 --- /dev/null +++ b/contracts/Makefile @@ -0,0 +1,36 @@ + +# Targets +.PHONY: deploy verify upgrade + +deploy: + + @echo "Deploying Owshen contract..." + PRIVATE_KEY=$(PRIVATE_KEY) forge script script/Owshen.s.sol:DeployOwshen --broadcast --fork-url $(PROVIDER) | tee deploy_output.txt + cat deploy_output.txt | grep "Transactions saved to:" | xargs -I @ python -c "print('@'.split(' ')[-1])" | xargs -I @ python -c "import json; d=json.load(open('@'));dt={'implementation_address': d['returns']['0']['value'], 'proxy_address': d['returns']['1']['value']}; dt.update({'abi': json.load(open('out/Owshen.sol/Owshen.json'))['abi']}); json.dump(dt, open('../config.json', 'w'), indent=4);" + +verify: + @echo "verifying logic contract ..." + forge verify-contract --chain-id $(CHAIN_ID) --etherscan-api-key $(ETHERSCAN_API) $(CONTRACT_ADDRESS) Owshen + + +upgrade: + @echo "upgrading owshen ... " + PRIVATE_KEY=$(PRIVATE_KEY) PROXY_ADDRESS=$(PROXY_ADDRESS) forge script script/UpgradeOwshen.s.sol:UpgradeOwshen --broadcast --fork-url $(PROVIDER) | tee upgrade_output.txt + + +clean: + @echo "Cleaning build artifacts..." + forge clean + +build: + @echo "Compiling contracts..." + forge build + + + +PRIVATE_KEY := +PROVIDER := https://ethereum-sepolia-rpc.publicnode.com +ETHERSCAN_API := +PROXY_ADDRESS := +CONTRACT_ADDRESS := +CHAIN_ID := 11155111 diff --git a/contracts/README.md b/contracts/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/contracts/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 0000000..839daee --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,12 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +solc_version = "0.8.26" +evm_version = "cancun" +ast = true +ffi = true +build_info = true +extra_output = ["storageLayout"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/contracts/lib/forge-std b/contracts/lib/forge-std new file mode 160000 index 0000000..e04104a --- /dev/null +++ b/contracts/lib/forge-std @@ -0,0 +1 @@ +Subproject commit e04104ab93e771441eab03fb76eda1402cb5927b diff --git a/contracts/lib/openzeppelin-contracts-upgradeable b/contracts/lib/openzeppelin-contracts-upgradeable new file mode 160000 index 0000000..deaf61e --- /dev/null +++ b/contracts/lib/openzeppelin-contracts-upgradeable @@ -0,0 +1 @@ +Subproject commit deaf61e8d17373dd8b07f0c7ed6d2e958f45eb62 diff --git a/contracts/lib/openzeppelin-foundry-upgrades b/contracts/lib/openzeppelin-foundry-upgrades new file mode 160000 index 0000000..dd9e5dd --- /dev/null +++ b/contracts/lib/openzeppelin-foundry-upgrades @@ -0,0 +1 @@ +Subproject commit dd9e5dd22b885b364354af6a1cbad8a36958e3df diff --git a/contracts/remappings.txt b/contracts/remappings.txt new file mode 100644 index 0000000..33d8345 --- /dev/null +++ b/contracts/remappings.txt @@ -0,0 +1,3 @@ +@openzeppelin/contracts/=lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/ +@openzeppelin/contracts-upgradeable/=lib/openzeppelin-contracts-upgradeable/contracts/ +@openzeppelin/foundry-upgrades/=lib/openzeppelin-foundry-upgrades/src/ \ No newline at end of file diff --git a/contracts/script/Owshen.s.sol b/contracts/script/Owshen.s.sol new file mode 100644 index 0000000..cf15772 --- /dev/null +++ b/contracts/script/Owshen.s.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../src/Owshen.sol"; +import "@openzeppelin/foundry-upgrades/Upgrades.sol"; + +contract DeployOwshen is Script { + function run() external returns (address, address) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + address _proxyAddress = Upgrades.deployUUPSProxy("Owshen.sol", abi.encodeCall(Owshen.initialize, (deployer))); + + address implementationAddress = Upgrades.getImplementationAddress(_proxyAddress); + + vm.stopBroadcast(); + + return (implementationAddress, _proxyAddress); + } +} + +// PRIVATE_KEY=1 forge script script/Owshen.s.sol --broadcast --fork-url http://localhost:8545 diff --git a/contracts/script/UpgradeOwshen.s.sol b/contracts/script/UpgradeOwshen.s.sol new file mode 100644 index 0000000..f6dd046 --- /dev/null +++ b/contracts/script/UpgradeOwshen.s.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.13; + +import "forge-std/Script.sol"; +import "../src/OwshenV2.sol"; +import "@openzeppelin/foundry-upgrades/Upgrades.sol"; + +contract UpgradeOwshen is Script { + function run() external returns (address) { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + vm.startBroadcast(deployerPrivateKey); + + address proxy = vm.envAddress("PROXY_ADDRESS"); + address newImpl = address(new OwshenV2()); + UnsafeUpgrades.upgradeProxy(proxy, newImpl, abi.encodeWithSignature("initializeV2(address)", deployer)); + address newImplAddress = Upgrades.getImplementationAddress(proxy); + + vm.stopBroadcast(); + + return (newImplAddress); + } +} diff --git a/contracts/src/Dive.sol b/contracts/src/Dive.sol new file mode 100644 index 0000000..4ae4bac --- /dev/null +++ b/contracts/src/Dive.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "openzeppelin-contracts/contracts/token/ERC20/ERC20.sol"; +import "forge-std/console.sol"; + +contract Dive is ERC20 { + constructor() ERC20("Dive", "DIVE") { + _mint(msg.sender, 1_000_000_000 ether); + } +} diff --git a/contracts/src/Owshen.sol b/contracts/src/Owshen.sol new file mode 100644 index 0000000..4dddeee --- /dev/null +++ b/contracts/src/Owshen.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract Owshen is Initializable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + + address private owner; + mapping(uint256 => bool) public isExecuted; + + modifier onlyOwner() { + require(owner == msg.sender, "ERROR: Caller is not the owner"); + _; + } + + event WithdrawExecuted(address indexed to, address indexed token, uint256 id, uint256 amount); + + function initialize(address _owner) public initializer { + __UUPSUpgradeable_init(); + owner = _owner; + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /** + * @notice Withdraws the specified amount of ERC20 tokens to the msg.sender if the signature is valid. + * @param _signature The signature to verify the withdraw request. + * @param _tokenAddress The address of the ERC20 token to withdraw. + * @param _amount The amount of ERC20 tokens to withdraw. + * @param _id The unique id of the withdrawl. + */ + function withdrawToken(bytes memory _signature, address _tokenAddress, uint256 _amount, uint256 _id) + public + nonReentrant + onlyProxy + { + _processWithdraw(_signature, _tokenAddress, _amount, _id); + IERC20(_tokenAddress).safeTransfer(msg.sender, _amount); + } + + /** + * @notice Withdraws the specified amount of the chains native coin to the msg.sender if the signature is valid. + * @param _signature The signature to verify the withdraw request. + * @param _amount The amount of native coin to withdraw. + * @param _id The unique id of the withdrawl. + */ + function withdrawNative(bytes memory _signature, uint256 _amount, uint256 _id) public nonReentrant onlyProxy { + require(address(this).balance >= _amount, "ERROR: insufficient contract balance."); + _processWithdraw(_signature, address(0), _amount, _id); + payable(msg.sender).transfer(_amount); + } + + /** + * @notice Internal function to process withdrawal requests. + * @param _signature The signature provided by the owner to authorize the withdrawal. + * @param _tokenAddress The address of the token to withdraw (use address(0) for native coin of the chain). + * @param _amount The amount of tokens or native coin to withdraw. + * @param _id The unique id of the withdrawl. + */ + function _processWithdraw(bytes memory _signature, address _tokenAddress, uint256 _amount, uint256 _id) internal { + require(!isExecuted[_id], "ERROR: withdraw already executed."); + + bytes32 hash = keccak256(abi.encode(msg.sender, _tokenAddress, _amount, _id, block.chainid)); + + bool isSigValid = SignatureChecker.isValidSignatureNow(owner, hash, _signature); + + require(isSigValid, "ERROR: invalid signature."); + + isExecuted[_id] = true; + + emit WithdrawExecuted(msg.sender, _tokenAddress, _id, _amount); + } + + /** + * @notice Allows the current owner to transfer ownership to a new address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "ERROR: New owner is the zero address"); + owner = newOwner; + } + + receive() external payable onlyProxy {} +} diff --git a/contracts/src/OwshenV2.sol b/contracts/src/OwshenV2.sol new file mode 100644 index 0000000..bd549a6 --- /dev/null +++ b/contracts/src/OwshenV2.sol @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/SignatureChecker.sol"; +import "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol"; + +contract OwshenV2 is Initializable, ReentrancyGuardUpgradeable, UUPSUpgradeable { + using SafeERC20 for IERC20; + + address private owner; + mapping(uint256 => bool) public isExecuted; + + modifier onlyOwner() { + require(owner == msg.sender, "ERROR: Caller is not the owner"); + _; + } + + event WithdrawExecuted(address indexed to, address indexed token, uint256 id, uint256 amount); + + function initializeV2(address _owner) public reinitializer(2) { + __UUPSUpgradeable_init(); + owner = _owner; + } + + function _authorizeUpgrade(address newImplementation) internal override onlyOwner {} + + /** + * @notice Withdraws the specified amount of ERC20 tokens to the msg.sender if the signature is valid. + * @param _signature The signature to verify the withdraw request. + * @param _tokenAddress The address of the ERC20 token to withdraw. + * @param _amount The amount of ERC20 tokens to withdraw. + * @param _id The unique id of the withdrawl. + */ + function withdrawToken(bytes memory _signature, address _tokenAddress, uint256 _amount, uint256 _id) + public + nonReentrant + onlyProxy + { + _processWithdraw(_signature, _tokenAddress, _amount, _id); + IERC20(_tokenAddress).safeTransfer(msg.sender, _amount); + } + + /** + * @notice Withdraws the specified amount of the chains native coin to the msg.sender if the signature is valid. + * @param _signature The signature to verify the withdraw request. + * @param _amount The amount of native coin to withdraw. + * @param _id The unique id of the withdrawl. + */ + function withdrawNative(bytes memory _signature, uint256 _amount, uint256 _id) public nonReentrant onlyProxy { + require(address(this).balance >= _amount, "ERROR: insufficient contract balance."); + _processWithdraw(_signature, address(0), _amount, _id); + payable(msg.sender).transfer(_amount); + } + + /** + * @notice Internal function to process withdrawal requests. + * @param _signature The signature provided by the owner to authorize the withdrawal. + * @param _tokenAddress The address of the token to withdraw (use address(0) for native coin of the chain). + * @param _amount The amount of tokens or native coin to withdraw. + * @param _id The unique id of the withdrawl. + */ + function _processWithdraw(bytes memory _signature, address _tokenAddress, uint256 _amount, uint256 _id) internal { + require(!isExecuted[_id], "ERROR: withdraw already executed."); + + bytes32 hash = keccak256(abi.encode(msg.sender, _tokenAddress, _amount, _id, block.chainid)); + + bool isSigValid = SignatureChecker.isValidSignatureNow(owner, hash, _signature); + + require(isSigValid, "ERROR: invalid signature."); + + isExecuted[_id] = true; + + emit WithdrawExecuted(msg.sender, _tokenAddress, _id, _amount); + } + + /** + * @notice Allows the current owner to transfer ownership to a new address. + * @param newOwner The address to transfer ownership to. + */ + function transferOwnership(address newOwner) public onlyOwner { + require(newOwner != address(0), "ERROR: New owner is the zero address"); + owner = newOwner; + } + + receive() external payable onlyProxy {} +} diff --git a/contracts/test/Owshen.t.sol b/contracts/test/Owshen.t.sol new file mode 100644 index 0000000..73a7d88 --- /dev/null +++ b/contracts/test/Owshen.t.sol @@ -0,0 +1,318 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.26; + +import {Test} from "forge-std/Test.sol"; +import "forge-std/console.sol"; + +import {Owshen} from "../src/Owshen.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/foundry-upgrades/Upgrades.sol"; + +contract SampleOwshenV2 is Owshen { + function initializeV2() public {} + + function echo() public pure returns (uint256) { + return 1; + } +} + +contract SampleERC20 is ERC20 { + constructor() ERC20("SampleERC20", "S") { + this; + } + + function mint(address to, uint256 amount) public { + _mint(to, amount); + } +} + +contract OwshenTest is Test { + Owshen public yoctoChain; + address public owner; + address public _proxyAddress; + address public _implAddress; + uint256 internal ownerPrivateKey; + address public user; + uint256 internal userPrivateKey; + SampleERC20 public token; + + error InvalidNumber(uint256 number); + + function setUp() public { + ownerPrivateKey = 1; + owner = vm.addr(ownerPrivateKey); + userPrivateKey = 2; + user = vm.addr(userPrivateKey); + + vm.startPrank(owner); + + _implAddress = address(new Owshen()); + _proxyAddress = + UnsafeUpgrades.deployUUPSProxy(_implAddress, abi.encodeWithSignature("initialize(address)", owner)); + + token = new SampleERC20(); + + vm.stopPrank(); + } + + function depoist(uint256 amount) internal { + vm.startPrank(user); + + token.mint(user, amount); + require(token.balanceOf(user) == amount, "balance of user is not equal to amount"); + + token.transfer(_proxyAddress, amount); + require(token.balanceOf(user) == 0, "balance of user should be 0"); + require(token.balanceOf(_proxyAddress) == amount, "balance of yoctoChain should be equal to amount"); + + vm.stopPrank(); + } + + function test_recive_success() public { + require(token.balanceOf(user) == 0, "balance of user should be 0"); + require(token.balanceOf(_proxyAddress) == 0, "balance of yoctoChain should be 0"); + depoist(100); + require(token.balanceOf(user) == 0, "balance of user should be 0"); + require(token.balanceOf(_proxyAddress) == 100, "balance of yoctoChain should be equal to amount"); + } + + function test_withdraw_token_success() public { + depoist(100); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(token), 100, 1, block.chainid)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + address signer = ecrecover(hash, v, r, s); + require(owner == signer, "owner should be signer"); + + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawToken(bytes,address,uint256,uint256)", signature, address(token), 100, 1) + ); + assertTrue(success); + + vm.stopPrank(); + + require(token.balanceOf(user) == 100, "balance of user should be 100"); + require(token.balanceOf(_proxyAddress) == 0, "balance of yoctoChain should be 0"); + } + + function test_withdraw_token_fail_invalid_signature() public { + depoist(100); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(token), 50, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawToken(bytes,address,uint256,uint256)", signature, address(token), 100, 1) + ); + + // call returns success because of revert was expected + assertTrue(success); + + vm.stopPrank(); + + require(token.balanceOf(user) == 0, "balance of user should be 0"); + require(token.balanceOf(_proxyAddress) == 100, "balance of yoctoChain should be 100"); + } + + function test_withdraw_token_fail_invalid_id() public { + depoist(100); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(token), 100, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(); + yoctoChain.withdrawToken(signature, address(token), 100, 2); + + vm.stopPrank(); + + require(token.balanceOf(user) == 0, "balance of user should be 0"); + require(token.balanceOf(_proxyAddress) == 100, "balance of yoctoChain should be 100"); + } + + function test_withdraw_token_fail_signature_replay() public { + depoist(100); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(token), 100, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + (bool success1,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawToken(bytes,address,uint256,uint256)", signature, address(token), 100, 1) + ); + assertTrue(success1); + + vm.expectRevert(); + (bool success2,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawToken(bytes,address,uint256,uint256)", signature, address(token), 100, 1) + ); + assertTrue(success2); + + vm.stopPrank(); + + require(token.balanceOf(user) == 100, "balance of user should be 100"); + require(token.balanceOf(_proxyAddress) == 0, "balance of yoctoChain should be 0"); + } + + function test_withdraw_native_success() public { + vm.deal(_proxyAddress, 100 ether); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(0), 100 ether, 1, block.chainid)); + + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + address signer = ecrecover(hash, v, r, s); + require(owner == signer, "owner should be signer"); + + require(user.balance == 0, "balance of user should be 0 ether"); + require(_proxyAddress.balance == 100 ether, "balance of yoctoChain should be 100 ether"); + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 100 ether, 1) + ); + assertTrue(success); + + vm.stopPrank(); + + require(user.balance == 100 ether, "balance of user should be 100 ether"); + require(_proxyAddress.balance == 0, "balance of yoctoChain should be 0 ether"); + } + + function test_withdraw_native_fail_invalid_signature() public { + vm.deal(_proxyAddress, 100 ether); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(0), 50, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 100 ether, 1) + ); + assertTrue(success); + + vm.stopPrank(); + + require(user.balance == 0, "balance of user should be 0 ether"); + } + + function test_withdraw_native_fail_invalid_id() public { + vm.deal(_proxyAddress, 100 ether); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(0), 100, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 100 ether, 2) + ); + assertTrue(success); + + vm.stopPrank(); + + require(user.balance == 0, "balance of user should be 0 ether"); + } + + function test_withdraw_native_fail_invalid_balance() public { + vm.deal(_proxyAddress, 100 ether); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(0), 100, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 101 ether, 1) + ); + assertTrue(success); + + vm.stopPrank(); + + require(user.balance == 0, "balance of user should be 0 ether"); + } + + function test_withdraw_native_fail_signature_replay() public { + vm.deal(_proxyAddress, 100 ether); + vm.startPrank(user); + + bytes32 hash = keccak256(abi.encode(user, address(0), 100, 1, block.chainid)); + (uint8 v, bytes32 r, bytes32 s) = vm.sign(ownerPrivateKey, hash); + bytes memory signature = abi.encodePacked(r, s, v); + + (bool success1,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 100, 1) + ); + assertTrue(success1); + + vm.expectRevert(); + (bool success2,) = address(_proxyAddress).call( + abi.encodeWithSignature("withdrawNative(bytes,uint256,uint256)", signature, 100, 1) + ); + assertTrue(success2); + + vm.stopPrank(); + + require(user.balance == 100, "balance of user should be 100 ether"); + } + + function test_transferOwnership_success() public { + vm.startPrank(owner); + (bool success1,) = address(_proxyAddress).call(abi.encodeWithSignature("transferOwnership(address)", user)); + assertTrue(success1); + + vm.startPrank(user); + (bool success2,) = address(_proxyAddress).call(abi.encodeWithSignature("transferOwnership(address)", owner)); + assertTrue(success2); + + vm.stopPrank(); + } + + function test_transferOwnership_fail_zero_address() public { + vm.startPrank(owner); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call(abi.encodeWithSignature("transferOwnership(address)", address(0))); + assertTrue(success); + + vm.stopPrank(); + } + + function test_transferOwnership_fail_not_owner() public { + vm.startPrank(user); + + vm.expectRevert(); + (bool success,) = address(_proxyAddress).call(abi.encodeWithSignature("transferOwnership(address)", user)); + assertTrue(success); + + vm.stopPrank(); + } + + function test_upgrade_success() public { + vm.startPrank(owner); + + address newImpl = address(new SampleOwshenV2()); + UnsafeUpgrades.upgradeProxy(_proxyAddress, newImpl, abi.encodeWithSignature("initializeV2()")); + address newImplAddress = Upgrades.getImplementationAddress(_proxyAddress); + + require(newImplAddress == newImpl, "new implementation address is wrong"); + require(newImplAddress != _implAddress, "new implementation address should be different from old one"); + + (bool success, bytes memory data) = address(_proxyAddress).staticcall(abi.encodeWithSignature("echo()")); + assertTrue(success); + uint256 result = abi.decode(data, (uint256)); + require(result == 1, "result should be 1"); + vm.stopPrank(); + } +} diff --git a/misc/Makefile b/misc/Makefile new file mode 100644 index 0000000..a125035 --- /dev/null +++ b/misc/Makefile @@ -0,0 +1,4 @@ +.PHONY=deploy + +deploy: + cd foundry-project && forge create --rpc-url http://127.0.0.1:8645 --private-key 0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d src/Counter.sol:Counter diff --git a/misc/foundry-project/.github/workflows/test.yml b/misc/foundry-project/.github/workflows/test.yml new file mode 100644 index 0000000..9282e82 --- /dev/null +++ b/misc/foundry-project/.github/workflows/test.yml @@ -0,0 +1,34 @@ +name: test + +on: workflow_dispatch + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Run Forge build + run: | + forge --version + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/misc/foundry-project/.gitignore b/misc/foundry-project/.gitignore new file mode 100644 index 0000000..85198aa --- /dev/null +++ b/misc/foundry-project/.gitignore @@ -0,0 +1,14 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env diff --git a/misc/foundry-project/README.md b/misc/foundry-project/README.md new file mode 100644 index 0000000..9265b45 --- /dev/null +++ b/misc/foundry-project/README.md @@ -0,0 +1,66 @@ +## Foundry + +**Foundry is a blazing fast, portable and modular toolkit for Ethereum application development written in Rust.** + +Foundry consists of: + +- **Forge**: Ethereum testing framework (like Truffle, Hardhat and DappTools). +- **Cast**: Swiss army knife for interacting with EVM smart contracts, sending transactions and getting chain data. +- **Anvil**: Local Ethereum node, akin to Ganache, Hardhat Network. +- **Chisel**: Fast, utilitarian, and verbose solidity REPL. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/misc/foundry-project/foundry.toml b/misc/foundry-project/foundry.toml new file mode 100644 index 0000000..25b918f --- /dev/null +++ b/misc/foundry-project/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/misc/foundry-project/lib/forge-std b/misc/foundry-project/lib/forge-std new file mode 160000 index 0000000..1714bee --- /dev/null +++ b/misc/foundry-project/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 1714bee72e286e73f76e320d110e0eaf5c4e649d diff --git a/misc/foundry-project/script/Counter.s.sol b/misc/foundry-project/script/Counter.s.sol new file mode 100644 index 0000000..df9ee8b --- /dev/null +++ b/misc/foundry-project/script/Counter.s.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Script, console} from "forge-std/Script.sol"; + +contract CounterScript is Script { + function setUp() public {} + + function run() public { + vm.broadcast(); + } +} diff --git a/misc/foundry-project/src/Counter.sol b/misc/foundry-project/src/Counter.sol new file mode 100644 index 0000000..aded799 --- /dev/null +++ b/misc/foundry-project/src/Counter.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +contract Counter { + uint256 public number; + + function setNumber(uint256 newNumber) public { + number = newNumber; + } + + function increment() public { + number++; + } +} diff --git a/misc/foundry-project/test/Counter.t.sol b/misc/foundry-project/test/Counter.t.sol new file mode 100644 index 0000000..54b724f --- /dev/null +++ b/misc/foundry-project/test/Counter.t.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import {Test, console} from "forge-std/Test.sol"; +import {Counter} from "../src/Counter.sol"; + +contract CounterTest is Test { + Counter public counter; + + function setUp() public { + counter = new Counter(); + counter.setNumber(0); + } + + function test_Increment() public { + counter.increment(); + assertEq(counter.number(), 1); + } + + function testFuzz_SetNumber(uint256 x) public { + counter.setNumber(x); + assertEq(counter.number(), x); + } +} diff --git a/src/blockchain/config.rs b/src/blockchain/config.rs new file mode 100644 index 0000000..4e04a53 --- /dev/null +++ b/src/blockchain/config.rs @@ -0,0 +1,15 @@ +use std::sync::Arc; + +use alloy::primitives::Address; + +use crate::genesis::Genesis; + +#[derive(Debug, Clone)] + +pub struct Config { + pub chain_id: u64, + pub owner: Option
, + pub owshen: Address, + pub genesis: Arc, + pub provider_address: reqwest::Url, +} diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs new file mode 100644 index 0000000..1b34ab6 --- /dev/null +++ b/src/blockchain/mod.rs @@ -0,0 +1,480 @@ +use std::collections::VecDeque; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::db::{Key, KvStore, MirrorKvStore, Value}; +use crate::services::ContextKvStore; +use crate::types::{ + BincodableOwshenTransaction, Block, CustomTxMsg, IncludedTransaction, OwshenTransaction, Token, +}; + +use alloy::primitives::{Address, FixedBytes, U256}; +use anyhow::{anyhow, Result}; + +mod config; +pub use config::Config; +mod ovm; +pub mod tx; + +pub trait Blockchain { + fn config(&self) -> &Config; + fn get_block(&self, index: usize) -> Result; + fn get_last_block(&self) -> Result>; + fn get_height(&self) -> Result; + fn get_balance(&self, token: Token, address: Address) -> Result; + fn get_allowance(&self, owner: Address, spender: Address, token: Token) -> Result; + fn get_custom_nonce(&self, address: Address) -> Result; + fn get_eth_nonce(&self, address: Address) -> Result; + fn push_block(&mut self, block: Block) -> Result<()>; + fn pop_block(&mut self) -> Result>; + fn draft_block(&self, txs: &mut TransactionQueue, timestamp: u64) -> Result; + fn get_transactions_by_block(&self, block_index: usize) -> Result>; + fn get_transaction_by_hash( + &self, + tx_hash: FixedBytes<32>, + ) -> Result; + fn get_user_withdrawals(&self, address: Address) -> Result>; + fn get_total_transactions(&self) -> Result; + fn get_transactions_per_second(&self) -> Result; + fn get_transactions_by_block_paginated( + &self, + index: usize, + offset: usize, + limit: usize, + ) -> Result>; + fn get_blocks(&self, offset: usize, limit: usize) -> Result>; + fn get_token_decimal(&self, token_address: Address) -> Result; + fn get_token_symbol(&self, token_address: Address) -> Result; +} + +#[derive(Debug, Clone)] +pub struct Owshenchain { + config: Config, + pub db: K, +} +#[derive(Debug, Clone)] +pub struct TransactionQueue { + queue: VecDeque, +} + +impl TransactionQueue { + pub fn new() -> Self { + Self { + queue: VecDeque::new(), + } + } + + pub fn enqueue(&mut self, tx: OwshenTransaction) { + self.queue.push_back(tx); + } + + pub fn dequeue(&mut self) -> Option { + self.queue.pop_front() + } + + pub fn queue(&self) -> &VecDeque { + &self.queue + } +} + +impl Owshenchain { + pub fn new(config: Config, db: K) -> Self { + Self { config, db } + } + fn fork<'a>(&'a self) -> Owshenchain> { + Owshenchain { + config: self.config.clone(), + db: MirrorKvStore::new(&self.db), + } + } + + fn atomic>) -> Result>( + &mut self, + f: F, + ) -> Result { + let mut fork = self.fork(); + let ret = (f)(&mut fork)?; + let buff = fork.db.buffer(); + self.db.batch_put_raw(buff.clone().into_iter())?; + Ok(ret) + } + + fn apply_tx(&mut self, tx: &OwshenTransaction) -> Result<()> { + self.atomic(|chain| { + let from = tx.signer()?; + + if tx.chain_id()? != chain.config.chain_id { + return Err(anyhow!("Chain id is not valid!")); + } + + match tx { + OwshenTransaction::Custom(custom_tx) => match custom_tx.msg()? { + // CustomTxMsg::OwshenAirdrop { + // owshen_address, + // owshen_sig, + // } => { + // log::info!("Someone is claiming his owshen airdrop, by {}!", from); + // } + CustomTxMsg::MintTx(mint_data) => { + tx::mint_tx( + chain, + mint_data.tx_hash.to_vec(), + mint_data.user_tx_hash, + mint_data.token, + mint_data.amount, + mint_data.address, + )?; + log::info!("Mint transaction, by {}!", from); + } + CustomTxMsg::BurnTx(burn_data) => { + tx::burn_tx(chain, burn_data)?; + log::info!("Burn transaction, by {}!", from); + } + }, + OwshenTransaction::Eth(eth_tx) => { + tx::eth(chain, from, eth_tx)?; + } + } + Ok(()) + }) + } + + fn store_block_hash(&mut self, block: Block) -> Result<()> { + let block_hash = block.hash()?; + let key = Key::BlockHash(U256::from_be_bytes(block_hash.into())); + let value = Value::Block(block); + self.db.put(key, Some(value)) + } +} + +impl Blockchain for Owshenchain { + fn config(&self) -> &Config { + &self.config + } + fn get_block(&self, index: usize) -> Result { + if index >= self.get_height()? { + Err(anyhow!("Block doesn't exist!")) + } else if let Some(blk) = self.db.get(Key::Block(index))? { + blk.as_block() + } else { + Err(anyhow!("Inconsistency!")) + } + } + fn get_last_block(&self) -> Result> { + let height = self.get_height()?; + if height > 0 { + Ok(Some(self.get_block(height - 1)?)) + } else { + Ok(None) + } + } + fn get_height(&self) -> Result { + if let Some(v) = self.db.get(Key::Height)? { + v.as_usize() + } else { + Ok(0) + } + } + fn get_custom_nonce(&self, address: Address) -> Result { + if let Some(v) = self.db.get(Key::NonceCustom(address))? { + v.as_u256() + } else { + Ok(U256::from(0)) + } + } + fn get_eth_nonce(&self, address: Address) -> Result { + if let Some(v) = self.db.get(Key::NonceEth(address))? { + v.as_u256() + } else { + Ok(U256::from(0)) + } + } + + fn get_balance(&self, token: Token, address: Address) -> Result { + if let Some(v) = self.db.get(Key::Balance(address, token.clone()))? { + return v.as_u256(); + } + if let Some(genesis_balance) = self + .config + .genesis + .tokens + .get(&token) + .and_then(|balances| balances.get(&address)) + { + return Ok(*genesis_balance); + } + Ok(U256::from(0)) + } + + fn get_allowance(&self, owner: Address, spender: Address, token: Token) -> Result { + if let Some(v) = self.db.get(Key::Allowance(owner, spender, token.clone()))? { + return v.as_u256(); + } else { + Ok(U256::from(0)) + } + } + + fn push_block(&mut self, block: Block) -> Result<()> { + self.atomic(move |chain| { + let height = chain.get_height()?; + let curr_hash = if let Some(b) = chain.get_last_block()? { + Some(b.hash()?) + } else { + None + }; + + if block.index != height { + return Err(anyhow!("Bad previous hash!")); + } + + if block.prev_hash != curr_hash { + return Err(anyhow!("Bad block index!")); + } + + if let Some(owner) = chain.config().owner { + if !block.is_signed_by(owner)? { + return Err(anyhow!("Block is not correctly signed!")); + } + } + + let tx_count = chain.get_total_transactions()?; + let new_tx_count = tx_count + U256::from(block.txs.len()); + chain + .db + .put(Key::TransactionCount, Some(Value::U256(new_tx_count)))?; + + for (ind, bin_tx) in block.txs.iter().enumerate() { + let tx = bin_tx.try_into()?; + chain.apply_tx(&tx)?; + chain.db.put( + Key::TransactionHash(tx.hash()?), + Some(Value::Transaction(IncludedTransaction { + transaction_index: ind, + tx: bin_tx.clone(), + block_hash: block.hash()?, + block_number: block.index, + })), + )?; + + // add tx to user_transactions + let key = Key::Transactions(tx.signer()?); + let mut transactions: Vec = match chain.db.get(key.clone())? { + Some(Value::Transactions(existing_transactions)) => existing_transactions, + None => Vec::new(), + _ => return Err(anyhow!("Unexpected value type")), + }; + + let included_transaction = IncludedTransaction { + tx: bin_tx.clone(), + block_hash: block.hash()?, + block_number: block.index, + transaction_index: ind, + }; + + transactions.push(included_transaction); + + chain.db.put(key, Some(Value::Transactions(transactions)))?; + } + + let _ = chain.store_block_hash(block.clone()); + chain.db.put(Key::Height, Some(Value::Usize(height + 1)))?; + chain + .db + .put(Key::Block(height), Some(Value::Block(block)))?; + + let delta = chain.db.rollback()?; + chain + .db + .put(Key::Delta(height + 1), Some(Value::BTreeMap(delta.clone())))?; + Ok(()) + }) + } + fn pop_block(&mut self) -> Result> { + self.atomic(|chain| { + let height = chain.get_height()?; + if height == 0 { + Ok(None) + } else { + let block = chain.get_block(height - 1)?; + let tx_count = chain.get_total_transactions()?; + let new_tx_count = tx_count + U256::from(block.txs.len()); + chain + .db + .put(Key::TransactionCount, Some(Value::U256(new_tx_count)))?; + + let delta_blob = chain + .db + .get(Key::Delta(height))? + .ok_or(anyhow!("Delta not found!"))?; + chain + .db + .batch_put_raw(delta_blob.as_btreemap()?.into_iter())?; + chain.db.put(Key::Delta(height), None)?; + Ok(Some(block)) + } + }) + } + fn draft_block(&self, txs: &mut TransactionQueue, timestamp: u64) -> Result { + let mut selected_txs = Vec::new(); + let mut fork = self.fork(); + + while let Some(tx) = txs.dequeue() { + if fork.apply_tx(&tx).is_ok() { + selected_txs.push(tx) + } else { + //pass + } + } + + let prev_hash = if let Some(b) = self.get_last_block()? { + Some(b.hash()?) + } else { + None + }; + + let blk = Block { + prev_hash, + index: self.get_height()?, + txs: selected_txs + .iter() + .map(|t| t.try_into()) + .collect::, _>>()?, + sig: None, + timestamp, + }; + + Ok(blk) + } + + fn get_transactions_by_block(&self, index: usize) -> Result> { + let blk = self.get_block(index)?; + let transactions = blk + .txs + .into_iter() + .map(|t| (&t).try_into()) + .collect::, _>>()?; + Ok(transactions) + } + + fn get_transactions_by_block_paginated( + &self, + index: usize, + offset: usize, + limit: usize, + ) -> Result> { + let blk = self.get_block(index)?; + let total_txs = blk.txs.len(); + if offset >= total_txs { + return Ok(vec![]); + } + let end = std::cmp::min(offset + limit, total_txs); + + let paginated_txs = blk.txs[offset..end] + .iter() + .map(|t| t.try_into()) + .collect::, _>>()?; + + Ok(paginated_txs) + } + + fn get_transaction_by_hash(&self, tx_hash: FixedBytes<32>) -> Result { + let key = Key::TransactionHash(tx_hash); + if let Some(Value::Transaction(tx)) = self.db.get(key)? { + Ok(tx.try_into()?) + } else { + Err(anyhow!("Transaction not found!")) + } + } + + fn get_user_withdrawals(&self, address: Address) -> Result> { + let key = Key::Transactions(address); + + if let Some(Value::Transactions(included_transactions)) = self.db.get(key)? { + let withdrawals: Vec = included_transactions + .into_iter() + .filter(|included_tx| { + if let Ok(OwshenTransaction::Custom(custom_tx)) = + included_tx.tx.clone().try_into() + { + if let Ok(CustomTxMsg::BurnTx(_)) = custom_tx.msg() { + return true; + } + } + false + }) + .collect(); + + Ok(withdrawals) + } else { + Err(anyhow!("No transactions found for this user!")) + } + } + + fn get_total_transactions(&self) -> Result { + if let Some(v) = self.db.get(Key::TransactionCount)? { + v.as_u256() + } else { + Ok(U256::default()) + } + } + + fn get_transactions_per_second(&self) -> Result { + let height = self.get_height()?; + + if height < 2 { + return Ok(0.0); + } + + let last_block = self.get_block(height - 1)?; + let previous_block = self.get_block(height - 2)?; + let last_timestamp = last_block.timestamp as f64; + let previous_timestamp = previous_block.timestamp as f64; + let num_transactions = last_block.txs.len() as f64; + let time_span = last_timestamp - previous_timestamp; + + if time_span > 0.0 { + let tps = num_transactions / time_span; + return Ok(tps); + } + + Ok(0.0) + } + + fn get_blocks(&self, offset: usize, limit: usize) -> Result> { + let height = self.get_height()?; + + if offset >= height { + return Ok(Vec::new()); // No blocks to return + } + + let end = std::cmp::min(offset + limit, height); + + let mut blocks = Vec::with_capacity(end - offset); + for index in offset..end { + if let Some(block) = self.get_block(index).ok() { + blocks.push(block); + } else { + return Err(anyhow!("Inconsistency detected while fetching blocks")); + } + } + + Ok(blocks) + } + + fn get_token_decimal(&self, token_address: Address) -> Result { + if let Some(v) = self.db.get(Key::TokenDecimal(token_address))? { + v.as_u256() + } else { + Ok(U256::from(0)) + } + } + fn get_token_symbol(&self, token_address: Address) -> Result { + if let Some(v) = self.db.get(Key::TokenSymbol(token_address))? { + v.as_string() + } else { + Ok(String::from("Unknown")) + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/blockchain/ovm/mod.rs b/src/blockchain/ovm/mod.rs new file mode 100644 index 0000000..b859081 --- /dev/null +++ b/src/blockchain/ovm/mod.rs @@ -0,0 +1,188 @@ +use std::cell::RefCell; + +use anyhow::Result; +use evm::{ + Capture, Context, CreateScheme, ExitError, ExitReason, ExternalOperation, Handler, Machine, + Opcode, Stack, Transfer, +}; +use primitive_types::{H160, H256, U256}; + +use crate::{ + db::{Key, KvStore, Value}, + services::ContextKvStore, +}; + +use super::Owshenchain; + +// Owshen Virtual Machine +pub struct Ovm<'a, K: ContextKvStore> { + error: RefCell>, + chain: &'a mut Owshenchain, +} + +impl<'a, K: ContextKvStore> Ovm<'a, K> { + pub fn new(chain: &'a mut Owshenchain) -> Self { + Self { + chain, + error: RefCell::new(None), + } + } +} + +impl<'a, K: ContextKvStore> Handler for Ovm<'a, K> { + type CallFeedback = (); + type CallInterrupt = (); + type CreateFeedback = (); + type CreateInterrupt = (); + fn balance(&self, _address: H160) -> U256 { + unimplemented!() + } + fn block_base_fee_per_gas(&self) -> U256 { + unimplemented!() + } + fn block_coinbase(&self) -> H160 { + unimplemented!() + } + fn block_difficulty(&self) -> U256 { + unimplemented!() + } + fn block_gas_limit(&self) -> U256 { + unimplemented!() + } + fn block_hash(&self, _number: U256) -> H256 { + unimplemented!() + } + fn block_number(&self) -> U256 { + unimplemented!() + } + fn block_randomness(&self) -> Option { + unimplemented!() + } + fn block_timestamp(&self) -> U256 { + unimplemented!() + } + fn call( + &mut self, + _code_address: H160, + _transfer: Option, + _input: Vec, + _target_gas: Option, + _is_static: bool, + _context: Context, + ) -> Capture<(ExitReason, Vec), Self::CallInterrupt> { + unimplemented!() + } + fn call_feedback(&mut self, _feedback: Self::CallFeedback) -> Result<(), ExitError> { + unimplemented!() + } + fn chain_id(&self) -> U256 { + unimplemented!() + } + fn code(&self, _address: H160) -> Vec { + unimplemented!() + } + fn code_hash(&self, _address: H160) -> H256 { + unimplemented!() + } + fn code_size(&self, _address: H160) -> U256 { + unimplemented!() + } + fn create( + &mut self, + _caller: H160, + _scheme: CreateScheme, + _value: U256, + _init_code: Vec, + _target_gas: Option, + ) -> Capture<(ExitReason, Option, Vec), Self::CreateInterrupt> { + unimplemented!() + } + fn create_feedback(&mut self, _feedback: Self::CreateFeedback) -> Result<(), ExitError> { + unimplemented!() + } + fn deleted(&self, _address: H160) -> bool { + unimplemented!() + } + fn exists(&self, _address: H160) -> bool { + unimplemented!() + } + fn gas_left(&self) -> U256 { + unimplemented!() + } + fn gas_price(&self) -> U256 { + unimplemented!() + } + fn is_cold(&mut self, _address: H160, _index: Option) -> Result { + unimplemented!() + } + fn log(&mut self, _address: H160, _topics: Vec, data: Vec) -> Result<(), ExitError> { + println!("Log: {:?}", data); + Ok(()) + } + fn mark_delete(&mut self, _address: H160, _target: H160) -> Result<(), ExitError> { + unimplemented!() + } + fn origin(&self) -> H160 { + unimplemented!() + } + fn original_storage(&self, _address: H160, _index: H256) -> H256 { + unimplemented!() + } + fn other(&mut self, _opcode: Opcode, _stack: &mut Machine) -> Result<(), ExitError> { + unimplemented!() + } + fn pre_validate( + &mut self, + _context: &Context, + _opcode: Opcode, + _stack: &Stack, + ) -> Result<(), ExitError> { + if let Some(err) = self.error.borrow().as_ref() { + return Err(ExitError::Other(format!("{}", err).into())); + } + Ok(()) + } + fn record_external_operation(&mut self, _op: ExternalOperation) -> Result<(), ExitError> { + unimplemented!() + } + fn set_storage(&mut self, address: H160, index: H256, _value: H256) -> Result<(), ExitError> { + let index_alloy = alloy::primitives::U256::from_le_bytes(index.to_fixed_bytes()); + let address_alloy = alloy::primitives::Address::from_slice(&address.to_fixed_bytes()); + self.chain + .db + .put( + Key::ContractStorage(address_alloy, index_alloy), + Some(Value::U256(index_alloy)), + ) + .map_err(|_| ExitError::Other("DB Failure".into()))?; + Ok(()) + } + fn storage(&self, address: H160, index: H256) -> H256 { + let f = move || -> Result { + let index_alloy = alloy::primitives::U256::from_le_bytes(index.to_fixed_bytes()); + let address_alloy = alloy::primitives::Address::from_slice(&address.to_fixed_bytes()); + Ok( + if let Some(val) = self + .chain + .db + .get(Key::ContractStorage(address_alloy, index_alloy))? + { + H256::from(val.as_u256()?.to_le_bytes()) + } else { + Default::default() + }, + ) + }; + + match f() { + Ok(res) => res, + Err(e) => { + *self.error.borrow_mut() = Some(e); + Default::default() + } + } + } +} + +#[cfg(test)] +mod tests; diff --git a/src/blockchain/ovm/tests.rs b/src/blockchain/ovm/tests.rs new file mode 100644 index 0000000..33b6c47 --- /dev/null +++ b/src/blockchain/ovm/tests.rs @@ -0,0 +1,41 @@ +// use alloy::{ +// network::{EthereumWallet, TransactionBuilder}, +// rpc::types::TransactionRequest, +// signers::local::PrivateKeySigner, +// }; + +// use crate::{ +// blockchain::Config, +// config::{self, CHAIN_ID}, +// db::RamKvStore, +// genesis::GENESIS, +// types::OwshenTransaction, +// }; + +// use super::*; + +// #[tokio::test] +// async fn test_contract_storage() { +// let conf = Config { +// chain_id: CHAIN_ID, +// owner: None, +// genesis: GENESIS.clone(), +// owshen: config::OWSHEN_CONTRACT, +// }; +// let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); +// let signer: PrivateKeySigner = PrivateKeySigner::random(); +// let vitalik = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" +// .parse() +// .unwrap(); +// let tx = TransactionRequest::default() +// .with_to(vitalik) +// .with_deploy_code(vec![0, 0, 0, 0]) +// .with_nonce(1) +// .with_gas_limit(100) +// .with_max_fee_per_gas(100) +// .with_max_priority_fee_per_gas(100) +// .with_chain_id(CHAIN_ID); +// let wallet = EthereumWallet::new(signer); +// let tx = OwshenTransaction::Eth(tx.build(&wallet).await.unwrap()); +// chain.apply_tx(&tx).unwrap(); +// } diff --git a/src/blockchain/tests.rs b/src/blockchain/tests.rs new file mode 100644 index 0000000..8a98531 --- /dev/null +++ b/src/blockchain/tests.rs @@ -0,0 +1,563 @@ +use std::clone; + +use super::*; +use crate::config; +use crate::db::{Key, KvStore, Value}; +use crate::types::{network::Network, Burn, CustomTx, Token}; +use crate::types::{Mint, ERC20}; +use crate::{db::RamKvStore, genesis::GENESIS}; +use alloy::primitives::Uint; +use alloy::primitives::{utils::parse_units, Address, U256}; +use alloy::signers::local::PrivateKeySigner; +use anyhow::Ok; +use std::time::{SystemTime, UNIX_EPOCH}; +use tokio::time::{sleep, Duration}; + +#[test] +fn test_block_storage() -> Result<(), anyhow::Error> { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + let mut tx_queue = TransactionQueue::new(); + + let timestamp: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + // Then you can pass it to draft_block + let _new_block = chain.draft_block(&mut tx_queue, timestamp).unwrap(); + assert_eq!(chain.get_height().unwrap(), 0); + let timestamp_2: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block = chain.draft_block(&mut tx_queue, timestamp_2).unwrap(); + chain.push_block(new_block).unwrap(); + assert_eq!(chain.get_height().unwrap(), 1); + let timestamp_3: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block = chain.draft_block(&mut tx_queue, timestamp_3).unwrap(); + chain.push_block(new_block).unwrap(); + assert_eq!(chain.get_height().unwrap(), 2); + let timestamp_4: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block = chain.draft_block(&mut tx_queue, timestamp_4).unwrap(); + chain.push_block(new_block).unwrap(); + assert_eq!(chain.get_height().unwrap(), 3); + + assert_eq!(chain.pop_block().unwrap().unwrap().index, 2); + assert_eq!(chain.get_height().unwrap(), 2); + assert_eq!(chain.pop_block().unwrap().unwrap().index, 1); + assert_eq!(chain.get_height().unwrap(), 1); + assert_eq!(chain.pop_block().unwrap().unwrap().index, 0); + assert_eq!(chain.get_height().unwrap(), 0); + + assert!(chain.pop_block().unwrap().is_none()); + Ok(()) +} + +#[test] +fn test_get_balance_without_genesis_balance() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let mock_user_address = Address::from([1; 20]); + let mock_token: Token = Token::Erc20(ERC20 { + address: Address::from([5;20]), + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }); + let mock_erc20_amount = U256::from(20); + let mock_notive_amount = U256::from(2); + + // native + assert_eq!( + chain.get_balance(Token::Native, mock_user_address).unwrap(), + U256::from(0) + ); + chain + .db + .put( + Key::Balance(mock_user_address, Token::Native), + Some(Value::U256(mock_notive_amount)), + ) + .unwrap(); + assert_eq!( + chain.get_balance(Token::Native, mock_user_address).unwrap(), + mock_notive_amount + ); + + print!( + "amount {:?}", + chain + .get_balance(mock_token.clone(), mock_user_address) + .unwrap() + ); + + // token + assert_eq!( + chain + .get_balance(mock_token.clone(), mock_user_address) + .unwrap(), + U256::from(0) + ); + chain + .db + .put( + Key::Balance(mock_user_address, mock_token.clone()), + Some(Value::U256(mock_erc20_amount)), + ) + .unwrap(); + assert_eq!( + chain + .get_balance(mock_token.clone(), mock_user_address) + .unwrap(), + mock_erc20_amount + ); +} + +#[test] +fn test_get_balance_with_genesis_balance() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let genesis_mock_user_address: Address = Address::from([2; 20]); + let genesis_token: Token = Token::Erc20(ERC20 { + address: Address::from([3;20]), + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }); + let mock_genesis_native_amount = parse_units("2", 18).unwrap().into(); + let mock_genesis_token_amount = parse_units("200", 18).unwrap().into(); + + // user with (native) genesis balance + assert_eq!( + chain + .get_balance(Token::Native, genesis_mock_user_address) + .unwrap(), + mock_genesis_native_amount + ); + + // genesis native amonut + 1 + let add_native_amount: Uint<256, 4> = parse_units("1", 18).unwrap().into(); + let new_user_native_balance = mock_genesis_native_amount + add_native_amount; + + chain + .db + .put( + Key::Balance(genesis_mock_user_address, Token::Native), + Some(Value::U256(new_user_native_balance)), + ) + .unwrap(); + + assert_eq!( + chain + .get_balance(Token::Native, genesis_mock_user_address) + .unwrap(), + new_user_native_balance + ); + + // user with (token) genesis balance + + assert_eq!( + chain + .get_balance(genesis_token.clone(), genesis_mock_user_address) + .unwrap(), + mock_genesis_token_amount + ); + + // genesis token amonut + 100 + let add_amount: Uint<256, 4> = parse_units("100", 18).unwrap().into(); + let new_user_balance = mock_genesis_token_amount + add_amount; + + chain + .db + .put( + Key::Balance(genesis_mock_user_address, genesis_token.clone()), + Some(Value::U256(new_user_balance)), + ) + .unwrap(); + + assert_eq!( + chain + .get_balance(genesis_token.clone(), genesis_mock_user_address) + .unwrap(), + new_user_balance + ); +} + +#[test] +fn test_get_nonce_eth() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let mock_address = Address::from([1; 20]); + + assert_eq!(chain.get_eth_nonce(mock_address).unwrap(), U256::from(0)); + + chain + .db + .put( + Key::NonceEth(mock_address), + Some(Value::U256(U256::from(5))), + ) + .unwrap(); + + assert_eq!(chain.get_eth_nonce(mock_address).unwrap(), U256::from(5)); +} + +#[test] +fn test_get_nonce_custom() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let mock_address = Address::from([2; 20]); + assert_eq!(chain.get_custom_nonce(mock_address).unwrap(), U256::from(0)); + + chain + .db + .put( + Key::NonceCustom(mock_address), + Some(Value::U256(U256::from(10))), + ) + .unwrap(); + + assert_eq!( + chain.get_custom_nonce(mock_address).unwrap(), + U256::from(10) + ); +} + +#[tokio::test] +async fn test_get_user_withdraw_transactions() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let tx = CustomTx::create( + &mut signer.clone(), + 123, + CustomTxMsg::BurnTx(Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: U256::from(100), + calldata: None, + }), + ) + .await + .unwrap(); + + let bincodable_tx: BincodableOwshenTransaction = tx.clone().try_into().unwrap(); + + let included_tx: IncludedTransaction = IncludedTransaction { + tx: bincodable_tx, + block_hash: FixedBytes::from([0u8; 32]), + block_number: 4321, + transaction_index: 1, + }; + let key = Key::Transactions(signer.address()); + let mut transactions: Vec = Vec::new(); + transactions.push(included_tx.clone()); + + chain + .db + .put(key, Some(Value::Transactions(transactions))) + .unwrap(); + + let withdrawals = chain.get_user_withdrawals(signer.address()).unwrap(); + assert_eq!(withdrawals.len(), 1); + assert_eq!(withdrawals[0].tx, included_tx.tx); + assert_eq!(withdrawals[0].block_number, included_tx.block_number); + assert_eq!(withdrawals[0].block_hash, included_tx.block_hash); + assert_eq!( + withdrawals[0].transaction_index, + included_tx.transaction_index + ); +} + +#[tokio::test] +async fn test_get_transactions_per_second() -> Result<(), anyhow::Error> { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf.clone(), RamKvStore::new()); + let mut tx_queue = TransactionQueue::new(); + + let tx_hash = vec![0u8; 32]; + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx1 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx1.clone()); + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx2 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + tx_queue.enqueue(tx2.clone()); + let timestamp: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block1 = chain.draft_block(&mut tx_queue, timestamp).unwrap(); + chain.push_block(new_block1.clone()).unwrap(); + + assert_eq!(chain.get_height().unwrap(), 1); + + sleep(Duration::from_secs(1)).await; + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx3 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx3); + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx4 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx4); + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx5 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx5); + let timestamp: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block2 = chain.draft_block(&mut tx_queue, timestamp).unwrap(); + chain.push_block(new_block2.clone()).unwrap(); + + assert_eq!(chain.get_height().unwrap(), 2); + + sleep(Duration::from_secs(1)).await; + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx6 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx6); + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx7 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx7); + + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let tx8 = CustomTx::create( + &mut signer.clone(), + conf.clone().chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + + tx_queue.enqueue(tx8); + let timestamp: u64 = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let new_block3 = chain.draft_block(&mut tx_queue, timestamp).unwrap(); + chain.push_block(new_block3.clone()).unwrap(); + + assert_eq!(chain.get_height().unwrap(), 3); + + // Log the TPS calculation + let tps = chain.get_transactions_per_second().unwrap(); + println!("TPS: {:?}", tps); + assert!(tps > 0.0, "TPS should be greater than zero"); + Ok(()) +} + +#[test] +fn test_get_allowance() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + let mut chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let mock_user_address = Address::from([1; 20]); + let mock_spender_address = Address::from([2; 20]); + let mock_token: Token = Token::Erc20(ERC20 { + address: Address::from([5; 20]), + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }); + let mock_erc20_amount = U256::from(20); + + assert_eq!( + chain + .get_allowance(mock_user_address, mock_spender_address, mock_token.clone()) + .unwrap(), + U256::from(0) + ); + + chain + .db + .put( + Key::Allowance(mock_user_address, mock_spender_address, mock_token.clone()), + Some(Value::U256(mock_erc20_amount)), + ) + .unwrap(); + assert_eq!( + chain + .get_allowance(mock_user_address, mock_spender_address, mock_token.clone()) + .unwrap(), + mock_erc20_amount + ); +} diff --git a/src/blockchain/tx/burn_tx.rs b/src/blockchain/tx/burn_tx.rs new file mode 100644 index 0000000..6dc2274 --- /dev/null +++ b/src/blockchain/tx/burn_tx.rs @@ -0,0 +1,176 @@ +use alloy::primitives::{Address, U256}; +use anyhow::Result; + +use crate::{ + blockchain::{Blockchain, Owshenchain}, + db::{Key, KvStore, Value}, + services::ContextKvStore, + types::{Burn, Token, WithdrawCalldata}, +}; + +pub fn burn_tx(_chain: &mut Owshenchain, _data: Burn) -> Result<()> { + let address = match _data.calldata { + Some(WithdrawCalldata::Eth { address }) => address, + _ => return Err(anyhow::anyhow!("Invalid calldata!")), + }; + + if _chain.db.get(Key::BurnId(_data.burn_id.clone()))?.is_some() { + return Err(anyhow::anyhow!("Burn id already used!")); + } + + let user_balance = _chain.get_balance(_data.token.clone(), address)?; + if user_balance < _data.amount { + return Err(anyhow::anyhow!("Insufficient balance!")); + } + + _chain.db.put( + Key::Balance(address, _data.token.clone()), + Some(Value::U256(user_balance - _data.amount)), + )?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloy::{primitives::FixedBytes, signers::local::PrivateKeySigner}; + + use crate::{ + blockchain::Config, config::OWSHEN_CONTRACT, db::RamKvStore, genesis::GENESIS, + types::network::Network, + }; + + use super::*; + + #[test] + fn test_burn_tx_success() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + chain + .db + .put( + Key::Balance(address, Token::Native), + Some(Value::U256(U256::from(1000))), + ) + .unwrap(); + + let data = Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: U256::from(100), + calldata: Some(WithdrawCalldata::Eth { address }), + }; + + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(1000) + ); + burn_tx(&mut chain, data).unwrap(); + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(900) + ); + } + + #[test] + fn test_burn_tx_fail_already_used_burn_id() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + chain + .db + .put( + Key::Balance(address, Token::Native), + Some(Value::U256(U256::from(1000))), + ) + .unwrap(); + + let data = Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: U256::from(100), + calldata: Some(WithdrawCalldata::Eth { address }), + }; + + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(1000) + ); + burn_tx(&mut chain, data.clone()).unwrap(); + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(900) + ); + + chain + .db + .put(Key::BurnId(data.burn_id.clone()), Some(Value::Void)) + .unwrap(); + assert!(burn_tx(&mut chain, data).is_err()); + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(900) + ); + } + + #[test] + fn test_burn_tx_insufficient_balance() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + chain + .db + .put( + Key::Balance(address, Token::Native), + Some(Value::U256(U256::from(1000))), + ) + .unwrap(); + + let data = Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: U256::from(1001), + calldata: Some(WithdrawCalldata::Eth { address }), + }; + + assert_eq!( + chain.get_balance(Token::Native, address).unwrap(), + U256::from(1000) + ); + assert!(burn_tx(&mut chain, data).is_err()); + } +} diff --git a/src/blockchain/tx/erc20.rs b/src/blockchain/tx/erc20.rs new file mode 100644 index 0000000..354658e --- /dev/null +++ b/src/blockchain/tx/erc20.rs @@ -0,0 +1,199 @@ +use std::{ops::Add, rc::Rc}; + +use alloy::consensus::Transaction; +use alloy::primitives::Uint; +use alloy::rlp::bytes::buf::Chain; +use alloy::rlp::Decodable; +use alloy::{ + consensus::TxEnvelope, + primitives::{keccak256, Address, TxKind, U256}, + sol_types::SolValue, +}; +use alloy_sol_types::abi::token; +use anyhow::{anyhow, Error, Result}; +use evm::{Capture, Context, ExitReason, Runtime}; + +use crate::types::{Token, ERC20}; +use crate::{ + blockchain::{Blockchain, Owshenchain}, + db::{Key, KvStore, Value}, + services::ContextKvStore, +}; + +pub enum Erc20Operation { + Transfer { + receiver: Address, + value: alloy::primitives::U256, + }, + TransferFrom { + from: Address, + receiver: Address, + value: alloy::primitives::U256, + }, + Approve { + spender: Address, + value: alloy::primitives::U256, + }, +} + +pub fn extract_erc20_transfer(tx: &TxEnvelope) -> Result> { + let tx = tx + .as_eip1559() + .ok_or(anyhow!("Only EIP-1559 is supported!"))?; + + if tx.tx().value > U256::from(0) { + return Ok(None); + } + + if tx.tx().input.len() < 4 { + return Err(anyhow!("Unknown transaction input!")); + } + + match &tx.tx().input[..4] { + &[169, 5, 156, 187] => { + let (receiver, value): (Address, alloy::primitives::U256) = + SolValue::abi_decode(&tx.tx().input[4..], true)?; + Ok(Some(Erc20Operation::Transfer { receiver, value })) + } + &[35, 184, 114, 221] => { + let (from, receiver, value): (Address, Address, alloy::primitives::U256) = + SolValue::abi_decode(&tx.tx().input[4..], true)?; + Ok(Some(Erc20Operation::TransferFrom { + from, + receiver, + value, + })) + } + &[9, 94, 167, 179] => { + let (spender, value): (Address, alloy::primitives::U256) = + SolValue::abi_decode(&tx.tx().input[4..], true)?; + Ok(Some(Erc20Operation::Approve { spender, value })) + } + _ => Err(anyhow!("Unknown function signature")), + } +} + +pub fn handle_erc20_transfer( + chain: &mut Owshenchain, + msg_sender: Address, + receiver: Address, + value: Uint<256, 4>, + token: Address, +) -> Result<()> { + let token_decimals = chain.get_token_decimal(token)?; + let token_symbol = chain.get_token_symbol(token)?; + let tx_token = Token::Erc20(ERC20 { + address: token, + decimals: token_decimals, + symbol: token_symbol, + }); + let sender_balance = chain.get_balance(tx_token.clone(), msg_sender)?; + + if sender_balance >= value { + chain.db.put( + Key::Balance(msg_sender, tx_token.clone()), + Some(Value::U256(sender_balance - value)), + )?; + + let receiver_balance = chain.get_balance(tx_token.clone(), receiver)?; + chain.db.put( + Key::Balance(receiver, tx_token.clone()), + Some(Value::U256(receiver_balance + value)), + )?; + + let current_nonce = chain.get_eth_nonce(msg_sender)?; + let incremented_nonce = current_nonce + U256::from(1); + chain.db.put( + Key::NonceEth(msg_sender), + Some(Value::U256(incremented_nonce)), + )?; + + Ok(()) + } else { + Err(anyhow!("Insufficient balance.")) + } +} + +pub fn handle_erc20_transfer_from( + chain: &mut Owshenchain, + msg_sender: Address, + from: Address, + receiver: Address, + value: Uint<256, 4>, + token: Address, +) -> Result<()> { + let token_decimals = chain.get_token_decimal(token)?; + let token_symbol = chain.get_token_symbol(token)?; + let tx_token = Token::Erc20(ERC20 { + address: token, + decimals: token_decimals, + symbol: token_symbol, + }); + let allowance = chain.get_allowance(from, msg_sender, tx_token.clone())?; + let sender_balance = chain.get_balance(tx_token.clone(), from)?; + + if sender_balance < value { + return Err(anyhow!("Insufficient balance")); + } + + if allowance < value { + return Err(anyhow!("Insufficient Allowance")); + } + + chain.db.put( + Key::Balance(from, tx_token.clone()), + Some(Value::U256(sender_balance - value)), + )?; + + chain.db.put( + Key::Allowance(from, msg_sender, tx_token.clone()), + Some(Value::U256(allowance - value)), + )?; + + let receiver_balance = chain.get_balance(tx_token.clone(), receiver)?; + + chain.db.put( + Key::Balance(receiver, tx_token.clone()), + Some(Value::U256(receiver_balance + value)), + )?; + + let current_nonce = chain.get_eth_nonce(msg_sender)?; + let incremented_nonce = current_nonce + U256::from(1); + chain.db.put( + Key::NonceEth(msg_sender), + Some(Value::U256(incremented_nonce)), + )?; + + Ok(()) +} + +pub fn handle_erc20_approve( + chain: &mut Owshenchain, + msg_sender: Address, + spender: Address, + value: Uint<256, 4>, + token: Address, +) -> Result<()> { + let token_decimals = chain.get_token_decimal(token)?; + let token_symbol = chain.get_token_symbol(token)?; + let tx_token = Token::Erc20(ERC20 { + address: token, + decimals: token_decimals, + symbol: token_symbol, + }); + let allowance = chain.get_allowance(msg_sender, spender, tx_token.clone())?; + + chain.db.put( + Key::Allowance(msg_sender, spender, tx_token.clone()), + Some(Value::U256(allowance + value)), + )?; + + let current_nonce = chain.get_eth_nonce(msg_sender)?; + let incremented_nonce = current_nonce + U256::from(1); + chain.db.put( + Key::NonceEth(msg_sender), + Some(Value::U256(incremented_nonce)), + )?; + + Ok(()) +} diff --git a/src/blockchain/tx/eth.rs b/src/blockchain/tx/eth.rs new file mode 100644 index 0000000..c74df4a --- /dev/null +++ b/src/blockchain/tx/eth.rs @@ -0,0 +1,484 @@ +use std::{ops::Add, rc::Rc, sync::Arc}; +use tokio::sync::Mutex; + +use alloy::{ + consensus::{Transaction, TxEip1559, TxEnvelope, TypedTransaction}, + network::{Ethereum, EthereumWallet, NetworkWallet}, + primitives::{keccak256, Address, Bytes, TxKind, Uint, B256, U256}, + rlp::Decodable, + rpc::types::{AccessList, AccessListItem}, + signers::local::PrivateKeySigner, + sol_types::SolValue, +}; +use alloy_sol_types::abi::encode; + +use anyhow::{anyhow, Result}; + +use evm::{Capture, ExitReason, Runtime}; + +use crate::{ + blockchain::{tx::erc20::*, Blockchain, Config, Owshenchain, TransactionQueue}, + config, + db::{Key, KvStore, RamKvStore, Value}, + genesis::GENESIS, + services::{Context, ContextKvStore}, + types::{Token, ERC20}, +}; + +pub fn eth( + _chain: &mut Owshenchain, + _msg_sender: Address, + _tx: &TxEnvelope, +) -> Result<()> { + log::info!("Ethereum transaction, by {}!", _msg_sender); + let tx = _tx + .as_eip1559() + .ok_or(anyhow!("Only EIP1559 transactions are supported!"))?; + match tx.tx().to { + TxKind::Create => { + return Err(anyhow!("Contract creation is not supported.")); + // let bytecode = tx.tx().input.to_vec(); + // let mut ovm = Ovm::new(chain); + // let ctx = Context { + // address: H160::random(), + // apparent_value: U256::from(0), + // caller: H160::random(), + // }; + // let mut runtime = Runtime::new(Rc::new(bytecode), Rc::new(vec![]), ctx, 10000, 10000); + // match runtime.run(&mut ovm) { + // Capture::Exit(e) => match e { + // ExitReason::Succeed(_s) => { + // let contract_code = runtime.machine().return_value(); + // let contract_address = Address::from_slice( + // &keccak256((by, chain.get_eth_nonce(by)?).abi_encode()).0[12..], + // ); + // chain.db.put( + // Key::ContractCode(contract_address), + // Some(Value::VecU8(contract_code)), + // )?; + // println!("Contract deployed on {}!", contract_address); + // } + // _ => { + // return Err(anyhow!("Failed!")); + // } + // }, + // Capture::Trap(_) => { + // return Err(anyhow!("Trapped!")); + // } + // } + } + TxKind::Call(to) => { + let bytecode = _chain.db.get(Key::ContractCode(to))?; + if let Some(bytecode) = bytecode { + return Err(anyhow!("Contract calls are not supported.")); + + // let bytecode = bytecode.as_vec_u8()?; + // let mut ovm = Ovm::new(chain); + // let ctx = Context { + // address: H160::random(), + // apparent_value: U256::from(0), + // caller: H160::random(), + // }; + // let mut runtime = Runtime::new( + // Rc::new(bytecode), + // Rc::new(tx.tx().input.to_vec()), + // ctx, + // 10000, + // 10000, + // ); + // match runtime.run(&mut ovm) { + // Capture::Exit(e) => match e { + // ExitReason::Succeed(_s) => { + // let _ret_value = runtime.machine().return_value(); + // } + // _ => { + // return Err(anyhow!("Failed!")); + // } + // }, + // Capture::Trap(_) => { + // return Err(anyhow!("Trapped!")); + // } + // } + } else { + let transaction = extract_erc20_transfer(&_tx)?; + match transaction { + Some(Erc20Operation::Transfer { receiver, value }) => { + handle_erc20_transfer(_chain, _msg_sender, receiver, value, to)?; + } + Some(Erc20Operation::TransferFrom { + from, + receiver, + value, + }) => { + handle_erc20_transfer_from(_chain, _msg_sender, from, receiver, value, to)? + } + Some(Erc20Operation::Approve { spender, value }) => { + handle_erc20_approve(_chain, _msg_sender, spender, value, to)? + } + None => { + let value = tx.tx().value(); + let sender_balance = _chain.get_balance(Token::Native, _msg_sender)?; + + if sender_balance >= value { + _chain.db.put( + Key::Balance(_msg_sender, Token::Native), + Some(Value::U256(sender_balance - value)), + )?; + + let privious_receiver_balance = + _chain.get_balance(Token::Native, to)?; + + _chain.db.put( + Key::Balance(to, Token::Native), + Some(Value::U256(privious_receiver_balance + value)), + )?; + let current_nonce = _chain.get_eth_nonce(_msg_sender)?; + let incremented_nonce = current_nonce + U256::from(1); + _chain.db.put( + Key::NonceEth(_msg_sender), + Some(Value::U256(incremented_nonce)), + )?; + } else { + return Err(anyhow!("Insufficient balance.")); + } + } + } + } + } + } + Ok(()) +} + +fn setup_mock_chain() -> Owshenchain { + let mock_db = RamKvStore::new(); + return Owshenchain { + db: mock_db.clone(), + config: Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }, + }; +} + +#[tokio::test] +async fn test_erc20_transfer() { + let mut chain = setup_mock_chain(); + let msg_sender = Address::from([1; 20]); + let receiver = Address::from([2; 20]); + let token_contract = Address::from([6; 20]); + + let transaction_value = Uint::<256, 4>::from(0); + + let token_decimals = chain.get_token_decimal(token_contract).unwrap(); + let token_symbol = chain.get_token_symbol(token_contract).unwrap(); + let tx_token = Token::Erc20(ERC20 { + address: token_contract, + decimals: token_decimals, + symbol: token_symbol, + }); + + let _ = chain.db.put( + Key::Balance(msg_sender, tx_token.clone()), + Some(Value::U256(U256::from(100000))), + ); + + let sender_pre_transaction_balance = chain.get_balance(tx_token.clone(), msg_sender).unwrap(); + let receiver_pre_transaction_balance = chain.get_balance(tx_token.clone(), receiver).unwrap(); + + let wallet = EthereumWallet::new(PrivateKeySigner::random()); + + let transfer_method = [169, 5, 156, 187]; + + let mut data = Vec::new(); + let re_encoded = receiver.abi_encode(); + let val_encoded = transaction_value.abi_encode(); + data.extend_from_slice(&transfer_method); + data.extend_from_slice(&re_encoded); + data.extend_from_slice(&val_encoded); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(token_contract), + value: Uint::<256, 4>::from(0), + input: Bytes::from(data.clone()), + chain_id: chain.config.chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx.clone()); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let pre_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + let result = eth(&mut chain, msg_sender, &signed_tx); + assert!(result.is_ok()); + + let sender_post_transaction_balance = chain.get_balance(tx_token.clone(), msg_sender).unwrap(); + let receiver_post_transaction_balance = chain.get_balance(tx_token.clone(), receiver).unwrap(); + let post_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + assert_eq!( + sender_post_transaction_balance, + sender_pre_transaction_balance - transaction_value + ); + assert_eq!( + receiver_post_transaction_balance, + receiver_pre_transaction_balance + transaction_value + ); + + assert_eq!(post_tx_nonce, pre_tx_nonce + U256::from(1)); + assert_eq!(tx.value().clone(), U256::from(0)); +} + +#[tokio::test] +async fn test_erc20_approve() { + let mut chain = setup_mock_chain(); + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + + let owner: Address = + >::default_signer_address(&wallet); + + let spender = Address::from([2; 20]); + let token_contract = Address::from([6; 20]); + + let token_decimals = chain.get_token_decimal(token_contract).unwrap(); + let token_symbol = chain.get_token_symbol(token_contract).unwrap(); + let tx_token = Token::Erc20(ERC20 { + address: token_contract, + decimals: token_decimals, + symbol: token_symbol, + }); + + let transaction_value = Uint::<256, 4>::from(0); + + let spender_pre_transaction_allowance = chain + .get_allowance(owner, spender, tx_token.clone()) + .unwrap(); + + let approve_method = [9, 94, 167, 179]; + + let mut data = Vec::new(); + let spender_encoded = spender.abi_encode(); + let val_encoded = transaction_value.abi_encode(); + data.extend_from_slice(&approve_method); + data.extend_from_slice(&spender_encoded); + data.extend_from_slice(&val_encoded); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(token_contract), + value: Uint::<256, 4>::from(0), + input: Bytes::from(data.clone()), + chain_id: chain.config.chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let pre_tx_nonce = chain.get_eth_nonce(owner).unwrap(); + + let result = eth(&mut chain, owner, &signed_tx); + + assert!(result.is_ok()); + + let post_tx_nonce = chain.get_eth_nonce(owner).unwrap(); + + let spender_post_transaction_allowance = chain + .get_allowance(owner, spender, tx_token.clone()) + .unwrap(); + + assert_eq!( + spender_post_transaction_allowance, + spender_pre_transaction_allowance + transaction_value + ); + + assert_eq!(post_tx_nonce, pre_tx_nonce + U256::from(1)); +} + +#[tokio::test] +async fn test_erc20_transfer_from() { + let mut chain = setup_mock_chain(); + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + let msg_sender: Address = + >::default_signer_address(&wallet); + + let receiver = Address::from([2; 20]); + let from = Address::from([3; 20]); + let token_contract = Address::from([6; 20]); + + let token_decimals = chain.get_token_decimal(token_contract).unwrap(); + let token_symbol = chain.get_token_symbol(token_contract).unwrap(); + let tx_token = Token::Erc20(ERC20 { + address: token_contract, + decimals: token_decimals, + symbol: token_symbol, + }); + + let transaction_value = Uint::<256, 4>::from(0); + + let _ = chain.db.put( + Key::Balance(from, tx_token.clone()), + Some(Value::U256(U256::from(100000))), + ); + + let _ = chain.db.put( + Key::Allowance(from, msg_sender, tx_token.clone()), + Some(Value::U256(U256::from(100000))), + ); + + let from_pre_transaction_balance = chain.get_balance(tx_token.clone(), from).unwrap(); + let receiver_pre_transaction_balance = chain.get_balance(tx_token.clone(), receiver).unwrap(); + let msg_sender_pre_transaction_allowance = chain + .get_allowance(from, msg_sender, tx_token.clone()) + .unwrap(); + let pre_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + let transfer_from_method = [35, 184, 114, 221]; + + let mut data = Vec::new(); + let from_encoded = from.abi_encode(); + let receiver_encoded = receiver.abi_encode(); + let val_encoded = transaction_value.abi_encode(); + data.extend_from_slice(&transfer_from_method); + data.extend_from_slice(&from_encoded); + data.extend_from_slice(&receiver_encoded); + data.extend_from_slice(&val_encoded); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(token_contract), + value: Uint::<256, 4>::from(0), + input: Bytes::from(data.clone()), + chain_id: chain.config.chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![ + AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }, + AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }, + ]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx.clone()); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let result = eth(&mut chain, msg_sender, &signed_tx); + assert!(result.is_ok()); + let post_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + let from_post_transaction_balance = chain.get_balance(tx_token.clone(), from).unwrap(); + let receiver_post_transaction_balance = chain.get_balance(tx_token.clone(), receiver).unwrap(); + + let msg_sender_post_transaction_allowance = chain + .get_allowance(from, msg_sender, tx_token.clone()) + .unwrap(); + + assert_eq!( + from_post_transaction_balance, + from_pre_transaction_balance - transaction_value + ); + assert_eq!( + receiver_post_transaction_balance, + receiver_pre_transaction_balance + transaction_value + ); + + assert_eq!( + msg_sender_post_transaction_allowance, + msg_sender_pre_transaction_allowance - transaction_value + ); + assert_eq!(post_tx_nonce, pre_tx_nonce + U256::from(1)); + assert_eq!(tx.value().clone(), U256::from(0)); +} + +#[tokio::test] +async fn test_eth_transfer() { + let mut chain = setup_mock_chain(); + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + let msg_sender: Address = + >::default_signer_address(&wallet); + + let receiver = Address::from([3; 20]); + let transaction_value = Uint::<256, 4>::from(1000); + + let _ = chain.db.put( + Key::Balance(msg_sender, Token::Native), + Some(Value::U256(U256::from(100000))), + ); + + let from_pre_transaction_balance = chain.get_balance(Token::Native, msg_sender).unwrap(); + let receiver_pre_transaction_balance = chain.get_balance(Token::Native, receiver).unwrap(); + let pre_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: TxKind::Call(receiver), + value: Uint::<256, 4>::from(1000), + input: Bytes::new(), + chain_id: chain.config.chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let result = eth(&mut chain, msg_sender, &signed_tx); + assert!(result.is_ok()); + + let from_post_transaction_balance = chain.get_balance(Token::Native, msg_sender).unwrap(); + let receiver_post_transaction_balance = chain.get_balance(Token::Native, receiver).unwrap(); + let post_tx_nonce = chain.get_eth_nonce(msg_sender).unwrap(); + + assert_eq!( + from_post_transaction_balance, + from_pre_transaction_balance - transaction_value + ); + assert_eq!( + receiver_post_transaction_balance, + receiver_pre_transaction_balance + transaction_value + ); + assert_eq!(post_tx_nonce, pre_tx_nonce + U256::from(1)); +} diff --git a/src/blockchain/tx/mint_tx.rs b/src/blockchain/tx/mint_tx.rs new file mode 100644 index 0000000..45c436c --- /dev/null +++ b/src/blockchain/tx/mint_tx.rs @@ -0,0 +1,250 @@ +use alloy::primitives::{Address, FixedBytes, U256}; +use anyhow::{Ok, Result}; + +use crate::{ + blockchain::{Blockchain, Owshenchain}, + db::{Key, Value}, + services::ContextKvStore, + types::Token, +}; + +pub fn mint_tx( + _chain: &mut Owshenchain, + _tx_hash: Vec, + _user_tx_hash: String, + _token: Token, + _amount: U256, + _address: Address, +) -> Result<(), anyhow::Error> { + if _tx_hash.len() != 32 { + return Err(anyhow::anyhow!("Transaction hash must be 32 bytes")); + } + + if _chain + .db + .get(crate::db::Key::DepositedTransaction(_user_tx_hash))? + .is_some() + { + return Err(anyhow::anyhow!("Transaction already exists")); + } + + if _chain + .db + .get(Key::TransactionHash(FixedBytes::<32>::from_slice( + &_tx_hash, + )))? + .is_some() + { + return Err(anyhow::anyhow!("Transaction already exists")); + } + + let user_balance = _chain.get_balance(_token.clone(), _address)?; + let new_balance = user_balance + _amount; + _chain.db.put( + Key::Balance(_address, _token.clone()), + Some(Value::U256(new_balance)), + )?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use alloy::signers::local::PrivateKeySigner; + + use crate::{ + blockchain::Config, + config::{CHAIN_ID, OWSHEN_CONTRACT}, + db::{KvStore, RamKvStore}, + genesis::GENESIS, + types::{CustomTx, CustomTxMsg, IncludedTransaction, Mint}, + }; + + use super::*; + + #[test] + fn test_mint_success() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx_hash = vec![0u8; 32]; + let token = Token::Native; + let amount = U256::from(100); + + let result = mint_tx(&mut chain, tx_hash, user_tx_hash, token.clone(), amount, address); + assert!(result.is_ok()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + } + + #[test] + fn test_mint_fail() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx_hash = vec![0u8; 32]; + let token = Token::Native; + let amount = U256::from(100); + + let result = mint_tx( + &mut chain, + tx_hash.clone(), + user_tx_hash.clone(), + token.clone(), + amount, + address, + ); + assert!(result.is_ok()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + + let result = mint_tx(&mut chain, tx_hash, user_tx_hash, token.clone(), amount, address); + assert!(result.is_ok()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount * U256::from(2)); + } + + #[tokio::test] + async fn test_mint_double_spend() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx_hash = vec![0u8; 32]; + let token = Token::Native; + let amount = U256::from(100); + + let result = mint_tx( + &mut chain, + tx_hash.clone(), + user_tx_hash.clone(), + token.clone(), + amount, + address, + ); + assert!(result.is_ok()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + + let tx = CustomTx::create( + &mut signer.clone(), + CHAIN_ID, + CustomTxMsg::MintTx(Mint { + tx_hash: tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token: token.clone(), + amount, + address, + }), + ) + .await + .unwrap(); + let bincodable_tx = tx.clone().try_into().unwrap(); + let block_number = 4321; + + let included_tx = IncludedTransaction { + tx: bincodable_tx, + block_hash: FixedBytes::from([0u8; 32]), + block_number, + transaction_index: 1, + }; + + chain + .db + .put( + Key::TransactionHash(FixedBytes::from_slice(&tx_hash)), + Some(Value::Transaction(included_tx)), + ) + .unwrap(); + + let result = mint_tx(&mut chain, tx_hash, user_tx_hash, token.clone(), amount, address); + assert!(result.is_err()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + } + + #[tokio::test] + async fn test_mint_double_spend_with_same_user_transaction_hash() { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let mut chain = Owshenchain::new(conf, RamKvStore::new()); + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx_hash = vec![0u8; 32]; + let token = Token::Native; + let amount = U256::from(100); + + let result = mint_tx( + &mut chain, + tx_hash.clone(), + user_tx_hash.clone(), + token.clone(), + amount, + address, + ); + assert!(result.is_ok()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + + chain + .db + .put( + Key::DepositedTransaction(user_tx_hash.clone()), + Some(Value::DepositedTransaction(user_tx_hash.clone())), + ) + .unwrap(); + + let result = mint_tx(&mut chain, tx_hash, user_tx_hash, token.clone(), amount, address); + assert!(result.is_err()); + + let balance = chain.get_balance(token.clone(), address).unwrap(); + assert_eq!(balance, amount); + } +} diff --git a/src/blockchain/tx/mod.rs b/src/blockchain/tx/mod.rs new file mode 100644 index 0000000..28a38be --- /dev/null +++ b/src/blockchain/tx/mod.rs @@ -0,0 +1,10 @@ +mod eth; +pub use eth::*; +pub mod owshen_airdrop; + +mod burn_tx; +mod erc20; +mod mint_tx; +pub use burn_tx::*; +pub use erc20::*; +pub use mint_tx::*; diff --git a/src/blockchain/tx/owshen_airdrop/babyjubjub/mod.rs b/src/blockchain/tx/owshen_airdrop/babyjubjub/mod.rs new file mode 100644 index 0000000..720bc05 --- /dev/null +++ b/src/blockchain/tx/owshen_airdrop/babyjubjub/mod.rs @@ -0,0 +1,237 @@ +use anyhow::anyhow; +use anyhow::Result; +use ff::PrimeField; +use num_integer::Integer; +use serde::{Deserialize, Serialize}; + +#[derive(PrimeField, Serialize, Deserialize)] +#[PrimeFieldModulus = "21888242871839275222246405745257275088548364400416034343698204186575808495617"] +#[PrimeFieldGenerator = "7"] +#[PrimeFieldReprEndianness = "little"] +pub struct Fp([u64; 4]); + +use std::ops::*; +use std::str::FromStr; + +use ff::{Field, PrimeFieldBits}; +use num_bigint::BigUint; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, Eq)] +pub struct PointCompressed(pub Fp, pub bool); + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default, Eq)] +pub struct PointAffine(pub Fp, pub Fp); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PointProjective(pub Fp, pub Fp, pub Fp); + +impl PointAffine { + fn add_assign(&mut self, other: &PointAffine) -> Result<()> { + if *self == *other { + *self = self.double()?; + return Ok(()); + } + let xx = Option::::from((Fp::ONE + *D * self.0 * other.0 * self.1 * other.1).invert()) + .ok_or(anyhow!("Cannot invert"))?; + let yy = Option::::from((Fp::ONE - *D * self.0 * other.0 * self.1 * other.1).invert()) + .ok_or(anyhow!("Cannot invert"))?; + *self = Self( + (self.0 * other.1 + self.1 * other.0) * xx, + (self.1 * other.1 - *A * self.0 * other.0) * yy, + ); + Ok(()) + } +} + +impl PointAffine { + pub fn is_on_curve(&self) -> bool { + self.1 * self.1 + *A * self.0 * self.0 == Fp::ONE + *D * self.0 * self.0 * self.1 * self.1 + } + pub fn is_infinity(&self) -> bool { + self.0.is_zero().into() && (self.1 == Fp::ONE || self.1 == -Fp::ONE) + } + pub fn zero() -> Self { + Self(Fp::ZERO, Fp::ONE) + } + pub fn double(&self) -> Result { + let xx = Option::::from((*A * self.0 * self.0 + self.1 * self.1).invert()) + .ok_or(anyhow!("Cannot invert"))?; + let yy = Option::::from( + (Fp::ONE + Fp::ONE - *A * self.0 * self.0 - self.1 * self.1).invert(), + ) + .ok_or(anyhow!("Cannot invert"))?; + Ok(Self( + ((self.0 * self.1) * xx).double(), + (self.1 * self.1 - *A * self.0 * self.0) * yy, + )) + } + pub fn multiply(&self, scalar: &Fp) -> Result { + let mut result = PointProjective::zero(); + let self_proj = self.to_projective(); + for bit in scalar.to_le_bits().iter().rev() { + result = result.double(); + if *bit { + result.add_assign(&self_proj)?; + } + } + result.to_affine() + } + pub fn to_projective(self) -> PointProjective { + PointProjective(self.0, self.1, Fp::ONE) + } + pub fn compress(&self) -> PointCompressed { + PointCompressed(self.0, self.1.is_odd().into()) + } +} + +impl PointCompressed { + pub fn decompress(&self) -> Result { + let inv = Option::::from((Fp::ONE - *D * self.0.square()).invert()) + .ok_or(anyhow!("Cannot invert"))?; + let mut y = Option::::from((inv * (Fp::ONE - *A * self.0.square())).sqrt()) + .ok_or(anyhow!("Cannot take sqrt"))?; + let is_odd: bool = y.is_odd().into(); + if self.1 != is_odd { + y = y.neg(); + } + Ok(PointAffine(self.0, y)) + } + pub fn verify(&self, message: Fp, sig: &Signature) -> Result { + let pk = self.decompress()?; + + if !pk.is_on_curve() || !sig.r.is_on_curve() { + return Ok(false); + } + + // h=H(R,A,M) + let h = hash(&[sig.r.0, sig.r.1, pk.0, pk.1, message]); + + let sb = BASE.multiply(&sig.s)?; + + let mut r_plus_ha = pk.multiply(&h)?; + r_plus_ha.add_assign(&sig.r)?; + + Ok(r_plus_ha == sb) + } +} + +impl PointProjective { + fn add_assign(&mut self, other: &PointProjective) -> Result<()> { + if self.is_zero() { + *self = *other; + return Ok(()); + } + if other.is_zero() { + return Ok(()); + } + if self.to_affine()? == other.to_affine()? { + *self = self.double(); + return Ok(()); + } + let a = self.2 * other.2; // A = Z1 * Z2 + let b = a.square(); // B = A^2 + let c = self.0 * other.0; // C = X1 * X2 + let d = self.1 * other.1; // D = Y1 * Y2 + let e = *D * c * d; // E = dC · D + let f = b - e; // F = B − E + let g = b + e; // G = B + E + self.0 = a * f * ((self.0 + self.1) * (other.0 + other.1) - c - d); + self.1 = a * g * (d - *A * c); + self.2 = f * g; + Ok(()) + } +} + +impl PointProjective { + pub fn zero() -> Self { + PointProjective(Fp::ZERO, Fp::ONE, Fp::ZERO) + } + pub fn is_zero(&self) -> bool { + self.2.is_zero().into() + } + pub fn double(&self) -> PointProjective { + if self.is_zero() { + return PointProjective::zero(); + } + let b = (self.0 + self.1).square(); + let c = self.0.square(); + let d = self.1.square(); + let e = *A * c; + let f = e + d; + let h = self.2.square(); + let j = f - h.double(); + PointProjective((b - c - d) * j, f * (e - d), f * j) + } + pub fn to_affine(self) -> Result { + if self.is_zero() { + return Ok(PointAffine::zero()); + } + let zinv = Option::::from(self.2.invert()).ok_or(anyhow!("Cannot invert"))?; + Ok(PointAffine(self.0 * zinv, self.1 * zinv)) + } +} + +lazy_static::lazy_static! { + pub static ref A: Fp = Fp::from(168700); + pub static ref D: Fp = Fp::from(168696); + pub static ref BASE: PointAffine = PointAffine( + Fp::from_str_vartime( + "5299619240641551281634865583518297030282874472190772894086521144482721001553" + ) + .unwrap(), + Fp::from_str_vartime("16950150798460657717958625567821834550301663161624707787222815936182638968203").unwrap() + ); + pub static ref BASE_COFACTOR: PointAffine = BASE.multiply(&Fp::from(8)).unwrap(); + pub static ref ORDER: BigUint = BigUint::from_str( + "21888242871839275222246405745257275088614511777268538073601725287587578984328" + ) + .unwrap(); +} + +#[cfg(test)] +mod tests; + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)] +pub struct Signature { + pub r: PointAffine, + pub s: Fp, +} + +pub struct PrivateKey(Fp); + +fn hash(inp: &[Fp]) -> Fp { + inp.iter().fold(Fp::ONE, |a, b| a * b) +} + +impl PrivateKey { + fn to_pub(&self) -> Result { + Ok(BASE.multiply(&self.0)?.compress()) + } + fn sign(&self, randomness: Fp, message: Fp) -> Result { + let pk = self.to_pub()?.decompress()?; + + // r=H(b,M) + let r = hash(&[randomness, message]); + + // R=rB + let rr = BASE.multiply(&r)?; + + // h=H(R,A,M) + let h = hash(&[rr.0, rr.1, pk.0, pk.1, message]); + + // s = (r + ha) mod ORDER + let mut s = BigUint::from_bytes_le(r.to_repr().as_ref()); + let mut ha = BigUint::from_bytes_le(h.to_repr().as_ref()); + ha.mul_assign(&BigUint::from_bytes_le(self.0.to_repr().as_ref())); + s.add_assign(&ha); + s = s.mod_floor(&*ORDER); + let s_as_fr = { + let s_bytes = s.to_bytes_le(); + let mut s_repr = FpRepr([0u8; 32]); + s_repr.0[0..s_bytes.len()].copy_from_slice(&s_bytes); + Option::::from(Fp::from_repr(s_repr)).ok_or(anyhow!("Invalid repr"))? + }; + + Ok(Signature { r: rr, s: s_as_fr }) + } +} diff --git a/src/blockchain/tx/owshen_airdrop/babyjubjub/tests.rs b/src/blockchain/tx/owshen_airdrop/babyjubjub/tests.rs new file mode 100644 index 0000000..cae9ec4 --- /dev/null +++ b/src/blockchain/tx/owshen_airdrop/babyjubjub/tests.rs @@ -0,0 +1,51 @@ +use super::*; + +#[test] +fn test_twisted_edwards_curve_ops() { + // ((2G) + G) + G + let mut a = BASE.double().unwrap(); + a.add_assign(&BASE).unwrap(); + a.add_assign(&BASE).unwrap(); + + // 2(2G) + let b = BASE.double().unwrap().double().unwrap(); + + assert_eq!(a, b); + + // G + G + G + G + let mut c = *BASE; + c.add_assign(&BASE).unwrap(); + c.add_assign(&BASE).unwrap(); + c.add_assign(&BASE).unwrap(); + + assert_eq!(b, c); + + // Check if projective points are working + let mut pnt1 = BASE.to_projective().double().double(); + pnt1.add_assign(&BASE.to_projective()).unwrap(); + let mut pnt2 = BASE.double().unwrap().double().unwrap(); + pnt2.add_assign(&BASE).unwrap(); + + assert_eq!(pnt1.to_affine().unwrap(), pnt2); +} + +#[test] +fn test_jubjub_public_key_compression() { + let p1 = BASE.multiply(&Fp::from(123_u64)).unwrap(); + let p2 = p1.compress().decompress().unwrap(); + + assert_eq!(p1, p2); +} + +#[test] +fn test_jubjub_signature_verification() { + let randomness = Fp::from(2345); + let sk = PrivateKey(Fp::from(12345)); + let pk = sk.to_pub().unwrap(); + let msg = Fp::from(123456); + let fake_msg = Fp::from(123457); + let sig = sk.sign(randomness, msg).unwrap(); + + assert!(pk.verify(msg, &sig).unwrap()); + assert!(!pk.verify(fake_msg, &sig).unwrap()); +} diff --git a/src/blockchain/tx/owshen_airdrop/mod.rs b/src/blockchain/tx/owshen_airdrop/mod.rs new file mode 100644 index 0000000..73d54fa --- /dev/null +++ b/src/blockchain/tx/owshen_airdrop/mod.rs @@ -0,0 +1,18 @@ +pub mod babyjubjub; +use babyjubjub::*; + +use alloy::primitives::Address; +use anyhow::Result; + +use crate::blockchain::Blockchain; + +pub fn owshen_airdrop( + _chain: &mut B, + by: Address, + owshen_address: PointCompressed, + owshen_sig: Signature, +) -> Result<()> { + log::info!("Someone is claiming his owshen airdrop, by {}!", by); + owshen_address.verify(Fp::from(123), &owshen_sig)?; + Ok(()) +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..afe42de --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,85 @@ +use std::path::PathBuf; + +use alloy::signers::{k256::ecdsa::SigningKey, local::PrivateKeySigner}; +use anyhow::{Ok, Result}; + +mod node; + +use crate::db::{DiskKvStore, RamKvStore}; +use hex::FromHex; +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +struct StartOpt { + #[structopt(long, default_value = "3000")] + api_port: u16, + #[structopt(long, default_value = "8645")] + rpc_port: u16, + #[structopt(long)] + db: Option, + #[structopt(long)] + private_key: Option, + #[structopt(long, default_value = "https://eth.llamarpc.com")] + provider_address: reqwest::Url, +} + +impl StartOpt { + fn parse_signing_key(&self) -> Result { + let private_key: String = match &self.private_key { + Some(pk) => pk.to_string(), + None => std::env::var("PRIVATE_KEY")?, + }; + + let private_key_str = if private_key.starts_with("0x") { + &private_key[2..] + } else { + &private_key + }; + + let key_bytes = <[u8; 32]>::from_hex(private_key_str)?; + + let signer = PrivateKeySigner::from_signing_key(SigningKey::from_slice(&key_bytes)?); + + Ok(signer) + } +} + +#[derive(Debug, StructOpt)] +#[structopt(name = "Owshen", about = "Owshen node software!")] +enum Opt { + Start(StartOpt), + Debug, +} + +pub async fn cli() -> Result<()> { + let opt = Opt::from_args(); + match opt { + Opt::Start(opt) => { + let signing_key = opt.parse_signing_key()?; + if let Some(db) = opt.db { + node::run_node( + DiskKvStore::new(db, 128)?, + opt.api_port, + opt.rpc_port, + opt.provider_address, + signing_key, + ) + .await?; + } else { + node::run_node( + RamKvStore::new(), + opt.api_port, + opt.rpc_port, + opt.provider_address, + signing_key, + ) + .await?; + } + } + Opt::Debug => { + println!("Nothing to do!"); + } + } + + Ok(()) +} diff --git a/src/cli/node.rs b/src/cli/node.rs new file mode 100644 index 0000000..8361abd --- /dev/null +++ b/src/cli/node.rs @@ -0,0 +1,104 @@ +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use alloy::{ + primitives::U256, + signers::{local::PrivateKeySigner, Signer}, +}; +use anyhow::Result; +use tokio::sync::Mutex; + +use crate::{ + blockchain::{Blockchain, Config, Owshenchain, TransactionQueue}, + config, + db::KvStore, + genesis::GENESIS, + safe_signer::SafeSigner, + services::{ + server::{api_server, rpc_server}, + Context, ContextKvStore, ContextSigner, + }, + types::{CustomTx, CustomTxMsg, Mint, Token}, +}; + +async fn block_producer( + ctx: Arc>>, +) -> Result<()> { + loop { + if let Err(e) = async { + let mut ctx = ctx.lock().await; + if ctx.exit { + log::info!("Terminating the block producer..."); + return Ok(()); + } + + let mut tx_queue = std::mem::replace(&mut ctx.tx_queue, TransactionQueue::new()); + let timestamp = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + let mut blk = ctx.chain.draft_block(&mut tx_queue, timestamp)?; + ctx.tx_queue = tx_queue; + + blk = blk.signed(ctx.signer.clone()).await?; + ctx.chain.push_block(blk.clone())?; + log::info!("Produced a new block: {}", blk.index); + Ok::<(), anyhow::Error>(()) + } + .await + { + log::info!("Error while producing a block: {}", e); + } + tokio::time::sleep(std::time::Duration::from_secs(3)).await; + } +} + +pub async fn run_node( + db: K, + api_port: u16, + rpc_port: u16, + provider_address: reqwest::Url, + private_key: PrivateKeySigner, +) -> Result<()> { + let signer = SafeSigner::new(private_key); + let conf = Config { + chain_id: config::CHAIN_ID, + owner: Some(signer.address()), + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address, + }; + + let ctx = Arc::new(Mutex::new(Context { + signer: signer.clone(), + exit: false, + tx_queue: TransactionQueue::new(), + chain: Owshenchain::new(conf.clone(), db), + })); + + let tx = CustomTx::create( + &mut signer.clone(), + conf.chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: vec![0u8; 32], + user_tx_hash: "0x1234567890abcdef".to_string(), + token: Token::Native, + amount: U256::from(100), + address: PrivateKeySigner::random().address(), + }), + ) + .await?; + ctx.lock().await.tx_queue.enqueue(tx); + + let block_producer_fut = block_producer(ctx.clone()); + let api_server_fut = api_server(ctx.clone(), api_port); + let rpc_server_fut = rpc_server(ctx.clone(), rpc_port); + + let entrypoint = format!("http://127.0.0.1:{}", api_port); + if webbrowser::open(&entrypoint).is_err() { + println!("Failed to open web browser. Please navigate to http://{entrypoint} manually"); + } + + tokio::try_join!(block_producer_fut, api_server_fut, rpc_server_fut)?; + + Ok(()) +} diff --git a/src/client/BlockDetails.rs b/src/client/BlockDetails.rs new file mode 100644 index 0000000..6d3fecf --- /dev/null +++ b/src/client/BlockDetails.rs @@ -0,0 +1,125 @@ +use crate::blockchain::{Blockchain, Config, Owshenchain}; +use crate::config; +use crate::services::{Context, ContextKvStore, ContextSigner}; // Your own services module +use crate::types::OwshenTransaction; +use crate::{db::RamKvStore, genesis::GENESIS}; +use anyhow::anyhow; +use axum::response::Html; +use serde::Serialize; +use std::str::FromStr; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; +use tinytemplate::TinyTemplate; +use tokio::sync::Mutex; + +#[derive(Serialize, Clone)] +pub struct BlockDetailsContext { + pub name: String, + pub id: String, +} + +#[derive(Serialize, Clone)] +struct BlockInfo { + block: String, + num_txs: u32, + who: String, + wen: String, +} +#[derive(Serialize, Clone)] +struct HeightInfo { + height: usize, +} + +#[derive(Serialize)] +struct TemplateContext { + height_info: HeightInfo, + block_txs: Vec, + time_stamp: u64, + block_id: String, + blocks: Vec, +} + +static TEMPLATE: &str = include_str!("./templates/block_details.html"); + +pub async fn block_details_handler( + ctx: Arc>>, + block_details_ctx: BlockDetailsContext, // Add this to receive block context +) -> Result, anyhow::Error> { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let chain: Owshenchain = Owshenchain::new(conf, RamKvStore::new()); + + let height_info = match chain.get_height() { + Ok(height) => HeightInfo { height }, + Err(_) => HeightInfo { height: 0 }, // Default value if get_height fails + }; + + let block_index = usize::from_str(&block_details_ctx.id).unwrap(); + + let block_txs = match chain.get_transactions_by_block(block_index) { + Ok(transactions) => transactions, + Err(_) => Vec::new(), // Return an empty vector if the operation fails + }; + + let blocks = vec![ + BlockInfo { + block: "#1431".to_string(), + num_txs: 189, + who: "0x8aF3d2E...".to_string(), + wen: "just now".to_string(), + }, + BlockInfo { + block: "#1430".to_string(), + num_txs: 123, + who: "0x8aF3d2E...".to_string(), + wen: "10 sec ago".to_string(), + }, + BlockInfo { + block: "#1429".to_string(), + num_txs: 764, + who: "0x8aF3d2E...".to_string(), + wen: "20 sec ago".to_string(), + }, + BlockInfo { + block: "#1428".to_string(), + num_txs: 904, + who: "0x8aF3d2E...".to_string(), + wen: "40 sec ago".to_string(), + }, + BlockInfo { + block: "#1427".to_string(), + num_txs: 102, + who: "0x8aF3d2E...".to_string(), + wen: "1 min ago".to_string(), + }, + ]; + + let time_stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + // Initialize TinyTemplate + let mut tt = TinyTemplate::new(); + tt.add_template("response", TEMPLATE)?; + + // Create a context and insert the blocks, height, and last block information + let context = TemplateContext { + height_info, + block_txs, + time_stamp, + block_id: block_details_ctx.id.clone(), + blocks, + }; + + match tt.render("response", &context) { + Ok(res) => Ok(Html::from(res)), + Err(e) => Err(anyhow!(e)), + } +} diff --git a/src/client/explorer.rs b/src/client/explorer.rs new file mode 100644 index 0000000..6b34495 --- /dev/null +++ b/src/client/explorer.rs @@ -0,0 +1,67 @@ +use std::sync::Arc; +use tokio::sync::Mutex; + +use crate::blockchain::Blockchain; +use crate::services::{ContextKvStore, ContextSigner}; +use crate::types::Block; +use anyhow::anyhow; +use axum::response::Html; +use serde::Serialize; +use std::time::{SystemTime, UNIX_EPOCH}; +use tinytemplate::TinyTemplate; + +use crate::services::Context; + +#[derive(Serialize, Clone)] +pub struct ExplorerContext { + pub name: String, +} + +#[derive(Serialize, Clone)] +struct HeightInfo { + height: usize, +} + +#[derive(Serialize)] +struct TemplateContext { + height_info: HeightInfo, + last_blocks: Vec, + time_stamp: u64, +} + +static TEMPLATE: &str = include_str!("./templates/explorer.html"); + +pub async fn explorer_handler( + ctx: Arc>>, +) -> Result, anyhow::Error> { + let mut _chain = &ctx.lock().await.chain; + + // Fetch the height information + let height_info = match _chain.get_height() { + Result::Ok(height) => HeightInfo { height }, + Result::Err(_) => HeightInfo { height: 0 }, // Default value if get_height fails + }; + + let last_blocks_result = _chain.get_blocks(0, 10); + let last_blocks = match last_blocks_result { + Ok(block_opt) => block_opt, + Err(_) => Vec::new(), // Return an empty vector instead of doing nothing + }; + let time_stamp = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs(); + + let mut tt = TinyTemplate::new(); + tt.add_template("response", TEMPLATE)?; + let context = TemplateContext { + height_info, + last_blocks, + time_stamp, + }; + + match tt.render("response", &context) { + Ok(res) => Ok(Html::from(res)), + Err(e) => Err(anyhow!(e)), + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..f35dd62 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,3 @@ +pub mod BlockDetails; +pub mod explorer; +pub mod style; diff --git a/src/client/style.rs b/src/client/style.rs new file mode 100644 index 0000000..268aa64 --- /dev/null +++ b/src/client/style.rs @@ -0,0 +1,58 @@ +use axum::http::{header, HeaderValue}; +use axum::response::Response; +use eyre::Report; +use serde::Serialize; + +#[derive(Serialize)] +pub struct StyleContext {} + +pub async fn css_handler( + _ctx: &StyleContext, // '_ctx' is not used, but retained if you might use it later +) -> Result, Report> { + // Your CSS file content + let css_content = r#" + body{ + background:black; + color:white; + } + h1 { + font-family: "JetBrains Mono"; + margin: 0.5em; + font-size: 3em; + text-align: center; + } + a{ + display: block; + width: 100%; + } + body { + font-family: "JetBrains Mono"; + line-height: 1.4em; + font-size: 1.1em; + } + + h2 { + font-size: 1.5em; + text-align: center; + } + + b { + font-weight: bold; + } + + th { + font-weight: bold; + border-bottom: 1px solid white; + } + + th, td { + padding: 0.2em; + } + "#; + + // Create the response with the CSS content + let mut resp = Response::new(css_content.to_string()); + resp.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("text/css")); + Ok(resp) +} diff --git a/src/client/templates/block_details.html b/src/client/templates/block_details.html new file mode 100644 index 0000000..3852f12 --- /dev/null +++ b/src/client/templates/block_details.html @@ -0,0 +1,53 @@ + + + + + OwshenScan + + + + + + + + + +

🐟 owshen scan 🤿

+
+

> height: {height_info.height}

+

> timestamp:{time_stamp}

+

> market cap: 1.237m $

+

> tps: 96

+
+ {{ if block_txs }} + + + {{ else }} + + No block data available + + {{ endif }} +

> block #{block_id}

+ + + + + + + + {{ for block in blocks }} + + + + + + + {{ endfor }} +
txwatwhohow much
{block.who}{block.num_txs}0x8aF3d2E...{block.wen}
+
+
+ + + \ No newline at end of file diff --git a/src/client/templates/explorer.html b/src/client/templates/explorer.html new file mode 100644 index 0000000..441ad5c --- /dev/null +++ b/src/client/templates/explorer.html @@ -0,0 +1,46 @@ + + + + + OwshenScan + + + + + + + +

🐟 owshen scan 🤿

+
+

> version: v0.1.0

+

> height: {height_info.height}

+

> timestamp: {time_stamp}

+

> market cap: 1.237m $

+

> tps: 96

+ +
+

> latest blocks;

+ + + + + + + + {{ for block in last_blocks }} + + + + + + + + {{ endfor }} +
blocknum-txswhowen
{block.index} 0x8aF3d2E...{block.timestamp}
+
+
+ + + \ No newline at end of file diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..74b3096 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,3 @@ +use alloy::primitives::{Address, FixedBytes}; +pub const CHAIN_ID: u64 = 918273; +pub const OWSHEN_CONTRACT: Address = Address(FixedBytes([1; 20])); diff --git a/src/db/disk.rs b/src/db/disk.rs new file mode 100644 index 0000000..4c2c895 --- /dev/null +++ b/src/db/disk.rs @@ -0,0 +1,64 @@ +use crate::services::ContextKvStore; + +use super::{Blob, KvStore}; +use anyhow::{anyhow, Result}; +use leveldb::batch::Batch; +use leveldb::database::batch::Writebatch; +use leveldb::database::cache::Cache; +use leveldb::database::Database; + +use leveldb::kv::KV; +use leveldb::options::{Options, ReadOptions, WriteOptions}; +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +impl db_key::Key for Blob { + fn from_u8(key: &[u8]) -> Self { + Self(key.to_vec()) + } + + fn as_slice T>(&self, f: F) -> T { + f(&self.0) + } +} + +pub struct DiskKvStore(Database); +impl DiskKvStore { + pub fn new>(path: P, cache_size: usize) -> Result { + fs::create_dir_all(&path)?; + let mut options = Options::new(); + options.create_if_missing = true; + options.cache = Some(Cache::new(cache_size)); + Ok(Self(Database::open(path.as_ref(), options)?)) + } +} + +impl ContextKvStore for DiskKvStore {} + +impl KvStore for DiskKvStore { + fn buffer(self) -> BTreeMap> { + BTreeMap::new() + } + fn get_raw(&self, k: &Blob) -> Result> { + let read_opts = ReadOptions::new(); + match self.0.get(read_opts, k) { + Ok(v) => Ok(v.map(Blob)), + Err(_) => Err(anyhow!("Database failure!")), + } + } + fn batch_put_raw)>>(&mut self, vals: I) -> Result<()> { + let write_opts = WriteOptions::new(); + let mut batch = Writebatch::new(); + for op in vals { + match op { + (k, None) => batch.delete(k.clone()), + (k, Some(v)) => batch.put(k.clone(), &v.0), + } + } + match self.0.write(write_opts, &batch) { + Ok(_) => Ok(()), + Err(_) => Err(anyhow!("Database failure!")), + } + } +} diff --git a/src/db/key.rs b/src/db/key.rs new file mode 100644 index 0000000..794892d --- /dev/null +++ b/src/db/key.rs @@ -0,0 +1,39 @@ +use super::Blob; +use crate::types::Token; +use alloy::primitives::{Address, FixedBytes, U256}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Key { + Height, + Block(usize), + Delta(usize), + ContractCode(Address), + ContractStorage(Address, U256), + TransactionHash(FixedBytes<32>), + BlockHash(U256), + TransactionCount, + Transactions(Address), + DepositedTransaction(String), + Balance(Address, Token), + Allowance(Address, Address, Token), + NonceEth(Address), + NonceCustom(Address), + BurnId(FixedBytes<32>), + TokenDecimal(Address), + TokenSymbol(Address), +} + +impl TryInto for Key { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result { + Ok(Blob(bincode::serialize(&self)?)) + } +} + +impl TryInto for &Key { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result { + Ok(Blob(bincode::serialize(&self)?)) + } +} diff --git a/src/db/mirror.rs b/src/db/mirror.rs new file mode 100644 index 0000000..de8f86e --- /dev/null +++ b/src/db/mirror.rs @@ -0,0 +1,46 @@ +use crate::services::ContextKvStore; + +use super::{Blob, KvStore}; +use anyhow::Result; +use std::collections::BTreeMap; + +pub struct MirrorKvStore<'a, K: ContextKvStore> { + base: &'a K, + overwrite: BTreeMap>, +} + +impl<'a, K: ContextKvStore> MirrorKvStore<'a, K> { + pub fn new(base: &'a K) -> Self { + Self { + base, + overwrite: BTreeMap::new(), + } + } + pub fn rollback(&self) -> Result>> { + let old_vals = self + .overwrite + .keys() + .map(|k| self.base.get_raw(k)) + .collect::, _>>()?; + Ok(self.overwrite.keys().cloned().zip(old_vals).collect()) + } +} + +impl<'a, K: ContextKvStore> ContextKvStore for MirrorKvStore<'a, K> {} + +impl<'a, K: ContextKvStore> KvStore for MirrorKvStore<'a, K> { + fn get_raw(&self, k: &Blob) -> Result> { + if let Some(overwrite) = self.overwrite.get(k) { + Ok(overwrite.clone()) + } else { + self.base.get_raw(k) + } + } + fn batch_put_raw)>>(&mut self, vals: I) -> Result<()> { + self.overwrite.extend(vals); + Ok(()) + } + fn buffer(self) -> BTreeMap> { + self.overwrite + } +} diff --git a/src/db/mod.rs b/src/db/mod.rs new file mode 100644 index 0000000..0955e22 --- /dev/null +++ b/src/db/mod.rs @@ -0,0 +1,52 @@ +mod disk; +mod key; +mod mirror; +mod ram; +mod value; + +pub use disk::DiskKvStore; +pub use key::Key; +pub use mirror::MirrorKvStore; +pub use ram::RamKvStore; + +pub use value::Value; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, Clone, PartialEq, PartialOrd, Eq, Ord, Serialize, Deserialize, Hash)] +pub struct Blob(Vec); + +// TODO‌: Range Search +// self.db.range(range) + +pub trait KvStore { + fn get(&self, k: Key) -> Result> { + Ok(if let Some(b) = self.get_raw(&k.try_into()?)? { + Some(bincode::deserialize(&b.0)?) + } else { + None + }) + } + fn put(&mut self, k: Key, v: Option) -> Result<()> { + self.batch_put([(k, v)].into_iter()) + } + fn batch_put)>>(&mut self, vals: I) -> Result<()> { + let mut conv = Vec::new(); + for (k, v) in vals { + conv.push(( + k.try_into()?, + if let Some(v) = v { + Some(v.try_into()?) + } else { + None + }, + )); + } + self.batch_put_raw(conv.into_iter()) + } + fn get_raw(&self, k: &Blob) -> Result>; + fn batch_put_raw)>>(&mut self, vals: I) -> Result<()>; + fn buffer(self) -> BTreeMap>; +} diff --git a/src/db/ram.rs b/src/db/ram.rs new file mode 100644 index 0000000..0c94b13 --- /dev/null +++ b/src/db/ram.rs @@ -0,0 +1,39 @@ +use crate::services::ContextKvStore; + +use super::{Blob, KvStore}; +use anyhow::Result; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub struct RamKvStore { + db: BTreeMap, +} + +impl ContextKvStore for RamKvStore {} + +impl KvStore for RamKvStore { + fn get_raw(&self, k: &Blob) -> Result> { + Ok(self.db.get(k).cloned()) + } + fn batch_put_raw)>>(&mut self, vals: I) -> Result<()> { + for (k, v) in vals { + if let Some(v) = v { + self.db.insert(k, v); + } else { + self.db.remove(&k); + } + } + Ok(()) + } + fn buffer(self) -> BTreeMap> { + BTreeMap::new() + } +} + +impl RamKvStore { + pub fn new() -> Self { + Self { + db: BTreeMap::new(), + } + } +} diff --git a/src/db/value.rs b/src/db/value.rs new file mode 100644 index 0000000..66828c6 --- /dev/null +++ b/src/db/value.rs @@ -0,0 +1,78 @@ +use super::Blob; +use crate::types::{BincodableOwshenTransaction, Block, IncludedTransaction, OwshenTransaction}; +use alloy::primitives::U256; +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum Value { + Void, + Usize(usize), + U256(U256), + BTreeMap(BTreeMap>), + Block(Block), + VecU8(Vec), + Transaction(IncludedTransaction), + Transactions(Vec), + DepositedTransaction(String), + Symbol(String) + +} + +impl TryInto for Value { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result { + (&self).try_into() + } +} + +impl TryInto for &Value { + type Error = anyhow::Error; + fn try_into(self) -> anyhow::Result { + Ok(Blob(bincode::serialize(&self)?)) + } +} + +impl Value { + pub fn as_usize(&self) -> Result { + match self { + Value::Usize(v) => Ok(*v), + _ => Err(anyhow!("Unexpected type!")), + } + } + + pub fn as_u256(&self) -> Result { + match self { + Value::U256(n) => Ok(*n), + _ => Err(anyhow!("Unexpected type!")), + } + } + + pub fn as_string(&self) -> Result { + match self { + Value::Symbol(n) => Ok(n.clone()), + _ => Err(anyhow!("Unexpected type!")), + } + } + + pub fn as_btreemap(&self) -> Result>> { + match self { + Value::BTreeMap(v) => Ok(v.clone()), + _ => Err(anyhow!("Unexpected type!")), + } + } + pub fn as_block(&self) -> Result { + match self { + Value::Block(v) => Ok(v.clone()), + _ => Err(anyhow!("Unexpected type!")), + } + } + pub fn as_vec_u8(&self) -> Result> { + match self { + Value::VecU8(v) => Ok(v.clone()), + _ => Err(anyhow!("Unexpected type!")), + } + } +} diff --git a/src/genesis/mod.rs b/src/genesis/mod.rs new file mode 100644 index 0000000..c123677 --- /dev/null +++ b/src/genesis/mod.rs @@ -0,0 +1,94 @@ +use crate::types::{Token, ERC20}; +use alloy::primitives::{utils::parse_units, Address, U256}; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; +use std::sync::Arc; + +#[derive(Debug, Clone, Deserialize)] +struct Balance { + address: String, + amount: String, +} + +#[derive(Debug, Clone, Deserialize)] +struct TokenData { + token_type: String, + contract_address: Option, + decimal: Option, + symbol: Option, + balances: Vec, +} + +#[derive(Debug, Clone)] +pub struct Genesis { + pub tokens: HashMap>, +} + +impl Genesis { + pub fn new(tokens: Vec<(Token, Vec<(Address, U256)>)>) -> Self { + let mut tokens_map = HashMap::new(); + + for (token, balances) in tokens { + let mut balance_map = HashMap::new(); + for (addr, bal) in balances { + balance_map.insert(addr, bal); + } + tokens_map.insert(token, balance_map); + } + + Genesis { tokens: tokens_map } + } +} + +fn init_genesis_from_json(json_file_path: &str) -> Result> { + let file = File::open(json_file_path)?; + let reader = BufReader::new(file); + + let token_data_list: Vec = serde_json::from_reader(reader)?; + + let mut tokens = Vec::new(); + + for token_data in token_data_list { + let token = match token_data.token_type.as_str() { + "Native" => Token::Native, + "Erc20" => { + if let (Some(contract_address), Some(decimal), Some(symbol)) = ( + token_data.contract_address.clone(), + token_data.decimal, + token_data.symbol.clone(), + ) { + Token::Erc20(ERC20 { + address: contract_address.parse()?, + decimals: decimal, + symbol: symbol, + }) + } else { + return Err("ERC20 token missing contract address".into()); + } + } + _ => return Err("Unknown token type".into()), + }; + + let balances = token_data + .balances + .into_iter() + .map(|balance| { + let address = balance.address.parse()?; + let amount = parse_units(&balance.amount, 18)?.into(); + Ok((address, amount)) + }) + .collect::, Box>>()?; + + tokens.push((token, balances)); + } + + Ok(Genesis::new(tokens)) +} + +lazy_static::lazy_static! { + pub static ref GENESIS: Arc = { + Arc::new(init_genesis_from_json("GENESIS.json").expect("Failed to initialize Genesis from JSON")) + }; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1ac120c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,19 @@ +mod blockchain; +mod cli; +mod client; +mod config; +mod db; +mod genesis; +mod safe_signer; +mod services; +mod types; +mod utils; + +#[tokio::main] +async fn main() { + env_logger::Builder::new() + .filter_level(log::LevelFilter::Info) + .init(); + + cli::cli().await.unwrap(); +} diff --git a/src/safe_signer.rs b/src/safe_signer.rs new file mode 100644 index 0000000..0c6b8e9 --- /dev/null +++ b/src/safe_signer.rs @@ -0,0 +1,39 @@ +use alloy::{ + primitives::{Address, ChainId, Signature, B256}, + signers::{local::PrivateKeySigner, Signer}, +}; +use async_trait::async_trait; + +use crate::services::ContextSigner; + +#[derive(Clone)] +pub struct SafeSigner { + private_key: PrivateKeySigner, +} + +impl SafeSigner { + pub fn new(private_key: PrivateKeySigner) -> Self { + Self { private_key } + } +} + +impl ContextSigner for SafeSigner {} + +#[async_trait] +impl Signer for SafeSigner { + async fn sign_hash(&self, hash: &B256) -> alloy::signers::Result { + self.private_key.sign_hash(hash).await + } + + fn address(&self) -> Address { + self.private_key.address() + } + + fn chain_id(&self) -> Option { + self.private_key.chain_id() + } + + fn set_chain_id(&mut self, chain_id: Option) { + self.private_key.set_chain_id(chain_id) + } +} diff --git a/src/services/api_services/assets/index.html b/src/services/api_services/assets/index.html new file mode 100644 index 0000000..ce599bb --- /dev/null +++ b/src/services/api_services/assets/index.html @@ -0,0 +1,155 @@ + + + + + + + + + + 🐟 Hello Owshen Network! 🤿 +

+ Deposit! + Withdraw! + Airdrop! +

+ + diff --git a/src/services/api_services/deposit.rs b/src/services/api_services/deposit.rs new file mode 100644 index 0000000..45ece65 --- /dev/null +++ b/src/services/api_services/deposit.rs @@ -0,0 +1,154 @@ +use alloy::{ + primitives::{Address, FixedBytes, U256}, + providers::{Provider, ProviderBuilder}, +}; +use axum::Json; +use serde::{Deserialize, Serialize}; +use std::{clone, sync::Arc}; +use tokio::sync::Mutex; + +use super::Context; +use crate::{ + blockchain::Blockchain, + db::Value, + services::{ContextKvStore, ContextSigner}, + types::{CustomTx, CustomTxMsg, Mint, Token, ERC20}, +}; + +#[derive(Debug, Deserialize, Clone)] +pub struct DepositRequest { + pub tx_hash: String, + pub token: String, + pub amount: U256, + pub address: Address, +} + +#[derive(Serialize, Clone)] +pub struct DepositResponse { + pub owshen_tx_hash: Option>, + pub success: bool, +} + +pub async fn deposit_handler( + ctx: Arc>>, + Json(payload): Json, +) -> Result, anyhow::Error> { + let mut _ctx = ctx.lock().await; + + let contract_address = _ctx.chain.config().owshen; + let t = payload.clone().token; + + let token = match t.as_str() { + "native" => Token::Native, + "erc20" => { + let addr = Address::from_slice(&hex::decode(t.trim_start_matches("0x"))?); + let decimals = _ctx.chain.get_token_decimal(addr)?; + let symbol = _ctx.chain.get_token_symbol(addr)?; + Token::Erc20(ERC20 { + address: addr, + decimals, + symbol, + }) + } + _ => { + return Err(anyhow::anyhow!("Invalid token type: {}", t)); + } + }; + + let amount = payload.amount; + let address = payload.address; + let tx_hash = payload.tx_hash; + let tx_hash_bytes = hex::decode(tx_hash.trim_start_matches("0x"))?; + let tx_hash_fixed_bytes = alloy::primitives::FixedBytes::from_slice(&tx_hash_bytes); + + let chain_id = _ctx.chain.config().chain_id; + let to_addr = match chain_id { + 1 => { + const PROVIDER_API_KEY: &str = "YOUR_INFURA_PROJECT_ID"; + let provider_url = + format!("https://mainnet.infura.io/v3/{}", PROVIDER_API_KEY).parse()?; + let provider = ProviderBuilder::new().on_http(provider_url); + let res = match Provider::get_transaction_by_hash(&provider, tx_hash_fixed_bytes).await + { + Ok(Some(transaction)) => transaction, + Ok(None) => { + return Err(anyhow::anyhow!("Transaction not found")); + } + Err(e) => { + return Err(anyhow::anyhow!( + "Failed to get transaction by hash: {:?}", + e + )); + } + }; + + let to_address = match res.to { + Some(address) => address, + None => { + return Err(anyhow::anyhow!("Transaction does not have a 'to' address")); + } + }; + to_address + } + 1387 => contract_address, + _ => { + return Err(anyhow::anyhow!("Unsupported chain_id: {}", chain_id)); + } + }; + + if to_addr == contract_address { + let raw_tx_bytes = hex::decode(tx_hash.trim_start_matches("0x"))?; + let hah: &[u8] = raw_tx_bytes.as_ref(); + let tx = CustomTx::create( + &mut _ctx.signer.clone(), + chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: hah.to_vec(), + user_tx_hash: tx_hash.clone(), + token, + amount, + address, + }), + ) + .await?; + + if _ctx + .chain + .db + .get(crate::db::Key::DepositedTransaction(tx_hash.clone()))? + .is_some() + { + return Err(anyhow::anyhow!("Transaction already exists")); + } + + _ctx.chain.db.put( + crate::db::Key::DepositedTransaction(tx_hash.clone()), + Some(Value::DepositedTransaction(tx_hash)), + )?; + + let queue = &mut _ctx.tx_queue.queue(); + + if queue.iter().any(|t| match t.hash() { + Ok(hash) => hash == tx.hash().unwrap_or_default(), + Err(_) => false, + }) { + return Err(anyhow::anyhow!("Transaction already exists")); + } + + if _ctx + .chain + .db + .get(crate::db::Key::TransactionHash(tx.hash()?))? + .is_some() + { + return Err(anyhow::anyhow!("Transaction already exists")); + } + + return Ok(Json(DepositResponse { + owshen_tx_hash: Some(tx.hash()?), + success: true, + })); + } else { + return Err(anyhow::anyhow!("Transaction is invalid!")); + } +} diff --git a/src/services/api_services/explorer.rs b/src/services/api_services/explorer.rs new file mode 100644 index 0000000..a3afc79 --- /dev/null +++ b/src/services/api_services/explorer.rs @@ -0,0 +1,87 @@ +use super::Context; +use crate::{ + blockchain::Blockchain, + db::KvStore, + services::{ContextKvStore, ContextSigner}, + types::{Block, IncludedTransaction, OwshenTransaction}, +}; +use alloy::{ + primitives::{FixedBytes, U256}, + signers::Signer, +}; +use axum::extract::Json; +use serde::{Deserialize, Serialize}; +use std::{str::FromStr, sync::Arc}; +use tokio::sync::Mutex; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTransactionsByBlockRequest { + pub block_index: usize, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTransactionByHashRequest { + pub tx_hash: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTransactionByHashResponse { + pub transaction: IncludedTransaction, + pub success: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetTransactionByBlockResponse { + pub transactions: Vec, + pub success: bool, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetBlockByHashRequest { + pub block_hash: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct GetBlockByHashResponse { + pub block: Block, + pub success: bool, +} + +pub async fn get_transactions_by_block_handler( + ctx: Arc>>, + Json(req): Json, +) -> Result, String> +where + K: KvStore + Sync + Send + 'static, +{ + let ctx = ctx.lock().await; + let chain = &ctx.chain; + match chain.get_transactions_by_block(req.block_index) { + Ok(txs) => Ok(Json(GetTransactionByBlockResponse { + success: true, + transactions: txs, + })), + Err(err) => Err(err.to_string()), + } +} + +pub async fn get_transaction_by_hash_handler( + ctx: Arc>>, + req: Json, +) -> Result, String> +where + K: KvStore + Sync + Send + 'static, +{ + let ctx = ctx.lock().await; + let tx_hash = match FixedBytes::<32>::from_str(&req.tx_hash) { + Ok(hash) => hash, + Err(_) => return Err("Invalid transaction hash".to_string()), + }; + match ctx.chain.get_transaction_by_hash(tx_hash) { + Ok(tx) => Ok(Json(GetTransactionByHashResponse { + success: true, + transaction: tx, + })), + Err(e) => Err(format!("Error: {}", e)), + } +} diff --git a/src/services/api_services/mod.rs b/src/services/api_services/mod.rs new file mode 100644 index 0000000..f33cb21 --- /dev/null +++ b/src/services/api_services/mod.rs @@ -0,0 +1,144 @@ +pub mod deposit; +pub mod explorer; +pub mod test; +pub mod withdraw; +pub mod withdrawals; + +use super::{Context, ContextKvStore, ContextSigner}; +use crate::client::explorer::{explorer_handler, ExplorerContext}; +use crate::client::style::{css_handler, StyleContext}; +use crate::client::BlockDetails::{block_details_handler, BlockDetailsContext}; +use crate::{db, utils}; +use axum::extract::Path; + +use axum::response::{Html, IntoResponse, Response}; +use axum::{ + extract, + routing::{get, post}, + Extension, Json, Router, +}; +use deposit::{deposit_handler, DepositRequest}; +use explorer::{ + get_transaction_by_hash_handler, get_transactions_by_block_handler, GetBlockByHashRequest, + GetTransactionByHashRequest, GetTransactionsByBlockRequest, +}; +use hyper::StatusCode; + +use std::sync::Arc; +use tokio::sync::Mutex; + +use utils::handle_error; +use withdraw::{withdraw_handler, WithdrawRequest}; +use withdrawals::{withdrawals_handler, WithdrawalsRequest}; + +pub async fn css_handler_endpoint() -> impl IntoResponse { + let style_ctx = StyleContext {}; + + match css_handler(&style_ctx).await { + Ok(response) => response, + Err(err) => { + log::error!("Error handling CSS request: {}", err); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body("Internal Server Error".to_string()) + .unwrap() + } + } +} + +fn explorer_routes(ctx: Arc>>) -> Router +where + S: ContextSigner + 'static, + K: ContextKvStore + 'static, +{ + Router::new() + .route( + "/by-block", + post({ + let ctx: Arc>> = ctx.clone(); + move |Json(req): Json| async move { + handle_error(Ok(get_transactions_by_block_handler( + ctx.clone(), + extract::Json(req), + ) + .await)) + } + }), + ) + .route( + "/by-tx", + get({ + let ctx = ctx.clone(); + move |Json(req): Json| async move { + handle_error(Ok(get_transaction_by_hash_handler( + ctx.clone(), + extract::Json(req), + ) + .await)) + } + }), + ) +} + +pub fn api_routes(ctx: Arc>>) -> Router +where + S: ContextSigner + 'static, + K: ContextKvStore + 'static, +{ + Router::new() + .route( + "/", + get(|| async { Html(include_str!("assets/index.html")) }), + ) + .route( + "/deposit", + post({ + let ctx = ctx.clone(); + move |Json(req): Json| async move { + handle_error(deposit_handler(ctx.clone(), extract::Json(req)).await) + } + }), + ) + .route( + "/withdraw", + get({ + let ctx = ctx.clone(); + move |Json(req): Json| async move { + handle_error(withdraw_handler(ctx.clone(), extract::Json(req)).await) + } + }), + ) + .route( + "/Withdrawals", + get({ + let ctx = ctx.clone(); + move |Json(req): Json| async move { + handle_error(withdrawals_handler(ctx.clone(), extract::Json(req)).await) + } + }), + ) + .nest("/explorer", explorer_routes(ctx.clone())) + .route( + "/explorer", + get({ + let ctx = ctx.clone(); + move || async move { handle_error(explorer_handler(ctx).await) } + }), + ) + .route("/style.css", get(css_handler_endpoint)) + .route( + "/explorer/:id", + get({ + let ctx = ctx.clone(); + move |Path(id): Path| async move { + let block_details_ctx = BlockDetailsContext { + name: String::from("Yoctochain Explorer"), + id: id.clone(), + }; + + handle_error(block_details_handler(ctx, block_details_ctx).await) + } + }), + ) + .layer(Extension(ctx)) +} diff --git a/src/services/api_services/test.rs b/src/services/api_services/test.rs new file mode 100644 index 0000000..71875c5 --- /dev/null +++ b/src/services/api_services/test.rs @@ -0,0 +1,336 @@ +use alloy::{ + network::{EthereumWallet, TransactionBuilder}, + primitives::{FixedBytes, U256}, + rpc::types::TransactionRequest, + signers::{local::PrivateKeySigner, Signer}, +}; +use axum::{ + body::{self, Body, HttpBody}, + http::{self, Request, StatusCode}, + routing::get, + Json, Router, +}; +use serde::de::Expected; +use serde_json::json; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower::{Service, ServiceExt}; + +use crate::{ + blockchain::{ + tx::owshen_airdrop::babyjubjub::PrivateKey, Blockchain, Config, Owshenchain, + TransactionQueue, + }, + config::{self, CHAIN_ID}, + db::{DiskKvStore, Key, KvStore, RamKvStore, Value}, + genesis::GENESIS, + safe_signer::{self, SafeSigner}, + services::{api_services::api_routes, Context}, + types::{ + network::Network, BincodableOwshenTransaction, Burn, CustomTx, CustomTxMsg, + IncludedTransaction, Mint, OwshenTransaction, Token, + }, +}; + +#[tokio::test] +async fn withdrawal_test() { + let (ctx, app) = test_config().await; + + let value = U256::from(100); + + let signer = PrivateKeySigner::random(); + let tx = CustomTx::create( + &mut signer.clone(), + 123, + CustomTxMsg::BurnTx(Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: value, + calldata: None, + }), + ) + .await + .unwrap(); + + let custom_tx = if let OwshenTransaction::Custom(custom_tx) = tx.clone() { + custom_tx + } else { + panic!("Expected custom transaction"); + }; + let signature = custom_tx.sig.clone(); + let bincodable_tx = tx.clone().try_into().unwrap(); + + let block_number = 4321; + let included_tx = IncludedTransaction { + tx: bincodable_tx, + block_hash: FixedBytes::from([0u8; 32]), + block_number: block_number, + transaction_index: 1, + }; + let key = Key::Transactions(signer.address()); + let mut transactions: Vec = Vec::new(); + transactions.push(included_tx.clone()); + + { + let mut ctx_guard = ctx.lock().await; + ctx_guard + .chain + .db + .put(key.clone(), Some(Value::Transactions(transactions))) + .unwrap(); + } + + let address_str = format!("{}", signer.address()); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::GET) + .uri("/Withdrawals") + .header("Content-Type", "application/json") + .body(Body::from( + serde_json::to_vec(&json!({ + "address": address_str.to_lowercase(), + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + assert_eq!( + body, + json!({ + "withdrawals": [{ + "amount": format!("{:#x}", value), + "block_number": block_number, + "network": format!("{:?}", Network::ETH), + "signature": { + "r": format!("{:#x}", signature.r()), + "s": format!("{:#x}", signature.s()), + "yParity": format!("{:#x}", signature.v().to_u64()), + }, + "token": format!("{:?}", Token::Native), + }] + }) + ); +} + +#[tokio::test] +async fn withdraw_test() { + let (ctx, app) = test_config().await; + + let signer = PrivateKeySigner::random(); + let address = signer.address(); + + let before_balance = ctx + .lock() + .await + .chain + .get_balance(Token::Native, address) + .unwrap(); + assert_eq!(before_balance, U256::from(0)); + + let base_value = U256::from(100); + ctx.lock() + .await + .chain + .db + .put( + Key::Balance(address, Token::Native), + Some(Value::U256(base_value)), + ) + .unwrap(); + let balance = ctx + .lock() + .await + .chain + .get_balance(Token::Native, address) + .unwrap(); + assert_eq!(balance, base_value); + + let burn_obj = Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: Network::ETH, + token: Token::Native, + amount: U256::from(100), + calldata: None, + }; + let burn_rlp = rlp::encode(&burn_obj); + let sig = signer.sign_message(&burn_rlp).await.unwrap(); + + let response = app + .oneshot( + Request::builder() + .method(http::Method::GET) + .uri("/withdraw") + .header("Content-Type", "application/json") + .body(Body::from( + serde_json::to_vec(&json!({ + "rlp_burn": burn_rlp, + "sig": sig, + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!(body.get("success").unwrap(), &serde_json::Value::Bool(true)); +} + +#[tokio::test] +async fn deposit_test() { + let (ctx, app) = test_config().await; + + let signer: PrivateKeySigner = PrivateKeySigner::random(); + let chain_id = ctx.lock().await.chain.config().chain_id; + let vitalik = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" + .parse() + .unwrap(); + let tx = TransactionRequest::default() + .with_to(vitalik) + .with_deploy_code(vec![0, 0, 0, 0]) + .with_nonce(1) + .with_gas_limit(100) + .with_max_fee_per_gas(100) + .with_max_priority_fee_per_gas(100) + .with_chain_id(chain_id) + .with_value(U256::from(100_000_000_000_000_000u128)); + let wallet = EthereumWallet::new(signer.clone()); + let user_tx_hash = tx + .clone() + .build(&wallet) + .await + .unwrap() + .tx_hash() + .to_string(); + let tx = OwshenTransaction::Eth(tx.build(&wallet).await.unwrap()); + let hash_result = tx.hash(); + let fixed_bytes_tx_hash = hash_result.as_ref().unwrap(); + let vec8_tx_hash = fixed_bytes_tx_hash.to_vec(); + let token = Token::Native; + let amount = U256::from(100_000_000_000_000_000u128); + let address = signer.address(); + + let txx = CustomTx::create( + &mut ctx.lock().await.signer, + chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash: vec8_tx_hash.clone(), + user_tx_hash: user_tx_hash.clone(), + token, + amount, + address, + }), + ) + .await + .unwrap(); + + let response = app.clone() + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/deposit") + .header("Content-Type", "application/json") + .body(Body::from( + serde_json::to_vec(&json!({"tx_hash": user_tx_hash, "token": "native", "amount": amount, "address": address})).unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + body, + json!({"owshen_tx_hash": format!("{:?}", txx.hash().unwrap()), "success": true}) + ); + + let bincodable_tx = txx.clone().try_into().unwrap(); + + let block_number = 4321; + let included_tx = IncludedTransaction { + tx: bincodable_tx, + block_hash: FixedBytes::from([0u8; 32]), + block_number, + transaction_index: 1, + }; + let mut transactions: Vec = Vec::new(); + transactions.push(included_tx.clone()); + + { + let mut ctx_guard = ctx.lock().await; + ctx_guard + .chain + .db + .put( + Key::TransactionHash(txx.hash().unwrap()), + Some(Value::Transaction(included_tx.clone())), + ) + .unwrap(); + } + + let response = app + .oneshot( + Request::builder() + .method(http::Method::POST) + .uri("/deposit") + .header("Content-Type", "application/json") + .body(Body::from( + serde_json::to_vec(&json!({"tx_hash": user_tx_hash, "token": "native", "amount": amount, "address": address})).unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let body = response.into_body().collect().await.unwrap().to_bytes(); + let body: serde_json::Value = serde_json::from_slice(&body).unwrap(); + + assert_eq!( + body, + json!({"message": "Transaction already exists", "error": true}) + ); +} + +async fn test_config() -> ( + Arc>>, + Router, +) { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let owner = SafeSigner::new(PrivateKeySigner::random()); + let ctx = Arc::new(Mutex::new(Context { + signer: owner.clone(), + exit: false, + tx_queue: TransactionQueue::new(), + chain: Owshenchain::new(conf, RamKvStore::new()), + })); + + let app = api_routes(ctx.clone()); + + (ctx, app) +} diff --git a/src/services/api_services/withdraw.rs b/src/services/api_services/withdraw.rs new file mode 100644 index 0000000..4697a7f --- /dev/null +++ b/src/services/api_services/withdraw.rs @@ -0,0 +1,71 @@ +use alloy::primitives::{keccak256, Signature}; +use alloy::primitives::{FixedBytes, U256}; +use alloy::sol_types::SolValue; +use axum::Json; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use super::Context; +use crate::config::CHAIN_ID; +use crate::services::{ContextKvStore, ContextSigner}; +use crate::types::{CustomTx, Token, WithdrawCalldata}; +use crate::{blockchain::Blockchain, types::Burn}; + +#[derive(Deserialize, Debug)] +pub struct WithdrawRequest { + pub rlp_burn: Vec, + pub sig: Signature, +} + +#[derive(Serialize)] +pub struct WithdrawResponse { + pub id: FixedBytes<32>, + pub success: bool, +} + +pub async fn withdraw_handler( + ctx: Arc>>, + Json(payload): Json, +) -> Result, anyhow::Error> { + let mut _ctx = ctx.lock().await; + + let mut burn: Burn = rlp::decode(&payload.rlp_burn)?; + let from_address = payload.sig.recover_address_from_msg(&payload.rlp_burn)?; + _ctx.chain.db.put( + crate::db::Key::Balance(from_address, Token::Native), + Some(crate::db::Value::U256(U256::from(123456789))), + )?; + + let burn_id = burn.burn_id.clone(); + if _ctx + .chain + .db + .get(crate::db::Key::BurnId(burn_id.clone()))? + .is_some() + { + return Err(anyhow::anyhow!("Burn id already used!")); + } + + let calldata = WithdrawCalldata::Eth { + address: from_address, + }; + + burn.calldata = Some(calldata); + + let tx = CustomTx::create( + &mut _ctx.signer, + CHAIN_ID, + crate::types::CustomTxMsg::BurnTx(burn), + ) + .await?; + + let id = tx.hash()?; + _ctx.tx_queue.enqueue(tx); + _ctx.chain.db.put( + crate::db::Key::BurnId(burn_id), + Some(crate::db::Value::Void), + )?; + + Ok(Json(WithdrawResponse { id, success: true })) +} diff --git a/src/services/api_services/withdrawals.rs b/src/services/api_services/withdrawals.rs new file mode 100644 index 0000000..31740c3 --- /dev/null +++ b/src/services/api_services/withdrawals.rs @@ -0,0 +1,78 @@ +use alloy::primitives::{Address, Signature, U256}; +use axum::Json; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::Mutex; + +use super::Context; +use crate::{ + blockchain::Blockchain, + services::{ContextKvStore, ContextSigner}, + types::network::Network, + types::Token, + types::{CustomTxMsg, OwshenTransaction}, +}; + +#[derive(Deserialize)] +pub struct WithdrawalsRequest { + pub address: Address, +} + +#[derive(Serialize)] +pub struct WithdrawalDetail { + pub block_number: usize, + pub signature: Signature, + pub network: String, + pub token: String, + pub amount: U256, +} + +#[derive(Serialize)] +pub struct WithdrawalsResponse { + pub withdrawals: Vec, +} + +pub async fn withdrawals_handler( + ctx: Arc>>, + Json(payload): Json, +) -> Result, anyhow::Error> { + let ctx_guard = ctx.lock().await; + let blockchain = &ctx_guard.chain; + + let withdrawals = blockchain.get_user_withdrawals(payload.address)?; + + let withdrawal_details: Vec = withdrawals + .into_iter() + .filter_map(|included_tx| { + let tx: Result = included_tx.tx.try_into(); + + if let Ok(OwshenTransaction::Custom(custom_tx)) = tx { + if let Ok(CustomTxMsg::BurnTx(burn_data)) = custom_tx.msg() { + let network = match burn_data.network { + Network::ETH => "ETH".to_string(), + Network::BSC => "BSC".to_string(), + }; + let token = match burn_data.token { + Token::Native => "Native".to_string(), + Token::Erc20(address) => format!("ERC20: {:?}", address), + }; + let amount = burn_data.amount; + let signature = custom_tx.sig.clone(); + + return Some(WithdrawalDetail { + block_number: included_tx.block_number, + signature, + network, + token, + amount, + }); + } + } + None + }) + .collect(); + + Ok(Json(WithdrawalsResponse { + withdrawals: withdrawal_details, + })) +} diff --git a/src/services/mod.rs b/src/services/mod.rs new file mode 100644 index 0000000..2678563 --- /dev/null +++ b/src/services/mod.rs @@ -0,0 +1,21 @@ +use alloy::signers::Signer; + +use crate::{ + blockchain::{Owshenchain, TransactionQueue}, + db::KvStore, +}; + +mod api_services; +mod rpc_services; +pub mod server; + +pub trait ContextSigner: Signer + Send + Sync + Clone {} + +pub trait ContextKvStore: KvStore + Send + Sync {} + +pub struct Context { + pub exit: bool, + pub signer: S, + pub tx_queue: TransactionQueue, + pub chain: Owshenchain, +} diff --git a/src/services/rpc_services/eth_block_number.rs b/src/services/rpc_services/eth_block_number.rs new file mode 100644 index 0000000..3a96e48 --- /dev/null +++ b/src/services/rpc_services/eth_block_number.rs @@ -0,0 +1,38 @@ +use std::sync::Arc; + +use crate::{ + blockchain::Blockchain, + db::{Key, KvStore, Value}, +}; +use alloy::primitives::Address; +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; + +pub async fn eth_block_number( + ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + let height = ctx.lock().await.chain.get_height()?; + Ok(format!("0x{:x}", height)) +} + +#[tokio::test] +async fn test_eth_block_number() { + let _ctx = test_config().await; + _ctx.lock() + .await + .chain + .db + .put(Key::Height, Some(Value::Usize(12345))) + .unwrap(); + + let params = Params::new(None); + + let result = eth_block_number(_ctx.into(), params).await.unwrap(); + + assert_eq!(result, "0x3039"); +} diff --git a/src/services/rpc_services/eth_call.rs b/src/services/rpc_services/eth_call.rs new file mode 100644 index 0000000..2eeafbd --- /dev/null +++ b/src/services/rpc_services/eth_call.rs @@ -0,0 +1,193 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use super::Context; +use crate::blockchain::Blockchain; +use crate::db::{Key, KvStore, Value}; +use crate::services::rpc_services::test_config; +use crate::services::{ContextKvStore, ContextSigner}; +use crate::types::{Token, ERC20}; +use alloy::hex::ToHexExt; +use alloy::primitives::{Address, U256}; +use alloy::sol_types::SolValue; +use anyhow::{anyhow, Result}; +use hex; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +pub async fn eth_call( + ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let first_param: HashMap = params.sequence().next()?; + let data = first_param + .get("data") + .ok_or(anyhow!("Data unavailable!"))?; + let contract_address: Address = first_param + .get("to") + .ok_or(anyhow!("Contract address unavailable!"))? + .parse()?; + let method_hash = &data[0..10]; + match method_hash { + "0x01ffc9a7" => { + return Ok(format!("0x{:x}", 1)); + } + // balanceOf(address) + "0x70a08231" => { + let address: Address = data[10..].parse()?; + let decimals = ctx.lock().await.chain.get_token_decimal(contract_address)?; + let symbol = ctx.lock().await.chain.get_token_symbol(contract_address)?; + let token = Token::Erc20(ERC20 { + address: contract_address, + decimals, + symbol, + }); + + let balance = ctx.lock().await.chain.get_balance(token, address)?; + return Ok((balance).abi_encode().encode_hex()); + } + // decimals() + "0x313ce567" => { + let decimals = ctx.lock().await.chain.get_token_decimal(contract_address)?; + return Ok((decimals).abi_encode().encode_hex()); + } + // symbol() + "0x95d89b41" => { + let symbol = ctx.lock().await.chain.get_token_symbol(contract_address)?; + return Ok((symbol).abi_encode().encode_hex()); + } + _ => { + log::warn!("Unknown method_hash: {}", method_hash); + } + } + Ok(format!("0x{:x}", 0)) +} + +#[tokio::test] +async fn test_eth_call() { + let _ctx = test_config().await; + + let contract_address: Address = Address::from([7; 20]); + let balance = U256::from(100); + let token_symbol = "USDT".to_owned(); + let token_decimals = U256::from(18); + + _ctx.lock() + .await + .chain + .db + .put( + Key::TokenDecimal(contract_address), + Some(Value::U256(token_decimals)), + ) + .unwrap(); + + _ctx.lock() + .await + .chain + .db + .put( + Key::TokenSymbol(contract_address), + Some(Value::Symbol(token_symbol.clone())), + ) + .unwrap(); + + { + let method_hash = "0x70a08231"; + let address: Address = Address::from([8; 20]); + let data = format!("{}{}", method_hash, hex::encode(address)); + let addr = contract_address.to_string(); + let param_map = json!([{ + "to": addr, + "data": data + }]) + .to_string(); + let addr_static: &'static str = Box::leak(param_map.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + _ctx.lock() + .await + .chain + .db + .put( + Key::Balance( + address, + Token::Erc20(ERC20 { + address: contract_address, + decimals: token_decimals, + symbol: token_symbol.clone(), + }), + ), + Some(Value::U256(balance)), + ) + .unwrap(); + + let result = eth_call(_ctx.clone().into(), params).await; + assert!(result.is_ok()); + let expected_result = balance.abi_encode().encode_hex(); + assert_eq!(result.unwrap(), expected_result); + } + + { + let method_hash = "0x313ce567"; + let address: Address = Address::from([8; 20]); + + let data = format!("{}{}", method_hash, hex::encode(address)); + let addr = contract_address.to_string(); + let param_map = json!([{ + "to": addr, + "data": data + }]) + .to_string(); + let addr_static: &'static str = Box::leak(param_map.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_call(_ctx.clone().into(), params).await; + + assert!(result.is_ok()); + let expected_result = token_decimals.abi_encode().encode_hex(); + assert_eq!(result.unwrap(), expected_result); + } + + { + let method_hash = "0x95d89b41"; + let address: Address = Address::from([8; 20]); + + let data = format!("{}{}", method_hash, hex::encode(address)); + let addr = contract_address.to_string(); + let param_map = json!([{ + "to": addr, + "data": data + }]) + .to_string(); + let addr_static: &'static str = Box::leak(param_map.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_call(_ctx.clone().into(), params).await; + + assert!(result.is_ok()); + let expected_result = token_symbol.abi_encode().encode_hex(); + assert_eq!(result.unwrap(), expected_result); + } + + { + let method_hash = "0x12345678"; + let address: Address = Address::from([8; 20]); + + let data = format!("{}{}", method_hash, hex::encode(address)); + let addr = contract_address.to_string(); + let param_map = json!([{ + "to": addr, + "data": data + }]) + .to_string(); + let addr_static: &'static str = Box::leak(param_map.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_call(_ctx.clone().into(), params).await; + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), format!("0x{:x}", 0)); + } +} diff --git a/src/services/rpc_services/eth_chain_id.rs b/src/services/rpc_services/eth_chain_id.rs new file mode 100644 index 0000000..ea52ed8 --- /dev/null +++ b/src/services/rpc_services/eth_chain_id.rs @@ -0,0 +1,31 @@ +use std::sync::Arc; + +use crate::blockchain::Blockchain; +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; + +pub async fn eth_chain_id( + ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + let chain_id = ctx.lock().await.chain.config().chain_id; + Ok(format!("0x{:x}", chain_id)) +} + +#[tokio::test] +async fn test_eth_chain_id() { + let _ctx = test_config().await; + + let params = Params::new(None); + + let result = eth_chain_id(_ctx.clone().into(), params).await.unwrap(); + + assert_eq!( + result, + format!("0x{:x}", _ctx.lock().await.chain.config().chain_id) + ); +} diff --git a/src/services/rpc_services/eth_estimate_gas.rs b/src/services/rpc_services/eth_estimate_gas.rs new file mode 100644 index 0000000..4102c4c --- /dev/null +++ b/src/services/rpc_services/eth_estimate_gas.rs @@ -0,0 +1,34 @@ +use std::sync::Arc; + +use anyhow::Result; +use jsonrpsee::types::Params; +use serde_json::{json, Value}; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; + +pub async fn eth_estimate_gas( + _ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse()?; + let data = params.get(0).map(|s| s.to_owned()).unwrap_or_default(); + let mut gas_estimate = 0u64; + + if !data.is_empty() { + gas_estimate += (data.len() as u64) * 68; + } + + Ok(format!("0x{:x}", gas_estimate)) +} + +#[tokio::test] +async fn test_eth_estimate_gas() { + let _ctx = test_config().await; + + let params = Params::new(Some("[]")); + let result = eth_estimate_gas(_ctx.clone().into(), params).await.unwrap(); + assert_eq!(result, "0x0"); + +} diff --git a/src/services/rpc_services/eth_fee_history.rs b/src/services/rpc_services/eth_fee_history.rs new file mode 100644 index 0000000..cf22de1 --- /dev/null +++ b/src/services/rpc_services/eth_fee_history.rs @@ -0,0 +1,82 @@ +use std::sync::Arc; + +use crate::services::{ContextKvStore, ContextSigner, rpc_services::test_config}; +use anyhow::Result; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Context; + +pub async fn eth_fee_history( + _ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + let _count = 0; + + Ok(json!({ + "baseFeePerGas": [ + "0", + "0", + "0", + "0", + "0", + "0" + ], + "gasUsedRatio": [ + 0, + 0, + 0, + 0, + 0 + ], + "oldestBlock": "0xfab8ac", + "reward": [ + [ + "0", + "0" + ], + [ + "0", + "0" + ], + [ + "0", + "0" + ], + [ + "0", + "0" + ], + [ + "0", + "0" + ] + ] + })) +} + + +#[tokio::test] +async fn test_eth_fee_history() { + let _ctx = test_config().await; + let params = Params::new(None); + let result = eth_fee_history(_ctx.into(), params).await; + match result { + Ok(value) => { + assert_eq!(value["baseFeePerGas"], json!(["0", "0", "0", "0", "0", "0"])); + assert_eq!(value["gasUsedRatio"], json!([0, 0, 0, 0, 0])); + assert_eq!(value["oldestBlock"], "0xfab8ac"); + assert_eq!(value["reward"], json!([ + ["0", "0"], + ["0", "0"], + ["0", "0"], + ["0", "0"], + ["0", "0"] + ])); + } + Err(e) => { + panic!("eth_fee_history failed: {:?}", e); + } + } +} diff --git a/src/services/rpc_services/eth_get_balance.rs b/src/services/rpc_services/eth_get_balance.rs new file mode 100644 index 0000000..fbc0c8a --- /dev/null +++ b/src/services/rpc_services/eth_get_balance.rs @@ -0,0 +1,60 @@ +use std::borrow::Cow; +use std::sync::Arc; + +use crate::blockchain::Blockchain; +use crate::db::{Key, KvStore, Value}; +use alloy::primitives::utils::parse_units; +use alloy::primitives::Address; +use alloy::signers::local::PrivateKeySigner; +use alloy::signers::Signer; +use anyhow::{anyhow, Result}; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; +use crate::types::Token; + +pub async fn eth_get_balance( + ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse()?; + let addr: Address = params + .get(0) + .ok_or(anyhow!("Address unavailable!"))? + .parse()?; + let balance = ctx.lock().await.chain.get_balance(Token::Native, addr)?; + Ok(format!("0x{:x}", balance)) +} + +#[tokio::test] +async fn test_eth_get_balance() { + let _ctx = test_config().await; + + let address: Address = Address::from([2; 20]); + let amount = parse_units("2", 18).unwrap().into(); + + _ctx.lock() + .await + .chain + .db + .put( + Key::Balance(address, Token::Native), + Some(Value::U256(amount)), + ) + .unwrap(); + + let addr = address.to_string(); + let j = json!([addr, "latest"]).to_string(); + let addr_static: &'static str = Box::leak(j.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_get_balance(_ctx.into(), params).await; + + assert!(result.is_ok()); + + let balance = result.unwrap(); + assert_eq!(balance, format!("0x{:x}", amount)); +} diff --git a/src/services/rpc_services/eth_get_block_by_number.rs b/src/services/rpc_services/eth_get_block_by_number.rs new file mode 100644 index 0000000..be3aa07 --- /dev/null +++ b/src/services/rpc_services/eth_get_block_by_number.rs @@ -0,0 +1,93 @@ +use alloy::{ + network::{Ethereum, Network}, + primitives::{fixed_bytes, keccak256}, + rpc::types::Block, + signers::{local::PrivateKeySigner, Signer}, +}; +use anyhow::{anyhow, Result}; +use std::sync::Arc; + +use super::Context; +use crate::{ + blockchain::{tx::owshen_airdrop::babyjubjub::PrivateKey, Blockchain}, + db::{Key, KvStore, Value}, + services::{rpc_services::test_config, ContextKvStore, ContextSigner}, + types, +}; + +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +pub async fn eth_get_block_by_number( + _ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse()?; + let index: usize = params + .get(0) + .ok_or(anyhow!("Address unavailable!"))? + .parse()?; + let block = _ctx.lock().await.chain.get_block(index)?; + + Ok(serde_json::json!(block)) +} + +#[tokio::test] +async fn test_eth_get_block_by_number() { + let _ctx = test_config().await; + + let block_number: usize = 1234; + let signer = PrivateKeySigner::random(); + let message = b"hello"; + let signature = signer.sign_message(message).await.unwrap(); + let prev_block_hash = keccak256(vec![1,2,3]); + + let block: types::Block = types::Block { + prev_hash: prev_block_hash + .into(), + index: block_number, + txs: Vec::new(), + sig: Some(signature.clone()), + timestamp: 32, + }; + + _ctx.lock() + .await + .chain + .db + .put(Key::Block(block_number), Some(Value::Block(block.clone()))) + .unwrap(); + + let _ = _ctx + .lock() + .await + .chain + .db + .put(Key::Height, Some(Value::Usize(1235))); + + + let block_number_str = block_number.to_string(); + let j = serde_json::json!([block_number_str, "latest"]).to_string(); + let addr_static: &'static str = Box::leak(j.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_get_block_by_number(_ctx.into(), params).await; + + + assert!(result.is_ok()); + + let result_block = result.unwrap(); + let expected_block_json = serde_json::json!({ + "prev_hash": prev_block_hash.to_string(), + "index": block_number, + "txs": [], + "sig": { + "r": format!("0x{:x}", signature.r()), + "s": format!("0x{:x}", signature.s()), + "yParity": format!("0x{:x}", signature.v().to_u64()), + }, + "timestamp": block.timestamp, + }); + + assert_eq!(result_block, expected_block_json); +} diff --git a/src/services/rpc_services/eth_get_code.rs b/src/services/rpc_services/eth_get_code.rs new file mode 100644 index 0000000..ec28b19 --- /dev/null +++ b/src/services/rpc_services/eth_get_code.rs @@ -0,0 +1,22 @@ +use std::sync::Arc; + +use anyhow::{Ok, Result}; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{ContextKvStore, ContextSigner}; + +pub async fn eth_get_code( + _ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse()?; + let _address = ¶ms[0]; + + //TODO: Handle the get code + + let code = "".to_string(); + + Ok(code) +} diff --git a/src/services/rpc_services/eth_get_gas_price.rs b/src/services/rpc_services/eth_get_gas_price.rs new file mode 100644 index 0000000..d2b3607 --- /dev/null +++ b/src/services/rpc_services/eth_get_gas_price.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{ContextKvStore, ContextSigner}; + +pub async fn eth_get_gas_price( + _ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + let gas_price = 0x0; + + Ok(format!("0x{:x}", gas_price)) +} diff --git a/src/services/rpc_services/eth_get_transaction_by_hash.rs b/src/services/rpc_services/eth_get_transaction_by_hash.rs new file mode 100644 index 0000000..33d3973 --- /dev/null +++ b/src/services/rpc_services/eth_get_transaction_by_hash.rs @@ -0,0 +1,166 @@ +use std::result; +use std::sync::Arc; + +use crate::blockchain::Blockchain; +use crate::db::{Key, KvStore, Value}; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; +use crate::types::network::Network; +use alloy::consensus::{TxEip1559, TypedTransaction}; +use alloy::hex::ToHexExt; +use alloy::network::{Ethereum, EthereumWallet, NetworkWallet}; +use alloy::primitives::{Address, Bytes, FixedBytes, Uint, B256, U256}; +use alloy::rpc::types::{AccessList, AccessListItem}; +use alloy::signers::local::PrivateKeySigner; +use anyhow::{anyhow, Result}; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Context; +use crate::types::{ + BincodableOwshenTransaction, Burn, CustomTx, CustomTxMsg, IncludedTransaction, + OwshenTransaction, Token, +}; + +pub async fn eth_get_transaction_by_hash( + ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse().unwrap_or_default(); + let tx_hash = params + .get(0) + .ok_or(anyhow!("Transaction hash not provided!"))?; + let inc_tx = ctx + .lock() + .await + .chain + .get_transaction_by_hash(tx_hash.parse()?)?; + + match inc_tx.tx.try_into()? { + OwshenTransaction::Custom(_) => Err(anyhow!("Not a eth transaction!")), + OwshenTransaction::Eth(envelope) => { + let mut tx = serde_json::to_value(&envelope)?; + let v = envelope + .as_eip1559() + .ok_or(anyhow!("Only EIP-1559 supported for now!"))? + .signature() + .v() + .to_u64(); + tx.as_object_mut().unwrap().insert( + "from".into(), + envelope + .recover_signer()? + .encode_hex_upper_with_prefix() + .into(), + ); + tx.as_object_mut().unwrap().insert( + "blockHash".into(), + inc_tx.block_hash.encode_hex_with_prefix().into(), + ); + tx.as_object_mut().unwrap().insert( + "blockNumber".into(), + format!("0x{:x}", inc_tx.block_number).into(), + ); + tx.as_object_mut().unwrap().insert( + "transactionIndex".into(), + format!("0x{:x}", inc_tx.transaction_index).into(), + ); + tx.as_object_mut() + .unwrap() + .insert("gas".into(), format!("0x{:x}", 0).into()); + tx.as_object_mut() + .unwrap() + .insert("gasPrice".into(), format!("0x{:x}", 0).into()); + tx.as_object_mut().unwrap().remove(&"gasLimit".to_string()); + tx.as_object_mut() + .unwrap() + .insert("v".into(), format!("0x{:x}", v).into()); + Ok(tx) + } + } +} + +#[tokio::test] +async fn test_eth_get_transaction_by_hash() { + let _ctx = test_config().await; + + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: alloy::primitives::TxKind::Call(Address::from([6; 20])), + value: Uint::<256, 4>::from(0), + input: Bytes::from("hello"), + chain_id: _ctx.lock().await.chain.config().chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx.clone()); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let eth_tx = OwshenTransaction::Eth(signed_tx.clone()); + + let tx_hash = eth_tx.hash().unwrap(); + + let included_tx: IncludedTransaction = IncludedTransaction { + tx: eth_tx.try_into().unwrap(), + block_hash: FixedBytes::from([0u8; 32]), + block_number: 4321, + transaction_index: 1, + }; + + let _ = _ctx.lock().await.chain.db.put( + Key::TransactionHash(tx_hash), + Some(Value::Transaction(included_tx.clone())), + ); + + let tx_hash_str = tx_hash.to_string(); + let j = json!([tx_hash_str, "latest"]).to_string(); + let hash_static: &'static str = Box::leak(j.into_boxed_str()); + let params = Params::new(Some(hash_static)); + + let result = eth_get_transaction_by_hash(_ctx.into(), params).await; + + let mut _tx = serde_json::to_value(&signed_tx).unwrap(); + let sig = signed_tx.as_eip1559().unwrap().signature(); + + assert!(result.is_ok()); + + let expected_tx_json = json!({ + "accessList": [{ + "address": "0x0000000000000000000000000000000000000000", + "storageKeys": ["0x0000000000000000000000000000000000000000000000000000000000000000"] + }], + "blockHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "blockNumber": format!("0x{:x}", included_tx.block_number), + "chainId": format!("0x{:x}", tx.clone().chain_id), + "from": format!("0x{}", signed_tx.recover_signer().unwrap().encode_hex_upper()), + "gas": "0x0", + "gasPrice": "0x0", + "hash": format!("0x{}", tx_hash.encode_hex()), + "input": "0x68656c6c6f", + "maxFeePerGas": format!("0x{:x}", tx.clone().max_fee_per_gas), + "maxPriorityFeePerGas": format!("0x{:x}", tx.clone().max_priority_fee_per_gas), + "nonce": format!("0x{:x}", 0), + "r": format!("0x{:x}", sig.clone().r()), + "s": format!("0x{:x}", sig.clone().s()), + "to": "0x0606060606060606060606060606060606060606", + "transactionIndex": format!("0x{:x}", included_tx.transaction_index), + "type": "0x2", + "v": format!("0x{:x}", sig.clone().v().to_u64()), + "value": "0x0", + "yParity": format!("0x{:x}", sig.clone().v().to_u64()), + }); + + assert_eq!(result.unwrap(), expected_tx_json); +} diff --git a/src/services/rpc_services/eth_get_transaction_count.rs b/src/services/rpc_services/eth_get_transaction_count.rs new file mode 100644 index 0000000..7821e6c --- /dev/null +++ b/src/services/rpc_services/eth_get_transaction_count.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use alloy::primitives::{Address, Uint, U256}; +use anyhow::{anyhow, Result}; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Context; +use crate::{ + blockchain::Blockchain, + db::{Key, KvStore, Value}, + services::{rpc_services::test_config, ContextKvStore, ContextSigner}, +}; + +pub async fn eth_get_transaction_count( + ctx: Arc>>>, + params: Params<'static>, +) -> Result, anyhow::Error> { + let params: Vec = params.parse()?; + let addr: Address = params + .get(0) + .ok_or(anyhow!("Address unavailable!"))? + .parse()?; + + let nonce = ctx.lock().await.chain.get_eth_nonce(addr).unwrap(); + + Ok(nonce) +} + +#[tokio::test] +async fn test_eth_get_transaction_count() { + let _ctx = test_config().await; + + let address: Address = Address::from([2; 20]); + + _ctx.lock() + .await + .chain + .db + .put(Key::NonceEth(address), Some(Value::U256(U256::from(10)))) + .unwrap(); + + let addr = address.to_string(); + let j = json!([addr, "latest"]).to_string(); + let addr_static: &'static str = Box::leak(j.into_boxed_str()); + let params = Params::new(Some(addr_static)); + + let result = eth_get_transaction_count(_ctx.into(), params).await; + + assert!(result.is_ok()); + + let nonce = result.unwrap(); + assert_eq!(nonce, U256::from(10)); +} diff --git a/src/services/rpc_services/eth_get_transaction_receipt.rs b/src/services/rpc_services/eth_get_transaction_receipt.rs new file mode 100644 index 0000000..c002136 --- /dev/null +++ b/src/services/rpc_services/eth_get_transaction_receipt.rs @@ -0,0 +1,161 @@ +use alloy::signers::local::PrivateKeySigner; +use anyhow::{anyhow, Result}; +use jsonrpsee::types::Params; +use serde_json::json; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::Mutex; + +use super::Context; +use crate::blockchain::Blockchain; +use crate::db::{Key, KvStore, Value}; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; +use crate::types::network::Network; +use alloy::consensus::{Transaction, TxEip1559, TypedTransaction}; +use alloy::hex::ToHexExt; +use alloy::network::{Ethereum, EthereumWallet, NetworkWallet}; +use alloy::primitives::{Address, Bytes, FixedBytes, Uint, B256, U256}; +use alloy::rpc::types::{AccessList, AccessListItem}; +use std::result; + +use crate::types::{ + BincodableOwshenTransaction, Burn, CustomTx, CustomTxMsg, IncludedTransaction, + OwshenTransaction, Token, +}; + +pub async fn eth_get_transaction_receipt( + ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse().unwrap_or_default(); + + let tx_hash = match FixedBytes::<32>::from_str(¶ms[0]) { + Ok(hash) => hash, + Err(_) => return Err(anyhow::Error::msg("Invalid transaction hash")), + }; + + let tx = ctx + .lock() + .await + .chain + .get_transaction_by_hash(tx_hash) + .unwrap(); + + let owshen_tx: OwshenTransaction = tx.tx.try_into()?; + + let (gas_used, effective_gas_price, sender, to, contract_address) = match owshen_tx { + OwshenTransaction::Eth(tx) => { + let gas_used = tx.gas_limit(); + let effective_gas_price = tx.as_eip1559().unwrap().tx().effective_gas_price(None); + let sender = tx.recover_signer()?.to_string(); + let to = match tx.to() { + alloy::primitives::TxKind::Create => serde_json::Value::Null, + alloy::primitives::TxKind::Call(address) => { + serde_json::Value::String(address.to_string()) + } + }; + let contract_address = serde_json::Value::Null; + (gas_used, effective_gas_price, sender, to, contract_address) + } + OwshenTransaction::Custom(_) => { + return Err(anyhow::Error::msg("Not an Ethereum transaction!")) + } + }; + + let receipt_json = json!({ + "transactionHash": format!("0x{:x}", tx_hash), + "transactionIndex": format!("0x{:x}", tx.transaction_index), + "blockHash": format!("0x{:x}", tx.block_hash), + "blockNumber": format!("0x{:x}", tx.block_number), + "gasUsed": format!("0x{:x}", gas_used), + "effectiveGasPrice": format!("0x{:x}", effective_gas_price), + "from": sender, + "to": to, + "contractAddress": contract_address, + "logs": [], + "cumulativeGasUsed": "0x1b4", + "status": "0x1", + "logsBloom": "0x".to_string() + &"0".repeat(512), + "type": "0x2", + }) + .to_string(); + + Ok(receipt_json) +} + +#[tokio::test] +async fn test_eth_get_transaction_receipt() { + let _ctx = test_config().await; + + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + let msg_sender: Address = + >::default_signer_address(&wallet); + + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: alloy::primitives::TxKind::Call(Address::from([6; 20])), + value: Uint::<256, 4>::from(0), + input: Bytes::from("hello"), + chain_id: _ctx.lock().await.chain.config().chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx); + + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let eth_tx = OwshenTransaction::Eth(signed_tx.clone()); + + let tx_hash = eth_tx.hash().unwrap(); + + let included_tx: IncludedTransaction = IncludedTransaction { + tx: eth_tx.try_into().unwrap(), + block_hash: FixedBytes::from([0u8; 32]), + block_number: 4321, + transaction_index: 1, + }; + + let _ = _ctx.lock().await.chain.db.put( + Key::TransactionHash(tx_hash), + Some(Value::Transaction(included_tx.clone())), + ); + + let tx_hash_str = tx_hash.to_string(); + let j = json!([tx_hash_str, "latest"]).to_string(); + let hash_static: &'static str = Box::leak(j.into_boxed_str()); + let params = Params::new(Some(hash_static)); + + let result = eth_get_transaction_receipt(_ctx.into(), params).await; + + assert!(result.is_ok()); + + let receipt_json: serde_json::Value = serde_json::from_str(result.as_ref().unwrap()).unwrap(); + + let expected_json = json!({ + "transactionHash": tx_hash_str, + "transactionIndex": format!("0x{:x}",included_tx.transaction_index), + "blockHash": format!("0x{:x}", included_tx.block_hash), + "blockNumber": format!("0x{:x}", included_tx.block_number), + "gasUsed": format!("0x{:x}",21000), + "effectiveGasPrice": format!("0x{:x}",300000000), + "from": msg_sender.to_string(), + "to": Address::from([6; 20]).to_string(), + "contractAddress": serde_json::Value::Null, + "logs": [], + "cumulativeGasUsed": "0x1b4", + "status": "0x1", + "logsBloom": "0x".to_string() + &"0".repeat(512), + "type": "0x2", + }); + + assert_eq!(receipt_json, expected_json); +} diff --git a/src/services/rpc_services/eth_request_accounts.rs b/src/services/rpc_services/eth_request_accounts.rs new file mode 100644 index 0000000..0401742 --- /dev/null +++ b/src/services/rpc_services/eth_request_accounts.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{ContextKvStore, ContextSigner}; + +pub async fn eth_request_accounts( + _ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + //TODO: Handle the request accounts + let accounts = "".to_string(); + Ok(accounts) +} diff --git a/src/services/rpc_services/eth_send_raw_transaction.rs b/src/services/rpc_services/eth_send_raw_transaction.rs new file mode 100644 index 0000000..b9503a2 --- /dev/null +++ b/src/services/rpc_services/eth_send_raw_transaction.rs @@ -0,0 +1,78 @@ +use std::sync::Arc; + +use alloy::consensus::{TxEip1559, TypedTransaction}; +use alloy::network::{Ethereum, EthereumWallet, NetworkWallet}; +use alloy::primitives::{Address, Bytes, Uint, B256}; +use alloy::rlp::Decodable; +use alloy::rpc::types::{AccessList, AccessListItem}; +use alloy::signers::local::PrivateKeySigner; +use anyhow::Result; +use jsonrpsee::types::Params; +use serde_json::json; +use tokio::sync::Mutex; + +use super::Context; +use crate::blockchain::Blockchain; +use crate::services::{rpc_services::test_config, ContextKvStore, ContextSigner}; +use crate::types::OwshenTransaction; + +pub async fn eth_send_raw_transaction( + ctx: Arc>>>, + params: Params<'static>, +) -> Result { + let params: Vec = params.parse().unwrap_or_default(); + let raw_tx = ¶ms[0]; + let raw_tx_bytes = hex::decode(raw_tx.trim_start_matches("0x"))?; + let mut hah = raw_tx_bytes.as_ref(); + let tx = OwshenTransaction::Eth(alloy::consensus::TxEnvelope::decode(&mut hah).unwrap()); + ctx.lock().await.tx_queue.enqueue(tx.clone()); + + Ok("Transaction sent successfully".to_string()) +} + +#[tokio::test] +async fn test_eth_send_raw_transaction() { + let _ctx = test_config().await; + + let wallet: EthereumWallet = EthereumWallet::new(PrivateKeySigner::random()); + let tx = TxEip1559 { + nonce: 0, + gas_limit: 21_000, + to: alloy::primitives::TxKind::Call(Address::from([6; 20])), + value: Uint::<256, 4>::from(0), + input: Bytes::from("hello"), + chain_id: _ctx.lock().await.chain.config().chain_id, + max_priority_fee_per_gas: 3_000_000, + max_fee_per_gas: 300_000_000, + access_list: AccessList(vec![AccessListItem { + address: Address::ZERO, + storage_keys: vec![B256::ZERO], + }]), + }; + + let typed_tx = TypedTransaction::Eip1559(tx); + let signed_tx = + >::sign_transaction(&wallet, typed_tx) + .await + .unwrap(); + + let raw_tx_bytes = alloy::rlp::encode(&signed_tx); + let raw_tx = hex::encode(raw_tx_bytes); + + let j = json!([format!("0x{}", raw_tx)]); + let raw_tx_static: &'static str = Box::leak(j.to_string().into_boxed_str()); + let params = Params::new(Some(raw_tx_static)); + + let result = eth_send_raw_transaction(_ctx.clone().into(), params).await; + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), "Transaction sent successfully"); + + let ctx = _ctx.lock().await; + let tx_queue = ctx.tx_queue.queue(); + assert_eq!(tx_queue.len(), 1); + let queued_tx = tx_queue[0].clone(); + let eth_tx: OwshenTransaction = OwshenTransaction::Eth(signed_tx); + + assert_eq!(queued_tx, eth_tx); +} diff --git a/src/services/rpc_services/mod.rs b/src/services/rpc_services/mod.rs new file mode 100644 index 0000000..a571c16 --- /dev/null +++ b/src/services/rpc_services/mod.rs @@ -0,0 +1,86 @@ +use super::Context; + +mod net_version; +pub use net_version::*; +mod eth_get_balance; +pub use eth_get_balance::*; +mod eth_chain_id; +pub use eth_chain_id::*; +mod eth_block_number; +pub use eth_block_number::*; +mod eth_call; +pub use eth_call::*; +mod eth_get_code; +pub use eth_get_code::*; +mod eth_request_accounts; +pub use eth_request_accounts::*; +mod eth_estimate_gas; +pub use eth_estimate_gas::*; +mod eth_get_gas_price; +pub use eth_get_gas_price::*; +mod eth_get_transaction_count; +pub use eth_get_transaction_count::*; +mod eth_get_transaction_receipt; +pub use eth_get_transaction_receipt::*; +mod eth_send_raw_transaction; +pub use eth_send_raw_transaction::*; +mod eth_get_block_by_number; +pub use eth_get_block_by_number::*; +mod eth_fee_history; +pub use eth_fee_history::*; +mod eth_get_transaction_by_hash; +pub use eth_get_transaction_by_hash::*; +mod todo; +pub use todo::*; + + +use alloy::{ + primitives::{utils::parse_units, Address, FixedBytes, Signature, U256}, + signers::{local::PrivateKeySigner, Signer}, +}; +use axum::{ + body::{Body, HttpBody}, + http::{self, Request, StatusCode}, + routing::get, + Json, Router, +}; +use serde::de::Expected; +use serde_json::json; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower::{Service, ServiceExt}; + +use crate::{ + blockchain::{ + tx::owshen_airdrop::babyjubjub::PrivateKey, Blockchain, Config, Owshenchain, + TransactionQueue, + }, + config, + db::{DiskKvStore, Key, KvStore, RamKvStore, Value}, + genesis::GENESIS, + safe_signer::{self, SafeSigner}, + services::{api_services::api_routes}, + types::{ + network::Network, Burn, CustomTx, CustomTxMsg, IncludedTransaction, OwshenTransaction, + Token, + }, +}; + +async fn test_config() -> Arc>> { + let conf = Config { + chain_id: 1387, + owner: None, + genesis: GENESIS.clone(), + owshen: config::OWSHEN_CONTRACT, + provider_address: "http://127.0.0.1:8888".parse().expect("faild to parse"), + }; + + let owner = SafeSigner::new(PrivateKeySigner::random()); + return Arc::new(Mutex::new(Context { + signer: owner.clone(), + exit: false, + tx_queue: TransactionQueue::new(), + chain: Owshenchain::new(conf, RamKvStore::new()), + })); +} + diff --git a/src/services/rpc_services/net_version.rs b/src/services/rpc_services/net_version.rs new file mode 100644 index 0000000..87ef607 --- /dev/null +++ b/src/services/rpc_services/net_version.rs @@ -0,0 +1,17 @@ +use std::sync::Arc; + +use crate::blockchain::Blockchain; +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +use super::Context; +use crate::services::{ContextKvStore, ContextSigner}; + +pub async fn net_version( + ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + let chain_id = ctx.lock().await.chain.config().chain_id; + Ok(format!("0x{:x}", chain_id)) +} diff --git a/src/services/rpc_services/todo.rs b/src/services/rpc_services/todo.rs new file mode 100644 index 0000000..10c96ff --- /dev/null +++ b/src/services/rpc_services/todo.rs @@ -0,0 +1,15 @@ +use std::sync::Arc; + +use super::Context; +use crate::services::{ContextKvStore, ContextSigner}; + +use anyhow::Result; +use jsonrpsee::types::Params; +use tokio::sync::Mutex; + +pub async fn todo( + _ctx: Arc>>>, + _params: Params<'static>, +) -> Result { + Ok("0x0".into()) +} diff --git a/src/services/server.rs b/src/services/server.rs new file mode 100644 index 0000000..9e406d5 --- /dev/null +++ b/src/services/server.rs @@ -0,0 +1,229 @@ +use crate::blockchain::Blockchain; + +use crate::services::api_services::api_routes; +use crate::services::Context; + +use anyhow::Result; +use jsonrpsee::server::{RpcModule, Server}; +use jsonrpsee::types::ErrorCode; +use std::net::{IpAddr, Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tower_http::cors::CorsLayer; +use tower_http_cors::cors::CorsLayer as CorsLayerAxum; + +use super::{ContextKvStore, ContextSigner}; + +pub async fn api_server( + ctx: Arc>>, + port: u16, +) -> Result<()> { + let app = api_routes(ctx); + let app_with_middleware = app.layer(CorsLayerAxum::permissive()); + + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), port); + log::info!("Running API server on: {}", addr); + + axum::Server::bind(&addr) + .serve(app_with_middleware.into_make_service()) + .await?; + + Ok(()) +} + +fn anyhow_to_rpc_error(e: anyhow::Error) -> ErrorCode { + log::error!("RPC Error: {}", e); + ErrorCode::InternalError +} + +pub async fn rpc_server( + ctx: Arc>>, + port: u16, +) -> Result<()> { + let chain_id = ctx.lock().await.chain.config().chain_id; + + let cors = CorsLayer::new() + .allow_methods(tower_http::cors::Any) + .allow_origin(tower_http::cors::Any) + .allow_headers(tower_http::cors::Any); + let middleware = tower::ServiceBuilder::new().layer(cors); + let rpc_addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port)); + let server = Server::builder() + .set_http_middleware(middleware) + .build(rpc_addr) + .await?; + let mut module = RpcModule::new(ctx); + + module.register_async_method("net_version", move |params, ctx, _| async move { + log::info!("net_version! {:?}", params); + crate::services::rpc_services::net_version(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_call", move |params, ctx, _| async move { + log::info!("eth_call! {:?}", params); + crate::services::rpc_services::eth_call(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_blockNumber", move |params, ctx, _| async move { + log::info!("eth_blockNumber! {:?}", params); + crate::services::rpc_services::eth_block_number(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + + module.register_async_method("eth_getBalance", move |params, ctx, _| async move { + log::info!("eth_getBalance! {:?}", params); + crate::services::rpc_services::eth_get_balance(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + + module.register_async_method("eth_sendTransaction", move |params, ctx, _| async move { + log::info!("eth_sendTransaction! {:?}", params); + crate::services::rpc_services::todo(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_sendRawTransaction", move |params, ctx, _| async move { + log::info!("eth_sendRawTransaction! {:?}", params); + crate::services::rpc_services::eth_send_raw_transaction(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_estimateGas", move |params, ctx, _| async move { + log::info!("eth_estimateGas! {:?}", params); + crate::services::rpc_services::eth_estimate_gas(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_gasPrice", move |params, ctx, _| async move { + log::info!("eth_gasPrice! {:?}", params); + crate::services::rpc_services::eth_get_gas_price(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method( + "eth_getTransactionCount", + move |params, ctx, _| async move { + log::info!("eth_getTransactionCount! {:?}", params); + crate::services::rpc_services::eth_get_transaction_count(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + }, + )?; + module.register_async_method( + "eth_getTransactionReceipt", + move |params, ctx, _| async move { + log::info!("eth_getTransactionReceipt! {:?}", params); + crate::services::rpc_services::eth_get_transaction_receipt(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + }, + )?; + module.register_async_method("eth_getBlockByNumber", move |params, ctx, _| async move { + log::info!("eth_getBlockByNumber! {:?}", params); + crate::services::rpc_services::eth_get_block_by_number(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_chainId", move |params, ctx, _| async move { + log::info!("eth_chainId! {:?}", params); + crate::services::rpc_services::eth_chain_id(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_requestAccounts", move |params, ctx, _| async move { + log::info!("eth_requestAccounts! {:?}", params); + crate::services::rpc_services::eth_request_accounts(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method("eth_feeHistory", move |params, ctx, _| async move { + log::info!("eth_feeHistory! {:?}", params); + crate::services::rpc_services::eth_fee_history(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + module.register_async_method( + "eth_getTransactionByHash", + move |params, ctx, _| async move { + log::info!("eth_getTransactionByHash! {:?}", params); + crate::services::rpc_services::eth_get_transaction_by_hash(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + }, + )?; + module.register_async_method("eth_get_code", move |params, ctx, _| async move { + log::info!("eth_getTransactionByHash! {:?}", params); + crate::services::rpc_services::eth_get_code(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + + for method_name in [ + "debug_getBadBlocks", + "debug_getRawBlock", + "debug_getRawHeader", + "debug_getRawReceipts", + "debug_getRawTransaction", + "engine_exchangeCapabilities", + "engine_exchangeTransitionConfigurationV1", + "engine_forkchoiceUpdatedV1", + "engine_forkchoiceUpdatedV2", + "engine_forkchoiceUpdatedV3", + "engine_getPayloadBodiesByHashV1", + "engine_getPayloadBodiesByHashV2", + "engine_getPayloadBodiesByRangeV1", + "engine_getPayloadBodiesByRangeV2", + "engine_getPayloadV1", + "engine_getPayloadV2", + "engine_getPayloadV3", + "engine_getPayloadV4", + "engine_newPayloadV1", + "engine_newPayloadV2", + "engine_newPayloadV3", + "engine_newPayloadV4", + "eth_accounts", + "eth_blobBaseFee", + "eth_coinbase", + "eth_createAccessList", + "eth_getBlockByHash", + "eth_getBlockReceipts", + "eth_getBlockTransactionCountByHash", + "eth_getBlockTransactionCountByNumber", + "eth_getCode", + "eth_getFilterChanges", + "eth_getFilterLogs", + "eth_getLogs", + "eth_getProof", + "eth_getStorageAt", + "eth_getTransactionByBlockHashAndIndex", + "eth_getTransactionByBlockNumberAndIndex", + "eth_getUncleCountByBlockHash", + "eth_getUncleCountByBlockNumber", + "eth_maxPriorityFeePerGas", + "eth_newBlockFilter", + "eth_newFilter", + "eth_newPendingTransactionFilter", + "eth_sign", + "eth_signTransaction", + "eth_syncing", + "eth_uninstallFilter", + ] { + module.register_async_method(method_name, move |params, ctx, _| async move { + log::info!("{}! {:?}", method_name, params); + crate::services::rpc_services::todo(ctx, params) + .await + .map_err(anyhow_to_rpc_error) + })?; + } + + let addr = server.local_addr()?; + log::info!("Running RPC server on: {} (Chain-id: {})", addr, chain_id); + let handle = server.start(module); + handle.stopped().await; + + Ok(()) +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..057257a --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,62 @@ +pub mod network; +mod tx; +use alloy::{ + primitives::{keccak256, Address, FixedBytes, U256}, + signers::Signer, +}; +use anyhow::Result; +use network::Network; +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; +pub use tx::{ + BincodableOwshenTransaction, Burn, CustomTx, CustomTxMsg, IncludedTransaction, Mint, + OwshenTransaction, WithdrawCalldata, +}; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Block { + pub prev_hash: Option>, + pub index: usize, + pub txs: Vec, + pub sig: Option, + pub timestamp: u64, +} + +impl Block { + pub fn unsigned_bytes(&self) -> Result> { + let mut blk = self.clone(); + blk.sig = None; + Ok(bincode::serialize(&blk)?) + } + pub fn hash(&self) -> Result> { + Ok(keccak256(self.unsigned_bytes()?)) + } + pub async fn signed(&self, signer: S) -> Result { + let bytes = self.hash()?; + let mut blk = self.clone(); + + blk.sig = Some(signer.sign_hash(&bytes).await?); + Ok(blk) + } + pub fn is_signed_by(&self, addr: Address) -> Result { + let hash = self.hash()?; + Ok(if let Some(sig) = self.sig { + sig.recover_address_from_prehash(&hash)? == addr + } else { + false + }) + } +} + +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Clone, Deserialize)] +pub enum Token { + Native, + Erc20(ERC20), +} + +#[derive(Debug, PartialEq, Eq, Hash, Serialize, Clone, Deserialize)] +pub struct ERC20 { + pub address: Address, + pub decimals: U256, + pub symbol: String, +} diff --git a/src/types/network.rs b/src/types/network.rs new file mode 100644 index 0000000..0319b94 --- /dev/null +++ b/src/types/network.rs @@ -0,0 +1,14 @@ +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Network { + ETH, + BSC, +} + +impl Network { + pub fn chain_id(&self) -> u64 { + match self { + Network::ETH => 1, + Network::BSC => 56, + } + } +} diff --git a/src/types/tx/custom.rs b/src/types/tx/custom.rs new file mode 100644 index 0000000..72c5f06 --- /dev/null +++ b/src/types/tx/custom.rs @@ -0,0 +1,287 @@ +use alloy::primitives::FixedBytes; +use alloy::sol_types::SolValue; +use alloy::{ + primitives::{Address, U256}, + signers::{Signature, Signer}, +}; +use anyhow::{anyhow, Result}; +use rlp::{DecoderError, RlpStream}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::io::Read; + +use crate::types::ERC20; +use crate::types::{network::Network, Token}; + +use super::OwshenTransaction; + +// TODO: OwshenAirdrop transaction (Should contain "Owshen address" and "Owshen signature") +// TODO: Mint transaction (Should contain chain_id (Depsotior Network-id) and tx_hash (Deposit TxHash)) +// TODO: Burn transaction (Should contain chain_id (Withdrawer Network-id) and withdraw_sig (Withdrawal signature)) + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Mint { + pub tx_hash: Vec, + pub user_tx_hash: String, + pub token: Token, + pub amount: U256, + pub address: Address, +} + +impl rlp::Encodable for Mint { + fn rlp_append(&self, s: &mut RlpStream) { + match &self.token { + Token::Native => { + s.begin_list(6); + s.append(&"mint"); + s.append(&"native"); + s.append(&self.tx_hash); + s.append(&self.user_tx_hash); + s.append(&self.amount.as_le_bytes().to_vec()); + s.append(&self.address.to_vec()); + } + Token::Erc20(ERC20 { + address, + decimals, + symbol, + }) => { + s.begin_list(9); + s.append(&"mint"); + s.append(&"erc20"); + s.append(&self.tx_hash); + s.append(&self.user_tx_hash); + s.append(&self.amount.as_le_bytes().to_vec()); + s.append(&self.address.to_vec()); + s.append(&address.to_vec()); + s.append(&decimals.as_le_bytes().to_vec()); + s.append(&symbol.as_str()); + } + } + } +} + +impl rlp::Decodable for Mint { + fn decode(rlp: &rlp::Rlp) -> Result { + let token_type: String = rlp.val_at(1)?; + let tx_hash: Vec = rlp.val_at(2)?; + let user_tx_hash: String = rlp.val_at(3)?; + let amount: Vec = rlp.val_at(4)?; + let address: Vec = rlp.val_at(5)?; + let token = match token_type.as_str() { + "native" => Token::Native, + "erc20" => { + let token_address: Vec = rlp.val_at(6)?; + let token_decimals: Vec = rlp.val_at(7)?; + let token_symbol: String = rlp.val_at(8)?; + let token_address = Address::from_slice(&token_address); + Token::Erc20(ERC20 { + address: token_address, + decimals: U256::from_le_slice(&token_decimals), + symbol: token_symbol, + }) + } + _ => return Err(rlp::DecoderError::RlpExpectedToBeData), + }; + Ok(Mint { + tx_hash, + user_tx_hash, + token, + amount: U256::from_le_slice(&amount), + address: Address::from_slice(&address), + }) + } +} + +// msg.sender, _tokenAddress, _amount, _id, block.chainid + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WithdrawCalldata { + Eth { address: Address }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Burn { + pub burn_id: FixedBytes<32>, + pub network: Network, + pub token: Token, + pub amount: U256, + pub calldata: Option, +} + +impl rlp::Encodable for Burn { + fn rlp_append(&self, s: &mut RlpStream) { + let dl = if self.calldata.is_some() { 2 } else { 0 }; + match &self.token { + Token::Native => { + s.begin_list(4 + dl); + s.append(&"burn"); + s.append(&self.burn_id.to_vec()); + s.append(&"native"); + s.append(&self.amount.as_le_bytes().to_vec()); + } + Token::Erc20(ERC20 { + address, + decimals, + symbol, + }) => { + s.begin_list(7 + dl); + s.append(&"burn"); + s.append(&self.burn_id.to_vec()); + s.append(&"erc20"); + s.append(&self.amount.as_le_bytes().to_vec()); + s.append(&address.to_vec()); + s.append(&decimals.as_le_bytes().to_vec()); + s.append(&symbol.as_str()); + } + } + let network = match self.network { + Network::ETH => "eth", + Network::BSC => "bsc", + }; + s.append(&network); + + if let Some(calldata) = &self.calldata { + match calldata { + WithdrawCalldata::Eth { address } => { + s.append(&address.to_vec()); + } + } + } + } +} + +impl rlp::Decodable for Burn { + fn decode(rlp: &rlp::Rlp) -> Result { + let burn_id: Vec = rlp.val_at(1)?; + let token_type: String = rlp.val_at(2)?; + let amount: Vec = rlp.val_at(3)?; + let network_idx; + let calldata_idx; + let token = match token_type.as_str() { + "native" => { + network_idx = 4; + calldata_idx = 5; + Token::Native + } + "erc20" => { + network_idx = 7; + calldata_idx = 8; + let address: Vec = rlp.val_at(4)?; + let token_decimals: Vec = rlp.val_at(5)?; + let token_symbol: String = rlp.val_at(6)?; + let token_address = Address::from_slice(&address); + Token::Erc20(ERC20 { + address: token_address, + decimals: U256::from_le_slice(&token_decimals), + symbol: token_symbol, + }) + } + _ => return Err(rlp::DecoderError::RlpExpectedToBeData), + }; + let network: String = rlp.val_at(network_idx)?; + let network = match network.as_str() { + "eth" => Network::ETH, + "bsc" => Network::BSC, + _ => return Err(rlp::DecoderError::RlpExpectedToBeData), + }; + + let address: Result, _> = rlp.val_at(calldata_idx); + match address { + Ok(address) => { + let address = Address::from_slice(&address); + let calldata = Some(WithdrawCalldata::Eth { address }); + return Ok(Burn { + burn_id: FixedBytes::from_slice(&burn_id), + network, + token, + amount: U256::from_le_slice(&amount), + calldata, + }); + } + Err(_) => { + return Ok(Burn { + burn_id: FixedBytes::from_slice(&burn_id), + network, + token, + amount: U256::from_le_slice(&amount), + calldata: None, + }); + } + } + } +} + +pub enum CustomTxMsg { + // OwshenAirdrop { + // owshen_address: Address, + // owshen_sig: tx::owshen_airdrop::babyjubjub::Signature, + // }, + MintTx(Mint), + BurnTx(Burn), +} +impl CustomTxMsg { + pub fn as_rlp(&self) -> Vec { + match self { + // CustomTxMsg::OwshenAirdrop { + // owshen_address, + // owshen_sig, + // } => { + // let mut stream = RlpStream::new_list(3); + // stream.append(&"owshen-airdrop"); + // stream.append(&owshen_address.to_vec()); + // stream.append(&owshen_sig.as_bytes().to_vec()); + // stream.out().into() + // } + CustomTxMsg::MintTx(mint_data) => rlp::encode(mint_data).into(), + CustomTxMsg::BurnTx(burn_data) => rlp::encode(burn_data).into(), + } + } + pub fn from_rlp(bytes: &[u8]) -> Result { + let rlp = rlp::Rlp::new(bytes); + let tx_type: String = rlp.val_at(0)?; + match tx_type.as_str() { + // "owshen-airdrop" => { + // let _owshen_address: Vec = rlp.val_at(1)?; + // let _owshen_sig: Vec = rlp.val_at(2)?; + // Ok(CustomTxMsg::OwshenAirdrop { + // owshen_address: Address::from_slice(_owshen_address.as_slice()), + // owshen_sig: alloy::primitives::Signature::try_from(_owshen_sig.as_slice())?, + // }) + // } + "mint" => Ok(CustomTxMsg::MintTx(rlp::decode(bytes)?)), + "burn" => Ok(CustomTxMsg::BurnTx(rlp::decode(bytes)?)), + _ => Err(anyhow!("Invalid tx!")), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +pub struct CustomTx { + pub chain_id: u64, + pub msg: Vec, + pub sig: alloy::primitives::Signature, +} + +impl CustomTx { + pub fn msg(&self) -> Result { + CustomTxMsg::from_rlp(&self.msg) + } + pub async fn create( + signer: &mut S, + chain_id: u64, + msg: CustomTxMsg, + ) -> Result { + let signer = signer.with_chain_id(Some(chain_id)); + let msg_rlp = msg.as_rlp(); + let sig = signer.sign_message(&msg_rlp).await?; + let tx = CustomTx { + msg: msg_rlp, + chain_id, + sig, + }; + Ok(OwshenTransaction::Custom(tx)) + } + pub fn signer(&self) -> Result
{ + Ok(self.sig.recover_address_from_msg(&self.msg)?) + } +} diff --git a/src/types/tx/mod.rs b/src/types/tx/mod.rs new file mode 100644 index 0000000..2f5ce4b --- /dev/null +++ b/src/types/tx/mod.rs @@ -0,0 +1,94 @@ +use alloy::rlp::{Decodable, Encodable}; +use alloy::{ + consensus::{Transaction, TxEnvelope}, + network::{Ethereum, Network}, + primitives::{keccak256, Address, FixedBytes}, +}; +use anyhow::{anyhow, Result}; +use serde::{Deserialize, Serialize}; +pub mod custom; +pub use custom::*; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum BincodableOwshenTransaction { + EncodedEth(Vec), + Custom(CustomTx), +} + +impl TryInto for &BincodableOwshenTransaction { + type Error = anyhow::Error; + fn try_into(self) -> Result { + match self { + BincodableOwshenTransaction::EncodedEth(enc) => Ok(OwshenTransaction::Eth( + ::TxEnvelope::decode(&mut enc.as_ref())?, + )), + BincodableOwshenTransaction::Custom(tx) => Ok(OwshenTransaction::Custom(tx.clone())), + } + } +} +impl TryInto for BincodableOwshenTransaction { + type Error = anyhow::Error; + fn try_into(self) -> Result { + (&self).try_into() + } +} + +impl TryInto for &OwshenTransaction { + type Error = anyhow::Error; + fn try_into(self) -> Result { + match self { + OwshenTransaction::Eth(tx) => { + let mut buf = Vec::new(); + tx.encode(&mut buf); + Ok(BincodableOwshenTransaction::EncodedEth(buf)) + } + OwshenTransaction::Custom(tx) => Ok(BincodableOwshenTransaction::Custom(tx.clone())), + } + } +} +impl TryInto for OwshenTransaction { + type Error = anyhow::Error; + fn try_into(self) -> Result { + (&self).try_into() + } +} + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, PartialEq, Serialize, Clone, Deserialize)] +pub enum OwshenTransaction { + Eth(::TxEnvelope), + Custom(CustomTx), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IncludedTransaction { + pub tx: BincodableOwshenTransaction, + pub block_hash: FixedBytes<32>, + pub block_number: usize, + pub transaction_index: usize, +} + +impl OwshenTransaction { + pub fn chain_id(&self) -> Result { + Ok(match self { + Self::Eth(tx) => tx.chain_id().ok_or(anyhow!("Chain-id not provided!"))?, + Self::Custom(tx) => tx.chain_id, + }) + } + pub fn signer(&self) -> Result
{ + Ok(match self { + Self::Eth(tx) => tx.recover_signer()?, + Self::Custom(tx) => tx.signer()?, + }) + } + pub fn hash(&self) -> Result> { + Ok(match self { + Self::Eth(tx) => *tx.tx_hash(), + Self::Custom(tx) => keccak256(bincode::serialize(&tx)?), + }) + } +} + +#[cfg(test)] +mod tests; diff --git a/src/types/tx/tests.rs b/src/types/tx/tests.rs new file mode 100644 index 0000000..6368e3f --- /dev/null +++ b/src/types/tx/tests.rs @@ -0,0 +1,148 @@ +use super::*; +use crate::types::{Token, ERC20}; + +use alloy::{primitives::U256, signers::local::PrivateKeySigner}; + +#[tokio::test] +async fn test_mint_tx_native() { + let chain_id = 2341; + let tx_hash = vec![0u8; 32]; + let signer = PrivateKeySigner::random(); + let user_tx_hash = "0x1234567890abcdef".to_string(); + let tx = CustomTx::create( + &mut signer.clone(), + chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash, + user_tx_hash, + token: Token::Native, + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + assert_eq!(tx.signer().unwrap(), signer.address()); + match tx { + OwshenTransaction::Custom(custom_tx) => match custom_tx.msg().unwrap() { + CustomTxMsg::MintTx(mint) => { + assert_eq!(mint.tx_hash, vec![0u8; 32]); + assert_eq!(mint.user_tx_hash, "0x1234567890abcdef"); + assert_eq!(mint.token, Token::Native); + assert_eq!(mint.amount, U256::from(100)); + assert_eq!(mint.address, signer.address()); + } + _ => panic!("Invalid tx!"), + }, + _ => panic!("Invalid tx!"), + } +} + +#[tokio::test] +async fn test_mint_tx_erc20() { + let chain_id = 2341; + let tx_hash = vec![0u8; 32]; + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let user_tx_hash = "0x1234567890abcdef".to_string(); + + let token = Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }); + + let tx = CustomTx::create( + &mut signer.clone(), + chain_id, + CustomTxMsg::MintTx(Mint { + tx_hash, + user_tx_hash, + token: token.clone(), + amount: U256::from(100), + address: signer.address(), + }), + ) + .await + .unwrap(); + assert_eq!(tx.signer().unwrap(), signer.address()); + match tx { + OwshenTransaction::Custom(custom_tx) => match custom_tx.msg().unwrap() { + CustomTxMsg::MintTx(mint) => { + assert_eq!(mint.tx_hash, vec![0u8; 32]); + assert_eq!(mint.user_tx_hash, "0x1234567890abcdef"); + assert_eq!(mint.token, token.clone()); + assert_eq!(mint.amount, U256::from(100)); + assert_eq!(mint.address, signer.address()); + } + _ => panic!("Invalid tx!"), + }, + _ => panic!("Invalid tx!"), + } +} + +#[tokio::test] +async fn test_burn_tx_native() { + let chain_id = 2341; + let signer = PrivateKeySigner::random(); + let tx = CustomTx::create( + &mut signer.clone(), + chain_id, + CustomTxMsg::BurnTx(Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: crate::types::network::Network::BSC, + token: Token::Native, + amount: U256::from(100), + calldata: None, + }), + ) + .await + .unwrap(); + assert_eq!(tx.signer().unwrap(), signer.address()); + match tx { + OwshenTransaction::Custom(custom_tx) => match custom_tx.msg().unwrap() { + CustomTxMsg::BurnTx(burn) => { + assert_eq!(burn.token, Token::Native); + assert_eq!(burn.amount, U256::from(100)); + } + _ => panic!("Invalid tx!"), + }, + _ => panic!("Invalid tx!"), + } +} + +#[tokio::test] +async fn test_burn_tx_erc20() { + let chain_id = 2341; + let signer = PrivateKeySigner::random(); + let random_token_address = PrivateKeySigner::random().address(); + let token = Token::Erc20(ERC20 { + address: random_token_address, + decimals: U256::from(18), + symbol: "USDT".to_owned(), + }); + let tx = CustomTx::create( + &mut signer.clone(), + chain_id, + CustomTxMsg::BurnTx(Burn { + burn_id: FixedBytes::from([1u8; 32]), + network: crate::types::network::Network::BSC, + token: token.clone(), + amount: U256::from(100), + calldata: None, + }), + ) + .await + .unwrap(); + assert_eq!(tx.signer().unwrap(), signer.address()); + match tx { + OwshenTransaction::Custom(custom_tx) => match custom_tx.msg().unwrap() { + CustomTxMsg::BurnTx(burn) => { + assert_eq!(burn.token, token.clone()); + assert_eq!(burn.amount, U256::from(100)); + } + _ => panic!("Invalid tx!"), + }, + _ => panic!("Invalid tx!"), + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..31442ae --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,20 @@ +use axum::http::StatusCode; +use axum::{response::IntoResponse, Json}; +use serde_json::json; + +pub fn handle_error(result: Result) -> impl IntoResponse { + match result { + Ok(response) => response.into_response(), + Err(e) => { + log::error!("{}", e); + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": true, + "message": e.to_string() + })), + ) + .into_response() + } + } +} diff --git a/static/insex.html b/static/insex.html new file mode 100644 index 0000000..7bff001 --- /dev/null +++ b/static/insex.html @@ -0,0 +1,137 @@ + + + + + OwshenScan + + + + + + + + + + +

🐟 owshen scan 🤿

+
+

> version: v0.1.0

+

> height: 1432

+

> timestamp: 1724075206

+

> market cap: 1.237m $

+

> tps: 96

+ +
+

> latest blocks;

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
blocknum-txswhowen
> #14311890x8aF3d2E...just now
> #14301230x8aF3d2E...10 sec ago
> #14297640x8aF3d2E...20 sec ago
> #14289040x8aF3d2E...40 sec ago
> #14271020x8aF3d2E...1 min ago
> #14262430x8aF3d2E...1 min ago
> #14251980x8aF3d2E...1 min ago
> #14244350x8aF3d2E...1 min ago
> #1423900x8aF3d2E...2 min ago
> #14221200x8aF3d2E...2 min ago
> ...
+
+
+ + + \ No newline at end of file diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..5934363 --- /dev/null +++ b/static/style.css @@ -0,0 +1,30 @@ +h1 { + font-family: "JetBrains Mono"; + margin: 0.5em; + font-size: 3em; + text-align: center; +} + +body { + font-family: "JetBrains Mono"; + line-height: 1.4em; + font-size: 1.1em; +} + +h2 { + font-size: 1.5em; + text-align: center; +} + +b { + font-weight: bold; +} + +th { + font-weight: bold; + border-bottom: 1px solid white; +} + +th, td { + padding: 0.2em; +} \ No newline at end of file